At a glance
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.
@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
@pendingscenario already describes it — implementation = wire verbs, drop the tag. - Some cases: an existing
@pendingscenario almost describes it — implementation = light refactor (extend an Outline, add an extraThen, 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.
/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.
- @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.featureandcontext-injection/agent-priming.featurequalify. - Business actions and verifications carry
status: validatedin YAML frontmatter when their L2 contract is wired and tested. - The old
@speculativetag and the “See also: business-actions/…” feature-file references have been removed — drop them from new files too.
@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.
| Walk | Existing @pending scenarios it untags | Gaps |
|---|---|---|
| 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 passwordregistration.feature · Newly registered user lands on the Welcome pageauthentication.feature · Developer authenticates the ox CLI via device flowauthentication.feature · Developer logs out of the ox CLI
|
None — all behaviors have scenarios already. |
| 03 |
team-creation.feature · Authenticated user creates a teamrepo-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 publicnew 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 journeyteam-membership.feature · New user joins a team via invite link after signing up — folded into the refactored invitation journeyteam-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 deletedteam-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 teamteam-visibility.feature · Member can access settings of a public teamteam-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 successfullyauthentication.feature · Developer logs out of the ox CLIauthentication.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 discussionsdiscussion-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:
|
| 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
| Axis | Feature 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.feature — new rule “Team owners can change visibility from private to public.” |
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 |
New / extended features
Three groups, in order of how much existing Gherkin is touched:
- Refactors to existing
@pendingscenarios — light edits todiscussion-browsing.featureanddiscussion-capture.featureso the existing scenarios become the right shape for Walks 06, 11, 11.1. - New rules added to existing features — make-public toggle in
team-visibility.feature, daemon start/stop inauthentication.feature. - Two genuinely new features —
sharing/share-links.featureandsecurity/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.featurecovering 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
Thenclauses. 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
Thenin the CLAUDE.md scenario (“AI coworker sessions in this repo should automatically prime with team context”) overshoots intoagent-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
Whenverb 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
Thenon 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”).
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 action | Walks | Notes |
|---|---|---|
upload-a-recording.md | 06, 10, 11 | Composite: initiate → S3 PUT → confirm; one fast verb wraps all three. Drives the recording-journey Outline rows for audio/video upload. |
record-live-discussion.md | — | Web-recorder live-capture path; drives the recording-journey Outline's “live recording” row. |
paste-a-transcript.md | — | Paste-transcript capture path; drives the paste-transcript sister scenario. |
wait-for-discussion-ready.md | 14 | Poll /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.md | 06, 11.1 | Navigate to /team/{id}/media/recordings/{id} |
import-content.md | — | ox 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.md | 07 (deferred) | CLI + slash-command variants; drives the session-lifecycle Outline rows. Currently deferred (Walk 07 stubbed pending TUI driver maturity). |
make-team-public.md | 03 | Settings → Make public → confirm dialog → type name → confirm |
generate-invite-link.md | 03 | Click Invite → extract /invite/… URL |
join-team-via-invite.md | 04 | GET invite URL → accept → assert membership |
change-team-section.md | 04, 05 | Navigate to /team/{id}/{section}; reusable across visibility scenarios |
start-ox-daemon.md / stop-ox-daemon.md | 03, 09 | CLI verbs |
logout-ox-cli.md | 02, 09 | ox logout -f |
create-share-link.md | 15 | POST /api/v1/teams/{id}/shares with audience / maxUses / expiresInSeconds |
resolve-share-link.md | 15 | GET /api/v1/shares/{token}/resolve (anon or as B-auth) |
revoke-share-link.md | 15 | DELETE /api/v1/shares/{id} |
regenerate-share-link.md | 15 | POST /api/v1/shares/{id}/regenerate |
New verifications
| Verification | Walks |
|---|---|
team-be-public.md | 03, visibility rule |
discussion-be-ready-in-list.md | 06, 10, 11, 14 |
discussion-has-playable-media.md | 06, 11.1 — parameterised over {media_kind, expected_mime}; checks element exists, src non-empty, no error state, MIME round-trips |
discussion-has-transcript.md | 06, 14 — transcript content visible; speaker labels + timestamps |
discussion-has-summary.md | 14 — AI-generated summary present |
session-be-committed-to-ledger.md | 07 (deferred) — drives the session-lifecycle Thens |
page-be-free-of-pii.md | 12 |
api-response-be-free-of-pii.md | 12 |
share-resolve-status-be.md | 15 — parameterized: 2xx / 4xx / not-5xx |
ledger-page-renders.md | 13 |
Driver verbs to add
drivers/cli/ox/
logout.ts02, 09status.ts02, 03, 09 — parses output to{authenticated, daemon}daemon/start.ts,daemon/stop.ts03, 09
drivers/web/team/
make-public.ts03 — opens settings, clicks Make public, fills confirm dialogget-invite-link.ts03 — opens Invite dialog, extracts URLjoin-via-invite.ts04navigate-section.ts04, 05 —{teamId, section}→ goto + assert no Access Requiredview-settings.ts05 — returns{accessible: boolean}
drivers/web/recording/ (new directory)
initiate-upload.ts,put-to-s3.ts,confirm-upload.ts06, 10, 11upload-composite.ts06, 10, 11 — wraps the three above; this is the verb the business action callsrecord-live.tscapture journey — drives the browser web-recorder for the live-recording journey row. Needspermissions: ['microphone']on the BrowserContext.paste-transcript.tscapture journey — submits a transcript via the paste-text UI; drives the paste-transcript sister scenario.get-status.ts06, 11, 14 — also returnsmime_typefor the journey MIME assertion.wait-for-ready.ts14 — polls with{intervalMs, timeoutMs}untilstatus=ready.view-detail.ts06, 11.1 — returns{statusOk, hasMediaElement, mediaKind, mediaSrc, mediaError, hasSummary}.check-transcript.ts06, 14, 11.1 — clicks Transcript tab, asserts content length, speaker labels, timestamps.
drivers/cli/ox/import/ (new directory)
import-url.tsimport journey —ox import <url>; supports Loom + direct URLs; returns{importId}.import-file.tsimport journey —ox import <path>; supports glob batch.wait-for-import-ready.tsimport journey — polls/imports/{id}untilstatus=completed. Could share implementation withweb/recording/wait-for-readyif the status surfaces are isomorphic.
drivers/tui/session/ (deferred until Walk 07 lands)
start-via-slash.ts,start-via-cli.tssession lifecycle — drives/ox-session-startthrough the TUI vsox session startfrom the shell. Two rows in the lifecycle Outline.stop.tssession lifecycle — drives/ox-session-stoporox session stop; waits for upload-to-ledger to settle.verify-in-ledger.ts,verify-on-sessions-page.ts,verify-on-repo-detail.tssession lifecycle — drive the three visibilityThenclauses.
drivers/web/share/ (new directory)
create.ts15 — POST with{entity_type, entity_id, audience, maxUses?, expiresInSeconds?}resolve.ts15 — optional cookie header; returns{status, body, redirectUrl}revoke.ts15regenerate.ts15
drivers/web/security/ (new directory) — or shared util
scan-pii.ts12 — runs in a Stagehand page; returns{emails: [], userIds: [], attrs: []}scan-api-pii.ts12 — fetch + body text scan; same shape
drivers/web/ledger/
view.ts13 —page.goto /ledger, returns{rendered, teamLinks, contentLength}
- 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
registerBauthUserin 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
| Cluster | New business actions introduced | Scenarios unblocked | Depends on |
|---|---|---|---|
| Recording journey highest fan-out |
upload-a-recordingrecord-live-discussionpaste-a-transcriptwait-for-discussion-readyview-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-publicinvite-someone-to-teamjoin-team-via-invitechange-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-daemonlogout-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-linkresolve-share-linkrevoke-share-linkregenerate-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-piiscan-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-contentwait-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)
-
Smoke wiring
Once the high-fan-out clusters have landed, add
tests/acceptance/smoke/smoke-recording-pipeline.tschaining 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. -
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
/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.
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.