Walks → BDD acceptance tests: porting plan

Source: ~/src/github.com/sageox/factory/walks/walk-{00..15}.ts · Target: sageox-monorepo/tests/acceptance/ · Drafted 2026-05-12

At a glance

17
walks reviewed
158
existing @pending scenarios
~30
existing @pending scenarios that walks unblock directly
2
genuinely new feature files
~22
new driver verbs

The walks form a single chained smoke (each walk depends on outputs of earlier walks). BDD scenarios in this repo are independent — each one arranges its own preconditions via Given steps that resolve to fast-path verbs. The port therefore breaks the chain and regroups behavior by surface, not by execution order.

Corrected framing: the implementation work is to flip existing @pending scenarios to validated by wiring up the verbs they need. We are not authoring duplicate scenarios. For each walks-derived behavior:
  • Most cases: an existing @pending scenario already describes it — implementation = wire verbs, drop the tag.
  • Some cases: an existing @pending scenario almost describes it — implementation = light refactor (extend an Outline, add an extra Then, split a rule into public/private halves) THEN wire verbs.
  • A few cases: no existing scenario — author new, in either an existing feature (new rule) or a brand-new feature file.
Recording upload caveat (acknowledged): the walks upload via the /api/v1/teams/{teamId}/recordings/upload + S3 PUT + confirm path because the live web recorder isn't easily exercised. We will mirror that in the L2 invocation contract for the upload-a-recording business action — the Gherkin reads as the user-journey (“Tina uploads a discussion”), the verb does the API path. To be revisited when the recording simulation work lands.
Tagging convention (as of PR #1245):
  • @pending — scenario is written but the verbs aren't wired up yet. Most scenarios in the repo are here. Doesn't run in CI.
  • (no tag) — scenario is implemented and validated. Today only onboarding/registration.feature and context-injection/agent-priming.feature qualify.
  • Business actions and verifications carry status: validated in YAML frontmatter when their L2 contract is wired and tested.
  • The old @speculative tag and the “See also: business-actions/…” feature-file references have been removed — drop them from new files too.
What this means for the plan: the implementation work flips existing @pending scenarios to validated by wiring their verbs. Any genuinely new scenarios proposed below land in the same PR as their verbs — they don't pass through @pending limbo. Hence the example Gherkin in this proposal shows no @pending tag on new content; tags shown on the BEFORE snippets in the refactor section accurately reflect today's state on main.

Walks → existing scenarios

For each walk, the existing @pending scenarios it would untag (assuming the verbs are wired), plus the gaps that need new authoring or refactoring.

WalkExisting @pending scenarios it untagsGaps
00 smoke Keep as build smoke. No BDD scenarios.
01 smoke Build artifact check. No BDD scenarios.
02 registration.feature · Successful registration with email and password
registration.feature · Newly registered user lands on the Welcome page
authentication.feature · Developer authenticates the ox CLI via device flow
authentication.feature · Developer logs out of the ox CLI
None — all behaviors have scenarios already.
03 team-creation.feature · Authenticated user creates a team
repo-connection.feature · Developer connects a repository to their team — absorbs the CLAUDE.md assertion via the refactor below
refactor team-membership.feature · collapse the “Team owner generates an invite link” scenario into the invitation journey (see refactors) — its outcome is mechanical, not user-perceived.
refactor repo-connection.feature · merge “Connection installs the ox agent prime hook in CLAUDE.md” into “Developer connects a repository to their team” (same Given/When, just additional Thens); reframe the rule; drop the cross-feature priming claim. See refactors.
new rule team-visibility.feature: Team owners can change visibility from private to public
new rule authentication.feature: The ox daemon can be started and stopped (start half)
04 team-membership.feature · Existing user joins a team via invite link — folded into the refactored invitation journey
team-membership.feature · New user joins a team via invite link after signing up — folded into the refactored invitation journey
team-membership.feature · Joined member is visible to other team members — folded as a second Then on each invitation scenario (see refactors); the standalone scenario and rule are deleted
team-membership.feature · Team member can access every section (Outline)
Refactor of the invitation rule structure carries over from Walk 03 (see refactors).
delete welcome-flow.feature · New user joins a team via invite code — redundant with team-membership; same outcome, different entry path. The Welcome-page entry funnel can be tested by a narrower scenario asserting only “clicking 'Join a team' lands on the invite-code entry” if we want it; otherwise drop.
05 team-visibility.feature · Non-member can view public sections of a public team (Outline)
team-visibility.feature · Non-member cannot access settings of a public team
team-visibility.feature · Member can access settings of a public team
team-visibility.feature · Non-member is blocked from a private team entirely
None — visibility matrix is well covered at the team-section level. See public/private axis for the section-level gaps.
06 discussion-capture.feature · the refactored audio Outline (one row in the consolidated journey scenario; see refactors) refactor Recording journey consolidation absorbs Walk 06's upload + processing + playback assertions into one outcome scenario, parameterised over format.
delete discussion-browsing.feature · Audio discussion has a player for playback (folded in).
07 skip Deferred. context-injection/session-recording.feature already has 9 @pending scenarios queued for when the TUI driver matures.
08 skip Redundant with 04/05.
09 authentication.feature · Signed-in user signs out successfully
authentication.feature · Developer logs out of the ox CLI
authentication.feature · Signed-out user cannot access protected pages
new rule authentication.feature: The ox daemon can be started and stopped (stop half)
10 Same as Walk 06 — M4A is one row in the consolidated journey Outline. None — covered by the journey consolidation refactor.
11 Same as Walk 06 — MP4 is one row in the consolidated journey Outline. The MIME-round-trip assertion (Walk 11's distinguishing concern) folds in as a Then. None — covered by the journey consolidation refactor.
11.1 discussion-browsing.feature · Non-member can browse a public team's discussions
discussion-browsing.feature · Non-member is blocked from a private team's discussions
refactor discussion-browsing.feature · both rules currently only cover the list. Extend each with a detail-page scenario:
  • public team + signed-in non-member: detail page opens, media element has non-empty src, transcript visible if present
  • private team + signed-in non-member: detail page returns Access Required
See refactors. The playback-element assertions themselves now live in the consolidated journey scenario; this refactor is purely about the public/private visibility boundary.
12 None. new feature security/pii-boundaries.feature
13 None directly. Adjacent: context-injection/team-context.feature · Team member can view Team Context history (currently scoped to git CLI semantics, not the /ledger UI). new feature context-injection/ledger-browsing.feature — UI surface for browsing the ledger. Coin-flip with extending team-context.feature; new file keeps content-vs-UI separation clean.
14 All four scenarios this walk maps to are verifications of the upload-process journey. The journey consolidation absorbs three of them and deletes the fourth (unfalsifiable). refactor Journey consolidation absorbs “all five stages complete,” “transcript with speakers + timing,” and “completed stages show timestamps” as Then clauses on the unified outcome scenario.
delete discussion-processing.feature · A short discussion is processed promptly (“within minutes” is unfalsifiable; performance belongs in observability, not BDD).
15 None. new feature sharing/share-links.feature

Headline: the walks land on roughly 30 existing @pending scenarios that untag once the verb stack is wired, plus two truly new features (pii-boundaries, share-links), plus one new feature (ledger-browsing) that is a coin-flip with extending team-context.feature, plus two new rules in existing features (make-public, daemon start/stop), plus two refactors of existing rules in discussion-browsing.feature and discussion-capture.feature. Most of the work is verbs and L2 contracts, not Gherkin.

Public/private team coverage axis

The walks conflate “lurker views a public team” with “recording is uploaded” — the test team is made public by Walk 03 and stays that way for the rest of the run. BDD should split this cleanly so we get explicit coverage of both public-team and private-team behavior at every visibility-sensitive surface.

The visibility matrix we want

Surface Member Signed-in non-member, public team Signed-in non-member, private team Anonymous, public team Anonymous, private team
Overview / Sessions / Repos lists denied ✓ (no PII) denied
Discussion list denied ✓ (no PII) denied
Discussion / recording detail page ✓ (Walk 11.1) denied ✓ (no PII) denied
Media element playback ✓ (Walk 06) ✓ (Walk 11.1, audio + video) denied not required today denied
Settings / web recorder denied denied denied (redirect to login) denied

The rows in bold are where current @pending coverage thins out. They're the refactor targets called out against Walks 06 and 11.1.

Where each axis lives

AxisFeature file
Team-section-level public-vs-private access (Overview, Settings, web recorder, etc.) team-management/team-visibility.feature — already structured this way; well covered.
Per-surface public-vs-private access (discussion list, discussion detail, playback) discussions/discussion-browsing.feature — partially covered for the list; refactor needed to also cover detail page + playback for both public and private team variants.
Anonymous viewer + PII guarantees on public surfaces security/pii-boundaries.feature (NEW) — covers the “no PII in payload” invariant separately from the access-control matrix above. Keeps the security concern isolated and explicit.
Visibility transition (private → public) team-visibility.featurenew rule “Team owners can change visibility from private to public.”
Why this split: public-team access and the anonymous-PII invariant are two different test concerns even though they often run against the same URL. Keeping them in different features means a regression in “non-members can view discussions” surfaces as visibility breakage, while a regression that adds an email to a public payload surfaces as PII breakage — without one masking the other.

BDD smell sweep across all @pending scenarios

Beyond the walks-driven work, a full pass over the 158 @pending scenarios surfaced a recurring set of smells. Catalog below. The first four entries are the same anti-patterns we already discussed (invite-link, repo-connection). The rest extend the same logic to every feature.

Scope note: not every entry here is in the walks' critical path. These are queued cleanup, not blockers for the walks port. Some can ride along with the walks PRs (e.g. the public/private Outline collapses in team-visibility.feature ship naturally with Walk 05's work). Others are independent cleanup PRs.

Smell catalog

Smell Feature · offending scenarios Why it smells Recommendation
A · Identical Given/When, split Then
Split assertions repo-connection.feature · Developer connects a repository + Connection installs the ox agent prime hook in CLAUDE.md + Connected repository appears on the team's repos page All three have the same Given+When; differ only on what they assert. Plus the second scenario's last Then overshoots into agent-priming.feature. Worked example in refactors: merge all three into one scenario, reframe the rule, drop the cross-feature claim.
Split assertions team-creation.feature · Authenticated user creates a team + Creator becomes the first member + New team is visible in the creator's navigation Three scenarios, same Given+When (create team), three different Thens. Each split gives a marginally clearer failure but at 3× setup cost. Either fold the consequences into one scenario's multi-Then, or keep them split and rename the rule to “Creating a team has multiple downstream effects” so the split is intentional. Recommend folding.
Split assertions registration.feature · Newly registered user lands on the Welcome page The same outcome (“directed to the Welcome page”) is already the third Then of Successful registration with email and password. Pure duplicate. Delete the scenario; the assertion is already covered.
A′ · When-clause is navigation-to-verify, not a behaviour
Verification-as-scenario team-membership.feature · Joined member is visible to other team members The Given does the work (Marcus joined). The When is navigation-to-verify (Tina opens the People page). The Then asserts a consequence of the join, not a new behaviour. Telltale sign: removing this scenario doesn't reduce coverage of any user behaviour — it just stops asserting a downstream consequence of a behaviour tested elsewhere. Fold the “appears in the team's People section” assertion into the refactored invitation scenarios (see refactors) as an additional Then. Delete this scenario and the rule above it. If we later want to test the People section as a surface (role display, sort, avatars), open that as a new rule with composition-as-Given, not narrative-as-Given.
Verification-as-scenario session-recording.feature · Completed session appears in the ledger + Completed session is visible on the sessions page + Session is also visible from the repository detail page All three: Given Marcus has completed a session, When view some page, Then session appears. Each is a verification of the stop action's outcome at a different surface. Same A′ shape as the People-section smell — three different navigation-to-verify Whens for one underlying behaviour. Absorbed by the session lifecycle refactor — all three assertions become Then clauses on the consolidated lifecycle outcome scenario.
Verification-as-scenario repo-connection.feature · Connected repository appears on the team's repos page Given+When are identical to the “Developer connects a repository” scenario. The Then asserts a consequence of the connect action. Same A′ shape. Absorbed by the three-scenario merge in repo-connection.feature.
Verification-as-scenario repo-browsing.feature · Repository associated with one team shows that team Given the repo is connected to "Acme Engineering", When view detail, Then "Acme Engineering" is listed as associated. The Given does the work; the When is navigation-to-verify. Either fold into the repo-detail scenario that proves the repo exists at all (multi-Then), or delete — the team-association is already a consequence of repo-connection.feature's outcome. Keep only if the repo-detail surface has its own genuinely-distinct concerns to test.
Verification-as-scenario welcome-flow.feature · Home Dashboard displays the user's teams and activity Given Marcus is signed in and on the Home Dashboard, When he views the Home Dashboard, Then he sees teams + activity + onboarding progress. The When is just “view” — navigation-to-verify against a state the Given already declared. Either reframe with composition-as-Given (“Given Marcus's account has teams T1, T2 and recent activity A1, A2”) so the When/Then assert the dashboard rendering as a surface, or fold into the “Returning user lands on the Home Dashboard” scenario as additional Then clauses.
Verification-as-scenario team-creation.feature · Team sections are accessible after creation (Outline) Given Tina has created a team, When she navigates to <section>, Then <section> is accessible. The When is just “navigate”; the Then is “loads”. Verifying that the team-creation action correctly provisioned each section's URL. The actual behaviours of each section are tested in their own features. Either fold into the team-creation outcome scenario as a single Then (“And all standard sections should be accessible”), or delete — if a section doesn't load post-creation, its own feature's tests will catch it.
B · Affordance-only outcome (mechanism, not user value)
Outcome is a mechanism team-membership.feature · Team owner generates an invite link Outcome “a link is available” is a mechanism. The user-perceived outcome is a teammate joining. Worked example in refactors: collapse three rules into one outcome-driven rule with two audience paths.
Outcome is a mechanism team-creation.feature · Team creation is available at the team creation page Outcome (“the page exists and the form works”) is already implicit in every other team-creation scenario. Proves nothing additional. Delete. The rule “Teams can be created from the web app” is also affordance-framed and can be dropped with the scenario.
Outcome is a mechanism welcome-flow.feature · New user starts creating a team Then is “navigated to the team creation form” — pure navigation assertion. The downstream behaviour (creating a team) is in team-creation.feature. Either delete, or refocus on the entry path: the scenario's value is testing the Welcome card → team-creation entry funnel, so frame as “clicking 'Create a team' on the Welcome page lands the user on the create-team flow.” Borderline.
Outcome is a mechanism welcome-flow.feature · Welcome page presents all three onboarding paths Pure UI-affordance check (three options visible). Borderline — visibility of choices on a critical-path page does have value. Keep, but accept it's a snapshot-style check. Consider running as part of a visual-regression layer if one exists.
Outcome stops at pipeline entry discussion-capture.feature · Uploading a supported audio format + Uploading a supported video format Outlines The Then is “a discussion appears in the list AND enters the processing pipeline.” “Enters the pipeline” is a mid-state assertion — the user-perceived value is the discussion becoming usable (playable, transcribed, summarised), not the state machine entering its first state. Reframed by the journey consolidation refactor: one fat Outline covering upload → processed → ready → playable → transcribed. See refactors.
Outcome is a mechanism registration.feature · Unauthenticated visitor can access the registration page First Then (“the registration page should be displayed”) is affordance-only. Second Then (“offer both email/password and GitHub options”) is the only one carrying value. Fold the GitHub-option assertion into the Successful registration via GitHub scenario; delete this scenario.
C · Should be a Scenario Outline
Outline candidate repo-browsing.feature · Public repository shows a Public badge + Private repository shows a Private badge Two scenarios, identical structure, vary on one parameter. Collapse to a 2-row Scenario Outline.
Mostly absorbed discussion-processing.feature · A new recording enters the pipeline at the first stage + A fully processed discussion has completed all five stages + Completed stages show when they finished + Stages not yet reached appear as pending These four scenarios are mostly verifications of the upload+process journey, not stepper-rendering tests. Pulling them apart shows that “all five stages complete”, “timestamps shown”, etc. are outcomes of processing finishing — not separate behaviours. Three of the four scenarios fold into the consolidated journey scenario (see refactors). “Stages not yet reached appear as pending” remains a legitimate stepper-rendering scenario for a discussion mid-pipeline — keep it (with composition-as-Given like “a discussion at the Transcribed stage”) but no Outline expansion needed.
Outline candidate team-visibility.feature · settings access pair + web recorder access pair Each pair varies only on actor (member / non-member) with mirrored outcomes (allowed / Access Required). Parameterise over {actor, expected_access}. Same shape works for both surfaces — could be two Outlines or one with a surface column.
Outline candidate repo-health.feature · individual OK/FAIL scenarios for authentication, registration, context-injection checks Six scenarios are already covered by the “Diagnostic check status indicators” Outline at the bottom of the file. They're redundant. Delete the six individual scenarios; keep the Outline.
Mechanism Outline (absorbed) session-recording.feature · Developer starts recording via slash command + Session recording can also be started via the CLI Two scenarios, same outcome, vary on mechanism. Absorbed by the session lifecycle refactor — mechanism becomes an Outline column on the consolidated lifecycle scenario.
Outline candidate welcome-flow.feature · First-time user sees the Welcome page + User who left all their teams sees the Welcome page Same outcome, vary on precondition (“new user” / “left all teams”). Outline over the user-state precondition.
Outline candidate discussion-capture.feature · size-limit pair + batch-upload pair Each pair tests a boundary just below and just above the limit. Outlines with {size, expected_outcome} columns — or keep, given that the under/over split makes the failure mode clearer in test output.
Outline candidate authentication.feature · wrong password rejected + unregistered email rejected Same outcome (same error, same redirect), vary on the rejection reason. The comment in the second scenario explicitly notes the error must match the first to prevent email enumeration. Outline over {credential_problem} with one shared Then that asserts a single “invalid credentials” error — that makes the enumeration-prevention property self-documenting.
Outline candidate authentication.feature · Successful sign-in with email and password + Successful sign-in via GitHub OAuth Same outcome (authenticated + active web session), vary on the sign-in method. Outline over {auth_method}. Two rows.
Outline candidate repo-connection.feature · Connection fails outside a git repository + Connection fails in a git repository with no commits + Connection fails without authentication Three error paths, each with a different missing precondition. Same When (attempt to connect), same shape of Then (specific guidance error). Outline over {missing_precondition, expected_guidance}. Three rows.
Outline candidate repo-health.feature · All checks passing produces exit code 0 + Any failing check produces exit code 1 Same shape, vary on health state and expected exit code. Outline over {state, exit_code}. Two rows.
Outline candidate team-context.feature · Updated coding conventions reach AI coworkers after priming + Removed conventions are no longer delivered after priming Same shape: edit AGENTS.md, sync, re-prime, assert new state propagated. Vary on add/remove direction. Outline over {edit_action, expected_state}. Both also touch the cross-feature concern (priming) — consider moving to agent-priming.feature as a single scenario asserting context-propagation.
D · Cross-feature / overshoot Then
Overshoot video-import.feature · Imported discussion is available to AI coworkers Requires running an agent-priming session to verify import completion. That's agent-priming.feature's territory. Move the assertion to agent-priming.feature (or delete — it may be redundant with existing “Claude primes with team context” coverage).
Overshoot discussion-processing.feature · Completed discussion content is available in team context Second Then (“AI coworkers primed on the team should have access”) requires priming machinery to verify. Cross-feature claim. Drop the second Then; the first one (“committed to team context”) is the legitimate boundary for this feature.
Overshoot team-context.feature · AI coworker can reference documents from the docs/ directory Requires priming. Same shape as above. Move to agent-priming.feature as a new rule (or fold into the existing “AI coworker receives team context on priming” scenario).
Duplicate cross-feature team-membership.feature · Developer connects a repository to their team via the CLI Duplicates the equivalent scenario in repo-connection.feature. Wrong feature for this assertion. Delete from team-membership.feature; keep the one in repo-connection.feature.
Duplicate cross-feature welcome-flow.feature · New user joins a team via invite code Same outcome as team-membership.feature · New user joins a team via invite link after signing up. The Welcome-page entry is a different funnel (code-entry form vs URL navigation), but the join itself is the same code path. Delete from welcome-flow.feature; the team-membership invitation refactor covers the join. If we want a Welcome-funnel test, write a narrower scenario asserting only “clicking 'Join a team' lands on the invite-code entry.”
E · Vague / untestable Then
Vague Then discussion-processing.feature · A short discussion is processed promptly Then all five stages should complete within minutes — “minutes” is unbounded. Test would either pass forever or never. Quantify: “within 5 minutes” or the actual SLA.
Vague Then discussion-processing.feature · Completed discussion has a structured summary Then the summary should include chapters, decisions, and action items — the verb has to assert what exactly? Tighten to assert presence of the relevant JSON fields, or that the rendered summary contains a chapters section, a decisions section, etc.
Vague Then session-recording.feature · Session artifacts reflect the interaction content Then the session summary should reflect the decisions made AND the full transcript should contain the complete interaction history — both untestable without a fixed input transcript. Fix the Given with a concrete known transcript and assert the summary contains specific expected fragments. Otherwise this is a vibes check.
Vague Then session-recording.feature · Team member previews the public view of sessions Then the page should display sessions as a non-member would see them — what is the verb supposed to compare? Either compare against the same URL fetched in an anonymous browser context, or assert specific PII-stripped fields are absent. Otherwise drop.
Vague Then agent-priming.feature · AI coworker receives team context on priming Third Then: Claude should receive the team's architectural decisions and domain terminology — how does the verb assert that? Either drop (the first two Thens cover AGENTS.md and SOUL.md presence) or tighten to a specific token observable in the primed context.
Vague Then agent-priming.feature · Primed AI coworker receives guidance on next actions Then Claude should receive suggested next actions relevant to the repository state — “relevant” is the entire assertion. Pin to a known repo state with a known expected suggestion (e.g., uninitialised repo → suggestion includes ox init).
F · Wrong testing layer (BDD or snapshot?)
Visual regression in BDD recording/public-team-awareness.feature · all three scenarios The whole feature asserts button colour (“teal”, “sage green”), label text (“Public”), icon presence (“globe icon”), and that the dot is in the same position. That's a visual-regression / snapshot concern, not an outcome-driven behaviour test. Flag for design discussion: keep as BDD (with concrete assertions on a class/data-attr rather than colour name), or move to a visual-regression layer if one exists. The behavioural half — that the recorder UI distinguishes public vs private — could be one outcome scenario per team type.
G · Implementation-detail Then (boundary case)
File-structure assertion team-context.feature · Team Context includes the standard file structure + Memory directory contains the three temporal layers Asserts specific filenames (AGENTS.md, SOUL.md, TEAM.md, MEMORY.md, etc.) exist. Tests the schema rather than user-perceived value. Probably keep — the file structure is the contract between team-context producers (humans, ox CLI, processing pipeline) and consumers (agent priming). But name the rule something like “Team Context exposes a stable file schema” so the intent is explicit.
Plumbing assertion team-context.feature · Team member can view Team Context history “Standard git operations work” — this is asserting git works, which it does by assumption. Drop, or refocus on the web ledger-history UI — in which case it belongs in the proposed ledger-browsing.feature instead.
H · Multi-stage user journey split into mid-state verifications
Journey split into mid-states discussion-capture.feature + discussion-processing.feature + discussion-browsing.feature The user-perceived recording journey is upload → processed → usable. The current scenarios verify mid-states (entered pipeline, all stages complete, transcript exists, player exists). Each is a verification of the same underlying outcome. Recording journey consolidation in refactors: one fat happy-path Outline per format absorbs 5 scenarios across 3 feature files.
Journey split into mid-states video-import.feature + import-tracking.feature Same shape: import → processed → artifacts. Mid-state verifications (got an import ID, state-machine transitions, watching to completion) sit alongside one buried happy-path scenario. Import journey consolidation in refactors. The status surface stays (list, error reason); the state-machine and journey-watch scenarios are absorbed or deleted.
Journey split into mid-states session-recording.feature · 6 scenarios spanning start → stop → visible-in-N-places Same shape, third instance. Three of the six scenarios are also Category A′ smells (verification of the stop outcome at different surfaces). Two are a mechanism-pair Outline candidate. Session lifecycle consolidation in refactors: one outcome scenario with mechanism as an Outline column and the three visibility verifications as Then clauses. Four kept-separate concerns survive (list rendering, daemon recovery, public-view toggle, transcript content).
Mid-state outcome (absorbed) discussion-capture.feature · Team member records a live discussion Same shape as the upload Outlines: ends at “discussion appears in list + enters processing pipeline”. Mid-state assertion, not user value. Live-recording-via-browser-mic is just another capture mechanism for the same outcome. Absorbed into the recording journey refactor as a third Examples block (one row: live recording → audio → audio/webm). The mic-permission error path stays as a separate scenario.
Mid-state outcome (sister scenario) discussion-capture.feature · Pasting a transcript creates a discussion Same mid-state smell. Different outcome shape from the upload journey (no media element to play), so it can't be a row in the Outline. Reframed as a sister scenario alongside the recording journey refactor: outcome is “transcript visible + summary generated”. Sits under its own rule in discussion-capture.feature.

