BDD Session Lifecycle cluster: implementation plan

Companion to 2026-05-13-walks-to-bdd-port.html · unblocks the deferred “Session lifecycle” row · drafted 2026-05-13

At a glance

3
TUI verbs already shipped
2
new CLI verbs (start, stop)
3
new ledger-FS verifications
1
new web verification (sessions page)
6 → 5
scenarios after consolidation
small
smallest-first PR scope
Headline finding. The deferral in the walks plan was framed as “TUI driver maturity” — the implicit claim being that we don't yet have a way to drive a real Claude Code TUI. That framing is wrong. The tui/claude-code/{start, prompt, stop} verbs already exist, are validated in production for agent-priming.feature, and were carefully engineered around agent-tui's quirks (apiKeyHelper auth, bdd-user privilege drop, daemon bootstrap, screen-stability polling). The blocker isn't driver capability; it's missing CLI verbs for ox agent session start/stop and missing filesystem verifications that read the on-disk ledger that ox already maintains.
Consequence. The session-lifecycle cluster is much smaller than the walks plan implied. Roughly: 2 CLI verbs + 3 verifications + 1 web verb + a BA refactor. No new TUI driver work required for the start/stop/verify journey. The TUI verbs we already have get reused; the new verbs are all CLI subprocesses and filesystem reads.

Scenarios this unblocks (and which to implement alongside the framework PR)

Implementing this plan means flipping these specific scenarios from @pending to validated. The list below is the full session-lifecycle cluster from the walks plan; the phasing in §Phasing says which PR each one lands in.

