1use crate::core::session::SessionState;
2use crate::core::workflow::{self, WorkflowRun, WorkflowSpec};
3use chrono::Utc;
4use serde_json::Value;
5
6pub fn handle_with_session(
7 args: &Option<serde_json::Map<String, Value>>,
8 session: &mut SessionState,
9) -> String {
10 let action = get_str(args, "action").unwrap_or_else(|| "status".to_string());
11
12 match action.as_str() {
13 "start" => handle_start(args),
14 "status" => handle_status(session),
15 "stop" => handle_stop(),
16 "transition" => handle_transition(args, session),
17 "complete" => handle_complete(args, session),
18 "evidence_add" => handle_evidence_add(args, session),
19 "evidence_list" => handle_evidence_list(session),
20 _ => "Unknown action. Use: start, status, transition, complete, evidence_add, evidence_list, stop".to_string(),
21 }
22}
23
24fn handle_start(args: &Option<serde_json::Map<String, Value>>) -> String {
25 let spec_json = get_str(args, "spec");
26 let name_override = get_str(args, "name");
27
28 let mut spec: WorkflowSpec = match spec_json.as_deref() {
29 Some(s) if !s.trim().is_empty() => match serde_json::from_str::<WorkflowSpec>(s) {
30 Ok(v) => v,
31 Err(e) => return format!("Invalid spec JSON: {e}"),
32 },
33 _ => WorkflowSpec::builtin_plan_code_test(),
34 };
35
36 if let Some(name) = name_override {
37 if !name.trim().is_empty() {
38 spec.name = name;
39 }
40 }
41
42 if let Err(e) = workflow::validate_spec(&spec) {
43 return format!("Invalid WorkflowSpec: {e}");
44 }
45
46 let run = WorkflowRun::new(spec);
47 if let Err(e) = workflow::save_active(&run) {
48 return format!("Failed to save workflow: {e}");
49 }
50
51 format!(
52 "Workflow started: {}\n State: {}\n Started: {}",
53 run.spec.name, run.current, run.started_at
54 )
55}
56
57fn handle_status(session: &SessionState) -> String {
58 let Ok(active) = workflow::load_active() else {
59 return "Error: failed to load active workflow.".to_string();
60 };
61 let Some(run) = active else {
62 return "No active workflow. Use action=start to begin.".to_string();
63 };
64
65 let mut lines = vec![
66 format!("Workflow: {}", run.spec.name),
67 format!(" State: {}", run.current),
68 format!(" Updated: {}", run.updated_at),
69 ];
70
71 if let Some(state) = run.spec.state(&run.current) {
72 if let Some(ref tools) = state.allowed_tools {
73 let mut tools = tools.clone();
74 tools.sort();
75 let tools = tools.into_iter().take(30).collect::<Vec<_>>();
76 lines.push(format!(
77 " Allowed tools ({} shown): {}",
78 tools.len(),
79 tools.join(", ")
80 ));
81 }
82 }
83
84 let transitions = workflow::allowed_transitions(&run.spec, &run.current);
85 if transitions.is_empty() {
86 lines.push(" Transitions: (none)".to_string());
87 } else {
88 lines.push(" Transitions:".to_string());
89 for t in transitions.iter().take(10) {
90 let missing = workflow::missing_evidence_for_state(&run.spec, &t.to, |k| {
91 run.evidence.iter().any(|e| e.key == k) || session.has_evidence_key(k)
92 });
93 if missing.is_empty() {
94 lines.push(format!(" → {} (ok)", t.to));
95 } else {
96 lines.push(format!(" → {} (missing: {})", t.to, missing.join(", ")));
97 }
98 }
99 }
100
101 lines.join("\n")
102}
103
104fn handle_stop() -> String {
105 match workflow::clear_active() {
106 Ok(()) => "Workflow stopped (active cleared).".to_string(),
107 Err(e) => format!("Error clearing workflow: {e}"),
108 }
109}
110
111fn handle_transition(
112 args: &Option<serde_json::Map<String, Value>>,
113 session: &SessionState,
114) -> String {
115 let to = match get_str(args, "to") {
116 Some(t) => t,
117 None => return "Error: 'to' is required for transition".to_string(),
118 };
119 let note = get_str(args, "value");
120
121 let Ok(active) = workflow::load_active() else {
122 return "Error: failed to load active workflow.".to_string();
123 };
124 let Some(mut run) = active else {
125 return "No active workflow. Use action=start to begin.".to_string();
126 };
127
128 if let Err(e) = workflow::can_transition(&run.spec, &run.current, &to, |k| {
129 run.evidence.iter().any(|e| e.key == k) || session.has_evidence_key(k)
130 }) {
131 return format!("Transition blocked: {e}");
132 }
133
134 let from = run.current.clone();
135 run.current = to.clone();
136 run.updated_at = Utc::now();
137 run.transitions
138 .push(crate::core::workflow::TransitionRecord {
139 from: from.clone(),
140 to: to.clone(),
141 note: note.clone(),
142 timestamp: Utc::now(),
143 });
144
145 if let Err(e) = workflow::save_active(&run) {
146 return format!("Failed to save workflow: {e}");
147 }
148
149 format!("Transition: {from} → {to}")
150}
151
152fn handle_complete(
153 args: &Option<serde_json::Map<String, Value>>,
154 session: &SessionState,
155) -> String {
156 let Ok(active) = workflow::load_active() else {
157 return "Error: failed to load active workflow.".to_string();
158 };
159 let Some(mut run) = active else {
160 return "No active workflow. Use action=start to begin.".to_string();
161 };
162 let note = get_str(args, "value");
163
164 let done = "done".to_string();
165 if workflow::find_transition(&run.spec, &run.current, &done).is_none() {
166 return format!("No transition to 'done' from '{}'", run.current);
167 }
168
169 if let Err(e) = workflow::can_transition(&run.spec, &run.current, &done, |k| {
170 run.evidence.iter().any(|e| e.key == k) || session.has_evidence_key(k)
171 }) {
172 return format!("Complete blocked: {e}");
173 }
174
175 let from = run.current.clone();
176 run.current = done.clone();
177 run.updated_at = Utc::now();
178 run.transitions
179 .push(crate::core::workflow::TransitionRecord {
180 from: from.clone(),
181 to: done.clone(),
182 note,
183 timestamp: Utc::now(),
184 });
185
186 if let Err(e) = workflow::save_active(&run) {
187 return format!("Failed to save workflow: {e}");
188 }
189
190 format!("Workflow completed: {from} → {done}")
191}
192
193fn handle_evidence_add(
194 args: &Option<serde_json::Map<String, Value>>,
195 session: &mut SessionState,
196) -> String {
197 let key = match get_str(args, "key") {
198 Some(k) => k,
199 None => return "Error: key is required".to_string(),
200 };
201 let value = get_str(args, "value");
202
203 let Ok(active) = workflow::load_active() else {
204 return "Error: failed to load active workflow.".to_string();
205 };
206 let Some(mut run) = active else {
207 return "No active workflow. Use action=start to begin.".to_string();
208 };
209
210 run.add_manual_evidence(&key, value.as_deref());
211 session.record_manual_evidence(&key, value.as_deref());
212
213 if let Err(e) = workflow::save_active(&run) {
214 return format!("Failed to save workflow: {e}");
215 }
216
217 format!("Evidence added: {key}")
218}
219
220fn handle_evidence_list(session: &SessionState) -> String {
221 let Ok(active) = workflow::load_active() else {
222 return "Error: failed to load active workflow.".to_string();
223 };
224 let Some(run) = active else {
225 return "No active workflow.".to_string();
226 };
227
228 let mut lines = vec![format!("Evidence (workflow: {}):", run.spec.name)];
229 if run.evidence.is_empty() && session.evidence.is_empty() {
230 lines.push(" (none)".to_string());
231 return lines.join("\n");
232 }
233
234 if !run.evidence.is_empty() {
235 lines.push(" Manual (workflow):".to_string());
236 for e in run.evidence.iter().rev().take(20) {
237 let v = e.value.as_deref().unwrap_or("-");
238 lines.push(format!(" {} = {} ({})", e.key, v, e.timestamp));
239 }
240 }
241
242 if !session.evidence.is_empty() {
243 lines.push(" Session receipts (latest):".to_string());
244 for e in session.evidence.iter().rev().take(20) {
245 lines.push(format!(" {} ({:?})", e.key, e.kind));
246 }
247 }
248
249 lines.join("\n")
250}
251
252fn get_str(args: &Option<serde_json::Map<String, Value>>, key: &str) -> Option<String> {
253 args.as_ref()?.get(key)?.as_str().map(|s| s.to_string())
254}