One worked example

(The previously-included “pipeline-stage scenarios → Outline” example is now subsumed by the recording journey consolidation refactor — see the refactors section.)

repo-browsing.feature · Public/Private badge → Outline

## BEFORE
  Rule: Repository detail shows a Public or Private badge
    @pending
    Scenario: Public repository shows a Public badge
      Given "Acme Engineering" has a connected public repository
      When Marcus views the repository detail
      Then a "Public" badge should be displayed

    @pending
    Scenario: Private repository shows a Private badge
      Given "Acme Engineering" has a connected private repository
      When Marcus views the repository detail
      Then a "Private" badge should be displayed

## AFTER
  Rule: Repository detail shows the repository's visibility as a badge
    Scenario Outline: <visibility> repository shows a <badge> badge
      Given "Acme Engineering" has a connected <visibility> repository
      When Marcus views the repository detail
      Then a "<badge>" badge should be displayed

      Examples:
        | visibility | badge   |
        | public     | Public  |
        | private    | Private |
Priority guidance. Tier H (multi-stage journeys) is the most structurally load-bearing — the three refactors there reshape how we test the largest chunks of the product. Tier A / A′ (split assertions, navigation-to-verify) and Tier B (mechanical outcomes) are the next highest-leverage — they directly tighten what we're claiming the product does. Tier C (Outlines) is mostly cosmetic but compounds well at scale. Tier D (cross-feature) is small-volume but important: every overshoot is a regression in failure-isolation. Tiers E–G are judgment calls; some entries are arguably fine and just want better naming.

