Skip to content

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

Open
idling11 wants to merge 5 commits into
Hmbown:mainfrom
idling11:feat/goal-to-hunt
Open

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

Conversation

@idling11
Copy link
Copy Markdown
Contributor

@idling11 idling11 commented May 28, 2026

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 /goal/hunt, introduces the HuntVerdict enum (Hunting, Hunted, Wounded, Escaped), replaces GoalState with HuntState, and adds a trophy-card writer that persists hunt outcomes to ~/.codewhale/trophies/<date>-<time>-<slug>.md. Old aliases (goal, mubiao) are preserved and a new CJK alias (狩猎) is added.

  • Data model (app.rs): GoalStateHuntState with quarry, token_budget, started_at, verdict; HuntVerdict gains serde derives for machine-readable trophy cards.
  • Command (goal.rs): four new verdict arms (hunted/wounded/escaped/clear) plus the trophy-card writer with time-based filenames and dash-collapsing slug logic.
  • Rename propagation (ui.rs, sidebar.rs, prompts.rs, mod.rs): field accesses and system-prompt heading updated throughout; relay instruction labels in mod.rs still use the old "Goal:" / "Goal token budget:" strings.

Confidence Score: 3/5

The rename is mechanically sound, but several issues flagged in prior review rounds remain unaddressed in goal.rs.

The trophy-card writer silently swallows write errors on the wounded and escaped paths while still telling the user progress saved, can be resumed — a direct contradiction. The verdict mutation arms also do not guard against an absent quarry, leaving the app in an inconsistent state. These functional gaps in the new code paths warrant a second look before merging.

crates/tui/src/commands/goal.rs — the wounded/escaped verdict arms and write_trophy_card error handling need attention before this is production-ready.

Important Files Changed

Filename Overview
crates/tui/src/commands/goal.rs Core /hunt command implementation: new verdict arms, trophy card writer, and updated tests. Several unresolved issues from previous review threads remain (silent error discarding, verdict mutation on absent hunts).
crates/tui/src/commands/mod.rs Command routing updated to alias goal/hunt/mubiao/狩猎 to the new handler; relay instruction field references updated but the human-readable labels still say "Goal:" / "Goal token budget:" rather than the renamed terminology.
crates/tui/src/tui/app.rs GoalState renamed to HuntState; HuntVerdict enum added with serde derives; goal field renamed to hunt on App. Clean mechanical rename with no logic changes.
crates/tui/src/prompts.rs System prompt section header renamed from "Current Session Goal" to "Current Hunt"; corresponding test assertion updated. Minimal, correct change.
crates/tui/src/tui/sidebar.rs Field access updated to use app.hunt; goal_completed now mapped as verdict == Hunted. Wounded and Escaped do not set goal_completed, which may leave the sidebar showing a non-complete state for those terminal verdicts.
crates/tui/src/tui/ui.rs Field references mechanically updated from app.goal to app.hunt; engine config now derives goal_completed as verdict == Hunted. Straightforward rename.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["/hunt &lt;quarry&gt;"] -->|sets quarry + verdict=Hunting| B[HuntState: Hunting]
    B -->|/hunt hunted| C[HuntState: Hunted]
    B -->|/hunt wounded| D[HuntState: Wounded]
    B -->|/hunt escaped| E[HuntState: Escaped]
    C -->|write_trophy_card| F[trophy .md file]
    D -->|write_trophy_card| F
    E -->|write_trophy_card| F
    C -->|/hunt clear| G[HuntState: reset]
    D -->|/hunt clear| G
    E -->|/hunt clear| G
    G -->|verdict = Hunting default| B
    B -->|/hunt &lt;quarry&gt; again| B
Loading

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

Reviews (3): Last reviewed commit: "fix: remove unused HuntVerdict import in..." | Re-trigger Greptile

Copy link
Copy Markdown
Contributor

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

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 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 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 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
Comment thread crates/tui/src/commands/goal.rs Outdated
Comment thread crates/tui/src/commands/goal.rs Outdated
Comment thread crates/tui/src/tui/app.rs
Comment on lines +1000 to +1003
impl Default for HuntVerdict {
fn default() -> Self {
Self::Hunting
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 HuntVerdict::default() is Hunting even when no hunt is active

After /hunt clear the code resets verdict to HuntVerdict::default() which is Hunting. This means app.goal.verdict == HuntVerdict::Hunting cannot distinguish "actively hunting" from "no hunt set" — both states look identical. If any future code branches on the verdict without also checking app.goal.quarry.is_some(), it will misread a cleared state as an active one.

Fix in Codex Fix in Claude Code Fix in Cursor

Comment on lines +32 to 41
Some("wound") | Some("wounded") => {
app.hunt.verdict = HuntVerdict::Wounded;
write_trophy_card(app);
CommandResult::message("Hunt wounded — progress saved, can be resumed.")
}
Some("escape") | Some("escaped") => {
app.hunt.verdict = HuntVerdict::Escaped;
write_trophy_card(app);
CommandResult::message("Hunt escaped — quarry abandoned.")
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Verdict commands operate on absent hunts, corrupting state and lying to users

The wounded, escaped, and hunted arms update app.hunt.verdict before calling write_trophy_card, but neither arm checks that app.hunt.quarry is Some first. When called with no active hunt, write_trophy_card returns None immediately (nothing is written), yet the state is left with verdict = Wounded (or Escaped/Hunted) and quarry = None. A subsequent /hunt query then hits the else branch and prints "No hunt set." — contradicting the verdict that was just silently applied. The wounded arm's confirmation message "progress saved, can be resumed." is especially misleading because the write was a no-op and no progress exists to resume.

Fix in Codex Fix in Claude Code Fix in Cursor

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented May 28, 2026

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

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

1 participant