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