New / extended features

Three groups, in order of how much existing Gherkin is touched:

  1. Refactors to existing @pending scenarios — light edits to discussion-browsing.feature and discussion-capture.feature so the existing scenarios become the right shape for Walks 06, 11, 11.1.
  2. New rules added to existing features — make-public toggle in team-visibility.feature, daemon start/stop in authentication.feature.
  3. Two genuinely new featuressharing/share-links.feature and security/pii-boundaries.feature. Plus a third (context-injection/ledger-browsing.feature) flagged as a coin-flip.

1. Refactors to existing @pending scenarios refactor

Recording journey consolidation — collapse upload + process + browse + playback into outcome scenarios

The current recording story is split across three feature files and many scenarios, most of which are verifications of the same underlying user journey: I upload audio/video; it's processed; my team can use it. Each feature file owns a slice of the pipeline, so the journey ended up sliced too. But upload-without-playback isn't user value, and "stepper shows Recorded complete" isn't either. The fix: one outcome scenario per format covering the whole journey, with the existing verification-shaped scenarios folded in as Then clauses.

This refactor touches three feature files. Scenarios get deleted in two of them and one fat happy-path Outline lands in discussion-capture.feature. The narrower feature files keep their narrower concerns (validation errors, stepper UI state, list rendering, visibility boundary).

