Feat/Rename /goal → /hunt with HuntVerdict + trophy cards (#2092)#2306
Conversation
There was a problem hiding this comment.
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.
| 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}")) | ||
| } |
There was a problem hiding this comment.
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}"))
}| Some("wound") | Some("wounded") => { | ||
| app.goal.verdict = HuntVerdict::Wounded; | ||
| let _ = write_trophy_card(app); | ||
| CommandResult::message("Hunt wounded — progress saved, can be resumed.") | ||
| } |
There was a problem hiding this comment.
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.")
}| Some("escape") | Some("escaped") => { | ||
| app.goal.verdict = HuntVerdict::Escaped; | ||
| let _ = write_trophy_card(app); | ||
| CommandResult::message("Hunt escaped — quarry abandoned.") | ||
| } |
There was a problem hiding this comment.
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). | |||
|
|
||
| /// 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> { |
There was a problem hiding this comment.
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.
| 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> { |
| pub viewport: ViewportState, | ||
| /// Goal sub-state. | ||
| pub goal: GoalState, | ||
| pub goal: HuntState, |
There was a problem hiding this comment.
| }, | ||
| viewport: ViewportState::default(), | ||
| goal: GoalState::default(), | ||
| goal: HuntState::default(), |
|
Want your agent to iterate on Greptile's feedback? Try greploops. |
|
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 |
|
Thanks @idling11, this is close in spirit and the The main blockers are: verdict commands should require an active quarry, Could you also update the stale user-facing surfaces ( |
|
Thanks again @idling11. I pushed one more maintainer follow-up after the rescue commit because current Maintainer verification on the refreshed head (
CI is running again now. |
|
One last maintainer follow-up pushed on top: Fresh local verification on this final head:
CI is running again. Thanks @idling11 — this is now much tighter. |
| let prev = app.hunt.verdict; | ||
| let should_write_trophy = prev != verdict || !matches!(verdict, HuntVerdict::Hunted); |
There was a problem hiding this comment.
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.
| 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; |
Summary
Rename
/goalto/hunt, introduce four verdict states, and generatetrophy cards on completion so interrupted sessions can be recovered.
Closes: #2092
Changes
Core data model
GoalState→HuntState(fields:quarry,token_budget,started_at,verdict)HuntVerdictenum:Hunting,Hunted,Wounded,EscapedCommand
/goal→/hunt(old aliasesgoal/mubiaopreserved)/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 huntTrophy cards
~/.codewhale/trophies/<date>-<slug>.mdUpdated files
crates/tui/src/tui/app.rscrates/tui/src/commands/goal.rscrates/tui/src/commands/mod.rscrates/tui/src/tui/ui.rscrates/tui/src/tui/sidebar.rscrates/tui/src/prompts.rsTests
Greptile Summary
This PR renames the
/goalcommand to/hunt, replacesGoalStatewithHuntState, and introduces aHuntVerdictenum (Hunting,Hunted,Wounded,Escaped) with trophy-card generation on completion so interrupted sessions can be recovered.GoalState→HuntStatewithquarry,token_budget,started_at,verdict;HuntVerdictisSerialize/Deserialize-ready for machine-readable trophy cards written to~/.codewhale/trophies/<date>-<time>-<slug>.md./hunt hunted | wounded | escaped | cleareach handled inclose_hunt; old aliasesgoalandmubiaopreserved alongside new狩猎.sidebar.rs,ui.rs, andprompts.rsupdated mechanically;Wounded/Escapedmap togoal_completed = falsein 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
/huntcommand rewrite — introducesclose_hunt,write_trophy_card, andTrophyCard. Theshould_write_trophyguard inclose_huntis asymmetric and causes duplicate trophy files on repeatedwounded/escapedcalls.HuntVerdictenum andHuntStatestruct replacingGoalState.HuntVerdictderivesSerialize/Deserializein preparation for trophy-card machine-reading.huntfields (including newverdictandtoken_budget) when a user command resets state. Tests updated and expanded with verdict/budget assertions.app.huntfields onto the existingSidebarWorkSummaryboolean API;Wounded/Escapedmap togoal_completed = false— semantically reasonable.app.goal.*toapp.hunt.*; straightforward mechanical rename.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"| NoneReviews (6): Last reviewed commit: "fix: reset hunt state for user commands" | Re-trigger Greptile