Skip to content

Commit 4a5627f

Browse files
authored
Merge pull request #561 from cloudyli/codex/pr4-transition-gates
feat: extend kanban transition gates
2 parents f96994b + 3e759fa commit 4a5627f

21 files changed

Lines changed: 1304 additions & 8 deletions

File tree

crates/routa-core/src/models/kanban.rs

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,34 @@ pub enum KanbanTransport {
1414
A2a,
1515
}
1616

17+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
18+
#[serde(rename_all = "lowercase")]
19+
pub enum KanbanTransitionGateMode {
20+
#[default]
21+
Blocking,
22+
Warning,
23+
}
24+
25+
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
26+
#[serde(rename_all = "camelCase")]
27+
pub struct KanbanContractRules {
28+
#[serde(skip_serializing_if = "Option::is_none")]
29+
pub require_canonical_story: Option<bool>,
30+
#[serde(skip_serializing_if = "Option::is_none")]
31+
pub loop_breaker_threshold: Option<i64>,
32+
}
33+
34+
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
35+
#[serde(rename_all = "camelCase")]
36+
pub struct KanbanDeliveryRules {
37+
#[serde(skip_serializing_if = "Option::is_none")]
38+
pub require_committed_changes: Option<bool>,
39+
#[serde(skip_serializing_if = "Option::is_none")]
40+
pub require_clean_worktree: Option<bool>,
41+
#[serde(skip_serializing_if = "Option::is_none")]
42+
pub require_pull_request_ready: Option<bool>,
43+
}
44+
1745
/// Automation configuration for a Kanban column.
1846
/// When a card is moved to this column, the automation can trigger an agent session.
1947
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
@@ -72,6 +100,24 @@ pub struct KanbanColumnAutomation {
72100
/// Required task fields before advancing
73101
#[serde(skip_serializing_if = "Option::is_none")]
74102
pub required_task_fields: Option<Vec<String>>,
103+
/// Canonical story contract requirements
104+
#[serde(skip_serializing_if = "Option::is_none")]
105+
pub contract_rules: Option<KanbanContractRules>,
106+
/// Delivery-readiness requirements
107+
#[serde(skip_serializing_if = "Option::is_none")]
108+
pub delivery_rules: Option<KanbanDeliveryRules>,
109+
/// Required checked checklist labels before advancing
110+
#[serde(skip_serializing_if = "Option::is_none")]
111+
pub required_checklist: Option<Vec<String>>,
112+
/// Require an explicit approved verification verdict before advancing
113+
#[serde(skip_serializing_if = "Option::is_none")]
114+
pub required_human_approval: Option<bool>,
115+
/// Declarative validator command that must be represented in verification evidence
116+
#[serde(skip_serializing_if = "Option::is_none")]
117+
pub validator_command: Option<String>,
118+
/// Whether unmet transition gates block movement or leave an audit warning
119+
#[serde(skip_serializing_if = "Option::is_none")]
120+
pub gate_mode: Option<KanbanTransitionGateMode>,
75121
/// Automatically advance card on session success
76122
#[serde(skip_serializing_if = "Option::is_none")]
77123
pub auto_advance_on_success: Option<bool>,
@@ -364,6 +410,12 @@ fn build_recommended_automation(
364410
auto_advance_on_success: Some(auto_advance_on_success),
365411
required_artifacts: None,
366412
required_task_fields: None,
413+
contract_rules: None,
414+
delivery_rules: None,
415+
required_checklist: None,
416+
required_human_approval: None,
417+
validator_command: None,
418+
gate_mode: None,
367419
provider_id: None,
368420
role: None,
369421
specialist_id: None,
@@ -580,6 +632,12 @@ pub fn apply_recommended_automation_to_columns(columns: Vec<KanbanColumn>) -> Ve
580632
.or(normalized_recommended.required_artifacts.clone())
581633
},
582634
required_task_fields: current.required_task_fields,
635+
contract_rules: current.contract_rules,
636+
delivery_rules: current.delivery_rules,
637+
required_checklist: current.required_checklist,
638+
required_human_approval: current.required_human_approval,
639+
validator_command: current.validator_command,
640+
gate_mode: current.gate_mode,
583641
auto_advance_on_success: normalized_recommended.auto_advance_on_success,
584642
};
585643

crates/routa-core/src/models/kanban_config.rs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use super::kanban::KanbanColumnAutomation;
44

55
const VALID_STAGES: &[&str] = &["backlog", "todo", "dev", "review", "blocked", "done"];
66
const VALID_TRANSITION_TYPES: &[&str] = &["entry", "exit", "both"];
7+
const VALID_GATE_MODES: &[&str] = &["blocking", "warning"];
78
const VALID_ARTIFACTS: &[&str] = &["screenshot", "test_results", "code_diff"];
89
const VALID_REQUIRED_TASK_FIELDS: &[&str] = &[
910
"scope",
@@ -134,6 +135,18 @@ impl KanbanConfig {
134135
));
135136
}
136137
}
138+
if let Some(mode) = &auto.gate_mode {
139+
let mode = match mode {
140+
super::kanban::KanbanTransitionGateMode::Blocking => "blocking",
141+
super::kanban::KanbanTransitionGateMode::Warning => "warning",
142+
};
143+
if !VALID_GATE_MODES.contains(&mode) {
144+
errors.push(format!(
145+
"{auto_prefix}.gateMode '{mode}' is invalid, expected one of: {}",
146+
VALID_GATE_MODES.join(", ")
147+
));
148+
}
149+
}
137150
if let Some(artifacts) = &auto.required_artifacts {
138151
for art in artifacts {
139152
if !VALID_ARTIFACTS.contains(&art.as_str()) {
@@ -156,6 +169,20 @@ impl KanbanConfig {
156169
}
157170
}
158171
}
172+
if let Some(checklist) = &auto.required_checklist {
173+
for item in checklist {
174+
if item.trim().is_empty() {
175+
errors.push(format!(
176+
"{auto_prefix}.requiredChecklist contains a blank item"
177+
));
178+
}
179+
}
180+
}
181+
if let Some(command) = &auto.validator_command {
182+
if command.trim().is_empty() {
183+
errors.push(format!("{auto_prefix}.validatorCommand is blank"));
184+
}
185+
}
159186
}
160187
}
161188
}
@@ -222,6 +249,11 @@ boards:
222249
requiredTaskFields:
223250
- scope
224251
- verification_plan
252+
requiredChecklist:
253+
- browser smoke
254+
requiredHumanApproval: true
255+
validatorCommand: npm test
256+
gateMode: warning
225257
autoAdvanceOnSuccess: false
226258
"#;
227259
let config = KanbanConfig::from_yaml(yaml).unwrap();
@@ -231,6 +263,16 @@ boards:
231263
assert_eq!(auto.provider_id.as_deref(), Some("routa-native"));
232264
assert_eq!(auto.required_artifacts.as_ref().unwrap().len(), 2);
233265
assert_eq!(auto.required_task_fields.as_ref().unwrap().len(), 2);
266+
assert_eq!(
267+
auto.required_checklist.as_ref().unwrap(),
268+
&vec!["browser smoke".to_string()]
269+
);
270+
assert_eq!(auto.required_human_approval, Some(true));
271+
assert_eq!(auto.validator_command.as_deref(), Some("npm test"));
272+
assert_eq!(
273+
auto.gate_mode,
274+
Some(super::super::kanban::KanbanTransitionGateMode::Warning)
275+
);
234276
assert!(config.validate().is_ok());
235277
}
236278

0 commit comments

Comments
 (0)