Skip to content

Feat/Rename /goal → /hunt with HuntVerdict + trophy cards (#2092)#2306

Merged
Hmbown merged 9 commits into
Hmbown:mainfrom
idling11:feat/goal-to-hunt
May 31, 2026
Merged

Feat/Rename /goal → /hunt with HuntVerdict + trophy cards (#2092)#2306
Hmbown merged 9 commits into
Hmbown:mainfrom
idling11:feat/goal-to-hunt

Conversation

@idling11

@idling11 idling11 commented May 28, 2026

Copy link
Copy Markdown
Contributor

Summary

Rename /goal to /hunt, introduce four verdict states, and generate
trophy cards on completion so interrupted sessions can be recovered.

Closes: #2092

Changes

Core data model

  • GoalStateHuntState (fields: quarry, token_budget, started_at, verdict)
  • New HuntVerdict enum: Hunting, Hunted, Wounded, Escaped

Command

  • /goal/hunt (old aliases goal / mubiao preserved)
  • /hunt hunted — mark complete, write trophy card
  • /hunt wounded — mark interrupted (resumable), write trophy card
  • /hunt escaped — mark abandoned, write trophy card
  • /hunt clear — remove current hunt
  • /hunt <quarry> [budget: N] — declare a new hunt

Trophy cards

  • Written to ~/.codewhale/trophies/<date>-<slug>.md
  • Contains verdict, elapsed time, token usage, timestamp
  • Machine-readable for the next session to pick up

Updated files

File Change
crates/tui/src/tui/app.rs HuntVerdict + HuntState definition
crates/tui/src/commands/goal.rs /hunt command + trophy card + tests
crates/tui/src/commands/mod.rs Command registration + routing
crates/tui/src/tui/ui.rs Field references updated
crates/tui/src/tui/sidebar.rs Field references updated
crates/tui/src/prompts.rs "Goal" → "Hunt" in system prompt

Tests

  • 6/6 hunt command tests pass
  • 3444/3448 full test suite pass (4 pre-existing failures)

Greptile Summary

This PR renames the /goal command to /hunt, replaces GoalState with HuntState, and introduces a HuntVerdict enum (Hunting, Hunted, Wounded, Escaped) with trophy-card generation on completion so interrupted sessions can be recovered.

  • Data model: GoalStateHuntState with quarry, token_budget, started_at, verdict; HuntVerdict is Serialize/Deserialize-ready for machine-readable trophy cards written to ~/.codewhale/trophies/<date>-<time>-<slug>.md.
  • Command surface: /hunt hunted | wounded | escaped | clear each handled in close_hunt; old aliases goal and mubiao preserved alongside new 狩猎.
  • UI / prompt wiring: All call-sites in sidebar.rs, ui.rs, and prompts.rs updated mechanically; Wounded/Escaped map to goal_completed = false in the sidebar, keeping the boolean API intact.

Confidence Score: 4/5

Safe to merge after fixing the duplicate-trophy guard in close_hunt; all other changes are mechanical renames.

The should_write_trophy condition in close_hunt is asymmetric: calling /hunt wounded or /hunt escaped a second time always writes another trophy file, because the !matches!(verdict, HuntVerdict::Hunted) term short-circuits the equality check for non-Hunted verdicts. Every extra trophy for the same hunt and quarry degrades the session-recovery feature the PR introduces. The rest of the change is a straightforward field rename across the codebase with no other logic concerns.

crates/tui/src/commands/goal.rs — the close_hunt function's trophy-write guard needs correction before the session-recovery feature behaves correctly.

Important Files Changed

Filename Overview
crates/tui/src/commands/goal.rs Core /hunt command rewrite — introduces close_hunt, write_trophy_card, and TrophyCard. The should_write_trophy guard in close_hunt is asymmetric and causes duplicate trophy files on repeated wounded/escaped calls.
crates/tui/src/commands/mod.rs Routing updated to dispatch `goal
crates/tui/src/tui/app.rs Introduces HuntVerdict enum and HuntState struct replacing GoalState. HuntVerdict derives Serialize/Deserialize in preparation for trophy-card machine-reading.
crates/tui/src/commands/user_commands.rs Updated to clear hunt fields (including new verdict and token_budget) when a user command resets state. Tests updated and expanded with verdict/budget assertions.
crates/tui/src/tui/sidebar.rs Maps app.hunt fields onto the existing SidebarWorkSummary boolean API; Wounded/Escaped map to goal_completed = false — semantically reasonable.
crates/tui/src/tui/ui.rs Field references updated from app.goal.* to app.hunt.*; straightforward mechanical rename.
crates/tui/src/prompts.rs Section header renamed from "Current Session Goal" to "Current Hunt" in system prompt template; test assertion updated to match.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    None["quarry = None\nverdict = Hunting (default)"]
    Hunting["quarry = Some(…)\nverdict = Hunting"]
    Hunted["quarry = Some(…)\nverdict = Hunted\n✅ trophy written"]
    Wounded["quarry = Some(…)\nverdict = Wounded\n📄 trophy written"]
    Escaped["quarry = Some(…)\nverdict = Escaped\n🏃 trophy written"]

    None -->|"/hunt quarry"| Hunting
    Hunting -->|"/hunt hunted"| Hunted
    Hunting -->|"/hunt wounded"| Wounded
    Hunting -->|"/hunt escaped"| Escaped
    Hunted -->|"/hunt wounded"| Wounded
    Hunted -->|"/hunt escaped"| Escaped
    Wounded -->|"/hunt wounded ⚠️ writes 2nd trophy"| Wounded
    Wounded -->|"/hunt hunted"| Hunted
    Escaped -->|"/hunt escaped ⚠️ writes 2nd trophy"| Escaped
    Hunting -->|"/hunt clear"| None
    Hunted -->|"/hunt clear"| None
    Wounded -->|"/hunt clear"| None
    Escaped -->|"/hunt clear"| None
Loading

Fix All in Codex Fix All in Claude Code Fix All in Cursor

Reviews (6): Last reviewed commit: "fix: reset hunt state for user commands" | Re-trigger Greptile

Greptile also left 1 inline comment on this PR.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request renames the /goal command to /hunt (and associated structures from GoalState to HuntState), introducing tracking for hunt verdicts (Hunting, Hunted, Wounded, Escaped) and writing trophy cards to disk upon completing, wounding, or escaping a hunt. The reviewer feedback highlights several important improvements: adding checks to ensure an active hunt exists before marking it as complete, wounded, or escaped; handling errors when writing trophy cards instead of silently ignoring them; renaming the goal.rs file and the goal field on the App struct to hunt for consistency; and changing write_trophy_card to take an immutable &App reference.

Comment thread crates/tui/src/commands/goal.rs Outdated
Comment on lines +19 to +35
Some("done") | Some("complete") | Some("hunted") => {
let prev = app.goal.verdict;
app.goal.verdict = HuntVerdict::Hunted;
let elapsed = app
.goal
.goal_started_at
.started_at
.map(|t| crate::tui::notifications::humanize_duration(t.elapsed()))
.unwrap_or_else(|| "unknown".to_string());
CommandResult::message(format!("Goal marked complete! Elapsed: {elapsed}"))
if prev != HuntVerdict::Hunted {
if let Err(e) = write_trophy_card(app) {
return CommandResult::error(format!(
"Hunt complete but trophy write failed: {e}"
));
}
}
CommandResult::message(format!("Hunt complete! Elapsed: {elapsed}"))
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

If there is no active hunt (i.e., app.goal.quarry is None), marking a hunt as complete will write a trophy card with the name "untitled" and print a success message. We should guard against this by returning an error if no active hunt exists.

        Some("done") | Some("complete") | Some("hunted") => {
            if app.goal.quarry.is_none() {
                return CommandResult::error("No active hunt to mark complete.");
            }
            let prev = app.goal.verdict;
            app.goal.verdict = HuntVerdict::Hunted;
            let elapsed = app
                .goal
                .started_at
                .map(|t| crate::tui::notifications::humanize_duration(t.elapsed()))
                .unwrap_or_else(|| "unknown".to_string());
            if prev != HuntVerdict::Hunted {
                if let Err(e) = write_trophy_card(app) {
                    return CommandResult::error(format!(
                        "Hunt complete but trophy write failed: {e}"
                    ));
                }
            }
            CommandResult::message(format!("Hunt complete! Elapsed: {elapsed}"))
        }

Comment thread crates/tui/src/commands/goal.rs Outdated
Comment on lines +36 to +40
Some("wound") | Some("wounded") => {
app.goal.verdict = HuntVerdict::Wounded;
let _ = write_trophy_card(app);
CommandResult::message("Hunt wounded — progress saved, can be resumed.")
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Ignoring the error from write_trophy_card silently using let _ = ... can lead to a poor user experience if the trophy file fails to write (e.g., due to permissions or disk space issues). Additionally, we should ensure there is an active hunt before marking it as wounded.

        Some("wound") | Some("wounded") => {
            if app.goal.quarry.is_none() {
                return CommandResult::error("No active hunt to mark wounded.");
            }
            app.goal.verdict = HuntVerdict::Wounded;
            if let Err(e) = write_trophy_card(app) {
                return CommandResult::error(format!(
                    "Hunt wounded but trophy write failed: {e}"
                ));
            }
            CommandResult::message("Hunt wounded — progress saved, can be resumed.")
        }

Comment thread crates/tui/src/commands/goal.rs Outdated
Comment on lines 41 to 45
Some("escape") | Some("escaped") => {
app.goal.verdict = HuntVerdict::Escaped;
let _ = write_trophy_card(app);
CommandResult::message("Hunt escaped — quarry abandoned.")
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Ignoring the error from write_trophy_card silently using let _ = ... can lead to a poor user experience if the trophy file fails to write. Additionally, we should ensure there is an active hunt before marking it as escaped.

        Some("escape") | Some("escaped") => {
            if app.goal.quarry.is_none() {
                return CommandResult::error("No active hunt to mark escaped.");
            }
            app.goal.verdict = HuntVerdict::Escaped;
            if let Err(e) = write_trophy_card(app) {
                return CommandResult::error(format!(
                    "Hunt escaped but trophy write failed: {e}"
                ));
            }
            CommandResult::message("Hunt escaped — quarry abandoned.")
        }

@@ -1,61 +1,75 @@
//! /goal command — set a session objective with token budget and progress tracking.
//! /hunt command — declare a quarry with token budget and verdict tracking (#2092).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Since the /goal command has been renamed to /hunt, and the associated data structures have been renamed from GoalState to HuntState, the file goal.rs should be renamed to hunt.rs to maintain consistency and improve project maintainability.

Comment thread crates/tui/src/commands/goal.rs Outdated

/// Write a trophy card to `~/.codewhale/trophies/<date>-<slug>.md` for the
/// current hunt verdict (#2092). Returns the path written on success.
fn write_trophy_card(app: &mut App) -> Result<std::path::PathBuf, std::io::Error> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The write_trophy_card function only reads from app and does not perform any mutations. It should take an immutable reference app: &App instead of a mutable one app: &mut App to follow Rust best practices and clarify the function's intent.

Suggested change
fn write_trophy_card(app: &mut App) -> Result<std::path::PathBuf, std::io::Error> {
fn write_trophy_card(app: &App) -> Result<std::path::PathBuf, std::io::Error> {

Comment thread crates/tui/src/tui/app.rs Outdated
pub viewport: ViewportState,
/// Goal sub-state.
pub goal: GoalState,
pub goal: HuntState,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

To align with the renaming of GoalState to HuntState and /goal to /hunt, the goal field on the App struct should be renamed to hunt (i.e., pub hunt: HuntState). This will prevent confusion and improve code readability.

Suggested change
pub goal: HuntState,
pub hunt: HuntState,

Comment thread crates/tui/src/tui/app.rs Outdated
},
viewport: ViewportState::default(),
goal: GoalState::default(),
goal: HuntState::default(),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Rename the goal field initialization to hunt to match the struct field rename.

Suggested change
goal: HuntState::default(),
hunt: HuntState::default(),

Comment thread crates/tui/src/commands/goal.rs Outdated
Comment thread crates/tui/src/commands/goal.rs
Comment thread crates/tui/src/commands/goal.rs Outdated
Comment thread crates/tui/src/tui/app.rs
Comment thread crates/tui/src/commands/goal.rs Outdated
@greptile-apps

greptile-apps Bot commented May 28, 2026

Copy link
Copy Markdown
Contributor

Want your agent to iterate on Greptile's feedback? Try greploops.

@Hmbown

Hmbown commented May 31, 2026

Copy link
Copy Markdown
Owner

Thanks @idling11 — the hunt metaphor has a lot of charm and it is very much in the neighborhood of the product voice. I’m leaving this open for a focused fix pass rather than merging it green-but-rough.

The main thing I’d want tightened before this lands: don’t tell the user progress was saved if the trophy-card write fails, don’t allow hunted / wounded / escaped with no active quarry, keep the /goal alias/deprecation path explicit, and make sure wounded/escaped do not write trophies unless that contract is deliberate. Once those are covered, cargo test -p codewhale-tui hunt -- --nocapture plus targeted no-active-hunt/write-failure/alias tests should make this much easier to land.

@Hmbown

Hmbown commented May 31, 2026

Copy link
Copy Markdown
Owner

Thanks @idling11, this is close in spirit and the /hunt vocabulary fits the product voice well. I’m holding this before merge because it does not yet satisfy #2092’s acceptance contract and would auto-close that issue too early.

The main blockers are: verdict commands should require an active quarry, /hunt wounded <reason> and /hunt escaped <reason> need to parse as verdicts instead of new quarry text, wounded/escaped should not write trophy cards unless we deliberately change #2092, trophy write failures should not be reported as success, and the current goal-tool bridge needs a clear mapping for wounded/escaped instead of treating them as still-active goals.

Could you also update the stale user-facing surfaces (/relay labels, command descriptions/localization/docs) and add focused tests for no-active-hunt, reason-bearing verdicts, write failure, /goal alias/deprecation, and trophy structure? Once those are covered, this should be much easier to land cleanly.

@Hmbown

Hmbown commented May 31, 2026

Copy link
Copy Markdown
Owner

Thanks again @idling11. I pushed one more maintainer follow-up after the rescue commit because current main brought in user command frontmatter metadata that still referenced the old app.goal fields. That now writes the description metadata to app.hunt.quarry / app.hunt.started_at, so /hunt stays consistent even for workspace-local slash commands.

Maintainer verification on the refreshed head (91d0921):

  • cargo fmt --all -- --check
  • git diff --check
  • python3 scripts/check-provider-registry.py
  • cargo test -p codewhale-tui commands::goal::tests -- --nocapture
  • cargo test -p codewhale-tui user_commands -- --nocapture
  • cargo check -p codewhale-tui --all-features --locked

CI is running again now.

@Hmbown

Hmbown commented May 31, 2026

Copy link
Copy Markdown
Owner

One last maintainer follow-up pushed on top: 137e4e61 resets app.hunt.verdict and app.hunt.token_budget when a workspace-local user command starts/clears hunt metadata, so frontmatter-driven hunts cannot inherit an old Escaped/Wounded state or token budget.

Fresh local verification on this final head:

  • cargo fmt --all -- --check
  • git diff --check
  • python3 scripts/check-provider-registry.py
  • cargo test -p codewhale-tui commands::goal::tests -- --nocapture
  • cargo test -p codewhale-tui user_commands -- --nocapture
  • cargo check -p codewhale-tui --all-features --locked

CI is running again. Thanks @idling11 — this is now much tighter.

Comment on lines +86 to +87
let prev = app.hunt.verdict;
let should_write_trophy = prev != verdict || !matches!(verdict, HuntVerdict::Hunted);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 The should_write_trophy guard is asymmetric: for Hunted it evaluates to prev != Hunted (idempotent — skips the write if already hunted), but for Wounded and Escaped the !matches!(verdict, HuntVerdict::Hunted) term is always true, so the whole expression is always true. Concretely, calling /hunt wounded a second time writes a second .md trophy file (with a distinct timestamp in the name). If this session is later auto-resumed, the recovery logic sees two wounded trophies for the same quarry and must choose between them, with no signal about which is authoritative. The intent of the guard is clearly "skip re-writing when the verdict hasn't changed"; the simplest correct form is prev != verdict.

Suggested change
let prev = app.hunt.verdict;
let should_write_trophy = prev != verdict || !matches!(verdict, HuntVerdict::Hunted);
let prev = app.hunt.verdict;
let should_write_trophy = prev != verdict;

Fix in Codex Fix in Claude Code Fix in Cursor

@Hmbown Hmbown merged commit 548771d into Hmbown:main May 31, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Rename /goal/hunt: quarry, verdict vocabulary, trophy card writer

2 participants