use anyhow::Context;
use once_cell::sync::Lazy;
use regex::Regex;
use serde::{Deserialize, Serialize};
use std::io::Read;
static TRAILING_COMMA_RE: Lazy<Regex> =
Lazy::new(|| Regex::new(r",(\s*[}\]])").expect("trailing comma regex is valid"));
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct AgentResponse {
pub status: String,
#[serde(default)]
pub summary: String,
#[serde(default)]
pub accomplished: Vec<String>,
#[serde(default)]
pub remaining: Vec<String>,
#[serde(default, alias = "files_changed", alias = "files_modified")]
pub files: Vec<String>,
#[serde(default)]
pub error: Option<String>,
#[serde(default)]
pub input_tokens: Option<u64>,
#[serde(default)]
pub output_tokens: Option<u64>,
#[serde(default)]
pub learnings: Vec<String>,
#[serde(default)]
pub delegations: Vec<Delegation>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Delegation {
pub title: String,
pub body: String,
#[serde(default)]
pub labels: Vec<String>,
}
pub fn parse_and_print(path: &str) -> anyhow::Result<()> {
let content = if path == "-" {
let mut buf = String::new();
std::io::stdin()
.read_to_string(&mut buf)
.context("reading stdin")?;
buf
} else {
std::fs::read_to_string(path).with_context(|| format!("reading {path}"))?
};
let response = parse(&content)?;
println!("{}", serde_json::to_string_pretty(&response)?);
Ok(())
}
fn status_is_known(status: &str) -> bool {
matches!(
status,
"new"
| "routed"
| "in_progress"
| "done"
| "completed"
| "ok"
| "success"
| "running"
| "blocked"
| "error"
| "failed"
| "in_review"
| "reviewing"
| "needs_review"
| "pending_review"
)
}
pub fn parse(raw: &str) -> anyhow::Result<AgentResponse> {
let mut last_err: Option<anyhow::Error> = None;
let mut saw_jsonish_candidate = false;
let mut best_candidate: Option<(AgentResponse, String)> = None;
for candidate in json_candidates(raw) {
match parse_candidate(&candidate) {
Ok(mut resp) => {
resp = normalize_status(resp);
if status_is_known(&resp.status) {
return Ok(resp);
}
if best_candidate.is_none() {
best_candidate = Some((resp, candidate));
}
}
Err(err) => {
if err.to_string() != "invalid agent response candidate" {
saw_jsonish_candidate = true;
last_err = Some(err);
}
}
}
}
if let Some((resp, raw_candidate)) = best_candidate {
if has_substantive_fields(&raw_candidate) {
return Ok(resp);
}
}
if saw_jsonish_candidate {
if let Some(err) = last_err {
return Err(err);
}
}
if let Some(err) = last_err {
return Err(err);
}
anyhow::bail!(
"agent output is not valid JSON or a supported JSON wrapper: {}",
raw.trim()
)
}
fn normalize_status(mut resp: AgentResponse) -> AgentResponse {
let normalized = resp.status.to_ascii_lowercase();
if normalized.starts_with("pushed to ") || normalized.starts_with("pushed ") {
resp.status = "done".to_string();
return resp;
}
resp.status = match normalized.as_str() {
"done"
| "completed"
| "complete"
| "skipped"
| "skip"
| "ok"
| "success"
| "noop"
| "green"
| "no_changes_needed"
| "no_trades_no_positions"
| "no_setups"
| "changes_made"
| "change_made"
| "changes_pushed"
| "pushed"
| "acknowledged"
| "flat"
| "no_action"
| "no_action_needed"
| "missed"
| "changes_addressed"
| "changes_requested_addressed"
| "review_addressed"
| "already_finalized_in_attempt_1"
| "alert"
| "alerts"
| "alerts_sent"
| "not_configured"
| "healthy"
| "alerts_fired"
| "verified"
| "fixed" => "done".to_string(),
"in_progress" | "running" | "partial" | "waiting" => "in_progress".to_string(),
"in_review" | "reviewing" => "in_review".to_string(),
"needs_review" | "pending_review" | "ready_for_review" => "needs_review".to_string(),
"blocked" | "error" | "failed" | "failure" => "blocked".to_string(),
s if s == "new"
|| s == "routed"
|| s == "in_progress"
|| s == "done"
|| s == "blocked"
|| s == "in_review"
|| s == "needs_review" =>
{
s.to_string()
}
_ if !resp.summary.is_empty()
|| !resp.accomplished.is_empty()
|| !resp.files.is_empty()
|| resp.error.is_some() =>
{
if crate::status_heuristics::status_looks_like_descriptive_completion(&normalized) {
"done".to_string()
} else {
resp.status.clone()
}
}
_ => resp.status.clone(),
};
resp
}
fn extract_json_block(text: &str) -> Option<String> {
let start = text.find("```json")?;
let fence_end = start + "```json".len();
let remainder = &text[fence_end..];
let newline_pos = remainder.find('\n');
let json_start_pos = remainder.find(['{', '[']);
let first_nl = newline_pos.unwrap_or(usize::MAX);
let is_inline = json_start_pos.is_some_and(|js| js < first_nl) || newline_pos.is_none();
if is_inline {
let closing_fence_pos = remainder.find("```")?;
let content_start = remainder
.char_indices()
.find(|(_, ch)| !ch.is_whitespace())
.map_or(fence_end, |(idx, _)| fence_end + idx);
let end = fence_end + closing_fence_pos;
if content_start > end {
return None;
}
Some(text[content_start..end].to_string())
} else {
let nl = newline_pos?;
let content_start = fence_end + nl + 1;
let content = &text[content_start..];
let closing_pos = find_closing_fence_at_line_boundary(content)?;
Some(text[content_start..content_start + closing_pos].to_string())
}
}
fn find_closing_fence_at_line_boundary(content: &str) -> Option<usize> {
if content.starts_with("```") {
return Some(0);
}
let mut search_from = 0;
while let Some(nl_idx) = content[search_from..].find('\n') {
let after_nl = search_from + nl_idx + 1;
if content[after_nl..].starts_with("```") {
return Some(after_nl);
}
search_from = after_nl;
}
None
}
fn has_substantive_fields(raw: &str) -> bool {
if let Ok(serde_json::Value::Object(obj)) = serde_json::from_str::<serde_json::Value>(raw) {
return SUBSTANTIVE_RESPONSE_FIELDS
.iter()
.any(|key| obj.contains_key(*key))
|| obj.contains_key("error");
}
false
}
const AGENT_RESPONSE_FIELDS: &[&str] = &[
"\"status\"",
"\"summary\"",
"\"accomplished\"",
"\"remaining\"",
"\"files\"",
"\"learnings\"",
"\"delegations\"",
"\"error\"",
];
fn agent_response_score(blob: &str) -> usize {
AGENT_RESPONSE_FIELDS
.iter()
.filter(|&&f| blob.contains(f))
.count()
}
fn json_candidates(raw: &str) -> Vec<String> {
let mut candidates = Vec::new();
if let Some(block) = extract_json_block(raw) {
candidates.push(block);
}
let mut all_json = extract_all_balanced_json(raw);
all_json.sort_by_key(|b| std::cmp::Reverse(agent_response_score(b)));
for candidate in all_json {
if !candidates.iter().any(|existing| existing == &candidate) {
candidates.push(candidate);
}
}
candidates.push(raw.to_string());
candidates
}
fn parse_candidate(candidate: &str) -> anyhow::Result<AgentResponse> {
if let Ok(resp) = serde_json::from_str::<AgentResponse>(candidate) {
return Ok(resp);
}
let repaired = repair_json_like(candidate);
if repaired != candidate {
if let Ok(resp) = serde_json::from_str::<AgentResponse>(&repaired) {
return Ok(resp);
}
}
if let Ok(val) = serde_json::from_str::<serde_json::Value>(candidate) {
return map_generic_response(&val);
}
if repaired != candidate {
if let Ok(val) = serde_json::from_str::<serde_json::Value>(&repaired) {
return map_generic_response(&val);
}
}
anyhow::bail!("invalid agent response candidate")
}
fn extract_all_balanced_json(text: &str) -> Vec<String> {
let mut results = Vec::new();
let mut pos = 0;
while pos < text.len() {
let Some(start_offset) = text[pos..]
.char_indices()
.find(|(_, ch)| matches!(ch, '{' | '['))
.map(|(idx, _)| idx)
else {
break;
};
let start = pos + start_offset;
let mut stack = Vec::new();
let mut in_string = false;
let mut escape = false;
let mut end: Option<usize> = None;
for (idx, ch) in text[start..].char_indices() {
let abs = start + idx;
if in_string {
if escape {
escape = false;
continue;
}
match ch {
'\\' => escape = true,
'"' => in_string = false,
_ => {}
}
continue;
}
match ch {
'"' => in_string = true,
'{' | '[' => stack.push(ch),
'}' => {
if stack.pop() != Some('{') {
break; }
if stack.is_empty() {
end = Some(abs);
break;
}
}
']' => {
if stack.pop() != Some('[') {
break; }
if stack.is_empty() {
end = Some(abs);
break;
}
}
_ => {}
}
}
if let Some(end_idx) = end {
results.push(text[start..=end_idx].to_string());
pos = end_idx + 1;
} else {
pos = start + 1;
}
}
results
}
fn repair_json_like(text: &str) -> String {
let mut repaired = text.trim().replace(['“', '”'], "\"");
repaired = repaired.replace(['‘', '’'], "'");
repaired = TRAILING_COMMA_RE.replace_all(&repaired, "$1").into_owned();
let mut stack = Vec::new();
let mut in_string = false;
let mut escape = false;
for ch in repaired.chars() {
if in_string {
if escape {
escape = false;
continue;
}
match ch {
'\\' => escape = true,
'"' => in_string = false,
_ => {}
}
continue;
}
match ch {
'"' => in_string = true,
'{' => stack.push('}'),
'[' => stack.push(']'),
'}' | ']' => {
if let Some(expected) = stack.last().copied() {
if expected == ch {
stack.pop();
}
}
}
_ => {}
}
}
while let Some(ch) = stack.pop() {
repaired.push(ch);
}
repaired
}
const SUBSTANTIVE_RESPONSE_FIELDS: &[&str] = &[
"summary",
"message",
"accomplished",
"remaining",
"files",
"files_changed",
"files_modified",
"learnings",
"delegations",
"output",
"pr_url",
"report",
];
fn infer_completion_status(obj: &serde_json::Map<String, serde_json::Value>) -> Option<String> {
if obj.get("success").and_then(|v| v.as_bool()) == Some(true) {
return Some("done".to_string());
}
if obj.get("type").and_then(|v| v.as_str()) == Some("task_complete") {
return Some("done".to_string());
}
if obj.get("ok").and_then(|v| v.as_bool()) == Some(true) {
return Some("done".to_string());
}
if obj.get("tests_passed").and_then(|v| v.as_bool()) == Some(true) {
return Some("done".to_string());
}
None
}
fn map_generic_response(val: &serde_json::Value) -> anyhow::Result<AgentResponse> {
let obj = val.as_object().context("expected JSON object")?;
let status = obj
.get("status")
.or_else(|| obj.get("result"))
.and_then(|v| v.as_str())
.map(String::from)
.or_else(|| infer_completion_status(obj))
.context("agent response is missing required 'status' or 'result' field")?;
let has_substantive_field = SUBSTANTIVE_RESPONSE_FIELDS
.iter()
.any(|key| obj.contains_key(*key));
let has_error = obj.contains_key("error");
if !has_substantive_field && !has_error {
anyhow::bail!("JSON object lacks substantive response fields");
}
let summary = obj
.get("summary")
.or_else(|| obj.get("message"))
.or_else(|| obj.get("output"))
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let accomplished = extract_string_array(obj.get("accomplished"));
let remaining = extract_string_array(obj.get("remaining"));
let files = {
let f = extract_string_array(obj.get("files"));
if !f.is_empty() {
f
} else {
let fc = extract_string_array(obj.get("files_changed"));
if !fc.is_empty() {
fc
} else {
extract_string_array(obj.get("files_modified"))
}
}
};
let error = obj.get("error").and_then(|v| v.as_str()).map(String::from);
let learnings = extract_string_array(obj.get("learnings"));
let input_tokens = extract_u64(obj.get("input_tokens"))
.or_else(|| extract_u64(obj.get("tokens_input")))
.or_else(|| extract_usage_tokens(obj.get("usage"), true));
let output_tokens = extract_u64(obj.get("output_tokens"))
.or_else(|| extract_u64(obj.get("tokens_output")))
.or_else(|| extract_usage_tokens(obj.get("usage"), false));
let delegations = if let Some(v) = obj.get("delegations") {
serde_json::from_value::<Vec<Delegation>>(v.clone())
.with_context(|| format!("invalid delegations field: {}", v))?
} else {
Vec::new()
};
Ok(AgentResponse {
status,
summary,
accomplished,
remaining,
files,
error,
input_tokens,
output_tokens,
learnings,
delegations,
})
}
fn extract_u64(val: Option<&serde_json::Value>) -> Option<u64> {
val.and_then(|v| v.as_u64())
}
fn extract_usage_tokens(usage: Option<&serde_json::Value>, is_input: bool) -> Option<u64> {
let obj = usage?.as_object()?;
let key = if is_input {
"input_tokens"
} else {
"output_tokens"
};
obj.get(key).and_then(|v| v.as_u64())
}
fn extract_string_array(val: Option<&serde_json::Value>) -> Vec<String> {
val.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect()
})
.unwrap_or_default()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_direct_json() {
let input = r#"{"status":"done","summary":"Fixed bug","accomplished":["fixed it"],"remaining":[],"files":["src/main.rs"]}"#;
let resp = parse(input).unwrap();
assert_eq!(resp.status, "done");
assert_eq!(resp.summary, "Fixed bug");
assert_eq!(resp.accomplished, vec!["fixed it"]);
assert_eq!(resp.files, vec!["src/main.rs"]);
assert!(resp.error.is_none());
}
#[test]
fn parse_json_in_markdown_block() {
let input = r#"Here is the result:
```json
{"status":"in_progress","summary":"Working on it","accomplished":[],"remaining":["finish tests"],"files":[]}
```
Done.
"#;
let resp = parse(input).unwrap();
assert_eq!(resp.status, "in_progress");
assert_eq!(resp.remaining, vec!["finish tests"]);
}
#[test]
fn parse_generic_json_with_different_field_names() {
let input = r#"{"result":"done","message":"All good","files":["a.rs","b.rs"]}"#;
let resp = parse(input).unwrap();
assert_eq!(resp.status, "done");
assert_eq!(resp.summary, "All good");
assert_eq!(resp.files, vec!["a.rs", "b.rs"]);
}
#[test]
fn parse_with_error_field() {
let input = r#"{"status":"blocked","summary":"Cannot proceed","accomplished":[],"remaining":[],"files":[],"error":"missing dependency"}"#;
let resp = parse(input).unwrap();
assert_eq!(resp.status, "blocked");
assert_eq!(resp.error, Some("missing dependency".to_string()));
}
#[test]
fn parse_fallback_raw_text() {
let input = "This is just plain text output from the agent.";
let err = parse(input).unwrap_err();
assert!(err
.to_string()
.contains("agent output is not valid JSON or a supported JSON wrapper"));
}
#[test]
fn parse_malformed_json_block_returns_error() {
let input = r#"Here is the result:
```json
{"status":"done","summary":"missing brace"
```
"#;
let resp = parse(input).unwrap();
assert_eq!(resp.status, "done");
assert_eq!(resp.summary, "missing brace");
}
#[test]
fn parse_json_with_trailing_comma() {
let input =
r#"{"status":"done","summary":"Fixed","accomplished":[],"remaining":[],"files":[],}"#;
let resp = parse(input).unwrap();
assert_eq!(resp.status, "done");
assert_eq!(resp.summary, "Fixed");
}
#[test]
fn parse_json_embedded_in_commentary() {
let input = r#"Done:
{"status":"in_progress","summary":"working","accomplished":[],"remaining":["tests"],"files":[]}
Thanks"#;
let resp = parse(input).unwrap();
assert_eq!(resp.status, "in_progress");
assert_eq!(resp.remaining, vec!["tests"]);
}
#[test]
fn extract_json_block_from_markdown() {
let text = "prefix\n```json\n{\"key\":\"value\"}\n```\nsuffix";
let block = extract_json_block(text).unwrap();
assert_eq!(block, "{\"key\":\"value\"}\n");
}
#[test]
fn extract_json_block_inline_fence() {
let text = "prefix\n```json{\"key\":\"value\"}```\nsuffix";
let block = extract_json_block(text).unwrap();
assert_eq!(block, "{\"key\":\"value\"}");
}
#[test]
fn extract_json_block_with_info_string() {
let text = "prefix\n```json some-meta\n{\"key\":\"value\"}\n```\nsuffix";
let block = extract_json_block(text).unwrap();
assert_eq!(block, "{\"key\":\"value\"}\n");
}
#[test]
fn extract_json_block_with_info_string_and_whitespace() {
let text = "prefix\n```json some-meta \n{\"key\":\"value\"}\n```\nsuffix";
let block = extract_json_block(text).unwrap();
assert_eq!(block, "{\"key\":\"value\"}\n");
}
#[test]
fn extract_json_block_inline_with_internal_newline() {
let text = "prefix\n```json{\"key\":\"value\\nwith newline\"}```\nsuffix";
let block = extract_json_block(text).unwrap();
assert_eq!(block, r#"{"key":"value\nwith newline"}"#);
}
#[test]
fn extract_json_block_inline_with_whitespace_newline() {
let text = "prefix\n```json{\"key\":\n\"value\"}```\nsuffix";
let block = extract_json_block(text).unwrap();
assert_eq!(block, "{\"key\":\n\"value\"}");
}
#[test]
fn extract_json_block_missing_closing_fence() {
let text = "prefix\n```json{\"key\":\"value\"}\n";
assert!(extract_json_block(text).is_none());
}
#[test]
fn extract_json_block_array_inline() {
let text = "prefix\n```json[1,2,3]```\nsuffix";
let block = extract_json_block(text).unwrap();
assert_eq!(block, "[1,2,3]");
}
#[test]
fn extract_json_block_multiple_fences() {
let text = "prefix\n```json{\"first\":1}``` middle\n```json{\"second\":2}``` suffix";
let block = extract_json_block(text).unwrap();
assert_eq!(block, "{\"first\":1}");
}
#[test]
fn extract_json_block_missing_returns_none() {
assert!(extract_json_block("no code block here").is_none());
}
#[test]
fn extract_string_array_from_json() {
let val: serde_json::Value = serde_json::json!(["a", "b", "c"]);
let result = extract_string_array(Some(&val));
assert_eq!(result, vec!["a", "b", "c"]);
}
#[test]
fn extract_string_array_none() {
let result = extract_string_array(None);
assert!(result.is_empty());
}
#[test]
fn parse_empty_object() {
let input = "{}";
let err = parse(input).unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("agent response is missing required")
|| msg.contains("JSON object lacks substantive"),
"unexpected error: {msg}"
);
}
#[test]
fn parse_with_learnings() {
let input = r#"{"status":"needs_review","summary":"Need to fix imports","accomplished":[],"remaining":["fix imports"],"files":["src/main.rs"],"learnings":["Use std::sync::Arc for shared state","Check imports before committing"]}"#;
let resp = parse(input).unwrap();
assert_eq!(resp.status, "needs_review");
assert_eq!(resp.summary, "Need to fix imports");
assert_eq!(resp.learnings.len(), 2);
assert_eq!(resp.learnings[0], "Use std::sync::Arc for shared state");
assert_eq!(resp.learnings[1], "Check imports before committing");
}
#[test]
fn parse_learnings_empty_by_default() {
let input = r#"{"status":"done","summary":"Completed","accomplished":["done"],"remaining":[],"files":[]}"#;
let resp = parse(input).unwrap();
assert!(resp.learnings.is_empty());
}
#[test]
fn parse_with_delegations() {
let input = r#"{
"status": "blocked",
"summary": "Task requires subtasks",
"accomplished": ["Analyzed requirements"],
"remaining": ["Waiting on subtasks"],
"files_changed": [],
"blockers": ["Waiting on delegated subtasks"],
"reason": "Decomposed into subtasks",
"delegations": [
{"title": "Implement login API", "body": "Create POST /api/login", "labels": ["backend"]},
{"title": "Add login form", "body": "Create React login component", "labels": ["frontend"]}
]
}"#;
let resp = parse(input).unwrap();
assert_eq!(resp.status, "blocked");
assert_eq!(resp.delegations.len(), 2);
assert_eq!(resp.delegations[0].title, "Implement login API");
assert_eq!(resp.delegations[0].body, "Create POST /api/login");
assert_eq!(resp.delegations[0].labels, vec!["backend"]);
assert_eq!(resp.delegations[1].title, "Add login form");
assert_eq!(resp.delegations[1].labels, vec!["frontend"]);
}
#[test]
fn parse_delegations_empty_by_default() {
let input =
r#"{"status":"done","summary":"Done","accomplished":[],"remaining":[],"files":[]}"#;
let resp = parse(input).unwrap();
assert!(resp.delegations.is_empty());
}
#[test]
fn parse_delegations_without_labels() {
let input = r#"{
"status": "blocked",
"summary": "Delegating",
"delegations": [
{"title": "Subtask", "body": "Do the thing"}
]
}"#;
let resp = parse(input).unwrap();
assert_eq!(resp.delegations.len(), 1);
assert_eq!(resp.delegations[0].title, "Subtask");
assert!(resp.delegations[0].labels.is_empty());
}
#[test]
fn parse_response_after_telemetry_json() {
let input = r#"Processing...
{"type":"telemetry","message":"starting task","tokens":42}
Some output here.
{"status":"done","summary":"Fixed the bug","accomplished":["patched parser"],"remaining":[],"files":["src/parser.rs"]}
"#;
let resp = parse(input).unwrap();
assert_eq!(resp.status, "done");
assert_eq!(resp.summary, "Fixed the bug");
assert_eq!(resp.files, vec!["src/parser.rs"]);
}
#[test]
fn extract_json_block_backticks_inside_json_string() {
let text = "```json\n{\"code\":\"example: ```snippet``` end\"}\n```\n";
let block = extract_json_block(text).unwrap();
assert_eq!(block, "{\"code\":\"example: ```snippet``` end\"}\n");
}
#[test]
fn extract_json_block_backticks_inside_json_string_parse_roundtrip() {
let input = "```json\n{\"status\":\"done\",\"summary\":\"use ```code``` here\"}\n```\n";
let resp = parse(input).unwrap();
assert_eq!(resp.status, "done");
assert_eq!(resp.summary, "use ```code``` here");
}
#[test]
fn extract_json_block_inline_with_embedded_backtick_pairs() {
let text = "```json{\"key\":\"a`b``c\"}```\n";
let block = extract_json_block(text).unwrap();
assert_eq!(block, "{\"key\":\"a`b``c\"}");
}
#[test]
fn parse_mixed_telemetry_and_fenced_response() {
let input = r#"Processing...
{"type":"log","msg":"calling tool: ```bash```"}
```json
{"status":"done","summary":"fixed it","accomplished":["patched"],"remaining":[],"files":["src/x.rs"]}
```
"#;
let resp = parse(input).unwrap();
assert_eq!(resp.status, "done");
assert_eq!(resp.summary, "fixed it");
assert_eq!(resp.files, vec!["src/x.rs"]);
}
#[test]
fn parse_response_from_ndjson_multiple_objects() {
let input = r#"{"type":"text","text":"Working on it..."}
{"type":"tool_call","name":"bash","input":{"command":"cargo test"}}
{"type":"tool_result","output":"test passed"}
{"status":"needs_review","summary":"All tests pass","accomplished":["fixed bug"],"remaining":[],"files":["src/lib.rs"]}
"#;
let resp = parse(input).unwrap();
assert_eq!(resp.status, "needs_review");
assert_eq!(resp.summary, "All tests pass");
assert_eq!(resp.files, vec!["src/lib.rs"]);
}
#[test]
fn parse_normalizes_ok_alias_to_done() {
let input =
r#"{"status":"ok","summary":"all good","accomplished":[],"remaining":[],"files":[]}"#;
let resp = parse(input).unwrap();
assert_eq!(resp.status, "done");
}
#[test]
fn parse_normalizes_success_alias_to_done() {
let input = r#"{"status":"success","summary":"fixed it","accomplished":[],"remaining":[],"files":[]}"#;
let resp = parse(input).unwrap();
assert_eq!(resp.status, "done");
}
#[test]
fn parse_normalizes_completed_alias_to_done() {
let input = r#"{"status":"completed","summary":"done","accomplished":[],"remaining":[],"files":[]}"#;
let resp = parse(input).unwrap();
assert_eq!(resp.status, "done");
}
#[test]
fn parse_normalizes_missed_alias_to_done_case_insensitive() {
let input = r#"{"status":"MISSED","summary":"no setups found","accomplished":[],"remaining":[],"files":[]}"#;
let resp = parse(input).unwrap();
assert_eq!(resp.status, "done");
}
#[test]
fn parse_normalizes_changes_addressed_alias_to_done() {
let input = r#"{"status":"changes_addressed","summary":"feedback addressed","accomplished":[],"remaining":[],"files":[]}"#;
let resp = parse(input).unwrap();
assert_eq!(resp.status, "done");
}
#[test]
fn parse_normalizes_changes_requested_addressed_alias_to_done() {
let input = r#"{"status":"changes_requested_addressed","summary":"review feedback addressed","accomplished":[],"remaining":[],"files":[]}"#;
let resp = parse(input).unwrap();
assert_eq!(resp.status, "done");
}
#[test]
fn parse_normalizes_review_addressed_alias_to_done() {
let input = r#"{"status":"review_addressed","summary":"review feedback addressed","accomplished":[],"remaining":[],"files":[]}"#;
let resp = parse(input).unwrap();
assert_eq!(resp.status, "done");
}
#[test]
fn parse_normalizes_already_finalized_in_attempt_1_alias_to_done() {
let input = r#"{"status":"already_finalized_in_attempt_1","summary":"work already done in attempt 1","accomplished":[],"remaining":[],"files":[]}"#;
let resp = parse(input).unwrap();
assert_eq!(resp.status, "done");
}
#[test]
fn parse_normalizes_noop_alias_to_done() {
let input = r#"{"status":"noop","summary":"nothing to do","accomplished":[],"remaining":[],"files":[]}"#;
let resp = parse(input).unwrap();
assert_eq!(resp.status, "done");
}
#[test]
fn parse_normalizes_green_alias_to_done() {
let input = r#"{"status":"green","summary":"all passing","accomplished":[],"remaining":[],"files":[]}"#;
let resp = parse(input).unwrap();
assert_eq!(resp.status, "done");
}
#[test]
fn parse_normalizes_alerts_sent_alias_to_done() {
let input = r#"{"status":"alerts_sent","summary":"notifications dispatched","accomplished":[],"remaining":[],"files":[]}"#;
let resp = parse(input).unwrap();
assert_eq!(resp.status, "done");
}
#[test]
fn parse_normalizes_verified_alias_to_done() {
let input = r#"{"status":"verified","summary":"result verified correct","accomplished":[],"remaining":[],"files":[]}"#;
let resp = parse(input).unwrap();
assert_eq!(resp.status, "done");
}
#[test]
fn parse_normalizes_fixed_alias_to_done() {
let input = r#"{"status":"fixed","summary":"bug has been fixed","accomplished":["fixed bug"],"remaining":[],"files":["src/parser.rs"]}"#;
let resp = parse(input).unwrap();
assert_eq!(resp.status, "done");
}
#[test]
fn parse_normalizes_alerts_fired_alias_to_done() {
let input = r#"{"status":"alerts_fired","summary":"alerts dispatched","accomplished":[],"remaining":[],"files":[]}"#;
let resp = parse(input).unwrap();
assert_eq!(resp.status, "done");
}
#[test]
fn parse_normalizes_waiting_alias_to_in_progress() {
let input = r#"{"status":"waiting","summary":"blocked on upstream","accomplished":[],"remaining":[],"files":[]}"#;
let resp = parse(input).unwrap();
assert_eq!(resp.status, "in_progress");
}
#[test]
fn parse_normalizes_changes_pushed_alias_to_done() {
let input = r#"{"status":"changes_pushed","summary":"changes pushed to branch","accomplished":[],"remaining":[],"files":[]}"#;
let resp = parse(input).unwrap();
assert_eq!(resp.status, "done");
}
#[test]
fn parse_normalizes_bare_pushed_alias_to_done() {
let input = r#"{"status":"pushed","summary":"changes pushed to branch","accomplished":[],"remaining":[],"files":[]}"#;
let resp = parse(input).unwrap();
assert_eq!(resp.status, "done");
}
#[test]
fn parse_normalizes_dynamic_pushed_to_branch_status_to_done() {
let input = r#"{"status":"pushed to internal-155195-weekly-review-patterns-progress-and-next","summary":"changes pushed to branch","accomplished":[],"remaining":[],"files":[]}"#;
let resp = parse(input).unwrap();
assert_eq!(resp.status, "done");
}
#[test]
fn parse_normalizes_running_alias_to_in_progress() {
let input = r#"{"status":"running","summary":"working","accomplished":[],"remaining":[],"files":[]}"#;
let resp = parse(input).unwrap();
assert_eq!(resp.status, "in_progress");
}
#[test]
fn parse_normalizes_reviewing_alias_to_in_review() {
let input = r#"{"status":"reviewing","summary":"reviewing","accomplished":[],"remaining":[],"files":[]}"#;
let resp = parse(input).unwrap();
assert_eq!(resp.status, "in_review");
}
#[test]
fn parse_normalizes_pending_review_alias_to_needs_review() {
let input = r#"{"status":"pending_review","summary":"ready","accomplished":[],"remaining":[],"files":[]}"#;
let resp = parse(input).unwrap();
assert_eq!(resp.status, "needs_review");
}
#[test]
fn parse_normalizes_error_alias_to_blocked() {
let input =
r#"{"status":"error","summary":"failed","accomplished":[],"remaining":[],"files":[]}"#;
let resp = parse(input).unwrap();
assert_eq!(resp.status, "blocked");
}
#[test]
fn parse_normalizes_failure_alias_to_blocked() {
let input = r#"{"status":"failure","summary":"could not complete","accomplished":[],"remaining":[],"files":[]}"#;
let resp = parse(input).unwrap();
assert_eq!(resp.status, "blocked");
}
#[test]
fn parse_keeps_canonical_statuses_unchanged() {
for status in &[
"new",
"routed",
"in_progress",
"done",
"blocked",
"in_review",
"needs_review",
] {
let input = format!(
r#"{{"status":"{status}","summary":"test","accomplished":[],"remaining":[],"files":[]}}"#
);
let resp = parse(&input).unwrap();
assert_eq!(
resp.status, *status,
"canonical status '{status}' should be unchanged"
);
}
}
#[test]
fn parse_keeps_non_canonical_status_as_is_for_audit_trail() {
let input = r#"{"status":"fix_deployed","summary":"deployed","accomplished":[],"remaining":[],"files":[]}"#;
let resp = parse(input).unwrap();
assert_eq!(resp.status, "fix_deployed");
}
#[test]
fn parse_normalizes_heuristic_completion_alias_to_done() {
for status in &["addressed", "duplicate_skipped"] {
let input = format!(
r#"{{"status":"{status}","summary":"done","accomplished":[],"remaining":[],"files":[]}}"#
);
let resp = parse(&input).unwrap();
assert_eq!(
resp.status, "done",
"status '{status}' should normalize to done"
);
}
}
#[test]
fn parse_fails_when_summary_present_but_status_missing() {
let input = r#"{"summary":"still working","accomplished":[],"remaining":[],"files":[]}"#;
let result = parse(input);
assert!(
result.is_err(),
"missing status should error, not default to done"
);
}
#[test]
fn parse_fails_when_files_present_but_status_missing() {
let input = r#"{"summary":"partial output","files":["src/foo.rs"]}"#;
let result = parse(input);
assert!(
result.is_err(),
"missing status should error, not default to done"
);
}
#[test]
fn parse_fails_when_only_message_present_without_status() {
let input = r#"{"message":"working on it"}"#;
let result = parse(input);
assert!(
result.is_err(),
"missing status should error, not default to done"
);
}
#[test]
fn parse_regression_2880_ready_for_review_normalized() {
let input = r#"{"status":"ready_for_review","summary":"Ready for review","accomplished":["done"],"remaining":[],"files":["src/main.rs"]}"#;
let resp = parse(input).unwrap();
assert_eq!(resp.status, "needs_review");
}
#[test]
fn parse_regression_2880_status_with_output_accepted() {
let input = r#"{"status":"done","output":"All tests passed"}"#;
let resp = parse(input).unwrap();
assert_eq!(resp.status, "done");
}
#[test]
fn parse_regression_2880_mixed_ndjson_chooses_best_candidate() {
let input = r#"{"status":"src/channels/discord_ws.rs","summary":"","accomplished":[],"remaining":[],"files":[]}
{"status":"done","summary":"Fixed websocket timeout","accomplished":["fixed"],"remaining":[],"files":["src/channels/discord_ws.rs"]}"#;
let resp = parse(input).unwrap();
assert_eq!(resp.status, "done");
assert_eq!(resp.summary, "Fixed websocket timeout");
}
#[test]
fn parse_regression_2880_normalize_aliases() {
for (input, expected) in [
("ok", "done"),
("success", "done"),
("completed", "done"),
("complete", "done"),
("skipped", "done"),
("skip", "done"),
("no_changes_needed", "done"),
("no_trades_no_positions", "done"),
("changes_made", "done"),
("changes_pushed", "done"),
("changes_addressed", "done"),
("changes_requested_addressed", "done"),
("fixed", "done"),
("running", "in_progress"),
("partial", "in_progress"),
("reviewing", "in_review"),
("pending_review", "needs_review"),
("ready_for_review", "needs_review"),
("error", "blocked"),
("failed", "blocked"),
] {
let input = format!(
r#"{{"status":"{input}","summary":"test","accomplished":[],"remaining":[],"files":[]}}"#
);
let resp = parse(&input).unwrap();
assert_eq!(
resp.status, expected,
"status '{input}' should normalize to '{expected}'"
);
}
}
#[test]
fn parse_regression_2880_telemetry_skipped() {
let input = r#"{"type":"telemetry","event":"tool_call"}
{"status":"done","summary":"Work complete","accomplished":["done"],"remaining":[],"files":["src/main.rs"]}"#;
let resp = parse(input).unwrap();
assert_eq!(resp.status, "done");
assert_eq!(resp.summary, "Work complete");
}
#[test]
fn parse_regression_2979_success_true_envelope() {
let input = r#"The fix removes all actionable entry/SL/TP/R:R parameters from the bearish-mode trade section. The three candidates are now a plain watchlist table.
```json
{"success": true, "summary": "Fixed bearish/longs contradiction: replaced actionable long setups with a non-executable watchlist table."}
```"#;
let resp = parse(input).unwrap();
assert_eq!(resp.status, "done");
assert_eq!(
resp.summary,
"Fixed bearish/longs contradiction: replaced actionable long setups with a non-executable watchlist table."
);
}
#[test]
fn parse_regression_2979_type_task_complete_envelope() {
let input = r#"The daily trending doc is complete at md/research/2026-04-22-daily-trending.md.
```json
{
"type": "task_complete",
"summary": "Fetched global trending topics and saved to md/research/2026-04-22-daily-trending.md.",
"files_changed": ["md/research/2026-04-22-daily-trending.md"]
}
```"#;
let resp = parse(input).unwrap();
assert_eq!(resp.status, "done");
assert!(resp.summary.contains("Fetched global trending topics"));
assert_eq!(resp.files, vec!["md/research/2026-04-22-daily-trending.md"]);
}
#[test]
fn parse_regression_2979_tests_passed_envelope() {
let input = r#"PR created: gabrielkoerich/orch#2969
The fix from attempt #8 was already committed. All quality gates pass.
```json
{
"summary": "Retry watermark persistence on store failure.",
"files_modified": ["src/engine/review_poll.rs"],
"pr_url": "https://github.com/gabrielkoerich/orch/pull/2969",
"tests_passed": true,
"quality_gates": ["cargo fmt", "cargo clippy", "cargo nextest run"]
}
```"#;
let resp = parse(input).unwrap();
assert_eq!(resp.status, "done");
assert!(resp.summary.contains("Retry watermark persistence"));
assert_eq!(resp.files, vec!["src/engine/review_poll.rs"]);
}
#[test]
fn parse_regression_2979_success_false_does_not_infer_done() {
let input = r#"```json
{"success": false, "summary": "Could not complete the task."}
```"#;
let result = parse(input);
assert!(
result.is_err(),
"success: false should not be treated as done"
);
}
#[test]
fn parse_regression_2979_type_other_does_not_infer_done() {
let input = r#"{"type": "tool_call", "summary": "calling bash"}"#;
let result = parse(input);
assert!(
result.is_err(),
"type other than task_complete should not be treated as done"
);
}
#[test]
fn parse_regression_2979_files_modified_alias() {
let input = r#"{"status": "done", "summary": "done", "files_modified": ["src/foo.rs", "src/bar.rs"]}"#;
let resp = parse(input).unwrap();
assert_eq!(resp.files, vec!["src/foo.rs", "src/bar.rs"]);
}
#[test]
fn parse_regression_3336_jsonl_domain_status_field_rejected() {
let input = r#"{"date":"2026-06-15","role":"Senior Solana Engineer","company":"M0","status":"inbound-not-submitted","source":"referral"}
{"date":"2026-06-15","role":"Rust Engineer (Solana)","company":"Exo Tech","status":"applied-follow-up-overdue","source":"linkedin"}"#;
let err = parse(input).unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("agent output is not valid JSON or a supported JSON wrapper")
|| msg.contains("JSON object lacks substantive"),
"unexpected error: {msg}"
);
}
#[test]
fn parse_regression_3336_single_domain_record_rejected() {
let input = r#"{"status":"inbound-not-submitted","date":"2026-06-15","role":"Engineer","company":"Acme"}"#;
let err = parse(input).unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("agent output is not valid JSON or a supported JSON wrapper")
|| msg.contains("JSON object lacks substantive"),
"unexpected error: {msg}"
);
}
#[test]
fn parse_regression_3336_noncanonical_status_with_summary_still_accepted() {
let input = r#"{"status":"fix_deployed","summary":"deployed to staging","accomplished":[],"remaining":[],"files":[]}"#;
let resp = parse(input).unwrap();
assert_eq!(resp.status, "fix_deployed");
assert_eq!(resp.summary, "deployed to staging");
}
#[test]
fn parse_regression_3336_jsonl_domain_plus_orch_response_prefers_orch() {
let input = r#"{"date":"2026-06-15","role":"Senior Solana Engineer","company":"M0","status":"inbound-not-submitted","source":"referral"}
{"date":"2026-06-15","role":"Rust Engineer (Solana)","company":"Exo Tech","status":"applied-follow-up-overdue","source":"linkedin"}
{"status":"done","summary":"Career radar written to md/career/2026-06-15-radar.md","accomplished":["Wrote career radar"],"remaining":[],"files":["md/career/2026-06-15-radar.md"]}"#;
let resp = parse(input).unwrap();
assert_eq!(resp.status, "done");
assert_eq!(
resp.summary,
"Career radar written to md/career/2026-06-15-radar.md"
);
assert_eq!(resp.files, vec!["md/career/2026-06-15-radar.md"]);
}
#[test]
fn parse_regression_3336_noncanonical_with_output_accepted() {
let input = r#"{"status":"mystery_status","output":"All tests passed"}"#;
let resp = parse(input).unwrap();
assert_eq!(resp.status, "mystery_status");
}
#[test]
fn parse_regression_3336_noncanonical_with_message_accepted() {
let input = r#"{"status":"mystery_status","message":"All tests passed"}"#;
let resp = parse(input).unwrap();
assert_eq!(resp.status, "mystery_status");
}
}