Feature fileExact scenario nameLands in
context-injection/session-recording.featureMarcus records a session started via ox session start CLI (Outline row 1 of consolidated lifecycle)PR 1 (primary outcome) + PR 2 (Outline expansion)
context-injection/session-recording.featureMarcus records a session started via /ox-session-start slash command (Outline row 2)PR 2
context-injection/session-recording.featureSessions from multiple repositories appear in one list (kept-separate, composition-as-Given)PR 4
context-injection/session-recording.featureInterrupted session is recovered by the daemon (kept-separate, failure-recovery property)PR 4
context-injection/session-recording.featureTeam member previews the public view of sessions (kept-separate, depends on #27 multi-persona for the "anonymous viewer" assertion)PR 3 (web verification) + #27 dependency
context-injection/session-recording.featureSession artifacts reflect the interaction content (kept-separate, transcript content assertion)PR 1 (FS verification path)

Count: 6 scenarios flip from @pending to validated. PR 1 (~1–1.5 days) flips the primary outcome scenario alone — that's the critical-path unblock. PR 2 adds the slash-command mechanism row. PR 3 needs #27 multi-persona to land first for the “public view of sessions” scenario's second-persona assertion. PR 4 fans out the remaining kept-separate scenarios.

What this list does not include: any scenarios outside session-recording.feature. The session-lifecycle cluster is bounded to the recording journey; agent-priming, team-context, and the rest of context-injection/ stay where they are.

Reframing the deferral

The walks plan deferred Walk 07 (“agent session recording”) and the entire session-recording.feature consolidation behind “TUI driver maturity.” The user has corrected this: the driver exists and works. The two real questions are:

  1. What needs to be added to the verb catalog (CLI · filesystem · web) so the existing TUI verbs can be composed into a full session-lifecycle scenario?
  2. Which assertions in the journey are best expressed as filesystem reads or ox CLI calls rather than re-entering the TUI for an LLM round-trip? The user's hint — “you need to consider using the file system and ox cli for verifying that sessions are recorded and landed” — is the load-bearing one. The ledger is a git repo on disk under ~/.cache/sageox/context/, and ox session list already merges local + ledger sessions into JSON. Most Then assertions can read that surface directly.

This plan is structured around those two questions.

Existing TUI driver inventory

Read in full: tests/acceptance/drivers/tui/claude-code/{start,stop,prompt}.ts. Summary below; the actual files are heavily commented and worth re-reading before authoring an adjacent verb.

VerbSignature (zod-derived)What it doesAlready-handled subtleties
tui/claude-code/start/start start(workspaceDir, navigateOnboarding?) → { sessionId, durationMs, navTrace? } Validates the workspace path (must live under /tmp/bdd-ws-*, lstat-rejects symlinks); writes an apiKeyHelper shell stub into .claude/settings.json; chowns the workspace to bdd; bootstraps the agent-tui daemon (idempotent, multi-session safe, with a sticky bootstrapped-latch); spawns Claude Code via tui-drive --json start claude-code --accept-trust as user bdd; sends the 5-keypress onboarding sequence (theme → security → trust → bypass-permissions → confirm) with 3500 ms inter-keypress settle; captures a navTrace screenshot for each step. OAuth-vs-API-key dialog routing, “Detected a custom API key” dialog suppression via apiKeyHelper, Shuru secret placeholder substitution, --dangerously-skip-permissions requiring non-root user, AGENT_TUI_API_TOKEN=none for WS auth, daemon HTTP health probe with bounded JSON reader, mutex-serialised first-time bootstrap.
tui/claude-code/prompt/prompt prompt(sessionId, prompt, timeoutMs?) → { responseScreen, durationMs } Sends a prompt via tui-drive prompt with a defensive trailing Enter resubmit (works around a ~30% lost-Enter bug); polls agent-tui screenshot until the screen is stable for 4 consecutive 500 ms polls AND at least 20 s have elapsed (guards against pre-inference “stable” reads); returns the last 4096 chars of the visible terminal. Cold-inference variance, marketplace-sync backpressure, status-bar glyph false-positive avoidance, --wait-idle unreliability worked around with screen-stable polling.
tui/claude-code/stop/stop stop(sessionId) → { stopped: true, alreadyStopped: boolean } Calls tui-drive stop <sid>; idempotent — exit 3 (session not found) returns alreadyStopped: true; defensive substring match on stderr for “not found / no such session / already stopped” covers future tui-drive exit-code drift; signal-terminated (exit code null) is correctly surfaced as failure rather than silent success. Multi-scenario daemon sharing, signal vs exit-3 disambiguation.

What's NOT in the existing TUI driver:

  • No verb wraps a slash command (/ox-session-start / /ox-session-stop). The slash command is — per extensions/claude/commands/ox-session-start.md — a thin wrapper that types ox agent session start into the TUI input box. So “slash command” is just tui/claude-code/prompt with the slash text. No new verb needed for the slash path.
  • No verb does “execute a CLI command inside the workspace's host context.” The CLI path (ox agent <id> session start) needs a new cli/ox/session/* family.
  • No verb reads the ledger filesystem to confirm artifacts landed.

agent-tui scope (clarification)

agent-tui is an external open-source project by Paulo Proença (github.com/pproenca/agent-tui), Rust-based, Unix-only, providing PTY emulation + keyboard/text input + screen capture + a JSON-RPC WebSocket for live preview. Not a SageOx package — we consume the published binary inside the bdd-tui Shuru checkpoint and talk to it via tui-drive (also external; the adapter that knows the Claude-Code-specific launch flags). Pinned to v1.0.1 in the BDD VM image.

Spec reference: ~/src/github.com/pproenca/agent-tui/README.md. Local IPC over AGENT_TUI_SOCKET, HTTP listener pinned to 127.0.0.1 inside the microVM. Session storage at ~/.agent-tui/* on the bdd user.

What this means for the plan: we don't own agent-tui's capability surface. New TUI verbs are limited to combinations of start / send-keys / prompt / screenshot / stop that tui-drive already exposes. The session-lifecycle journey doesn't need anything outside that surface.

The session recording journey (recap of walks-plan §Session lifecycle consolidation)

The journey: start recording → drive Claude interactions → stop → artifacts committed to the ledger → visible in N places. The walks plan collapses the current 6 split scenarios into:

## AFTER — one outcome scenario, mechanism as a column row
  Rule: Recording a session produces a committed, team-visible artifact
    Scenario Outline: Marcus records a session started via <mechanism>
      Given Claude has been primed in Marcus's repository
      When Marcus starts session recording via <mechanism>
      And he interacts with Claude
      And he stops the session recording
      Then the session should be committed to the repository's ledger with a summary and transcript
      And the session should appear on the "Acme Engineering" team's sessions page
      And the session should appear in the recent sessions on the repository detail page

      Examples:
        | mechanism                       |
        | /ox-session-start slash command |
        | ox session start CLI            |

Kept separately (legitimate distinct concerns):

  1. Sessions from multiple repositories appear in one list composition-as-Given
  2. Interrupted session is recovered by the daemon failure-recovery property
  3. Team member previews the public view of sessions visibility toggle
  4. Session artifacts reflect the interaction content content fidelity

Then-assertion machinery needed (where each clause lands):

Then assertionSurface to readNew?
“the session should be committed to the repository's ledger”Filesystem: ~/.cache/sageox/context/sessions/<sid>/ contains raw.jsonl + summary.md + session.md. Or: ox session list --json includes the sid with uploaded: true.new (CLI/FS)
“… with a summary and transcript”Filesystem: summary.md is non-empty + raw.jsonl has ≥1 line. Or ox session show <sid> --json exposes both.new (FS)
“appear on the team's sessions page”Web: navigate to /teams/<team>/sessions, assert the session card is present.new (web)
“appear in recent sessions on the repository detail page”Web: navigate to /teams/<team>/repos/<repo>, assert the session row is present.new (web)

System interactions needed (new verbs)

Mapped to the journey's three phases. Existing verbs in green; new in amber.

PhaseStepVerb (existing or proposed)Notes
SetupWorkspace + ox init + Claude TUI upcli/workspace/create, cli/ox/init, tui/claude-code/startAll existing. Drives the “Given Claude has been primed in Marcus's repository” step.
Connect repo to a teamcli/ox/team/create, cli/ox/repo/connectExisting. Listed in walks plan as part of repo-connection cluster.
Start recording (mechanism A: slash)Type /ox-session-start into the running Claude TUItui/claude-code/promptThe slash command's body in .claude/commands/ox-session-start.md is just $ox agent session start — Claude shells out. Existing prompt verb suffices, just with a different input string.
Verify a recording is activecli/ox/session/status smallNew: shell out ox session list --status=active --json against the workspace, assert one active row.
Start recording (mechanism B: CLI)Run ox agent <id> session start in a sibling subprocess against the workspacecli/ox/session/start smallNew: thin shell-out. Note that the canonical command is ox agent <id> session start (the agent_id comes from the priming step). The user-facing alias ox session start does NOT exist as a Cobra root in the current ox CLI — it's ox agent <id> session.
InteractSend Marcus's prompt(s) to Claude, capture replies into the recorded sessiontui/claude-code/promptExisting. Repeat as needed. Use a low-token canned prompt to keep cost down (see §Resources).
Stop recordingSlash: type /ox-session-stoptui/claude-code/promptSame as slash-start. The slash body is $ox agent session stop.
CLI: run ox agent <id> session stopcli/ox/session/stop smallNew: thin shell-out. Idempotent envelope (exit code 0 if no active session, surface via JSON).
VerifySession committed to ledger w/ summary + transcriptverifications/session-be-committed-to-ledger smallNew. Reads filesystem OR runs ox session list/show --json. See §Filesystem verifications.
Session appears on team sessions pageverifications/session-appears-on-team-sessions-page mediumNew web verb. Uses the existing Playwright driver pattern under tests/acceptance/drivers/web/. Add a small web/sessions/list.ts driver.
Session appears on repo detail pageverifications/session-appears-on-repo-detail-page mediumNew web verb. Add a small web/sessions/repo-detail.ts driver.
Session artifacts reflect interaction contentverifications/session-transcript-contains smallNew FS verb. Greps raw.jsonl for an expected token from the canned prompt.

Tally of new work: 4 new CLI/FS verbs (all small — shell-out + JSON parse + assertion), 2 new web verbs (medium — new selectors needed but stock Playwright pattern), 0 new TUI verbs.

Filesystem + ox CLI verifications (the user's hint, made concrete)

The ledger is a git repo on disk. ox already maintains it. The cheapest, most deterministic verification surface is therefore the on-disk artifacts and ox's own read commands — not a re-entry into the TUI.

AssertionCheapest readWhy over alternatives
session-be-committed-to-ledger Primary: ox session list --json, parse JSON, find the row with the expected session_id AND uploaded == true.
Fallback: directly stat ~/.cache/sageox/context/sessions/<sid>/raw.jsonl.
ox session list is already the agent-facing UX (default JSON; --text is opt-in) and exposes a ledger_available + uploaded bool per row. It also handles the cache-vs-uploaded transition window. git -C <ledger> log --grep <sid> is heavier (forks git, requires the ledger to be checked out at the right ref) and exposes us to commit-message format drift.
summary-and-transcript-present FS: raw.jsonl non-empty (at least one JSONL line) and summary.md non-empty under ~/.cache/sageox/context/sessions/<sid>/. The artifact filenames are stable Go constants in internal/session/pipeline/types.go (LedgerFileRaw, LedgerFileSummaryMD). Reading them directly is one syscall per file; the alternative is parsing ox session show --json which adds a subprocess hop without adding signal.
active recording exists (start-side gate) ox session list --status=active --json — expect exactly one row matching the workspace's project root. An alternative would be to grep the TUI screen for “recording started” status, but that's text-mode-dependent and brittle. CLI JSON is the contract.
transcript content fidelity FS: grep raw.jsonl for a token we know the canned prompt contained (e.g. the BDD scenario seeded the prompt with a unique nonce; we grep for that nonce). Token nonce + grep is content-stable regardless of how Claude phrased its reply. Reading summary.md for the same nonce is unreliable — summarization may abstract it away.
multi-repo sessions list Web only (this assertion is specifically about the team-level list view; the FS doesn't render the merged surface). Filesystem + CLI can't validate the team page; that's the point of the kept-separate concern. The two CLI-level scenarios are already covered by session-be-committed-to-ledger, so we don't double up.
L3 observation. Once you have ox session list --json as a verification surface, the four sub-assertions of “session lands in the ledger” (committed, has summary, has transcript, has content) all collapse to one subprocess call + four field reads. Compared to the original walks-plan instinct of “drive the web app to assert the session appears on three pages,” the CLI-first approach is ~10× cheaper per scenario (no headless browser warm-up, no waiting for upload→push→pull→render). Keep the web assertions for the things only the web surfaces (list rendering, repo-detail integration, public-view toggle) — don't use them as the primary “did it commit” gate.

Driver implementation gaps

For each verb the cluster needs, here's whether it exists and what the work looks like.

VerbStatusEffortImplementation outline
tui/claude-code/starthaveReused as-is. Same setup that agent-priming.feature already validates.
tui/claude-code/prompthaveReused for slash-command typing AND for the “he interacts with Claude” step. The slash-command body resolves to the same ox agent session start subprocess that the CLI mechanism row drives directly, so the two Outline rows converge on identical Then-state.
tui/claude-code/stophaveReused as-is (kills the Claude PTY after the recording-stop has flushed artifacts; idempotent).
cli/ox/session/startmissingsmallShell out ox agent <id> session start --json in the workspace dir. Parse JSON. Surface session_id, notice, guidance. Wire $AGENT_ID from a Given that primed Claude (or from ox agent prime --json if running CLI-only without Claude). Pattern: copy cli/ox/init.
cli/ox/session/stopmissingsmallShell out ox agent <id> session stop --json. Parse JSON. Surface session_id, summary_prompt (sync mode), or success in async mode. Idempotent: if no active session, surface that cleanly rather than throwing.
cli/ox/session/listmissingsmallShell out ox session list --json. Parse to a typed object. Returns { sessions: [], ledger_available: bool }. Used by the “committed to ledger” verification.
cli/ox/session/showmissingsmallShell out ox session show <sid> --json. Used as a fallback if list-based assertions need richer fields (e.g. summary length, plan path).
fs/ledger/stat-sessionmissingsmallRead ~/.cache/sageox/context/sessions/<sid>/{raw.jsonl,summary.md,session.md,plan.md?}; return { exists, sizeBytes, lineCount } per file. Used as cheap alternative to spawning ox. Path constants live in internal/session/pipeline/types.go; mirror them in a TS constants module rather than re-deriving.
fs/ledger/grep-transcriptmissingsmallReads raw.jsonl line by line, returns whether a given regex matches any line's content field. Used for content-fidelity assertion.
web/sessions/listmissingmediumPlaywright: navigate to /teams/<team>/sessions, wait for the sessions grid, return the list of session-card data attributes. Pattern matches web/ledger/view closely.
web/sessions/repo-detailmissingmediumPlaywright: navigate to /teams/<team>/repos/<repo>, scroll to the recent-sessions section, return its session list. Same pattern.

Total effort: 6 small (each ~1 hour incl. tests) + 2 medium (each ~half-day, mostly figuring out the right Playwright selectors against the actual web app). Two days of focused work end-to-end. Compare to the walks-plan deferral framing which implied weeks of TUI driver work.

ox CLI surface pointers (for the BA author)

Findings from reading ~/src/github.com/sageox/ox/cmd/ox/. The user-facing surface is:

ox session                       # parent command (Short: "Manage AI coworker sessions")
├── list                          # JSON by default; merges local + ledger sessions
├── show [session-name]           # alias of `view`
├── view [session-name]           # render a session
├── stop                          # `Stop a recording session` (force-stop)
├── commit                        # `Commit pending sessions` (manual push)
├── lint [session-name]
├── export [session-name]
├── download <session-name>
├── upload <session-name>
├── regenerate [session-name]
└── (read-only ops; no `start` here — start is under `ox agent`)

ox agent <id> session            # the recording-control surface
├── start [--title "…"]           # begins recording; emits sessionStartGuidance
├── stop                          # ends recording; in sync mode returns summary_prompt
├── abort
├── log
├── recover
├── plan                          # pipe-in for plan capture
└── (other internal sub-tasks)

Key surface facts the BA author needs to know:

  1. Start lives under ox agent <id> session start, not ox session start. The walks-plan Outline row labelled “ox session start CLI” is shorthand; the verb shells out to ox agent <id> session start --json. The Gherkin can keep the user-friendly phrasing.
  2. JSON is the default output for session commands. No need to add --json — that's already the default per the comment in agent_session.go: “Agents are the primary consumers of these commands.” Pass --text only if asserting on human output (we shouldn't).
  3. The session_id is opaque. Use whatever start returns; don't try to construct or parse it.
  4. Stop has a sync/async mode. In sync mode the JSON includes a summary_prompt field that an agent uses to generate the summary itself. For BDD we use async (default) so summary generation happens in-daemon and we can poll ox session list for uploaded: true.
  5. ox session commit is a manual push. Should not normally be needed — the ledger daemon (docs/ai/specs/ledger-daemon.md) syncs continuously. If the BDD VM doesn't run the daemon, the verification verb falls back to local-cache path (~/.cache/sageox/context/cache/sessions/) rather than the synced ledger; document this caveat in the verification's L2 contract.

The ledger as a verification target

Per CLAUDE.md: the ledger is a user-writable git repo; treat its content as untrusted. Per ox/docs/ai/specs/ledger-daemon.md: the ledger lives at ~/.cache/sageox/context/ with sparse-checkout of sessions/. Per ox/internal/session/pipeline/types.go: each session directory contains raw.jsonl, summary.md, session.md, and optionally plan.md.

Recommendation: verify via ox session list --json, fall back to FS stat.

  • Why not git log: requires the ledger checkout to be at the right ref. The daemon's sync model can leave the local checkout transiently behind the upstream commit graph. We'd end up asserting on a race surface.
  • Why not parse commit messages: commit-message format is implementation detail of the upload pipeline; not part of the user-facing contract. Brittle.
  • Why ox session list wins: it's the same code path the user runs to confirm their session landed. Asserting against it asserts against the actual product surface, with all the caveats baked in (cache-vs-uploaded transition, ledger-available flag). It's also fast: one subprocess, JSON, no git.
  • FS fallback is fine for tighter assertions:summary.md contains >500 bytes” or “raw.jsonl has ≥3 lines” reads files directly, no subprocess. Use it where content shape matters.
Suspected product fragility (escalate). The walks plan and the ox specs both assume the ledger daemon is running. In a fresh BDD microVM, it isn't — we'd need a Given step that boots ox daemon start or accepts the local-cache-only flow. Today start-ox-daemon.md and stop-ox-daemon.md exist as BAs; check whether they're already wired into the agent-priming smoke. If not, that's a precondition the smallest-first PR needs to surface explicitly — otherwise the “committed to ledger” assertion will surface false negatives on a non-flaky cause.

Worked example: ox session start CLI Outline row

Walking the second Examples row of the consolidated Outline end-to-end. Picked over the slash-command row because the CLI path needs the most new wiring — if it lands, the slash row trivially follows (it's tui/claude-code/prompt with the slash text and existing TUI capture).

Gherkin (target)

  Scenario Outline: Marcus records a session started via <mechanism>
    Given Claude has been primed in Marcus's repository
    When Marcus starts session recording via <mechanism>
    And he asks Claude about widget pricing                      ## canned, nonce-seeded prompt
    And he stops the session recording
    Then the session should be committed to the repository's ledger with a summary and transcript
    And the session transcript should mention the widget pricing question

    Examples:
      | mechanism                       |
      | /ox-session-start slash command |
      | ox session start CLI            |

Step-by-step verb plan for the “CLI” Examples row

Gherkin stepBusiness action / verificationVerbs (existing or new)
Given Claude has been primed in Marcus's repository BA: start-coding-session have Existing chain: cli/workspace/createcli/ox/inittui/claude-code/start. Exports $WORKSPACE_DIR, $AGENT_ID, $TUI_SID.
When Marcus starts session recording via ox session start CLI BA: start-session-recording new (variant cli) New verb cli/ox/session/start/start invokes ox agent $AGENT_ID session start --json in $WORKSPACE_DIR. Exports $RECORDING_SID from the JSON's session_id field.
And he asks Claude about widget pricing BA: ask-claude-canned-question new small (wraps existing prompt verb with a nonce-stable prompt) Existing tui/claude-code/prompt/prompt with body "What is the price of widget <nonce>? Reply with a one-sentence answer." The nonce comes from the scenario's data table or a derived UUID, exported as $WIDGET_NONCE.
And he stops the session recording BA: stop-session-recording new (variant cli) New verb cli/ox/session/stop/stop invokes ox agent $AGENT_ID session stop --json. No exports needed; the session id is already pinned in $RECORDING_SID.
Then the session should be committed … with a summary and transcript Verification: session-be-committed-to-ledger new Two-op verb: (1) shell-out ox session list --json, find $RECORDING_SID, assert uploaded == true; (2) fs/ledger/stat-session with the sid, assert raw.jsonl has ≥2 lines AND summary.md non-empty. Poll-with-backoff up to ~15 s for the async upload to land. Failure tail returns the parsed JSON and the file stats.
And the session transcript should mention the widget pricing question Verification: session-transcript-contains new fs/ledger/grep-transcript reads raw.jsonl for $WIDGET_NONCE. Pass if any line matches; fail with the file's first 4 KB if not.

What each Then reads (concretely)

# session-be-committed-to-ledger, operation 1
ox session list --json
# expected JSON shape (per ox cmd/ox/session_list.go):
# {
#   "ledger_available": true,
#   "sessions": [
#     { "session_id": "<RECORDING_SID>", "uploaded": true, "title": "...", ... }
#   ]
# }
# Assertion: find a session row whose session_id matches $RECORDING_SID
# AND whose `uploaded` field is true. If `ledger_available` is false,
# surface that explicitly — it almost always means the ox daemon
# isn't running, which is a real BDD precondition gap not a product bug.

# session-be-committed-to-ledger, operation 2
stat ~/.cache/sageox/context/sessions/<RECORDING_SID>/raw.jsonl
stat ~/.cache/sageox/context/sessions/<RECORDING_SID>/summary.md
# both must exist and be non-empty.

# session-transcript-contains
grep -c "<WIDGET_NONCE>" ~/.cache/sageox/context/sessions/<RECORDING_SID>/raw.jsonl
# expected: >= 1.

What's NOT in this row's verification chain: the team-sessions-page and repo-detail-page Then assertions. Those go on a follow-up PR (see Phasing). The smallest-first PR proves the start→interact→stop→committed loop end-to-end without bringing in the web driver.

Dependencies on other plans (#26 Outline substitution, #27 multi-persona)

NeedSource planStatus
Examples-table column substitution in the mechanism Outline (<mechanism> resolves to a BA variant rather than a string match) #26 Outline substitution Soft dep. We can land the cluster with two separate scenarios (one per mechanism) instead of an Outline, defer the Outline collapse to when #26 ships. The walks-plan already lists this as an open judgment call. Suggest: ship as two scenarios first, refactor to Outline once #26 lands.
Multi-persona orchestration (Marcus's session on workspace A while Tina views the team page on workspace B) #27 multi-persona Hard dep, but only for the “sessions appear on the team's sessions page” clause — Marcus's CLI workspace and Tina's browser session are different personas. The first PR avoids this by deferring the web assertions; the second PR depends on #27.
Ledger daemon running in the BDD VM (none yet) Open question. The start-ox-daemon BA exists but may not be wired into the agent-priming smoke. Audit before the first session-lifecycle PR. If the daemon isn't running, ox session list falls back to the local cache and uploaded never flips true. The smallest PR may need to add a Background step that starts the daemon.
Recording-journey cluster (for “multi-repo sessions list” kept-separate concern) walks plan §Recording journey Soft dep. The multi-repo concern needs at least one other repo with sessions for the team. The recording-journey cluster's connect-a-repository verbs can be composed in. Defer this kept-separate scenario to the second PR.

Resource considerations (LLM token cost)

The session-lifecycle journey unavoidably touches a real Claude Code TUI — that's the whole point. The dimensions of cost:

Source of tokensEstimated cost per scenario runMitigation
Claude's first response to the priming envelope (already paid by tui/claude-code/start) ~3-5k tokens (envelope is ~2-3k, response is <500 toks per prompt verb's MIN_RENDER_WAIT pattern) Already absorbed by the agent-priming smoke. No incremental cost here.
The interaction prompt (“widget pricing”) ~200-500 tokens prompt + ~100-300 tokens reply Keep prompts deliberately short and constrained. Pattern from prompt.ts caller contract: “Reply with EXACTLY one sentence: …”. The nonce-grep assertion doesn't need a long reply, just any reply that echoes the nonce.
Async session summary (the Anthropic call ox makes after stop) ~1-3k tokens depending on transcript length This one we can't avoid — it's the product behaviour under test. But: with a single short interaction, the transcript is small, so the summary call is cheap.

Per-scenario cost estimate: ~5-10k tokens of Sonnet, <$0.05 at current pricing. Two scenarios (slash + CLI mechanisms) = ~$0.10 per BDD run. Acceptable; comparable to the existing agent-priming scenario which is already in CI.

Should we mock the LLM? No. The whole journey is “real session got recorded & landed”; replacing Claude with a stub would gut the integration value. The cost is the price of admission for this cluster. If cost balloons later, the mitigation is to run the cluster nightly rather than per-PR, not to mock the model.

L1 verification: our existing TUI verbs already pay this cost in agent-priming's validated scenario. We're piggybacking on a budget the team has already accepted.

Phasing & smallest-first PR

PR 1 — ledger-FS verification proves the loop smallest first

Goal: one CLI-mechanism scenario, validated, end-to-end, with FS+CLI verifications only. No web. No Outline. No multi-persona.

Feature: AI Coworker Session Recording

  Rule: A recorded session is committed to the repository's ledger
    Scenario: Marcus records a session via the CLI and it lands in the ledger
      Given Claude has been primed in Marcus's repository
      When Marcus starts session recording via the ox CLI
      And he asks Claude a question seeded with a unique token
      And he stops the session recording via the ox CLI
      Then the session should be committed to the ledger with a summary and transcript
      And the transcript should contain the unique token

Ships in this PR:

  • New BAs: start-session-recording (variant cli), stop-session-recording (variant cli), ask-claude-canned-question
  • New verifications: session-be-committed-to-ledger, session-transcript-contains
  • New driver verbs: cli/ox/session/{start,stop,list}, fs/ledger/{stat-session,grep-transcript}
  • Audit + (if needed) wire the ox-daemon-start step into the Given chain
  • No @pending tag — ships validated, per the tagging convention

That's 2 verifications + 5 driver verbs + 2 BAs in one PR. About 1-1.5 days of work including the daemon-state audit. Reuses 100% of the existing TUI driver.

PR 2 — slash-command mechanism row

Adds the /ox-session-start + /ox-session-stop path. Adds nothing new at the driver level — the slash command is just tui/claude-code/prompt with the slash text. Adds a second scenario (or, if #26 Outline substitution lands first, collapses both into one Outline). Wires the two BAs' slash variants alongside the cli variants that already exist.

PR 3 — team-page & repo-detail web verifications

Depends on #27 multi-persona (Marcus records; Tina views). Adds web/sessions/{list,repo-detail} drivers + matching verifications. Extends the Outline's Then clauses to include the page assertions.

PR 4 — the four kept-separate scenarios

One at a time, each in its own PR:

  1. Multi-repo sessions list (depends on PR 3 + the recording-journey cluster's repo-connection verbs).
  2. Interrupted-session daemon recovery (failure-recovery property; needs a verb to forcibly kill the active recording process and a verification that polls for eventual cleanup).
  3. Public-view toggle on the sessions page (visibility test; reuse the existing public/private team plumbing the team-management cluster establishes).
  4. Session artifacts reflect interaction content (sharpens the vague Then in today's pending scenario; mostly an FS-grep verification, low risk).

Critical-path: PR 1 unblocks the cluster's tag flip on the primary outcome scenario. PR 2 is one-day follow-up. PR 3 waits on #27. PR 4 fans out independently.

What this is NOT

  • Not new TUI driver work. Three robust verbs ship today; they're reused as-is. The deferral framing in the walks plan was wrong; correcting it is half the value of this plan.
  • Not a refactor of agent-priming.feature. That feature stays exactly as it is — the session-lifecycle cluster composes on top of the primed-Claude precondition that priming already establishes.
  • Not multi-agent or multi-model. Only Claude Code is in scope. Other agent adapters (Codex, Aider, Amp, opencode, droid, pi) exist in the ox CLI but aren't part of this cluster.
  • Not testing summary quality. We assert the summary exists and is non-empty; content correctness of Anthropic-generated summaries is a model-eval problem, not a BDD problem.
  • Not testing ledger sync to remote. The push-to-remote path (ledger daemon → GitLab) is the ox CLI's problem and has its own integration tests upstream. BDD verifies up to the local ledger checkout, which is the surface the user sees on the team sessions page.
  • Not testing ox session view --html rendering. That's a separate ox feature with its own unit tests.
  • Not authoring new walks. Walk 07 (the original session-recording walk) was deferred as a walk-level artifact. We're going straight to BDD without re-creating a walk.