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 ledger = crate::core::evidence_ledger::EvidenceLedgerV1::load();
66
67 let elapsed_min = chrono::Utc::now()
68 .signed_duration_since(run.updated_at)
69 .num_minutes();
70 let mut lines = vec![
71 format!("Workflow: {}", run.spec.name),
72 format!(" State: {}", run.current),
73 format!(" Updated: {} ({elapsed_min}m ago)", run.updated_at),
74 ];
75 if elapsed_min > 20 {
76 lines.push(" WARNING: Workflow inactive >20min, will auto-expire at 30min. Use action=stop to exit now.".to_string());
77 }
78
79 if let Some(state) = run.spec.state(&run.current) {
80 if let Some(ref tools) = state.allowed_tools {
81 let mut tools = tools.clone();
82 tools.sort();
83 let tools = tools.into_iter().take(30).collect::<Vec<_>>();
84 lines.push(format!(
85 " Allowed tools ({} shown): {}",
86 tools.len(),
87 tools.join(", ")
88 ));
89 }
90 }
91
92 let transitions = workflow::allowed_transitions(&run.spec, &run.current);
93 if transitions.is_empty() {
94 lines.push(" Transitions: (none)".to_string());
95 } else {
96 lines.push(" Transitions:".to_string());
97 for t in transitions.iter().take(10) {
98 let missing = workflow::missing_evidence_for_state(&run.spec, &t.to, |k| {
99 run.evidence.iter().any(|e| e.key == k)
100 || session.has_evidence_key(k)
101 || ledger.has_key(k)
102 });
103 if missing.is_empty() {
104 lines.push(format!(" → {} (ok)", t.to));
105 } else {
106 lines.push(format!(" → {} (missing: {})", t.to, missing.join(", ")));
107 }
108 }
109 }
110
111 lines.join("\n")
112}
113
114fn handle_stop() -> String {
115 match workflow::clear_active() {
116 Ok(()) => "Workflow stopped (active cleared).".to_string(),
117 Err(e) => format!("Error clearing workflow: {e}"),
118 }
119}
120
121fn handle_transition(
122 args: Option<&serde_json::Map<String, Value>>,
123 session: &SessionState,
124) -> String {
125 let Some(to) = get_str(args, "to") else {
126 return "Error: 'to' is required for transition".to_string();
127 };
128 let note = get_str(args, "value");
129
130 let Ok(active) = workflow::load_active() else {
131 return "Error: failed to load active workflow.".to_string();
132 };
133 let Some(mut run) = active else {
134 return "No active workflow. Use action=start to begin.".to_string();
135 };
136
137 let ledger = crate::core::evidence_ledger::EvidenceLedgerV1::load();
138 if let Err(e) = workflow::can_transition(&run.spec, &run.current, &to, |k| {
139 run.evidence.iter().any(|e| e.key == k) || session.has_evidence_key(k) || ledger.has_key(k)
140 }) {
141 return format!("Transition blocked: {e}");
142 }
143
144 let from = run.current.clone();
145 run.current.clone_from(&to);
146 run.updated_at = Utc::now();
147 run.transitions
148 .push(crate::core::workflow::TransitionRecord {
149 from: from.clone(),
150 to: to.clone(),
151 note: note.clone(),
152 timestamp: Utc::now(),
153 });
154
155 if let Err(e) = workflow::save_active(&run) {
156 return format!("Failed to save workflow: {e}");
157 }
158
159 format!("Transition: {from} → {to}")
160}
161
162fn handle_complete(
163 args: Option<&serde_json::Map<String, Value>>,
164 session: &SessionState,
165) -> String {
166 let Ok(active) = workflow::load_active() else {
167 return "Error: failed to load active workflow.".to_string();
168 };
169 let Some(mut run) = active else {
170 return "No active workflow. Use action=start to begin.".to_string();
171 };
172 let note = get_str(args, "value");
173
174 let done = "done".to_string();
175 if workflow::find_transition(&run.spec, &run.current, &done).is_none() {
176 return format!("No transition to 'done' from '{}'", run.current);
177 }
178
179 let ledger = crate::core::evidence_ledger::EvidenceLedgerV1::load();
180 if let Err(e) = workflow::can_transition(&run.spec, &run.current, &done, |k| {
181 run.evidence.iter().any(|e| e.key == k) || session.has_evidence_key(k) || ledger.has_key(k)
182 }) {
183 return format!("Complete blocked: {e}");
184 }
185
186 let from = run.current.clone();
187 run.current.clone_from(&done);
188 run.updated_at = Utc::now();
189 run.transitions
190 .push(crate::core::workflow::TransitionRecord {
191 from: from.clone(),
192 to: done.clone(),
193 note,
194 timestamp: Utc::now(),
195 });
196
197 if let Err(e) = workflow::clear_active() {
200 return format!("Workflow completed but failed to clear: {e}");
201 }
202
203 format!("Workflow completed: {from} → done (workflow cleared)")
204}
205
206fn handle_evidence_add(
207 args: Option<&serde_json::Map<String, Value>>,
208 session: &mut SessionState,
209) -> String {
210 let Some(key) = get_str(args, "key") else {
211 return "Error: key is required".to_string();
212 };
213 let value = get_str(args, "value");
214
215 let Ok(active) = workflow::load_active() else {
216 return "Error: failed to load active workflow.".to_string();
217 };
218 let Some(mut run) = active else {
219 return "No active workflow. Use action=start to begin.".to_string();
220 };
221
222 run.add_manual_evidence(&key, value.as_deref());
223 session.record_manual_evidence(&key, value.as_deref());
224 {
225 let mut ledger = crate::core::evidence_ledger::EvidenceLedgerV1::load();
226 ledger.record_manual(&key, value.as_deref(), chrono::Utc::now());
227 let _ = ledger.save();
228 }
229
230 if let Err(e) = workflow::save_active(&run) {
231 return format!("Failed to save workflow: {e}");
232 }
233
234 format!("Evidence added: {key}")
235}
236
237fn handle_evidence_list(session: &SessionState) -> String {
238 let Ok(active) = workflow::load_active() else {
239 return "Error: failed to load active workflow.".to_string();
240 };
241 let Some(run) = active else {
242 return "No active workflow.".to_string();
243 };
244
245 let ledger = crate::core::evidence_ledger::EvidenceLedgerV1::load();
246 let mut lines = vec![format!("Evidence (workflow: {}):", run.spec.name)];
247 if run.evidence.is_empty() && session.evidence.is_empty() && ledger.items.is_empty() {
248 lines.push(" (none)".to_string());
249 return lines.join("\n");
250 }
251
252 if !run.evidence.is_empty() {
253 lines.push(" Manual (workflow):".to_string());
254 for e in run.evidence.iter().rev().take(20) {
255 let v = e.value.as_deref().unwrap_or("-");
256 lines.push(format!(" {} = {} ({})", e.key, v, e.timestamp));
257 }
258 }
259
260 if !session.evidence.is_empty() {
261 lines.push(" Session receipts (latest):".to_string());
262 for e in session.evidence.iter().rev().take(20) {
263 lines.push(format!(" {} ({:?})", e.key, e.kind));
264 }
265 }
266
267 if !ledger.items.is_empty() {
268 lines.push(" Ledger (latest):".to_string());
269 for e in ledger.items.iter().rev().take(20) {
270 lines.push(format!(" {} ({:?})", e.key, e.kind));
271 }
272 }
273
274 lines.join("\n")
275}
276
277fn get_str(args: Option<&serde_json::Map<String, Value>>, key: &str) -> Option<String> {
278 args?
279 .get(key)?
280 .as_str()
281 .map(std::string::ToString::to_string)
282}