06 · Create the intuitive trog command layer — one CLI, central orchestrator, retire make #21

Closed
opened 2026-07-24 22:52:18 -04:00 by cmoriarty · 5 comments
Owner

The make targets are accruing (asset-bench, assets-drain, cycle-test, critic-test, seat-up/run/down, hello, health…) on varying underlying scripting strategies — bash sourcing bash, python drivers, docker-exec'd python, heredocs — and it's getting unkept. Redesign the user-facing layer (user direction, 2026-07-24; supersedes much of the UX built in #17, whose plumbing stays).

The idea: one trog command, obvious things trivially easy.

trog image "red car driving at night pixel art 64x64"
trog music "rap song with violins about a small dog"
trog sfx "finger snap"
trog animate "knight walking pixel art 64x64"
trog critique "dark sky with white pixel stars" /image/stars.png
trog update

Examples, not a final surface — subcommands take params for extra functionality, but the most useful case per command must be a one-liner with a plain-English prompt.

Requirements (from the direction):

  • One language for the whole layer — python preferred (unless something is clearly more elegant)
  • All model-swap-requiring commands route through a central orchestrator (possibly a message queue): the orchestrator owns seat lifecycle, GPU tenancy, and brain bracketing, so a one-off image "just happens" — the user never thinks about which seat is loaded
  • Retire make, or reserve it strictly for what make is good at (build/push of images, maybe test)

What already exists to build on (don't rebuild, absorb):

  • assetq (Postgres queue) — already the handoff for batch drains; the orchestrator could be its standing consumer
  • scripts/seat.sh + profiles (#17) — the seat lifecycle the orchestrator would own; profiles stay the per-model truth
  • conductor — today's batch orchestrator; long-term the daemon and the conductor converge (one component that drains queues AND serves one-offs)
  • The studio UI (v2+) rides the same orchestrator: UI → API → queue → orchestrator; the CLI is just the first client

Design questions to settle first (proposal before code):

  • CLI framework + packaging: typer/click; installed how (uv tool, pipx, repo venv)? Where does it run from (laptop, talking to trogdor)?
  • Orchestrator placement: standing service in the trog stack? Inside aegra or its own container? What survives restarts?
  • Queue: reuse assetq (Postgres, exists, has semantics) vs introduce a broker (redis already in stack) — what does a one-off need that assetq lacks (priority? sync response channel? progress events?)
  • One-off UX: block with streamed progress until the asset lands (and print the committed URL / open the review page?), or submit-and-notify? What does trog image print on success?
  • GPU policy as orchestrator policy: brain bracketing rules (idle-wait, refuse, preempt?) move from seat.sh flags into orchestrator config; mock/cpu modes become flags or env for the same commands (testing tiers become trog test cycle / trog test critics?)
  • trog critique: judge an arbitrary file against a prompt — needs a critic entrypoint decoupled from game-repo specs (brief comes from argv, file from path/repo)
  • trog update: which update story does this wrap (check-updates/try-updates? deploy? both?)
  • Migration map: every current make target → its trog equivalent or its retirement; make keeps build/push only
  • Output/asset destination for one-offs: seat-scratch repo? local file? both (--repo, --out)?

Decision output: a short design doc (docs/command-layer.md) answering the above, then implementation in phases — CLI skeleton + orchestrator MVP covering image/sfx/music one-offs first, drain/test/critique absorption after.

Related: #17 (tiers/plumbing this layer sits on), ticket 10 (E2E), studio UI (v2 milestone).

The make targets are accruing (asset-bench, assets-drain, cycle-test, critic-test, seat-up/run/down, hello, health…) on varying underlying scripting strategies — bash sourcing bash, python drivers, docker-exec'd python, heredocs — and it's getting unkept. Redesign the user-facing layer (user direction, 2026-07-24; supersedes much of the UX built in #17, whose plumbing stays). **The idea: one `trog` command, obvious things trivially easy.** ``` trog image "red car driving at night pixel art 64x64" trog music "rap song with violins about a small dog" trog sfx "finger snap" trog animate "knight walking pixel art 64x64" trog critique "dark sky with white pixel stars" /image/stars.png trog update ``` Examples, not a final surface — subcommands take params for extra functionality, but the most useful case per command must be a one-liner with a plain-English prompt. **Requirements (from the direction):** - One language for the whole layer — **python preferred** (unless something is clearly more elegant) - All model-swap-requiring commands route through a **central orchestrator** (possibly a message queue): the orchestrator owns seat lifecycle, GPU tenancy, and brain bracketing, so a one-off image "just happens" — the user never thinks about which seat is loaded - **Retire make**, or reserve it strictly for what make is good at (build/push of images, maybe test) **What already exists to build on (don't rebuild, absorb):** - `assetq` (Postgres queue) — already the handoff for batch drains; the orchestrator could be its standing consumer - `scripts/seat.sh` + profiles (#17) — the seat lifecycle the orchestrator would own; profiles stay the per-model truth - conductor — today's batch orchestrator; long-term the daemon and the conductor converge (one component that drains queues AND serves one-offs) - The studio UI (v2+) rides the same orchestrator: UI → API → queue → orchestrator; the CLI is just the first client **Design questions to settle first (proposal before code):** - [ ] CLI framework + packaging: typer/click; installed how (uv tool, pipx, repo venv)? Where does it run from (laptop, talking to trogdor)? - [ ] Orchestrator placement: standing service in the trog stack? Inside aegra or its own container? What survives restarts? - [ ] Queue: reuse assetq (Postgres, exists, has semantics) vs introduce a broker (redis already in stack) — what does a one-off need that assetq lacks (priority? sync response channel? progress events?) - [ ] One-off UX: block with streamed progress until the asset lands (and print the committed URL / open the review page?), or submit-and-notify? What does `trog image` print on success? - [ ] GPU policy as orchestrator policy: brain bracketing rules (idle-wait, refuse, preempt?) move from seat.sh flags into orchestrator config; mock/cpu modes become flags or env for the same commands (testing tiers become `trog test cycle` / `trog test critics`?) - [ ] `trog critique`: judge an arbitrary file against a prompt — needs a critic entrypoint decoupled from game-repo specs (brief comes from argv, file from path/repo) - [ ] `trog update`: which update story does this wrap (check-updates/try-updates? deploy? both?) - [ ] Migration map: every current make target → its trog equivalent or its retirement; make keeps build/push only - [ ] Output/asset destination for one-offs: seat-scratch repo? local file? both (`--repo`, `--out`)? **Decision output:** a short design doc (docs/command-layer.md) answering the above, then implementation in phases — CLI skeleton + orchestrator MVP covering image/sfx/music one-offs first, drain/test/critique absorption after. Related: #17 (tiers/plumbing this layer sits on), ticket 10 (E2E), studio UI (v2 milestone).
Author
Owner

Phase 1 (MVP) is live (commits f7ae09e, 9381845; stack redeployed). Decisions honored: python everywhere, orchestrator in its own container, assetq as the reliable backing queue — Postgres, durable, transactional claims (FOR UPDATE SKIP LOCKED), one source of truth shared with the drain via an origin column so the conductor and orchestrator can never double-consume.

Install (laptop):

uv tool install --from cli/ trog-cli     # from the trog checkout (or: pip install -e cli/)

Working today, all smoked against the live stack:

trog sfx "finger snap"                                  # 35s real run: brain handed GPUs to the
                                                          # audio seat, generated, committed, restored
trog image "red car driving at night pixel art" --size 64x64
trog music "calm tavern theme" --seconds 45 --loop
trog animate "knight walking" --base assets/sprite/knight.png
trog critique "a single crisp finger snap" snap.wav      # CLAP 0.61 PASS
trog critique "a shiny red apple..." apple.png           # brain-vision critique, PASS
trog status / trog job <id>

Every job streams its progress events to the terminal and ends with the committed Forgejo URL. One-offs land in trog-games/seat-scratch (--repo overrides).

How it works: CLI → orchestrator API (:8200) → assetq origin='oneoff' → single worker: brain idle-check → stop brain → seat container on the stack network (no host ports, no ssh, no laptop in the data path) → same drivers the conductor uses (now canonical in trog_lib; scripts/ keeps shims) → commit → always restore the brain (finally-block + boot-time reconcile that removes orphan seats and restarts a downed brain after a crash). SEAT_MODE=mock runs the whole path with the in-repo stub — both mock paths smoked too. /critique is the spec-decoupled judge (brief from argv, file uploaded; brain vision or analyzer CLAP).

Validation: 64 unit tests green (8 new for the CLI→spec mapping), lint clean, make cycle-test still PASSED (67s — conductor unaffected by the driver moves), orchestrator + brain + aegra healthy after all smokes.

Not yet (phases in docs/command-layer.md): seat keep-warm TTL (consecutive one-offs currently bracket the brain each time), trog drain/trog bench (conductor absorption), trog test * (make retirement completes there), SSE instead of polling, trog update's full story. Design doc + make-retirement map committed as docs/command-layer.md.

**Phase 1 (MVP) is live** (commits f7ae09e, 9381845; stack redeployed). Decisions honored: python everywhere, orchestrator in its own container, **assetq as the reliable backing queue** — Postgres, durable, transactional claims (`FOR UPDATE SKIP LOCKED`), one source of truth shared with the drain via an `origin` column so the conductor and orchestrator can never double-consume. **Install (laptop):** ``` uv tool install --from cli/ trog-cli # from the trog checkout (or: pip install -e cli/) ``` **Working today, all smoked against the live stack:** ``` trog sfx "finger snap" # 35s real run: brain handed GPUs to the # audio seat, generated, committed, restored trog image "red car driving at night pixel art" --size 64x64 trog music "calm tavern theme" --seconds 45 --loop trog animate "knight walking" --base assets/sprite/knight.png trog critique "a single crisp finger snap" snap.wav # CLAP 0.61 PASS trog critique "a shiny red apple..." apple.png # brain-vision critique, PASS trog status / trog job <id> ``` Every job streams its progress events to the terminal and ends with the committed Forgejo URL. One-offs land in `trog-games/seat-scratch` (`--repo` overrides). **How it works:** CLI → orchestrator API (:8200) → `assetq` `origin='oneoff'` → single worker: brain idle-check → stop brain → seat container on the stack network (no host ports, no ssh, no laptop in the data path) → same drivers the conductor uses (now canonical in `trog_lib`; `scripts/` keeps shims) → commit → **always restore the brain** (finally-block + boot-time reconcile that removes orphan seats and restarts a downed brain after a crash). `SEAT_MODE=mock` runs the whole path with the in-repo stub — both mock paths smoked too. `/critique` is the spec-decoupled judge (brief from argv, file uploaded; brain vision or analyzer CLAP). **Validation:** 64 unit tests green (8 new for the CLI→spec mapping), lint clean, `make cycle-test` still PASSED (67s — conductor unaffected by the driver moves), orchestrator + brain + aegra healthy after all smokes. **Not yet (phases in docs/command-layer.md):** seat keep-warm TTL (consecutive one-offs currently bracket the brain each time), `trog drain`/`trog bench` (conductor absorption), `trog test *` (make retirement completes there), SSE instead of polling, `trog update`'s full story. Design doc + make-retirement map committed as docs/command-layer.md.
Author
Owner

Seat keep-warm TTL shipped and verified live (commit pushed, stack redeployed):

  • cold trog sfx "wooden door creaking open"36s (brain hands over GPUs, seat up, generate, commit)
  • immediately after, trog sfx "heavy door slamming shut"19s — event says seat 'audio' already warm; generating, zero brain churn
  • 180s idle → worker's sweep released the seat and restored the brain on its own (verified: brain down during the warm window, up after)

ORCH_SEAT_TTL env tunes the window (default 180s). Profile switches swap seats without bouncing the brain twice. trog status shows the warm seat and its remaining window. Worker shutdown also releases — the brain can't be orphaned by a restart.

Remaining phases unchanged: trog drain/bench (conductor absorption), trog test * (make retirement completes), SSE, trog update story.

**Seat keep-warm TTL shipped and verified live** (commit pushed, stack redeployed): - cold `trog sfx "wooden door creaking open"` → **36s** (brain hands over GPUs, seat up, generate, commit) - immediately after, `trog sfx "heavy door slamming shut"` → **19s** — event says `seat 'audio' already warm; generating`, zero brain churn - 180s idle → worker's sweep released the seat and restored the brain on its own (verified: brain down during the warm window, up after) `ORCH_SEAT_TTL` env tunes the window (default 180s). Profile switches swap seats without bouncing the brain twice. `trog status` shows the warm seat and its remaining window. Worker shutdown also releases — the brain can't be orphaned by a restart. Remaining phases unchanged: `trog drain`/`bench` (conductor absorption), `trog test *` (make retirement completes), SSE, `trog update` story.
Author
Owner

Delivery redesign shipped (user direction: CLI one-offs are throwaways; git makes them permanent and they pile up):

  • Drivers split generation from delivery: produce() returns artifacts as bytes; the caller picks the destination. The conductor's run_item (produce + git) is unchanged — cycle-test still PASSED (68s).
  • No --repo = throwaway: date-bucketed plain file under /mnt/output/oneoffs/<date>/, served at :3923/oneoffs/… — and trogdor-files grew a dedicated share with 14-day auto-expiry (same lifetime flag as intermediates), so throwaways literally clean themselves up.
  • --repo owner/name = keeper: git delivery into that game repo, Forgejo URL printed. The game-design graph's drain always delivers to git — git stays truth for production.
  • trog animate gained --base-repo for one-offs whose base sprite lives in a repo the job isn't delivering to.
  • seat-scratch is retired as a default destination (no more junk commits).

Live smoke:

$ trog sfx "coin dropping into a metal cup"
  seat 'audio' up — brain handed over the GPUs; generating
  saved to file server (throwaway — rm anytime) (34s) — seat stays warm 180s
done: http://trogdor:3923/oneoffs/2026-07-25/coin-dropping-into-a-763d-035644.wav   ← serves, plays in-browser

65 tests green, both stacks redeployed (trog + trogdor-files 8e99b83), brain restored by TTL sweep, queue empty.

**Delivery redesign shipped** (user direction: CLI one-offs are throwaways; git makes them permanent and they pile up): - Drivers split **generation from delivery**: `produce()` returns artifacts as bytes; the caller picks the destination. The conductor's `run_item` (produce + git) is unchanged — cycle-test still PASSED (68s). - **No `--repo` = throwaway**: date-bucketed plain file under `/mnt/output/oneoffs/<date>/`, served at `:3923/oneoffs/…` — and trogdor-files grew a dedicated share with **14-day auto-expiry** (same lifetime flag as intermediates), so throwaways literally clean themselves up. - **`--repo owner/name` = keeper**: git delivery into that game repo, Forgejo URL printed. The game-design graph's drain always delivers to git — git stays truth for production. - `trog animate` gained `--base-repo` for one-offs whose base sprite lives in a repo the job isn't delivering to. - seat-scratch is retired as a default destination (no more junk commits). Live smoke: ``` $ trog sfx "coin dropping into a metal cup" seat 'audio' up — brain handed over the GPUs; generating saved to file server (throwaway — rm anytime) (34s) — seat stays warm 180s done: http://trogdor:3923/oneoffs/2026-07-25/coin-dropping-into-a-763d-035644.wav ← serves, plays in-browser ``` 65 tests green, both stacks redeployed (trog + trogdor-files 8e99b83), brain restored by TTL sweep, queue empty.
Author
Owner

Three more phase-2 features shipped and live-smoked in one session:

1 · Live queue position + spinner UX. One animated dots line narrates: Job is #N in the queue counts down live (N = your place among one-offs — the multiplayer-studio contract), then Swapping brain for image stackGenerating sprite → green done line. Phase events fire at phase START. The bigserial never shows: jobs surface as small per-day refs (trog job 10 re-attaches), and a daily sweep prunes terminal one-offs older than 14 days.

2 · Parallel seats on free GPUs. SeatManager is multi-seat with the GPU truth table (audio=1, animate=0/2/3, image=all): a submitted job starts in parallel whenever its cards are free; same-profile jobs share the warm seat. A blocked exclusive job preempts idle warm seats immediately (brain kept down — no thrash) instead of waiting out the TTL. Smoked: music + sfx ran together on the audio seat; the image job showed its countdown, preempted the idle audio seat, and took the rig.

3 · trog image --from <path-or-url> — tweak an existing image. Local path or any http URL (file-server links work). New flux2-pixel-i2i workflow re-runs the tail of the schedule over the encoded source (--denoise 0=copy…1=ignore, default 0.55) through the same pixel post-pass. Smoked end-to-end: generated a blue hedgehog, then --from its file-server URL with "make it red with white gloves" — same character, same pose, gloves added. Audio --from is refused with an honest 422 until the seat's cover/repaint routes are wired.

$ trog sfx "sword clash"
⠋ Job is #2 in the queue
⠙ Swapping brain for audio stack
⠸ Generating sfx
done: http://trogdor:3923/oneoffs/2026-07-25/sword-clash-9096-043049.wav

67 tests green, stack redeployed, brain restored by the TTL sweep after the smokes.

**Three more phase-2 features shipped and live-smoked in one session:** **1 · Live queue position + spinner UX.** One animated dots line narrates: `Job is #N in the queue` counts down live (N = your place among one-offs — the multiplayer-studio contract), then `Swapping brain for image stack` → `Generating sprite` → green done line. Phase events fire at phase START. The bigserial never shows: jobs surface as small per-day refs (`trog job 10` re-attaches), and a daily sweep prunes terminal one-offs older than 14 days. **2 · Parallel seats on free GPUs.** SeatManager is multi-seat with the GPU truth table (audio=1, animate=0/2/3, image=all): a submitted job starts in parallel whenever its cards are free; same-profile jobs share the warm seat. A blocked exclusive job preempts idle warm seats immediately (brain kept down — no thrash) instead of waiting out the TTL. Smoked: music + sfx ran together on the audio seat; the image job showed its countdown, preempted the idle audio seat, and took the rig. **3 · `trog image --from <path-or-url>` — tweak an existing image.** Local path or any http URL (file-server links work). New `flux2-pixel-i2i` workflow re-runs the tail of the schedule over the encoded source (`--denoise` 0=copy…1=ignore, default 0.55) through the same pixel post-pass. Smoked end-to-end: generated a blue hedgehog, then `--from` its file-server URL with "make it red with white gloves" — same character, same pose, gloves added. Audio `--from` is refused with an honest 422 until the seat's cover/repaint routes are wired. ``` $ trog sfx "sword clash" ⠋ Job is #2 in the queue ⠙ Swapping brain for audio stack ⠸ Generating sfx done: http://trogdor:3923/oneoffs/2026-07-25/sword-clash-9096-043049.wav ``` 67 tests green, stack redeployed, brain restored by the TTL sweep after the smokes.
Author
Owner

Closing: the command layer stands as a complete unit. Shipped, deployed, and live-smoked in this ticket:

  • trog CLI (uv/pip): image / music / sfx / animate / critique / job / status — plain-English one-liners with a dots-spinner narration, live #N in the queue countdown, per-day job refs (bigserial never shows)
  • Orchestrator container (:8200): single consumer of one-off jobs on assetq (Postgres, transactional claims, origin-split from the drain), seat lifecycle via the docker socket, brain bracket with always-restore + boot reconcile
  • Parallel seats on the GPU truth table (audio ∥ animate; image exclusive), warm-seat keep-alive with TTL, immediate preemption for blocked exclusive jobs
  • Delivery split: throwaway one-offs → file server (:3923/oneoffs/, 14-day auto-expiry both file- and queue-side); --repo → git; drains stay git — git is truth for production
  • --from image tweak (local path or URL, --denoise knob, i2i workflow through the same pixel post-pass); /critique decoupled from game specs
  • Mock mode end-to-end, 67 unit tests, design + phasing in docs/command-layer.md

Follow-ups spun out: #22 (06.1 drain absorption — conductor + bash profiles retire, trog drain/bench/update, audio --from) and #23 (06.2 testing absorption + make retirement — trog test *, seat.sh retires, SSE, --out). The make targets live until those land.

**Closing: the command layer stands as a complete unit.** Shipped, deployed, and live-smoked in this ticket: - `trog` CLI (uv/pip): image / music / sfx / animate / critique / job / status — plain-English one-liners with a dots-spinner narration, live `#N in the queue` countdown, per-day job refs (bigserial never shows) - Orchestrator container (:8200): single consumer of one-off jobs on **assetq** (Postgres, transactional claims, origin-split from the drain), seat lifecycle via the docker socket, brain bracket with always-restore + boot reconcile - **Parallel seats** on the GPU truth table (audio ∥ animate; image exclusive), warm-seat keep-alive with TTL, immediate preemption for blocked exclusive jobs - **Delivery split**: throwaway one-offs → file server (`:3923/oneoffs/`, 14-day auto-expiry both file- and queue-side); `--repo` → git; drains stay git — git is truth for production - **`--from` image tweak** (local path or URL, `--denoise` knob, i2i workflow through the same pixel post-pass); `/critique` decoupled from game specs - Mock mode end-to-end, 67 unit tests, design + phasing in docs/command-layer.md **Follow-ups spun out:** #22 (06.1 drain absorption — conductor + bash profiles retire, `trog drain/bench/update`, audio `--from`) and #23 (06.2 testing absorption + make retirement — `trog test *`, seat.sh retires, SSE, `--out`). The make targets live until those land.
Sign in to join this conversation.
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
cmoriarty/trog#21
No description provided.