Repository navigation
feat(workers-dev): introducing dev tui for easier dev experience #311
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 1 commit
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| [workspace] | ||
|
|
||
| [package] | ||
| name = "workers-dev" | ||
| version = "0.1.0" | ||
| edition = "2021" | ||
| publish = false | ||
| description = "Local dev orchestrator for iii workers in this repo" | ||
|
|
||
| [[bin]] | ||
| name = "workers-dev" | ||
| path = "src/main.rs" | ||
|
|
||
| [dependencies] | ||
| anyhow = "1" | ||
| clap = { version = "4", features = ["derive"] } | ||
| crossterm = "0.28" | ||
| ratatui = "0.29" | ||
| serde = { version = "1", features = ["derive"] } | ||
| serde_json = "1" | ||
| serde_yaml = "0.9" | ||
| tokio = { version = "1", features = ["rt-multi-thread", "macros", "sync", "signal", "time", "io-util", "process"] } | ||
| tokio-stream = { version = "0.1", features = ["sync"] } | ||
|
|
||
| [dev-dependencies] | ||
| tempfile = "3" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,258 @@ | ||
| # workers-dev — local dev orchestrator | ||
|
|
||
| A single TUI/CLI to spin up, supervise, restart (rebuild), and tail the seven | ||
| core workers that make up the harness stack, against a locally-running iii | ||
| engine. | ||
|
|
||
| Managed workers: | ||
|
|
||
| ``` | ||
| session-manager llm-router context-manager approval-gate | ||
| provider-anthropic provider-openai harness | ||
| ``` | ||
|
|
||
| --- | ||
|
|
||
| ## 1. Mental model | ||
|
|
||
| - The **iii engine** is the bus. It runs separately (`iii --use-default-config`, | ||
| bridge WS on `:49134`). harness-dev does **not** reimplement it. | ||
| - Each worker is a **child process** = `cargo run --manifest-path <worker>/Cargo.toml -- --url ws://127.0.0.1:49134`. | ||
| - "Restart" = kill the child, then `cargo run` again → cargo rebuilds if the | ||
| source changed, so restart *is* rebuild-and-run. | ||
| - harness-dev owns three responsibilities per worker: | ||
| 1. **Process lifecycle** — spawn / kill / restart, track PID + run state. | ||
| 2. **Log capture** — pipe child stdout+stderr into an in-memory ring buffer | ||
| (last N lines) *and* tee to a file under `harness-dev/.logs/<worker>.log`. | ||
| 3. **Connection status** — poll the engine's `engine::workers::list` and match | ||
| by worker name to learn `connected` / `available` / `disconnected`. | ||
| - A worker has two independent state axes that the dashboard shows together: | ||
| - **process state**: `stopped | building | running | crashed` | ||
| (from our own child handle + exit codes) | ||
| - **engine state**: `connected | available | disconnected | unknown` | ||
| (from `engine::workers::list`) | ||
|
|
||
| --- | ||
|
|
||
| ## 2. Tech stack | ||
|
|
||
| Standalone Rust crate (matches the repo; gives us a real TUI): | ||
|
|
||
| - `ratatui` + `crossterm` — TUI dashboard. | ||
| - `clap` (derive) — subcommands. | ||
| - `tokio` — async process supervision, log pumps, status poll loop. | ||
| - `tokio::process::Command` — spawn `cargo run`, capture piped stdout/stderr. | ||
| - `serde` / `serde_json` — parse `engine::workers::list`. | ||
| - `serde_yaml` — read each worker's `iii.worker.yaml` to build the dep graph. | ||
|
|
||
| Engine status is read by **shelling out to `iii trigger engine::workers::list | ||
| --json '{}'`** and parsing stdout JSON. (Rationale: zero coupling to a specific | ||
| `iii-sdk` version, and it's a dev tool. If we later want push updates instead of | ||
| polling, swap to an `iii-sdk` WS client — isolated behind one `EngineClient` | ||
| trait.) | ||
|
|
||
| --- | ||
|
|
||
| ## 3. Crate layout | ||
|
|
||
| ``` | ||
| harness-dev/ | ||
| ├── Cargo.toml | ||
| ├── PLAN.md | ||
| ├── workers.toml # the worker registry (see §4) — or derive from yaml | ||
| ├── src/ | ||
| │ ├── main.rs # clap entrypoint, dispatch | ||
| │ ├── registry.rs # load workers + build dependency / dependents graph | ||
| │ ├── supervisor.rs # spawn/kill/restart, child handles, exit watching | ||
| │ ├── logs.rs # per-worker ring buffer + file tee + tail reader | ||
| │ ├── engine.rs # EngineClient: poll engine::workers::list -> status | ||
| │ ├── graph.rs # topo sort + reverse-dependency (dependents) walk | ||
| │ ├── state.rs # shared AppState (Arc<Mutex<…>>) the TUI renders | ||
| │ └── tui/ | ||
| │ ├── mod.rs # event loop, key handling | ||
| │ ├── dashboard.rs # the multi-worker grid view | ||
| │ └── logview.rs # full-screen single-worker log pager | ||
| └── .logs/ # gitignored; captured stdout/stderr per worker | ||
| ``` | ||
|
|
||
| `harness-dev` has **no `iii.worker.yaml`**, so the repo's CI worker-discovery | ||
| ignores it. | ||
|
|
||
| --- | ||
|
|
||
| ## 4. Worker registry & dependency graph | ||
|
|
||
| Source of truth for deps is each worker's existing `iii.worker.yaml`. Two options: | ||
|
|
||
| - **A (recommended): derive at runtime.** On startup, read the 7 | ||
| `iii.worker.yaml` files, take their `dependencies` keys, and keep only edges | ||
| pointing at other *managed* workers (drop engine built-ins like | ||
| `configuration`, `iii-state`, `iii-queue`, `iii-directory`). No duplication — | ||
| the graph always matches the yaml. | ||
| - **B: a small `workers.toml`** listing dir + bin + extra run args, for cases | ||
| where we want to override (e.g. add `--config ./dev.yaml`, set env). Start with | ||
| A; add B only if per-worker overrides are needed. | ||
|
|
||
| Computed graph: | ||
|
|
||
| ``` | ||
| tier 0: session-manager llm-router | ||
| tier 1: approval-gate context-manager provider-anthropic provider-openai | ||
| tier 2: harness | ||
| ``` | ||
|
|
||
| - **Start order**: tier 0 → 1 → 2 (a topological sort). | ||
| - **Dependents map** (reverse edges), used by restart: | ||
|
|
||
| | restart this | also restart, in order | | ||
| |---|---| | ||
| | session-manager | approval-gate, harness | | ||
| | llm-router | context-manager, provider-anthropic, provider-openai, harness | | ||
| | context-manager | harness | | ||
| | approval-gate | harness | | ||
| | provider-anthropic | harness | | ||
| | provider-openai | harness | | ||
| | harness | — | | ||
|
|
||
| --- | ||
|
|
||
| ## 5. Supervisor design | ||
|
|
||
| Per worker we hold: | ||
|
|
||
| ```rust | ||
| struct Worker { | ||
| name: String, | ||
| dir: PathBuf, // <repo>/<name> | ||
| deps: Vec<String>, // managed-only | ||
| child: Option<Child>, // tokio process handle | ||
| proc_state: ProcState, // Stopped|Building|Running|Crashed | ||
| started_at: Option<Instant>, | ||
| restarts: u32, | ||
| logs: RingBuffer<Line>, // last 500 lines, color-tagged stdout/stderr | ||
| } | ||
| ``` | ||
|
|
||
| - **spawn(name)**: `cargo run --manifest-path <dir>/Cargo.toml -- --url ws://127.0.0.1:49134`, | ||
| `Stdio::piped()` on stdout+stderr. Set `proc_state = Building`; flip to | ||
| `Running` on the first captured line (or after the SDK "registered" log line — | ||
| we can pattern-match `register_worker` output). Two tokio tasks per child pump | ||
| stdout/stderr → `logs` ring buffer + the `.logs/<name>.log` file. | ||
| - **exit watching**: `child.wait()` in a task; on exit, set `Crashed` (nonzero) | ||
| or `Stopped` (we asked it to stop) and surface the exit code in the dashboard. | ||
| - **kill(name)**: send SIGTERM to the child, escalate to SIGKILL after a grace | ||
| period. (Important: `cargo run` spawns the worker as a *grandchild*. Spawn each | ||
| child in its **own process group** and signal the group, or the worker binary | ||
| outlives cargo. On Unix: `Command::process_group(0)` then `kill(-pgid)`.) | ||
|
|
||
| --- | ||
|
|
||
| ## 6. Engine status polling | ||
|
|
||
| - One background task every ~1s: run `iii trigger engine::workers::list --json '{}'`, | ||
| parse `{ workers: [...] }`, build `name -> status` map, write into shared state. | ||
| - Match engine entries to managed workers by `name`. Unmatched managed worker → | ||
| `disconnected`. Engine unreachable (trigger errors/timeouts) → all `unknown` | ||
| and a banner "engine down — run `iii --use-default-config`". | ||
| - Optional `--manage-engine` flag: harness-dev also spawns/supervises the engine | ||
| itself as worker tier "-1". Default off (assume engine already running). | ||
|
|
||
| --- | ||
|
|
||
| ## 7. Log handling | ||
|
|
||
| - In-memory **ring buffer** (e.g. 500 lines) per worker, each line tagged | ||
| `stdout`/`stderr` + timestamp; dashboard shows the **last 5**. | ||
| - Simultaneously **tee to `harness-dev/.logs/<name>.log`** (truncated on each | ||
| fresh `up`, appended across restarts within a session). | ||
| - `harness-dev logs <name> [-f]` reads from the ring buffer / file; `-f` follows. | ||
|
|
||
| --- | ||
|
|
||
| ## 8. CLI surface | ||
|
|
||
| ``` | ||
| harness-dev # launch the TUI dashboard (default) | ||
| harness-dev up # start all 7 in dependency order, then dashboard | ||
| harness-dev up <name> # start one (+ its deps first if not running) | ||
| harness-dev down # stop all (reverse dependency order) | ||
| harness-dev restart <name> # rebuild+restart <name>, then its dependents (§4) | ||
| harness-dev restart --all # rolling restart, dependency order | ||
| harness-dev logs <name> [-f] # tail a worker's logs (follow with -f) | ||
| harness-dev status # one-shot table (proc + engine state), no TUI | ||
| ``` | ||
|
|
||
| `restart <name>` algorithm: | ||
| 1. compute `[name] + dependents(name)` ordered topologically, | ||
| 2. for each: kill → `cargo run` (rebuild) → wait until engine reports | ||
| `connected` (or timeout), | ||
| 3. report per-step result. | ||
|
|
||
| --- | ||
|
|
||
| ## 9. TUI dashboard layout | ||
|
|
||
| Default view — a grid/list of all workers, each cell showing status badges + | ||
| last 5 log lines: | ||
|
|
||
| ``` | ||
| ┌ harness-dev ─────────────────────────────── engine: ● connected (:49134) ┐ | ||
| │ │ | ||
| │ ┌ session-manager ───────── ● running ◆ connected pid 8121 ↻0 ──────┐ │ | ||
| │ │ 12:03:01 INFO registered worker session-manager v1.2.0 │ │ | ||
| │ │ 12:03:01 INFO 14 functions, 6 trigger types registered │ │ | ||
| │ │ 12:03:02 INFO listening for session::* invocations │ │ | ||
| │ │ 12:03:09 INFO session::append ok (turn 3) │ │ | ||
| │ │ 12:03:10 INFO snapshot flushed │ │ | ||
| │ └───────────────────────────────────────────────────────────────────────┘ │ | ||
| │ ┌ llm-router ────────────── ● running ◆ connected pid 8122 ↻1 ──────┐ │ | ||
| │ │ 12:03:01 INFO router up; 0 providers bound │ │ | ||
| │ │ … │ │ | ||
| │ └───────────────────────────────────────────────────────────────────────┘ │ | ||
| │ ┌ provider-anthropic ────── ● building ◇ disconnected …rebuilding… ↻2 ─┐ │ | ||
| │ │ 12:04:55 Compiling provider-anthropic v0.4.1 │ │ | ||
| │ └───────────────────────────────────────────────────────────────────────┘ │ | ||
| │ ┌ harness ───────────────── ◼ crashed ◇ disconnected exit=101 ↻0 ─────┐ │ | ||
| │ │ 12:03:40 ERROR llm-router unreachable: connection refused │ │ | ||
| │ └───────────────────────────────────────────────────────────────────────┘ │ | ||
| │ (… context-manager, approval-gate, provider-openai …) │ | ||
| │ │ | ||
| ├────────────────────────────────────────────────────────────────────────────┤ | ||
| │ ↑/↓ select r restart(+deps) s stop u start l logs q quit │ | ||
| └────────────────────────────────────────────────────────────────────────────┘ | ||
| ``` | ||
|
|
||
| Badges: | ||
| - proc: `● running` `◼ crashed` `○ stopped` `● building` | ||
| - engine: `◆ connected` `◈ available` `◇ disconnected` `? unknown` | ||
| - `↻N` = restart count this session; `pid`, `exit=` shown when relevant. | ||
|
|
||
| Keys: `↑/↓` select a worker, `r` restart selected (+dependents), `s` stop, | ||
| `u` start, `l` open full-screen log pager for the selected worker (the | ||
| "console scroll" pager — scrollable, `/` search, `f` follow), `q` quit | ||
| (prompts to stop children or leave them running). | ||
|
|
||
| --- | ||
|
|
||
| ## 10. Build phases | ||
|
|
||
| 1. **Skeleton + registry**: crate, clap, read the 7 yaml files, build + print | ||
| the dep graph and topo order (`harness-dev status` stub). Validates §4. | ||
| 2. **Supervisor**: spawn/kill one worker via `cargo run`, process-group signal | ||
| handling, exit watching. `up <name>` / `down`. | ||
| 3. **Log capture**: ring buffer + file tee + `logs <name> [-f]`. | ||
| 4. **Engine polling**: `EngineClient` + `status` table merging proc+engine state. | ||
| 5. **TUI dashboard**: render the grid, last-5 logs, badges, live refresh. | ||
| 6. **Restart-with-dependents** + the log pager + key bindings. | ||
| 7. Polish: `--manage-engine`, readiness-wait on restart, config overrides. | ||
|
|
||
| --- | ||
|
|
||
| ## 11. Open decisions (defaults chosen, easy to flip) | ||
|
|
||
| - **debug vs release builds**: default `cargo run` (debug, fast rebuilds); | ||
| `--release` flag to opt in. | ||
| - **engine lifecycle**: default assume it's already up; `--manage-engine` to let | ||
| harness-dev own it. | ||
| - **registry source**: derive from `iii.worker.yaml` (option A); add | ||
| `workers.toml` only when per-worker run overrides are needed. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,107 @@ | ||
| # workers-dev | ||
|
|
||
| Local dev orchestrator for all iii workers in this repo. Starts Rust binary workers via `cargo run`, shows a TUI dashboard with live engine connection status, and supports dependency-aware restarts. | ||
|
|
||
| ## Prerequisites | ||
|
|
||
| 1. **`iii` CLI** on `PATH`. | ||
| 2. **Running iii engine** (this tool does not start the engine): | ||
|
|
||
| ```bash | ||
| iii -c harness/engine.config.yaml | ||
| ``` | ||
|
|
||
| 3. **Provider API keys** for the harness stack: | ||
|
|
||
| ```bash | ||
| export ANTHROPIC_API_KEY=sk-ant-... | ||
| export OPENAI_API_KEY=sk-... | ||
| ``` | ||
|
|
||
| ## Install | ||
|
|
||
| ```bash | ||
| cargo install --path workers-dev | ||
| ``` | ||
|
|
||
| ## Worker groups | ||
|
|
||
| Workers are **discovered automatically** from top-level `*/iii.worker.yaml` in this repo. | ||
|
|
||
| | Group | Workers | Started by | | ||
| |-------|---------|------------| | ||
| | **harness stack** | `session-manager`, `llm-router`, `context-manager`, `provider-anthropic`, `provider-openai`, `approval-gate`, `harness` | `workers-dev up`, `Ctrl+u` in TUI, `workers-dev start` | | ||
| | **other** | All remaining repo workers (e.g. `telegram-bot`, `shell`, `console`, …) | `workers-dev start <name>`, `workers-dev start --all`, `Ctrl+a` in TUI | | ||
|
|
||
| Only **Rust `deploy: binary`** workers can be started with `cargo run`. Node/bundle workers show as `(iii worker add)` — install them via the iii registry instead. | ||
|
|
||
| ## Usage | ||
|
|
||
| ```bash | ||
| workers-dev up # start harness stack + TUI | ||
| workers-dev # TUI only | ||
| workers-dev start # harness stack (CLI, waits for connect) | ||
| workers-dev start --all # every discovered Rust worker | ||
| workers-dev start telegram-bot # one worker (+ deps) | ||
| workers-dev restart llm-router # rebuild + restart dependents | ||
| workers-dev logs harness -f | ||
| workers-dev status | ||
| ``` | ||
|
|
||
| Global flags: `--repo`, `--url`, `--port`, `--release`, `--config workers-dev.yaml`, `--stop-on-exit`, `--color auto|always|never`. | ||
|
|
||
| Environment: `WORKERS_DEV_REPO` overrides repo auto-detection. Set `NO_COLOR` to disable colors (also respected when `--color auto`). | ||
|
|
||
| ## Colors | ||
|
|
||
| Semantic colors are applied at render time (logs stay plain text in the ring buffer): | ||
|
|
||
| - **Worker table**: green/yellow/red/gray for status, process, and engine columns | ||
| - **Log pane / `workers-dev logs -f`**: cargo progress (yellow), build done (cyan), errors (red), tracing levels (INFO/WARN/DEBUG) | ||
|
|
||
| Use `--color never` or `NO_COLOR=1` to force plain output. Default `--color auto` enables colors on a TTY and disables them when stdout is piped. | ||
|
|
||
| ## TUI keys | ||
|
|
||
| | Key | Action | | ||
| |-----|--------| | ||
| | `↑`/`↓` | Select worker (skips group headers) | | ||
| | `s` | Start selected worker | | ||
| | `x` | Stop selected worker | | ||
| | `r` | Restart selected worker + dependents | | ||
| | `l` | Follow logs | | ||
| | `Ctrl+u` | Start harness stack | | ||
| | `Ctrl+a` | Start all managed Rust workers | | ||
| | `q` | Quit | | ||
|
|
||
| The log pane shows the last 20 lines for the **selected worker only** (cargo build output + worker logs). Lines are sanitized (no ANSI, no `\r` overwrite garbage). | ||
|
|
||
| ## Config (`workers-dev.yaml`) | ||
|
|
||
| ```yaml | ||
| repo: /path/to/workers | ||
| engine_url: ws://127.0.0.1:49134 | ||
| release: false | ||
| workers: # optional override; default = all discovered | ||
| - session-manager | ||
| - harness | ||
| harness_stack: # optional override; default = harness stack subset | ||
| - session-manager | ||
| - llm-router | ||
| - harness | ||
| color: auto # auto | always | never (respects NO_COLOR) | ||
| ``` | ||
|
|
||
| ## Troubleshooting | ||
|
|
||
| **Garbled log lines in the dashboard** | ||
|
|
||
| Usually caused by cargo `\r` progress lines or ANSI color codes. Current versions normalize both. Reinstall: `cargo install --path workers-dev`. | ||
|
|
||
| **Engine not reachable** | ||
|
|
||
| Start the engine: `iii -c harness/engine.config.yaml` | ||
|
|
||
| **Non-Rust worker won't start** | ||
|
|
||
| Expected — use `iii worker add <name>` for JavaScript/bundle workers. |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.