## BEFORE — happy-path verifications spread across 5 scenarios in 3 feature files

## discussion-capture.feature
  Rule: Team members can upload audio files as discussions
    @pending Scenario Outline: Uploading a supported audio format
      When he uploads a <format> audio file titled "Architecture Review"
      Then a discussion called "Architecture Review" should appear in the team's discussions list
      And the discussion should enter the processing pipeline   ## mechanical, not user value

  Rule: Team members can upload video files as discussions
    @pending Scenario Outline: Uploading a supported video format     ## same shape, video formats

## discussion-processing.feature
  @pending Scenario: A new recording enters the pipeline at the first stage  ## state-machine entry; delete
  @pending Scenario: A fully processed discussion has completed all five stages  ## fold into outcome scenario
  @pending Scenario: Transcript identifies speakers and timing                  ## fold into outcome scenario
  @pending Scenario: A short discussion is processed promptly               ## unfalsifiable; delete

## discussion-browsing.feature
  @pending Scenario: Audio discussion has a player for playback              ## verification of upload-process; fold in

## AFTER — one fat happy-path Outline in discussion-capture.feature; deletions elsewhere

## discussion-capture.feature
  Rule: Uploaded audio and video become fully-usable team discussions
    Scenario Outline: Uploaded <format> becomes a playable, transcribed discussion
      Given Tina is a member of "Acme Engineering"
      When she uploads a <format> file titled "Architecture Review"
      And processing completes
      Then "Architecture Review" should appear in the team's discussions list as Ready
      And it should have a <media_kind> element with a non-empty src and no error state
      And its stored MIME type should be <expected_mime>
      And it should have a transcript with speaker labels and timestamps
      And it should have an AI-generated summary

      Examples: Audio formats
        | format | media_kind | expected_mime |
        | MP3    | audio      | audio/mpeg    |
        | WAV    | audio      | audio/wav     |
        | M4A    | audio      | audio/mp4     |
        | OGG    | audio      | audio/ogg     |
        | FLAC   | audio      | audio/flac    |

      Examples: Video formats
        | format | media_kind | expected_mime    |
        | MP4    | video      | video/mp4        |
        | MOV    | video      | video/quicktime  |
        | WebM   | video      | video/webm       |
        | MKV    | video      | video/x-matroska |

      Examples: Live web recording (replaces “Team member records a live discussion”)
        | format         | media_kind | expected_mime |
        | live recording | audio      | audio/webm    |

  ## Error / validation paths stay as separate scenarios under their existing rules:
  ##   - File exceeding size limit is rejected
  ##   - Discussion without a title cannot be created
  ##   - Recording requires microphone permission (live-recording-specific error)
  ##   - Batch upload caps, etc.

  ## Pasted transcripts have a different outcome shape (no media element), so they
  ## get a small sister scenario rather than a row in the Outline above:
  Rule: Pasted transcripts become fully-usable team discussions
    Scenario: Pasting a transcript yields a viewable, summarised discussion
      Given Tina is a member of "Acme Engineering"
      When she pastes a transcript titled "Product Sync Notes"
      And processing completes
      Then "Product Sync Notes" should appear in the team's discussions list as Ready
      And the transcript should be visible on the detail page
      And it should have an AI-generated summary

## discussion-processing.feature — narrower scope: failure indication + stepper UI rendering
  ## KEEP: "Processing failure is clearly indicated"
  ## REFRAME: the four scenarios that test stepper rendering at various states
  ##          collapse into one Scenario Outline parameterised over (state, stage, indicator)
  ##          with composition-as-Given. See sweep entry C / pipeline-stage Outline.
  ## DELETE: "A new recording enters the pipeline at the first stage" (state-machine entry)
  ## DELETE: "A short discussion is processed promptly" (unfalsifiable)
  ## DELETE: "A fully processed discussion has completed all five stages" (absorbed)
  ## DELETE: "Transcript identifies speakers and timing" (absorbed)
  ## DELETE: "Completed stages show when they finished" (absorbed)

## discussion-browsing.feature — narrower scope: list rendering + detail-page visibility matrix
  ## DELETE: "Audio discussion has a player for playback" (absorbed into outcome scenario)
  ## KEEP + RENAME: "Team member sees discussions in the list" — rename to drop actor-framing,
  ##                   the Given is already composition-as-Given (the team has 3 discussions)
  ## KEEP: list-header count, sort, empty state, type badge, summary visibility scenarios
  ## NEW RULES: non-member detail-page visibility (see refactor #4 below)

