Skip to content

Commit 385dc74

Browse files
danielkovclaude
andauthored
feat: review-mode workflow and fix review gate edge cases (#26)
* docs: review mode plan * feat: add review mode workflow and fix review gate edge cases * fix: clippy warnings * feat: add review mode workflow and release notes Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 026c2ef commit 385dc74

File tree

24 files changed

+1538
-25
lines changed

24 files changed

+1538
-25
lines changed
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
### Review mode workflow
2+
3+
Granary now supports an optional review gate in the task and project lifecycle. When enabled, completed work transitions to `in_review` instead of going straight to `done`/`completed`, giving a reviewer (human or agent) the chance to approve or reject it before it's finalized.
4+
5+
**Two scopes:**
6+
7+
- **`task` mode**`granary work done` moves the task to `in_review` and emits a `task.review` event. A reviewer approves or rejects individual tasks.
8+
- **`project` mode** — Tasks still complete normally, but when all tasks are done, the project enters `in_review` instead of `completed`. Reviewers approve the project as a whole, or reject it by creating follow-up tasks and reopening the project.
9+
10+
**New `granary review` command:**
11+
12+
- `granary review <id>` — displays reviewer context (task/project details, comments, suggested actions)
13+
- `granary review <id> approve ["comment"]` — approves and completes the entity
14+
- `granary review <id> reject "feedback"` — rejects with feedback; tasks return to `todo`, projects reopen to `active` with draft tasks promoted to `todo`
15+
16+
Review comments use a new `review` comment kind, and review events (`task.review`, `project.review`) are emitted so downstream agents or integrations can react.
17+
18+
### Review mode configuration
19+
20+
Review mode is stored in the workspace database (`config` table) under the key `workflow.review_mode`. Enable it with:
21+
22+
```
23+
granary config set workflow.review_mode task # or 'project'
24+
granary config unset workflow.review_mode # disable
25+
```
26+
27+
### Updated SQL triggers
28+
29+
The `trg_project_auto_complete` trigger is now config-aware — when `workflow.review_mode` is set to `project`, it transitions the project to `in_review` instead of `completed`. New triggers emit `task.review` and `project.review` events on status transitions.

AGENTS.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Agent Instructions
22

3+
## Formatting
4+
5+
After implementation is complete, use `cargo fmt` to format files. Always run `cargo clippy` before committing to ensure there are no warnings or errors.
6+
37
## SQL Migrations
48

59
- Always create migrations via SQLx CLI from the repo root:

CLAUDE.md

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,19 @@
1-
After implementation is complete, use `cargo fmt` to format files.
1+
# Agent Instructions
22

3-
IMPORTANT: when user requests to "use granary", run `granary` command before performing any other task.
3+
## Formatting
4+
5+
After implementation is complete, use `cargo fmt` to format files. Always run `cargo clippy` before committing to ensure there are no warnings or errors.
6+
7+
## SQL Migrations
48

5-
## Iced GUI Development
9+
- Always create migrations via SQLx CLI from the repo root:
10+
- `sqlx migrate add <name>`
11+
- Do not hand-write timestamp/version prefixes in migration filenames.
12+
- If you hit a duplicate migration version error, regenerate one migration with `sqlx migrate add <name>` and move the SQL into the newly generated file.
613

7-
The `crates/silo/` crate uses the Iced GUI library. When working on files in this crate, the `.claude/skills/iced-development.md` skill is automatically applied. It contains best practices for:
14+
## Build Guard
815

9-
- Application architecture (Elm Architecture / MVU)
10-
- Message enum patterns
11-
- State management
12-
- Styling and theming
13-
- Component composition
14-
- Async operations with Tasks
15-
- Subscriptions for background events
16+
- This repo has a build-time check in `build.rs` that fails compilation when two migration files share the same numeric version prefix.
17+
- Treat that failure as a naming/version collision and regenerate one of the conflicting migrations.
18+
19+
IMPORTANT: when user requests to "use granary", run `granary` command before performing any other task.

crates/granary-types/src/comment.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ pub enum CommentKind {
1515
Handoff,
1616
Incident,
1717
Context,
18+
Review,
1819
}
1920

2021
impl CommentKind {
@@ -27,6 +28,7 @@ impl CommentKind {
2728
CommentKind::Handoff => "handoff",
2829
CommentKind::Incident => "incident",
2930
CommentKind::Context => "context",
31+
CommentKind::Review => "review",
3032
}
3133
}
3234

@@ -39,6 +41,7 @@ impl CommentKind {
3941
CommentKind::Handoff,
4042
CommentKind::Incident,
4143
CommentKind::Context,
44+
CommentKind::Review,
4245
]
4346
}
4447
}
@@ -61,6 +64,7 @@ impl std::str::FromStr for CommentKind {
6164
"handoff" => Ok(CommentKind::Handoff),
6265
"incident" => Ok(CommentKind::Incident),
6366
"context" => Ok(CommentKind::Context),
67+
"review" => Ok(CommentKind::Review),
6468
_ => Err(()),
6569
}
6670
}

