diff --git a/docs/demo.md b/docs/demo.md index 14c0dd1..3f3b704 100644 --- a/docs/demo.md +++ b/docs/demo.md @@ -198,21 +198,28 @@ duration; unset leaves the built-in 10-minute default. ## Act 2 — Observe -One team, three different agent harnesses, one event stream. `mixed-adapters` -runs a `claude`, a `codex`, and an `opencode` role, each headless with a small -prompt. Marvel redirects each harness's structured output through its adapter, -normalizes the harness-specific dialect into one event vocabulary, and tags -every event with workspace, team, role, and session. +One team, three different agent harnesses, one event stream — and both CTX% +producers side by side. `mixed-adapters` runs a `claude`, a `codex`, and an +`opencode` role headless with a small prompt, plus a fourth role: the same +claude binary run as an interactive TUI. + +Role names carry the mode so the table reads at a glance: `-p` = +print/headless (`analyst-p`, `builder-p`, `scout-p`), `-t` = TUI +(`analyst-t`). The suffix rides into every session name and event tag. + +Marvel redirects each headless harness's structured output through its +adapter, normalizes the harness-specific dialect into one event vocabulary, +and tags every event with workspace, team, role, and session. ```sh ./bin/marvel work examples/mixed-adapters.toml sleep 4 -./bin/marvel get sessions # CPU% and RSS populated for all three harnesses +./bin/marvel get sessions # CPU% and RSS populated for all four sessions ``` `marvel get sessions` samples the process table, so CPU% and RSS are real for every session regardless of harness, and need no auth. Watch the normalized -agent stream from all three: +agent stream from the three headless roles: ```sh ./bin/marvel events --workspace mixed @@ -220,6 +227,22 @@ agent stream from all three: ./bin/marvel events --kind agent.session.ended # per-session cost, tokens, duration ``` +The `-t` row is the CTX% beat. A TUI emits no parseable stream, so its +context figure arrives through the other producer: `context_feed = +"statusline"` projects statusline hooks that forward the harness's own +measurement through `marvel ctx-forward` to the heartbeat RPC +(finding-011). CTX% is `-` until the session takes a turn, so inject one: + +```sh +./bin/marvel inject mixed/matrix-analyst-t-g1-0 "say only the word ready" -e +sleep 20 +./bin/marvel get sessions # -p rows meter via stream; the -t row via statusline +``` + +The `-t` pane's own bottom line shows the human-facing half of the same +feed ("Haiku 4.5 · CTX 13% · $0.04"); attach to it via the DESK column to +drive it by hand. + A verified run produced, across the three harnesses uniformly: `agent.session.started`, `agent.turn.started`, `agent.turn.completed` (with token counts), `agent.message.completed`, and `agent.session.ended` (with cost, @@ -234,16 +257,18 @@ Deterministic vs auth-dependent: harness to authenticate and take a real model turn. Without auth a harness exits early and you see `session.crashed` instead of an agent stream. -TODO(finding): the CTX% column shows `-` for these harnesses. Context pressure -has exactly one producer in marvel today, the heartbeat RPC, and none of the -claude, codex, or opencode adapters send it. The column exists and is correct -(it renders absence rather than a misleading 0%), but "context" is not an -observable signal for these three harnesses yet. The task brief's Act 2 -"cpu/rss/context" is accurate for cpu and rss; context is not populated. +CTX% producers, current state (supersedes an earlier TODO here that said +the heartbeat RPC was the only producer): there are three. The stream-fed +usage accountant meters headless sessions; the statusline feed +(`context_feed`, finding-011) meters interactive claude via the heartbeat +RPC; and the heartbeat RPC accepts any cooperative reporter (the +simulator). Interactive codex/opencode remain unmetered — no statusline +equivalent has been probed for them yet (aae-orc-7hzb). -Because each role is headless with a one-shot prompt, the harness exits when its -turn completes, and marvel reaps the vacated pane with `session.crashed`. That -is expected for a headless one-shot: the work is done, the process is gone. +Because each `-p` role is headless with a one-shot prompt, the harness exits +when its turn completes, and marvel reaps the vacated pane with +`session.crashed`. That is expected for a headless one-shot: the work is +done, the process is gone. The `-t` role stays running. Minor: the claude adapter logged one `agent.error` "unmapped: assistant thinking block (no v1 event kind)" during the verified run. It is a benign parse gap (a @@ -296,6 +321,29 @@ Roles whose harness has no Claude Code settings surface (codex, opencode, generic) log the policy as advisory and are not projected. Nothing is dropped silently. +### Act 3 extension — shift onto a new metering contract (PLANNED, not written) + +The intended beat: apply a team without `context_feed`, confirm the TUI +session's CTX% is `-`, add `context_feed = "statusline"` to the manifest, +re-apply, then `marvel shift` the team — generation 2 spawns with the feed +and CTX% appears on the fresh sessions. + +Why shift and not just re-apply: a session's runtime is frozen at spawn. +Re-projection reads `context_feed` from the SESSION's runtime copy, so a +manifest change reaches only sessions created after it — the live +re-projection that works for policy content does not retrofit the feed onto +running sessions. Tracked as a known wrinkle (bd: frozen-runtime +re-projection); this beat is not in the runbook until either the wrinkle is +fixed or the shift-based sequence is verified end to end. + +### Act 5 — Meter (PLANNED, not written) + +A dedicated metering act: `context-feed.toml` + injected turns + watching +CTX% climb across the table, tied into budget admission (`max_tokens` +clauses refusing over-budget work). Deferred until the per-subagent context +surface and the OTEL topology decision (aae-orc-mqgf) land, so the act +demonstrates a settled layer rather than a moving one. + --- ## Cleanup diff --git a/examples/claude.toml b/examples/claude.toml index 6e07195..242a644 100644 --- a/examples/claude.toml +++ b/examples/claude.toml @@ -38,9 +38,15 @@ name = "claude-squad" replicas = 2 permissions = "plan" + # context_feed gives these interactive (TUI) coders live CTX% in + # `marvel get sessions`: the projection layer injects statusline + # hooks that forward the harness's own context figure through + # `marvel ctx-forward` to the heartbeat RPC (finding-011). Each + # coder's pane also shows "model · CTX n% · $cost" at the bottom. [team.role.runtime] image = "claude" command = "claude" + context_feed = "statusline" [[team.role]] name = "supervisor" diff --git a/examples/mixed-adapters.toml b/examples/mixed-adapters.toml index 2b8506d..0b8736d 100644 --- a/examples/mixed-adapters.toml +++ b/examples/mixed-adapters.toml @@ -1,34 +1,49 @@ # Marvel example — the harness matrix: three BYOA harnesses, one workspace, -# one normalized event path. +# one normalized event path, and both CTX% producers side by side. # -# Each role runs a different agent harness headlessly. Marvel redirects each +# Role-name convention: `-p` = print/headless (claude -p style, one-shot +# prompt, parseable stream), `-t` = TUI (interactive, long-lived pane). +# The suffix rides into every session name and event tag, so +# `matrix-analyst-p-g1-0` vs `matrix-analyst-t-g1-0` reads at a glance in +# `marvel get sessions` and `marvel events`. +# +# The three -p roles stream through their adapters. Marvel redirects each # harness's structured output into a FIFO, parses the harness-specific # dialect into the shared event vocabulary, and lands every event in the -# daemon ring tagged with workspace/team/role/session. `marvel events` shows -# all three harnesses in one stream. +# daemon ring tagged with workspace/team/role/session. Their CTX% comes +# from the stream-fed usage accountant. # # claude → claude-code stream-json (internal/runtime/claudecode) # codex → codex exec --json (internal/runtime/codex) # opencode → opencode run --format json (internal/runtime/opencode) # +# The -t role is the same claude binary run interactively. A TUI emits no +# parseable stream, so its CTX% comes from the OTHER producer: context_feed +# = "statusline" projects statusLine hooks that forward the harness's own +# context figure through `marvel ctx-forward` to the heartbeat RPC +# (finding-011). It needs one injected prompt before CTX% shows a number. +# # Setup: # cd ~/work/aae-orc/marvel -# ./marvel daemon & -# ./marvel work examples/mixed-adapters.toml +# ./bin/marvel daemon & +# ./bin/marvel work examples/mixed-adapters.toml # -# Verify (daemon log shows three adapters, each streaming its own format): -# session mixed/matrix-analyst-g1-0 streaming claude-code/stream-json -# session mixed/matrix-builder-g1-0 streaming codex/jsonl -# session mixed/matrix-scout-g1-0 streaming opencode/json +# Verify (daemon log shows three streaming adapters; the -t role does not stream): +# session mixed/matrix-analyst-p-g1-0 streaming claude-code/stream-json +# session mixed/matrix-builder-p-g1-0 streaming codex/jsonl +# session mixed/matrix-scout-p-g1-0 streaming opencode/json # -# Watch the normalized events from all three: -# ./marvel events --workspace mixed -# ./marvel get sessions -# ./marvel stop +# Watch the normalized events and both CTX% producers: +# ./bin/marvel events --workspace mixed +# ./bin/marvel inject mixed/matrix-analyst-t-g1-0 "say only the word ready" -e +# ./bin/marvel get sessions # -p rows meter via stream, -t via statusline +# ./bin/marvel stop # # Notes: -# - Each role is headless with a prompt; that is what makes the harness -# produce a parseable one-shot stream. +# - Each -p role is headless with a prompt; that is what makes the harness +# produce a parseable one-shot stream. It exits when the turn completes. +# - The -t role stays alive; attach to its pane (DESK column) to drive it +# by hand. # - codex takes sandbox/approval flags via runtime.args; opencode takes # --model and --auto via runtime.args. Neither is injected by marvel, # so autonomous tool use is an explicit operator choice. @@ -40,7 +55,7 @@ name = "mixed" name = "matrix" [[team.role]] - name = "analyst" + name = "analyst-p" replicas = 1 permissions = "plan" @@ -51,7 +66,7 @@ name = "matrix" prompt = "Summarize what a marvel adapter does in one sentence." [[team.role]] - name = "builder" + name = "builder-p" replicas = 1 [team.role.runtime] @@ -61,7 +76,7 @@ name = "matrix" prompt = "Reply with the single word: ok" [[team.role]] - name = "scout" + name = "scout-p" replicas = 1 [team.role.runtime] @@ -70,3 +85,14 @@ name = "matrix" mode = "headless" args = ["-m", "opencode/deepseek-v4-flash-free"] prompt = "Reply with the single word: ok" + + [[team.role]] + name = "analyst-t" + replicas = 1 + permissions = "plan" + + [team.role.runtime] + image = "claude" + command = "claude" + args = ["--model", "haiku"] + context_feed = "statusline" diff --git a/examples/mixed-adapters.yaml b/examples/mixed-adapters.yaml index 66fa2f6..12b9198 100644 --- a/examples/mixed-adapters.yaml +++ b/examples/mixed-adapters.yaml @@ -1,37 +1,27 @@ # Marvel example — the harness matrix: three BYOA harnesses, one workspace, -# one normalized event path. +# one normalized event path, and both CTX% producers side by side. # -# Each role runs a different agent harness headlessly. Marvel redirects each -# harness's structured output into a FIFO, parses the harness-specific -# dialect into the shared event vocabulary, and lands every event in the -# daemon ring tagged with workspace/team/role/session. `marvel events` shows -# all three harnesses in one stream. -# -# claude → claude-code stream-json (internal/runtime/claudecode) -# codex → codex exec --json (internal/runtime/codex) -# opencode → opencode run --format json (internal/runtime/opencode) +# YAML twin of mixed-adapters.toml — see that file's header for the full +# walkthrough. Role-name convention: `-p` = print/headless (one-shot +# prompt, parseable stream, CTX% via the stream-fed usage accountant), +# `-t` = TUI (interactive, long-lived pane, CTX% via context_feed = +# "statusline" and `marvel ctx-forward`, finding-011). # # Setup: # cd ~/work/aae-orc/marvel -# ./marvel daemon & -# ./marvel work examples/mixed-adapters.yaml -# -# Verify (daemon log shows three adapters, each streaming its own format): -# session mixed/matrix-analyst-g1-0 streaming claude-code/stream-json -# session mixed/matrix-builder-g1-0 streaming codex/jsonl -# session mixed/matrix-scout-g1-0 streaming opencode/json +# ./bin/marvel daemon & +# ./bin/marvel work examples/mixed-adapters.yaml # -# Watch the normalized events from all three: -# ./marvel events --workspace mixed -# ./marvel get sessions -# ./marvel stop +# Verify (daemon log shows three streaming adapters; the -t role does not stream): +# session mixed/matrix-analyst-p-g1-0 streaming claude-code/stream-json +# session mixed/matrix-builder-p-g1-0 streaming codex/jsonl +# session mixed/matrix-scout-p-g1-0 streaming opencode/json # -# Notes: -# - Each role is headless with a prompt; that is what makes the harness -# produce a parseable one-shot stream. -# - codex takes sandbox/approval flags via runtime.args; opencode takes -# --model and --auto via runtime.args. Neither is injected by marvel, -# so autonomous tool use is an explicit operator choice. +# Watch the normalized events and both CTX% producers: +# ./bin/marvel events --workspace mixed +# ./bin/marvel inject mixed/matrix-analyst-t-g1-0 "say only the word ready" -e +# ./bin/marvel get sessions # -p rows meter via stream, -t via statusline +# ./bin/marvel stop workspace: name: mixed @@ -39,7 +29,7 @@ workspace: teams: - name: matrix roles: - - name: analyst + - name: analyst-p replicas: 1 permissions: plan runtime: @@ -48,7 +38,7 @@ teams: mode: headless prompt: "Summarize what a marvel adapter does in one sentence." - - name: builder + - name: builder-p replicas: 1 runtime: image: codex @@ -56,7 +46,7 @@ teams: mode: headless prompt: "Reply with the single word: ok" - - name: scout + - name: scout-p replicas: 1 runtime: image: opencode @@ -64,3 +54,12 @@ teams: mode: headless args: ["-m", "opencode/deepseek-v4-flash-free"] prompt: "Reply with the single word: ok" + + - name: analyst-t + replicas: 1 + permissions: plan + runtime: + image: claude + command: claude + args: ["--model", "haiku"] + context_feed: statusline diff --git a/justfile b/justfile index 3ca41eb..da3ffee 100644 --- a/justfile +++ b/justfile @@ -141,20 +141,23 @@ demo-act1: build @echo " ./bin/marvel work examples/demo-act1-roles-removed.toml" @echo " ./bin/marvel events --kind role.removed" -# Act 2 — Observe: run the three-harness matrix, print the event-watch commands +# Act 2 — Observe: run the harness matrix (-p headless + -t TUI), print the event-watch commands demo-act2: build - @echo "==> Act 2 (Observe). Loading the {claude, codex, opencode} matrix..." - @echo " Needs the three harness binaries and working auth for the agent stream." + @echo "==> Act 2 (Observe). Loading the {claude, codex, opencode} matrix + a TUI claude..." + @echo " Role names carry the mode: -p = print/headless, -t = TUI (interactive)." + @echo " Needs the harness binaries and working auth for the agent stream." ./bin/marvel work examples/mixed-adapters.toml @sleep 4 @echo "" - @echo "==> Sessions (CPU% and RSS populate uniformly; CTX% is '-' for these harnesses):" + @echo "==> Sessions (CPU% and RSS populate uniformly; CTX% arrives per producer):" ./bin/marvel get sessions @echo "" - @echo "Watch the normalized agent stream across all three harnesses:" + @echo "Watch the normalized agent stream and both CTX% producers:" @echo " ./bin/marvel events --workspace mixed" @echo " ./bin/marvel events --kind agent.turn.completed # tokens in/out per turn" @echo " ./bin/marvel events --kind agent.session.ended # per-session cost and duration" + @echo " ./bin/marvel inject mixed/matrix-analyst-t-g1-0 'say only the word ready' -e" + @echo " ./bin/marvel get sessions # -p rows: stream accountant · -t row: statusline feed" # Act 3 — Control plane: project a policy, then re-project live with no restart demo-act3: build