feat(p1): visible next-frontier per session (acceptance #19) + audit refresh#31
Conversation
…doc closure header Closes audit acceptance #19: 'Every play session ends with at least one visible unresolved next-step frontier.' The TUI now appends a 'Next: <frontier>' line to KingdomView so the player always has a reason to come back. bridge/frontier.py: - next_frontier(state) — pure function returning a single short medieval-voice string. Priority order: redirected (acute) > waiting (input expected) > inactive (registered but quiet) > active near momentum-tier-up > active general > all-suspended invite. Empty state gets a 'found your first city' onboarding nudge. - Always non-empty so acceptance #19's 'at least one visible' guarantee is enforced by construction. - frontier_priority_kind(state) — stable identifier for telemetry + tests; mirrors the branch tree of next_frontier. tui/app.py — KingdomView.render_state appends a 'Next: ...' line at the bottom on every refresh. bridge/tests/test_frontier.py — 16 tests: - Empty state → 'first city' frontier; all-suspended → chronicles - Priority: redirected > active; waiting > inactive; inactive > active - Active near tier-up calls out exact gap (e.g. '2 from') - Active far from tier uses generic message - Active at exact tier (gap=10) uses generic - Highest-momentum active session is chosen as the frontier - Every SessionStatus value yields a non-empty string (parametrized over SessionStatus) - Session name appears in output docs/audit-2026-05-08.md — header update: every P0 and P1 line item identified in the audit is now closed (PR refs listed). Original grid preserved as the historical baseline so the journey is auditable. Total: 250 passing (was 234; adds 16). Closes audit P1: acceptance #19. Refs: PR #22, kanban t_26404be3. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request implements the 'next-frontier' suggestion system to satisfy PRD acceptance criterion #19, ensuring players always have a clear next step. The implementation includes a new prioritization logic for session states, medieval-themed string generation, and TUI integration. Feedback includes removing unused imports in bridge/frontier.py, refactoring duplicated logic between next_frontier and frontier_priority_kind into a shared helper to reduce maintenance overhead, and updating the audit documentation to explicitly reference this PR's contribution to the project's goals.
| from typing import Optional | ||
|
|
||
| from bridge.session_state import Session, SessionState, SessionStatus |
There was a problem hiding this comment.
The imports Optional and Session are unused in this file. Removing them keeps the code clean and adheres to standard Python practices.
| from typing import Optional | |
| from bridge.session_state import Session, SessionState, SessionStatus | |
| from bridge.session_state import SessionState, SessionStatus |
| def next_frontier(state: SessionState) -> str: | ||
| """Return one short medieval-voice 'come back to do X' string.""" | ||
| sessions = state.sessions | ||
|
|
||
| if not sessions: | ||
| return "Found your first city — open a Claude Code session and the realm begins." | ||
|
|
||
| # Priority order: redirected (acute, transient) > waiting (input expected) | ||
| # > inactive (registered but quiet) > momentum-buildup > momentum-tier-up | ||
| # > goal-pending > generic come-back. | ||
|
|
||
| # 1) Any redirected session — surface that first; momentum is on the line. | ||
| redirected = [s for s in sessions if s.status == SessionStatus.REDIRECTED] | ||
| if redirected: | ||
| s = redirected[0] | ||
| return f"'{s.name}' is redirected — return to rebuild momentum." | ||
|
|
||
| # 2) Waiting sessions — the agent expects input, this is the warmest re-entry. | ||
| waiting = [s for s in sessions if s.status == SessionStatus.WAITING] | ||
| if waiting: | ||
| s = waiting[0] | ||
| return f"'{s.name}' awaits your council. The next charter starts with you." | ||
|
|
||
| # 3) Inactive sessions — registered but never engaged. | ||
| inactive = [s for s in sessions if s.status == SessionStatus.INACTIVE] | ||
| if inactive: | ||
| s = inactive[0] | ||
| return f"'{s.name}' is registered but quiet — open the session to begin." | ||
|
|
||
| # 4) Active session about to tier up its momentum (next +1 culture). | ||
| actives = [s for s in sessions if s.status == SessionStatus.ACTIVE] | ||
| if actives: | ||
| s = max(actives, key=lambda x: x.momentum) | ||
| next_tier = ((s.momentum // 10) + 1) * 10 | ||
| gap = next_tier - s.momentum | ||
| if 0 < gap <= 3: | ||
| return f"'{s.name}' is {gap} from the next culture tier." | ||
| # 5) Generic come-back for active sessions. | ||
| return f"'{s.name}' is at work; return to claim the next charter." | ||
|
|
||
| # 6) All suspended (game-closed) — give a return invite. | ||
| return "The chronicles preserve your work. Return when you're ready." | ||
|
|
||
|
|
||
| def frontier_priority_kind(state: SessionState) -> str: | ||
| """Return a stable identifier for which frontier kind fired. | ||
|
|
||
| Useful for testing + telemetry. Mirrors next_frontier's branches. | ||
| """ | ||
| sessions = state.sessions | ||
| if not sessions: | ||
| return "first_city" | ||
| if any(s.status == SessionStatus.REDIRECTED for s in sessions): | ||
| return "redirected" | ||
| if any(s.status == SessionStatus.WAITING for s in sessions): | ||
| return "waiting" | ||
| if any(s.status == SessionStatus.INACTIVE for s in sessions): | ||
| return "inactive" | ||
| actives = [s for s in sessions if s.status == SessionStatus.ACTIVE] | ||
| if actives: | ||
| s = max(actives, key=lambda x: x.momentum) | ||
| next_tier = ((s.momentum // 10) + 1) * 10 | ||
| gap = next_tier - s.momentum | ||
| if 0 < gap <= 3: | ||
| return "momentum_tier_up_imminent" | ||
| return "active_general" | ||
| return "all_suspended" |
There was a problem hiding this comment.
There is significant logic duplication between next_frontier and frontier_priority_kind, particularly in the session filtering and the momentum tier calculation for active sessions. This increases maintenance overhead and the risk of logic drift if the priority rules or tier thresholds change.
Consider refactoring the shared logic into a private helper function (e.g., _get_frontier_info) that returns the relevant session and a category identifier. This would also allow for a more efficient implementation that avoids multiple list comprehensions over the sessions list by using a single-pass search.
| > **Closure update — every P0 and P1 line item identified in this audit | ||
| > is now closed.** PR refs: #23 (P0 #1 enum), #24 (P0 #3 Stop→COMPLETED), | ||
| > #25 (P0 #2 schema), #26 (BR-9), #27 (#11 + #12), #28 (GM-8), #29 | ||
| > (GM-5), #30 (#9 + #20). The original audit grid below is preserved | ||
| > as the historical baseline. |
There was a problem hiding this comment.
The 'Closure update' block states that every P0 and P1 item is now closed, but it does not include a reference to the current PR or explicitly mention the closure of acceptance criterion #19 (which this PR addresses). Adding this reference would ensure the audit documentation accurately reflects the current state of the project and provides a complete audit trail.
Closes audit P1 acceptance #19. TUI KingdomView always shows 'Next: ' at the bottom — a one-line medieval-voice prompt giving the player a reason to return. 16 new tests; 250 total. Audit doc header refreshed with closure status (every P0 and P1 from the original audit is now closed). Refs PR #22, kanban t_26404be3.