crates/granary-types/src/event.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,14 @@ pub enum EventType {
1313
ProjectCompleted,
1414
ProjectArchived,
1515
ProjectUnarchived,
16+
ProjectReview,
1617

1718
// Task events
1819
TaskCreated,
1920
TaskUpdated,
2021
TaskStarted,
2122
TaskCompleted,
23+
TaskReview,
2224
TaskBlocked,
2325
TaskUnblocked,
2426
TaskClaimed,
@@ -60,10 +62,12 @@ impl EventType {
6062
EventType::ProjectCompleted => "project.completed".to_string(),
6163
EventType::ProjectArchived => "project.archived".to_string(),
6264
EventType::ProjectUnarchived => "project.unarchived".to_string(),
65+
EventType::ProjectReview => "project.review".to_string(),
6366
EventType::TaskCreated => "task.created".to_string(),
6467
EventType::TaskUpdated => "task.updated".to_string(),
6568
EventType::TaskStarted => "task.started".to_string(),
6669
EventType::TaskCompleted => "task.completed".to_string(),
70+
EventType::TaskReview => "task.review".to_string(),
6771
EventType::TaskBlocked => "task.blocked".to_string(),
6872
EventType::TaskUnblocked => "task.unblocked".to_string(),
6973
EventType::TaskClaimed => "task.claimed".to_string(),
@@ -97,10 +101,12 @@ impl std::str::FromStr for EventType {
97101
"project.completed" => EventType::ProjectCompleted,
98102
"project.archived" => EventType::ProjectArchived,
99103
"project.unarchived" => EventType::ProjectUnarchived,
104+
"project.review" => EventType::ProjectReview,
100105
"task.created" => EventType::TaskCreated,
101106
"task.updated" => EventType::TaskUpdated,
102107
"task.started" => EventType::TaskStarted,
103108
"task.completed" => EventType::TaskCompleted,
109+
"task.review" => EventType::TaskReview,
104110
"task.blocked" => EventType::TaskBlocked,
105111
"task.unblocked" => EventType::TaskUnblocked,
106112
"task.claimed" => EventType::TaskClaimed,

crates/granary-types/src/project.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use sqlx::FromRow;
99
pub enum ProjectStatus {
1010
#[default]
1111
Active,
12+
InReview,
1213
Completed,
1314
Archived,
1415
}
@@ -17,6 +18,7 @@ impl ProjectStatus {
1718
pub fn as_str(&self) -> &'static str {
1819
match self {
1920
ProjectStatus::Active => "active",
21+
ProjectStatus::InReview => "in_review",
2022
ProjectStatus::Completed => "completed",
2123
ProjectStatus::Archived => "archived",
2224
}
@@ -26,6 +28,10 @@ impl ProjectStatus {
2628
matches!(self, ProjectStatus::Active)
2729
}
2830

31+
pub fn is_in_review(&self) -> bool {
32+
matches!(self, ProjectStatus::InReview)
33+
}
34+
2935
pub fn is_completed(&self) -> bool {
3036
matches!(self, ProjectStatus::Completed)
3137
}
@@ -43,6 +49,7 @@ impl std::str::FromStr for ProjectStatus {
4349
fn from_str(s: &str) -> Result<Self, Self::Err> {
4450
match s.to_lowercase().as_str() {
4551
"active" => Ok(ProjectStatus::Active),
52+
"in_review" | "in-review" | "inreview" => Ok(ProjectStatus::InReview),
4653
"completed" => Ok(ProjectStatus::Completed),
4754
"archived" => Ok(ProjectStatus::Archived),
4855
_ => Err(()),

crates/granary-types/src/task.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ pub enum TaskStatus {
115115
Draft,
116116
Todo,
117117
InProgress,
118+
InReview,
118119
Done,
119120
Blocked,
120121
}
@@ -125,6 +126,7 @@ impl TaskStatus {
125126
TaskStatus::Draft => "draft",
126127
TaskStatus::Todo => "todo",
127128
TaskStatus::InProgress => "in_progress",
129+
TaskStatus::InReview => "in_review",
128130
TaskStatus::Done => "done",
129131
TaskStatus::Blocked => "blocked",
130132
}
@@ -142,6 +144,10 @@ impl TaskStatus {
142144
matches!(self, TaskStatus::InProgress)
143145
}
144146

147+
pub fn is_in_review(&self) -> bool {
148+
matches!(self, TaskStatus::InReview)
149+
}
150+
145151
pub fn is_draft(&self) -> bool {
146152
matches!(self, TaskStatus::Draft)
147153
}
@@ -161,6 +167,7 @@ impl std::str::FromStr for TaskStatus {
161167
"draft" => Ok(TaskStatus::Draft),
162168
"todo" => Ok(TaskStatus::Todo),
163169
"in_progress" | "in-progress" | "inprogress" => Ok(TaskStatus::InProgress),
170+
"in_review" | "in-review" | "inreview" => Ok(TaskStatus::InReview),
164171
"done" | "completed" => Ok(TaskStatus::Done),
165172
"blocked" => Ok(TaskStatus::Blocked),
166173
_ => Err(()),

crates/silo/src/screen/edit_task.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@ fn view_form<'a>(state: &EditTaskScreenState<'a>, palette: &'a Palette) -> Eleme
168168
TaskStatus::Draft,
169169
TaskStatus::Todo,
170170
TaskStatus::InProgress,
171+
TaskStatus::InReview,
171172
TaskStatus::Done,
172173
TaskStatus::Blocked,
173174
];

crates/silo/src/screen/main_screen.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -737,6 +737,9 @@ fn view_task_row<'a>(
737737
palette,
738738
));
739739
}
740+
TaskStatus::InReview => {
741+
// In review - no quick actions from GUI
742+
}
740743
TaskStatus::Blocked => {
741744
action_btns.push(widget::action_button(
742745
"Unblock",
@@ -790,6 +793,10 @@ fn view_task_row<'a>(
790793
TaskStatus::InProgress => {
791794
widget::action_button("Done", Message::CompleteTask(task_id.clone()), palette)
792795
}
796+
TaskStatus::InReview => text("in review")
797+
.size(11)
798+
.color(palette.status_progress)
799+
.into(),
793800
TaskStatus::Done => {
794801
widget::action_button("Re-open", Message::ReopenTask(task_id.clone()), palette)
795802
}

crates/silo/src/screen/project_detail.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -486,6 +486,7 @@ fn view_task_row<'a>(
486486
let (status_color, status_label): (Color, &'static str) = match status {
487487
TaskStatus::Done => (palette.status_done, "done"),
488488
TaskStatus::InProgress => (palette.status_progress, "in progress"),
489+
TaskStatus::InReview => (palette.status_progress, "in review"),
489490
TaskStatus::Blocked => (palette.status_blocked, "blocked"),
490491
TaskStatus::Draft => (palette.text_muted, "draft"),
491492
TaskStatus::Todo => (palette.status_todo, "todo"),

0 commit comments

Comments
 (0)