Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 64 additions & 16 deletions docs/demo.md
Original file line number Diff line number Diff line change
Expand Up @@ -198,28 +198,51 @@ 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
./bin/marvel events --kind agent.turn.completed # tokens in/out per turn
./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,
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions examples/claude.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
64 changes: 45 additions & 19 deletions examples/mixed-adapters.toml
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -40,7 +55,7 @@ name = "mixed"
name = "matrix"

[[team.role]]
name = "analyst"
name = "analyst-p"
replicas = 1
permissions = "plan"

Expand All @@ -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]
Expand All @@ -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]
Expand All @@ -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"
59 changes: 29 additions & 30 deletions examples/mixed-adapters.yaml
Original file line number Diff line number Diff line change
@@ -1,45 +1,35 @@
# 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

teams:
- name: matrix
roles:
- name: analyst
- name: analyst-p
replicas: 1
permissions: plan
runtime:
Expand All @@ -48,19 +38,28 @@ teams:
mode: headless
prompt: "Summarize what a marvel adapter does in one sentence."

- name: builder
- name: builder-p
replicas: 1
runtime:
image: codex
command: codex
mode: headless
prompt: "Reply with the single word: ok"

- name: scout
- name: scout-p
replicas: 1
runtime:
image: opencode
command: opencode
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
13 changes: 8 additions & 5 deletions justfile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down