Net effect:

  • One new fat scenario in discussion-capture.feature covering the full happy path, parameterised over 10 formats.
  • Five scenarios deleted in discussion-processing.feature (state-machine entry, promptly, all five stages, transcript-speakers, stage-timestamps).
  • One scenario deleted in discussion-browsing.feature (audio player).
  • The unified scenario carries all the assertions previously spread across the deleted scenarios as Then clauses. A regression anywhere in upload → process → playback surfaces as a clear, single failure.

The Outline format covers both the audio and video upload tables previously in discussion-capture.feature. The MIME-round-trip assertion (Walk 11) folds in naturally as a Then. The transcript and summary assertions (Walk 14) ride along. The playback assertion (Walks 06, 11.1) rides along.

Import journey consolidation (mirror of the recording journey)

The CLI import path has the same shape as the recording journey: a user initiates an import → the server downloads/processes → eventually artifacts exist that they can use. The current scenarios verify mid-states (got an import ID; transitioned pending→processing; eventually completed) plus one scenario at the end that asserts the actual outcome. Same fix: one outcome scenario per (source, format), with the mid-state verifications folded in.

## BEFORE — mid-state assertions spread across two feature files

## content-import/video-import.feature
  Rule: Importing a URL triggers server-side download and processing
    @pending Scenario: Import a Loom recording by URL
      Then she should receive an import ID for tracking            ## mid-state: have an ID
      And the server should begin downloading and processing the video  ## mid-state: pipeline entered
    @pending Scenario: Import a direct video URL  ## same shape, different source
    @pending Scenario: Import a local video file  ## same shape, file source
    @pending Scenario Outline: Importing a supported media format  ## same shape, all formats

  Rule: Import produces standard artifacts after processing
    @pending Scenario: Completed import generates transcript, summary, and metadata  ## the actual outcome — buried at the end

## content-import/import-tracking.feature
  @pending Scenario Outline: Import transitions through processing stages  ## pure state machine
  @pending Scenario: Watching an import until completion  ## verification of journey completion
  @pending Scenario: Completed import shows where artifacts are stored  ## verification of completion

## AFTER — outcome scenario per (source, format) in video-import.feature; tracking feature narrows scope

## content-import/video-import.feature
  Rule: Importing produces a fully-processed team discussion
    Scenario Outline: Importing a <format> from <source> produces a ready discussion
      Given Tina has authenticated the ox CLI
      And her repository is connected to "Acme Engineering"
      When she imports a <source> source with format <format> titled "Architecture Review"
      And processing completes
      Then "Architecture Review" should be a ready discussion in the team's list
      And it should have a transcript and an AI-generated summary
      And the artifacts should live at a published path in Team Context

      Examples:
        | source              | format |
        | Loom share URL      | MP4    |
        | direct video URL    | MP4    |
        | local file          | MP4    |
        | local file          | M4A    |
        | local file          | MP3    |

  ## Error / boundary scenarios stay separate:
  ##   - Import fails without authentication
  ##   - Import fails outside an initialized repository without --team
  ##   - Importing the same content twice produces an error / force re-imports
  ##   - Glob batch import

## content-import/import-tracking.feature — narrower scope: status surface, not journey verification
  ## KEEP:    Listing imports shows recent imports / empty result (list rendering)
  ## KEEP:    Failed import displays why it failed (error path)
  ## DELETE:  Import transitions through processing stages (pure state machine; not user value)
  ## DELETE:  Watching an import until completion (verification of journey completion; absorbed)
  ## DELETE:  Completed import shows where artifacts are stored (absorbed)
  ## REFOCUS: Checking status of a pending import / Watching an import that fails — keep but reframe
  ##          as “import status surface” tests with composition-as-Given

Walk 14's lessons apply here too: the “Imported discussion is available to AI coworkers” scenario in video-import.feature overshoots into agent-priming and gets deleted (the cross-feature smell was already flagged in the sweep). The new outcome Outline doesn't claim downstream priming behaviour — that's agent-priming.feature's job.

Session lifecycle consolidation (mirror of the recording journey, applied to session-recording.feature)

Same pattern, third instance. A developer records an AI coworker session: start → stop → artifacts committed → visible in N places. The current feature splits this across “start (slash command)”, “start (CLI)”, “stop and finalize”, “committed to ledger”, “visible on sessions page”, “visible on repo detail”. The last three are verifications of the stop action's outcome (Category A′ smell).

## BEFORE — 6 scenarios for the start-to-visible journey

## context-injection/session-recording.feature
  @pending Scenario: Developer starts recording a session via slash command  ## start mechanism A
  @pending Scenario: Session recording can also be started via the CLI         ## start mechanism B
  @pending Scenario: Developer stops recording and session is finalized        ## stop action
  @pending Scenario: Completed session appears in the ledger                  ## verification of stop outcome
  @pending Scenario: Completed session is visible on the sessions page          ## verification of stop outcome
  @pending Scenario: Session is also visible from the repository detail page    ## verification of stop outcome

## AFTER — one outcome scenario covering the full lifecycle, with start as a parameter
  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            |

  ## Keep separately (legitimate distinct concerns):
  ##   - Sessions from multiple repositories appear in one list (list rendering, composition-as-Given)
  ##   - Interrupted session is recovered by the daemon (failure-recovery property)
  ##   - Team member previews the public view of sessions (visibility toggle — sharpen the vague Then)
  ##   - Session artifacts reflect the interaction content (currently vague; tighten with concrete inputs)

Six scenarios collapse to one outcome scenario plus the four kept-separate concerns. The start-mechanism Outline removes the slash-vs-CLI smell flagged in the sweep (Category C). The three visibility verifications fold in as Then clauses since they're all consequences of the stop action.

discussions/discussion-browsing.feature — non-member access on public vs private teams (detail-page level)

With the playback assertions now folded into the unified outcome scenario (refactor above), discussion-browsing.feature's remaining responsibility for the visibility matrix is the non-member detail-page access. The two existing public/private rules in this feature only cover the list. Extend each to also cover detail-page access — this is the surface-level visibility split the public/private axis section called out as the missing coverage.

## BEFORE — list browsing only
  Rule: Non-members of public teams can view discussions
    @pending
    Scenario: Non-member can browse a public team's discussions
      Given the "Acme Engineering" team is a public team
      And the team has 5 discussions
      When Priya views the discussions list for "Acme Engineering"
      Then she should see 5 discussions

  Rule: … private team blocked …
    @pending
    Scenario: Non-member is blocked from a private team's discussions
      Given the "Acme Engineering" team is a private team
      When Priya views the discussions list for "Acme Engineering"
      Then she should see an "Access Required" message

## AFTER — add detail-page scenarios alongside the list ones; no @pending
  Rule: Non-members of public teams can view discussions and play media
    Scenario: Non-member can browse a public team's discussion list
      Given the "Acme Engineering" team is a public team
      And the team has 5 discussions
      When Priya views the discussions list for "Acme Engineering"
      Then she should see 5 discussions

    Scenario: Non-member can open a discussion detail page on a public team
      Given the "Acme Engineering" team is a public team
      And the team has a ready discussion titled "Architecture Review"
      When Priya opens the discussion detail page for "Architecture Review"
      Then a playable media element should be present
      And the transcript should be visible if available

  Rule: Non-members of private teams are blocked from discussions
    Scenario: Non-member is blocked from a private team's discussion list
      Given the "Acme Engineering" team is a private team
      When Priya views the discussions list for "Acme Engineering"
      Then she should see an "Access Required" message

    Scenario: Non-member is blocked from a private team's discussion detail
      Given the "Acme Engineering" team is a private team
      And the team has a discussion titled "Architecture Review"
      When Priya attempts to open the discussion detail page for "Architecture Review"
      Then she should see an "Access Required" message

repository/repo-connection.feature — three scenarios with identical Given/When, split only by Then

The first three scenarios in this feature are the classic “one action, split assertions” anti-pattern. All three share the same actor, preconditions, and verb — they differ only in what they assert afterwards. That's one scenario, not three. Two extra issues worth fixing in the same refactor:

  • The first rule (“Connecting a repository creates the .sageox/ configuration directory”) doesn't actually cover the second scenario (which is about CLAUDE.md). The third scenario sits under its own rule but has the same Given+When as the first two.
  • The last Then in the CLAUDE.md scenario (“AI coworker sessions in this repo should automatically prime with team context”) overshoots into agent-priming.feature's responsibility. This feature should end at hook installed; what the hook does is exercised elsewhere.
## BEFORE — three scenarios split by Then; same Given+When for all three; cross-feature claim
  Rule: Connecting a repository creates the .sageox/ configuration directory
    @pending
    Scenario: Developer connects a repository to their team
      Given Marcus has authenticated the ox CLI
      And he is inside a git repository with at least one commit
      When he connects the repository to the "Acme Engineering" team
      Then a .sageox/ directory should exist in the repository root
      And .sageox/ should contain a config.json with the repository ID and team ID
      And the created files should be staged in git

    @pending
    Scenario: Connection installs the ox agent prime hook in CLAUDE.md
      Given Marcus has authenticated the ox CLI            ## identical
      And he is inside a git repository with at least one commit  ## identical
      When he connects the repository to the "Acme Engineering" team  ## identical
      Then CLAUDE.md should contain the ox agent prime directive
      And AI coworker sessions in this repo should automatically prime with team context  ## belongs in agent-priming.feature

  Rule: Connecting a repository registers it with the SageOx platform
    @pending
    Scenario: Connected repository appears on the team's repos page
      Given Marcus has authenticated the ox CLI            ## identical
      And he is inside a git repository with at least one commit  ## identical
      When he connects the repository to the "Acme Engineering" team  ## identical
      Then the repository should be registered with the SageOx service
      And it should appear on the "Acme Engineering" team's repos page

## AFTER — one scenario, all consequences in one Then block; rule reframed; cross-feature claim dropped
  Rule: Connecting a repository configures the repo for SageOx and registers it with the team
    Scenario: Developer connects a repository to their team
      Given Marcus has authenticated the ox CLI
      And he is inside a git repository with at least one commit
      When he connects the repository to the "Acme Engineering" team
      Then a .sageox/ directory should exist in the repository root
      And .sageox/ should contain a config.json with the repository ID and team ID
      And the created files should be staged in git
      And CLAUDE.md should contain the ox agent prime directive
      And the repository should be registered with the SageOx service
      And it should appear on the "Acme Engineering" team's repos page

Three scenarios collapse to one. The cross-feature priming claim (“sessions automatically prime”) is dropped — already covered by context-injection/agent-priming.feature.

team-management/team-membership.feature — invite-link rule is mechanical, not outcome-driven

The existing “Team owner generates an invite link” scenario is a BDD smell — its outcome (“a link is available”) is a mechanism, not a business value. The user-perceived outcome is getting a teammate onto the team; the link is an intermediate artifact in service of that. The rule above it (“Team members can generate invite links”) is phrased as a feature affordance, not a business outcome. Collapse these three rules into one outcome-driven rule with two paths:

## BEFORE — three rules, mechanism-first; one of them has a degenerate outcome
  Rule: Team members can generate invite links
    @pending
    Scenario: Team owner generates an invite link
      Given Tina is a member of the "Acme Engineering" team
      When she generates an invite link for "Acme Engineering"
      Then an invite link for "Acme Engineering" should be available

  Rule: Invite links allow existing users to join the team
    @pending
    Scenario: Existing user joins a team via invite link
      Given Tina has generated an invite link for "Acme Engineering"
      And Marcus is an authenticated user who is not a member of "Acme Engineering"
      When Marcus opens the invite link
      Then Marcus should be a member of "Acme Engineering"

  Rule: Invite links allow new users to join the team
    @pending
    Scenario: New user joins a team via invite link after signing up
      Given Tina has generated an invite link for "Acme Engineering"
      And Gabe is an authenticated user with no teams
      When Gabe opens the invite link
      Then Gabe should be a member of "Acme Engineering"

  Rule: New members appear in the team's People section
    @pending
    Scenario: Joined member is visible to other team members  ## verification-of-precondition smell; see catalog A′
      Given Marcus has joined "Acme Engineering" via an invite link
      When Tina views the People section of "Acme Engineering"
      Then Marcus should appear in the team's member list

## AFTER — one rule, outcome-first; two paths share the same outcome shape;
## the People-section consequence folds into each scenario's Then block
  Rule: Team owners can invite others to join their team via a shareable link
    Scenario: Tina invites Marcus, an existing SageOx user
      Given Tina is the owner of "Acme Engineering"
      And Marcus is a registered SageOx user who is not on "Acme Engineering"
      When Tina invites Marcus to "Acme Engineering" with a shareable link
      And Marcus opens the invite link
      Then Marcus should be a member of "Acme Engineering"
      And Marcus should appear in the team's People section

    Scenario: Tina invites Gabe, who is not yet a SageOx user
      Given Tina is the owner of "Acme Engineering"
      And Gabe does not yet have a SageOx account
      When Tina invites Gabe to "Acme Engineering" with a shareable link
      And Gabe opens the invite link and completes registration
      Then Gabe should be a member of "Acme Engineering"
      And Gabe should appear in the team's People section

What changes:

  • The When verb is invite, not generate — the link is implicit in the action. (The L2 contract can still be a two-step verb that creates and surfaces the URL; the Gherkin doesn't need to spell that out.)
  • Rule name is the outcome, not the feature affordance.
  • The two audience paths (existing user / new user) become two scenarios under one rule, sharing the outcome shape. The new-user path is one extra step.
  • The “appears in the People section” assertion (previously a standalone scenario) is now the second Then on each invitation scenario — the People-section visibility is the user-observable outcome of inviting someone. The standalone scenario and its rule (“New members appear in the team's People section”) are deleted.
  • “Members can access all team sections” stays as a separate rule (access control). It has a legitimate, distinct outcome.
  • If we later want to test the People section as a surface (role display, sort order, avatars), open a new rule with composition-as-Given (“Given Acme Engineering has members Tina, Marcus, and Priya”) rather than narrative-as-Given (“Marcus has joined”).

2. NEW sharing/share-links.feature new

Captures Walk 15's 10 gates. Recording is the worked example; the same scenarios run for session and discussion via a Scenario Outline. Lands in the same PR as the share verbs — no @pending limbo.

Feature: Share Links
  Team members create share links to grant view access to a single
  recording, session, or discussion without inviting the recipient to
  the team. Shares support anonymous and authenticated audiences,
  revocation, expiry, single-use caps, and regeneration.

  Background:
    Given Tina is a member of "Acme Engineering"
    And the team has a recording titled "Architecture Review"

  Rule: A team member can create an anonymous share link
    Scenario: Anonymous share returns a resolvable URL
      When Tina creates an anonymous share link for the "Architecture Review" recording
      Then the share link should resolve for an unauthenticated viewer
      And the resolved response should contain no PII

  Rule: Revoked share links return a client error, never a server error
    Scenario: Resolving a revoked share returns 4xx
      Given Tina has created an anonymous share link for "Architecture Review"
      When she revokes the share
      And an unauthenticated viewer attempts to resolve it
      Then the resolve response should be a 4xx status
      And the resolve response should not be a 5xx status

  Rule: Authenticated shares require sign-in before revealing content
    Scenario: Anonymous viewer of an authenticated share is sent to login
      Given Tina has created an authenticated share link for "Architecture Review"
      When an unauthenticated viewer opens the share link
      Then they should be redirected to the Login page

    Scenario: Signed-in non-member of the team can resolve an authenticated share
      Given Tina has created an authenticated share link for "Architecture Review"
      And Priya is an authenticated user who is not a member of "Acme Engineering"
      When Priya resolves the share link
      Then the resolve should succeed
      And the response should contain no PII other than Priya's own email

  Rule: Share links honour an expiry window
    Scenario: Expired share returns 4xx after its TTL elapses
      Given Tina has created an anonymous share link for "Architecture Review" with a 2-second expiry
      When 3 seconds pass
      And an unauthenticated viewer attempts to resolve it
      Then the resolve response should be a 4xx status

  Rule: Share links honour a max-uses cap
    Scenario: Single-use share is rejected on the second resolve
      Given Tina has created an anonymous share link for "Architecture Review" capped at 1 use
      When the link is resolved once successfully
      And it is resolved a second time
      Then the second resolve should be a 4xx status

  Rule: Regenerating a share retires the old token
    Scenario: Old token rejects, new token resolves
      Given Tina has created an anonymous share link for "Architecture Review"
      When she regenerates the share
      Then resolving the original token should return a 4xx status
      And resolving the new token should succeed

  Rule: All gates apply to every shareable entity type
    Scenario Outline: Anonymous share lifecycle for <entity>
      Given Tina has an <entity> she can share
      When she creates an anonymous share, then revokes it
      Then resolving the revoked share should return a 4xx status

      Examples:
        | entity     |
        | recording  |
        | session    |
        | discussion |

3. NEW security/pii-boundaries.feature new

Walk 12, structured by surface (page vs API).

Feature: PII Boundaries on Public Surfaces
  Public team pages and public APIs MUST NOT leak member emails or
  raw user IDs to unauthenticated viewers. AI coworkers and ledger
  consumers rely on this guarantee.

  Background:
    Given "Acme Engineering" is a public team

  Rule: Public team pages do not expose member PII
    Scenario Outline: Anonymous viewer sees no PII on <page>
      When an unauthenticated viewer opens the <page> of "Acme Engineering"
      Then the rendered page should contain no email addresses
      And the rendered page should contain no usr_ identifiers
      And page attributes (title, alt, data-email, data-user-id) should contain no PII

      Examples:
        | page        |
        | Overview    |
        | Sessions    |
        | Repos       |
        | Discussions |

  Rule: Public team APIs do not expose member PII
    Scenario Outline: Public API <endpoint> contains no PII
      When an unauthenticated client fetches <endpoint> for "Acme Engineering"
      Then the response body should contain no email addresses
      And the response body should contain no usr_ identifiers
      And the response body should not contain an "invited_by" field

      Examples:
        | endpoint                                       |
        | /api/v1/public/teams/{teamId}/info             |
        | /api/v1/public/teams/{teamId}/recordings       |
        | /api/v1/public/teams/{teamId}/merge-candidates |

4. NEW context-injection/ledger-browsing.feature new

Feature: Ledger Browsing
  Authenticated users browse their ledger to see the teams whose
  context they participate in.

  Rule: Signed-in users can open the ledger page
    Scenario: Ledger page renders for a signed-in user with teams
      Given Marcus is a member of "Acme Engineering"
      When he opens the ledger page
      Then the ledger page should render without error
      And "Acme Engineering" should appear in his ledger

    Scenario: Ledger page renders for a signed-in user with no teams
      Given Gabe is signed in with no teams
      When he opens the ledger page
      Then the ledger page should render without error
      And the ledger should show an empty state

5. New rules added to existing features extend

team-management/team-visibility.feature — add Walk 03's “Make public” rule

  Rule: Team owners can change visibility from private to public
    Scenario: Owner makes their team public
      Given Tina has created a private team called "Acme Engineering"
      When she changes the team's visibility to public
      Then "Acme Engineering" should be a public team
      And an authenticated non-member should be able to view its Overview

onboarding/authentication.feature — add daemon lifecycle (Walks 03 + 09)

  Rule: The ox daemon can be started and stopped from the CLI
    Scenario: Starting the daemon reports a healthy status
      Given Marcus has authenticated the ox CLI
      When he runs ox daemon start
      Then ox status should report the daemon as running

    Scenario: Stopping the daemon clears its running state
      Given Marcus has authenticated the ox CLI
      And the ox daemon is running
      When he runs ox daemon stop
      Then ox status should report the daemon as not running

New business actions

Each gets its own markdown file under tests/acceptance/business-actions/ with the same shape as register-for-account.md — YAML frontmatter (doc-audience: human, status: validated once wired, aliases buckets) + body + L2 invocation contract. New business actions ship with status: validated in the same PR that adds the underlying verb so the contract isn't drifting from the implementation.

Business actionWalksNotes
upload-a-recording.md06, 10, 11Composite: initiate → S3 PUT → confirm; one fast verb wraps all three. Drives the recording-journey Outline rows for audio/video upload.
record-live-discussion.mdWeb-recorder live-capture path; drives the recording-journey Outline's “live recording” row.
paste-a-transcript.mdPaste-transcript capture path; drives the paste-transcript sister scenario.
wait-for-discussion-ready.md14Poll /recordings/{id} until status=ready; bounded timeout. (Renamed from wait-for-recording-ready — the journey scenarios assert on a fully-processed discussion, not just a recording.) Reusable across recording/import journeys.
view-recording-detail.md06, 11.1Navigate to /team/{id}/media/recordings/{id}
import-content.mdox import for URL or local file; drives the import-journey Outline. Composite over source (Loom URL / direct URL / local file) and format.
start-session-recording.md / stop-session-recording.md07 (deferred)CLI + slash-command variants; drives the session-lifecycle Outline rows. Currently deferred (Walk 07 stubbed pending TUI driver maturity).
make-team-public.md03Settings → Make public → confirm dialog → type name → confirm
generate-invite-link.md03Click Invite → extract /invite/… URL
join-team-via-invite.md04GET invite URL → accept → assert membership
change-team-section.md04, 05Navigate to /team/{id}/{section}; reusable across visibility scenarios
start-ox-daemon.md / stop-ox-daemon.md03, 09CLI verbs
logout-ox-cli.md02, 09ox logout -f
create-share-link.md15POST /api/v1/teams/{id}/shares with audience / maxUses / expiresInSeconds
resolve-share-link.md15GET /api/v1/shares/{token}/resolve (anon or as B-auth)
revoke-share-link.md15DELETE /api/v1/shares/{id}
regenerate-share-link.md15POST /api/v1/shares/{id}/regenerate

New verifications

VerificationWalks
team-be-public.md03, visibility rule
discussion-be-ready-in-list.md06, 10, 11, 14
discussion-has-playable-media.md06, 11.1 — parameterised over {media_kind, expected_mime}; checks element exists, src non-empty, no error state, MIME round-trips
discussion-has-transcript.md06, 14 — transcript content visible; speaker labels + timestamps
discussion-has-summary.md14 — AI-generated summary present
session-be-committed-to-ledger.md07 (deferred) — drives the session-lifecycle Thens
page-be-free-of-pii.md12
api-response-be-free-of-pii.md12
share-resolve-status-be.md15 — parameterized: 2xx / 4xx / not-5xx
ledger-page-renders.md13

Driver verbs to add

drivers/cli/ox/

  • logout.ts  02, 09
  • status.ts  02, 03, 09 — parses output to {authenticated, daemon}
  • daemon/start.ts, daemon/stop.ts  03, 09

drivers/web/team/

  • make-public.ts  03 — opens settings, clicks Make public, fills confirm dialog
  • get-invite-link.ts  03 — opens Invite dialog, extracts URL
  • join-via-invite.ts  04
  • navigate-section.ts  04, 05{teamId, section} → goto + assert no Access Required
  • view-settings.ts  05 — returns {accessible: boolean}

drivers/web/recording/ (new directory)

  • initiate-upload.ts, put-to-s3.ts, confirm-upload.ts  06, 10, 11
  • upload-composite.ts  06, 10, 11 — wraps the three above; this is the verb the business action calls
  • record-live.ts  capture journey — drives the browser web-recorder for the live-recording journey row. Needs permissions: ['microphone'] on the BrowserContext.
  • paste-transcript.ts  capture journey — submits a transcript via the paste-text UI; drives the paste-transcript sister scenario.
  • get-status.ts  06, 11, 14 — also returns mime_type for the journey MIME assertion.
  • wait-for-ready.ts  14 — polls with {intervalMs, timeoutMs} until status=ready.
  • view-detail.ts  06, 11.1 — returns {statusOk, hasMediaElement, mediaKind, mediaSrc, mediaError, hasSummary}.
  • check-transcript.ts  06, 14, 11.1 — clicks Transcript tab, asserts content length, speaker labels, timestamps.

drivers/cli/ox/import/ (new directory)

  • import-url.ts  import journeyox import <url>; supports Loom + direct URLs; returns {importId}.
  • import-file.ts  import journeyox import <path>; supports glob batch.
  • wait-for-import-ready.ts  import journey — polls /imports/{id} until status=completed. Could share implementation with web/recording/wait-for-ready if the status surfaces are isomorphic.

drivers/tui/session/ (deferred until Walk 07 lands)

  • start-via-slash.ts, start-via-cli.ts  session lifecycle — drives /ox-session-start through the TUI vs ox session start from the shell. Two rows in the lifecycle Outline.
  • stop.ts  session lifecycle — drives /ox-session-stop or ox session stop; waits for upload-to-ledger to settle.
  • verify-in-ledger.ts, verify-on-sessions-page.ts, verify-on-repo-detail.ts  session lifecycle — drive the three visibility Then clauses.

drivers/web/share/ (new directory)

  • create.ts  15 — POST with {entity_type, entity_id, audience, maxUses?, expiresInSeconds?}
  • resolve.ts  15 — optional cookie header; returns {status, body, redirectUrl}
  • revoke.ts  15
  • regenerate.ts  15

drivers/web/security/ (new directory) — or shared util

  • scan-pii.ts  12 — runs in a Stagehand page; returns {emails: [], userIds: [], attrs: []}
  • scan-api-pii.ts  12 — fetch + body text scan; same shape

drivers/web/ledger/

  • view.ts  13page.goto /ledger, returns {rendered, teamLinks, contentLength}
Design notes:
  • PII verb shape: return matches as data; the verification asserts .data.matches | length == 0. Avoid making the verb decide pass/fail — keeps the contract clean (per ADR-036).
  • S3 PUT: the presigned PUT call should run outside the Stagehand page (Node-side fetch), as Walk 06 does. Embed it in the composite verb rather than expose it as standalone — nobody else needs it.
  • B-auth viewer for Walk 15 gate 7: needs a second signed-in user. Model as Priya in a second Stagehand BrowserContext (mirrors registerBauthUser in walk-15), or as a second sub-session. Existing harness supports parallel sessions.

Implementation plan

Unit of work is the scenario, not the phase. Each scenario PR brings the scenario (new or refactored) and the new business actions it requires and the verifications it asserts and the underlying driver verbs — all wired and validated end-to-end. No artificial split between “land all verbs, then all BAs, then all scenarios” — that's how scenarios drift away from their implementations. The two existing validated features (registration, agent-priming) shipped this way.

Parallelization comes from the business-action dependency graph. Scenarios that share new business actions cluster together — first scenario in the cluster carries the BA implementation cost, subsequent scenarios reuse. Scenarios in different clusters have no shared work and can run in parallel.

Foundation: existing validated business actions

Five business actions are already wired and shipping today (status: validated in their frontmatter). New scenarios layer onto these without re-implementing:

  • register-for-account · authenticate-via-cli · create-a-team · connect-a-repository · start-coding-session

Clusters — scenarios grouped by which new business actions they share

ClusterNew business actions introducedScenarios unblockedDepends on
Recording journey
highest fan-out
upload-a-recording
record-live-discussion
paste-a-transcript
wait-for-discussion-ready
view-recording-detail
Recording journey Outline (10 audio/video rows + 1 live recording row + 1 paste-transcript sister scenario) · Refactored discussion-detail visibility rules in discussion-browsing.feature (4 scenarios across public/private + member/non-member) · Kept-as-is scenarios in discussion-browsing.feature (list rendering, sort, empty state, type badge) · Kept-as-is in discussion-processing.feature (failure indication, stepper rendering) foundation only
Team management make-team-public
invite-someone-to-team
join-team-via-invite
change-team-section
Team-visibility “Make public” new rule · Team-visibility Outlines (settings + web recorder access pairs → Outline) · Team-membership invitation refactor (2 scenarios covering existing-user and new-user audience paths) · Team-membership “Team member can access every section” Outline · Non-member-can-view-public-sections Outline in team-visibility.feature foundation only
CLI ops start-ox-daemon / stop-ox-daemon
logout-ox-cli
New “ox daemon can be started and stopped” rule in authentication.feature (2 scenarios) · Sign-out + CLI logout scenarios already in authentication.feature · Web sign-in scenarios (could be Outline) foundation only
Repo-connection refactor
small
none new — reuses existing connect-a-repository Three-way merge in repo-connection.feature (3 scenarios → 1) · Other repo-connection scenarios (error paths, re-connect, disconnect) foundation only
Share links create-share-link
resolve-share-link
revoke-share-link
regenerate-share-link
New sharing/share-links.feature (9 scenarios across anonymous create, revoke, auth audience gate, expiry, max-uses, regenerate, entity-type Outline) Recording journey (needs an entity to share)
PII boundaries scan-pii
scan-api-pii
New security/pii-boundaries.feature (2 Outlines covering 4 public pages + 3 public APIs) Team management (needs make-team-public) and Recording journey (for content to scan)
Ledger browsing
small
view-ledger-page New context-injection/ledger-browsing.feature (2 scenarios — with-teams and empty-state) foundation only
Import journey import-content
wait-for-import-ready (could share implementation with wait-for-discussion-ready)
Import journey Outline (5 rows across Loom URL / direct URL / local file × MP4/M4A/MP3) · Import error paths (no auth, no init, duplicate) · Import status surface scenarios in import-tracking.feature (kept after journey absorption) foundation only; can share wait-for-discussion-ready with Recording journey if it lands first
Session lifecycle
deferred
start-session-recording / stop-session-recording Session-lifecycle consolidation in session-recording.feature (1 outcome Outline with 2 mechanism rows + 4 kept-separate concerns) TUI driver maturity (Walk 07 stub)

Parallelization map

Independent clusters can run concurrently. The dependency arrows in the “Depends on” column define the ordering constraints:

foundation (already validated)
    │
    ├─▶ Recording journey ────┬─▶ Share links
    │                         └─▶ PII boundaries ◀─┐
    │                                              │
    ├─▶ Team management ───────────────────────────┘
    │
    ├─▶ CLI ops
    │
    ├─▶ Repo-connection refactor
    │
    ├─▶ Ledger browsing
    │
    └─▶ Import journey (can share wait-for-ready with Recording journey)

(deferred until TUI driver matures)
    └─▶ Session lifecycle

Suggested first wave (week 1): Recording journey (highest fan-out), Team management, CLI ops, Repo-connection refactor, Ledger browsing — all in parallel. Five independent tracks, no cross-cluster contention.

Second wave (week 2): Share links + PII boundaries + Import journey — in parallel with each other once their upstream clusters land.

Within a cluster: the first scenario PR carries the new business action(s) cost. Subsequent scenarios in the cluster are smaller — just Gherkin + L2 wiring on already-validated BAs. Prefer ordering scenarios in a cluster “simplest first” so the BA work lands behind a quick scenario rather than a fat one.

End-of-implementation phases (sequential)

  1. Smoke wiring

    Once the high-fan-out clusters have landed, add tests/acceptance/smoke/smoke-recording-pipeline.ts chaining recording journey + share-link round-trip end-to-end. The BDD analogue of the full walks chain — this is what runs in pre-prod gates.

  2. Walk retirement

    Once the corresponding scenarios are validated and running under the BDD smoke, delete the matching walk from factory/walks/. Walks 00, 01, 07, 08 stay (not BDD candidates). Walks 02–06, 09–15 retire.

Two judgment calls flagged for your steer

1. Walk 00 (/ready health): recommend leaving OUT of BDD entirely — infra smoke, not a user-journey, would force modelling the HTTP layer as a driver. Counter: it's the cheapest signal in the suite. Override if you want it BDD-fied.
2. Walk 01 (CLI install): recommend keeping as a shell script in CI. No user-perceived behavior to describe. Override if you want a platform/cli-bootstrap.feature.

Suggested first move

If you want to proceed, start Phase 1 with recording verbs + share verbs in parallel — those unblock the two biggest new features (recording-playback.feature, share-links.feature). Or pick a single feature and let me spec it out in full L2 detail (aliases, contract YAML, exports) before any code lands.