1use std::path::{Path, PathBuf};
10use std::sync::{Arc, Mutex};
11
12use serde::{Deserialize, Serialize};
13use speccoder_core::stage::{Stage, StageState};
14use speccoder_core::{CommandData, CommandResult, CommandStatus};
15
16use crate::substrate::model_config::Prices;
17
18pub const FILE: &str = "live.json";
20
21#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
23pub struct PlannedTask {
24 pub id: String,
26 pub title: String,
28 pub state: String,
30}
31
32#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
34pub struct PlanEntry {
35 pub content: String,
37 pub status: String,
39}
40
41pub const RECENT: usize = 12;
43
44#[derive(Debug, Clone, PartialEq, Serialize)]
46#[serde(tag = "event", rename_all = "snake_case")]
47pub enum Event {
48 RunStarted {
51 feature: String,
53 tasks: Vec<PlannedTask>,
55 },
56 TaskStarted {
58 feature: String,
60 task: String,
62 title: String,
64 },
65 Stage {
67 task: String,
69 stage: Stage,
71 state: StageState,
73 detail: Option<String>,
75 elapsed_ms: Option<u64>,
77 tail: Vec<String>,
79 },
80 Resumed {
82 task: String,
84 stage: Option<Stage>,
86 last_call: Option<String>,
88 },
89 ApprovalRequested {
91 task: String,
93 call: String,
95 tool: String,
97 what: String,
99 class: String,
102 object: String,
105 },
106 ApprovalDecided {
108 task: String,
110 call: String,
112 decision: String,
114 scope: Option<String>,
116 by: String,
118 how: String,
120 class: String,
122 object: String,
124 },
125 ToolCall {
127 task: String,
129 turn: u32,
131 tool: String,
133 target: String,
135 decision: String,
137 ok: Option<bool>,
139 tail: Vec<String>,
141 },
142 Usage {
144 task: String,
146 input_tokens: u64,
148 output_tokens: u64,
150 cache_read_tokens: u64,
152 cache_write_tokens: u64,
154 usd: Option<f64>,
156 },
157 TaskFinished {
159 task: String,
161 outcome: String,
163 message: String,
165 run: Option<String>,
167 input_tokens: u64,
169 output_tokens: u64,
171 cache_read_tokens: u64,
173 cache_write_tokens: u64,
175 usd: Option<f64>,
177 regression: Option<speccoder_core::regression::Regression>,
179 },
180 CollectionChecked {
183 task: String,
185 files: Vec<CollectionFile>,
187 },
188 RegressionBisected {
190 task: String,
192 runs: u32,
194 result: Option<String>,
196 },
197 AgentStarted {
201 task: String,
203 agent: String,
205 mode: String,
207 decides_alone: bool,
209 known: bool,
212 },
213 AgentUnasked {
216 task: String,
218 calls: u32,
220 },
221 AgentPlan {
224 task: String,
226 entries: Vec<PlanEntry>,
228 },
229 Said {
233 task: String,
235 turn: u32,
237 text: String,
239 },
240 Reasoning {
244 task: String,
246 turn: u32,
248 text: String,
250 },
251 Notice {
253 text: String,
255 },
256 Bug {
258 task: String,
260 step: BugEvent,
262 },
263 Oracle {
266 step: OracleEvent,
268 },
269 PlanningStopped {
271 unit: Option<String>,
273 kind: String,
275 next_skill: Option<String>,
277 },
278 Project {
282 step: ProjectEvent,
284 },
285}
286
287#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
289#[serde(tag = "event", rename_all = "snake_case")]
290pub enum ProjectEvent {
291 ScaffoldStarted {
293 step: String,
295 argv: Vec<String>,
297 access: String,
299 },
300 ScaffoldFinished {
302 step: String,
304 state: String,
306 exit: Option<i32>,
308 duration_ms: u64,
310 },
311 Created {
313 path_digest: String,
315 mode: String,
317 stack: String,
319 format: String,
321 commit: String,
323 via: String,
325 },
326 Undone {
328 path_digest: String,
330 how: String,
332 via: String,
334 },
335}
336
337#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
339pub struct CollectionFile {
340 pub path: String,
342 pub state: speccoder_core::collection::FileState,
344 pub decided_by: Option<speccoder_core::collection::DecidedBy>,
346}
347
348#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
350#[serde(tag = "kind", rename_all = "snake_case")]
351pub enum OracleEvent {
352 Stage {
354 requirement: String,
356 stage: String,
358 n: u32,
360 of: u32,
362 },
363 Frozen {
365 unit: String,
367 requirement: String,
369 manifest_sha256: String,
371 approval_sha256: String,
373 visible_count: usize,
375 held_out_count: usize,
377 by: String,
379 via: String,
381 },
382 Lapsed {
384 unit: String,
386 requirement: String,
388 frozen_wording_sha256: String,
390 current_wording_sha256: String,
392 },
393}
394
395#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
397pub struct StatementClass {
398 pub statement: String,
400 pub class: String,
402}
403
404#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
406pub struct ShownReproduction {
407 pub statement: String,
409 pub file: String,
411 pub test: String,
413 pub digest: String,
415}
416
417#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
419pub struct ShownSpecialCase {
420 pub statement: String,
422 pub rejected: bool,
424}
425
426#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
428#[serde(tag = "kind", rename_all = "snake_case")]
429pub enum BugEvent {
430 Recorded {
432 home: String,
434 unit: String,
436 requirement: String,
438 triad_digest: String,
440 via: String,
442 },
443 ReproduceTry {
445 n: u32,
447 of: u32,
449 per_statement: Vec<StatementClass>,
451 },
452 Reproduced {
454 reproductions: Vec<ShownReproduction>,
456 },
457 ConfirmWaiting {
459 digest: String,
461 next: String,
463 },
464 Confirmed {
466 digest: String,
468 by: String,
470 via: String,
472 basis: String,
474 },
475 ConfirmRejected {
477 digest: String,
479 reason: String,
481 },
482 UnchangedSelected {
484 state: String,
486 count: usize,
488 },
489 Discrimination {
491 revert: String,
493 special_case: Vec<ShownSpecialCase>,
495 },
496 Outcome {
498 outcome: String,
500 verdict: Option<String>,
502 code: Option<String>,
504 next: Vec<String>,
506 },
507}
508
509impl BugEvent {
510 #[must_use]
513 pub const fn name(&self) -> &'static str {
514 match self {
515 Self::Recorded { .. } => "bug.recorded",
516 Self::ReproduceTry { .. } => "bug.reproduce_try",
517 Self::Reproduced { .. } => "bug.reproduced",
518 Self::ConfirmWaiting { .. } => "bug.confirm_waiting",
519 Self::Confirmed { .. } => "bug.confirmed",
520 Self::ConfirmRejected { .. } => "bug.confirm_rejected",
521 Self::UnchangedSelected { .. } => "bug.unchanged_selected",
522 Self::Discrimination { .. } => "bug.discrimination",
523 Self::Outcome { .. } => "bug.outcome",
524 }
525 }
526
527 #[must_use]
529 pub fn line(&self) -> String {
530 match self {
531 Self::Recorded {
532 unit, requirement, ..
533 } => format!("bug recorded as {requirement} in {unit}"),
534 Self::ReproduceTry {
535 n,
536 of,
537 per_statement,
538 } => format!(
539 "reproduce try {n}/{of}: {}",
540 per_statement
541 .iter()
542 .map(|s| format!("{} {}", s.statement, s.class))
543 .collect::<Vec<_>>()
544 .join(", ")
545 ),
546 Self::Reproduced { reproductions } => format!(
547 "reproduced: {} test(s) fail on an assertion",
548 reproductions
549 .iter()
550 .filter(|r| r.statement.starts_with('E'))
551 .count()
552 ),
553 Self::ConfirmWaiting { next, .. } => format!("the reproductions wait for you: {next}"),
554 Self::Confirmed { by, basis, .. } => {
555 format!("reproductions confirmed by {by} ({basis})")
556 }
557 Self::ConfirmRejected { reason, .. } => format!("reproductions rejected: {reason}"),
558 Self::UnchangedSelected { state, count } => {
559 format!("pass-to-pass: {count} test(s) ({state})")
560 }
561 Self::Discrimination {
562 revert,
563 special_case,
564 } => format!(
565 "revert {revert}; {} special case(s), {} rejected",
566 special_case.len(),
567 special_case.iter().filter(|s| s.rejected).count()
568 ),
569 Self::Outcome {
570 outcome, verdict, ..
571 } => match verdict {
572 Some(v) => format!("bug {outcome}: {v}"),
573 None => format!("bug {outcome}"),
574 },
575 }
576 }
577}
578
579pub trait Progress: Send + Sync + std::fmt::Debug {
581 fn event(&self, event: &Event);
583}
584
585#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
587pub struct LiveState {
588 pub pid: u32,
590 pub feature: String,
592 pub task: String,
594 pub title: String,
596 #[serde(default)]
598 pub stage: Option<Stage>,
599 #[serde(default)]
601 pub stage_detail: Option<String>,
602 pub last_call: Option<String>,
604 #[serde(default)]
606 pub said: Option<String>,
607 #[serde(default)]
609 pub thinking: Option<String>,
610 #[serde(default)]
612 pub recent: Vec<String>,
613 pub input_tokens: u64,
615 pub output_tokens: u64,
617 pub usd: Option<f64>,
619 pub finished: Option<String>,
621 pub started_at: u64,
623 pub updated_at: u64,
625 #[serde(default, skip_serializing_if = "Option::is_none")]
627 pub agent: Option<LiveAgent>,
628}
629
630#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
633pub struct LiveAgent {
634 pub name: String,
636 pub mode: String,
638 pub decides_alone: bool,
641 #[serde(default)]
643 pub known: bool,
644 #[serde(default)]
646 pub unasked: u32,
647 #[serde(default)]
649 pub plan: Vec<PlanEntry>,
650 pub asked: u32,
652 pub allowed: u32,
654 pub waited: u32,
656 pub refused: u32,
658}
659
660fn now() -> u64 {
661 std::time::SystemTime::now()
662 .duration_since(std::time::UNIX_EPOCH)
663 .map_or(0, |d| d.as_secs())
664}
665
666#[must_use]
668pub fn usd(
669 prices: Option<Prices>,
670 input: u64,
671 output: u64,
672 cache_read: u64,
673 cache_write: u64,
674) -> Option<f64> {
675 prices.map(|p| p.usd(input, output, cache_read, cache_write))
676}
677
678#[derive(Debug)]
680pub struct Live {
681 file: PathBuf,
682 prices: Option<Prices>,
683 sink: Option<Arc<dyn Progress>>,
684 state: Mutex<LiveState>,
685}
686
687impl Live {
688 #[must_use]
690 pub fn new(dir: &Path, prices: Option<Prices>, sink: Option<Arc<dyn Progress>>) -> Self {
691 let _ = std::fs::create_dir_all(dir);
692 Self {
693 file: dir.join(FILE),
694 prices,
695 sink,
696 state: Mutex::new(LiveState {
697 pid: std::process::id(),
698 ..LiveState::default()
699 }),
700 }
701 }
702
703 fn write(&self, state: &LiveState) {
704 if let Ok(text) = serde_json::to_string_pretty(state) {
705 let tmp = self.file.with_extension("json.tmp");
706 if std::fs::write(&tmp, text).is_ok() {
707 let _ = std::fs::rename(&tmp, &self.file);
708 }
709 }
710 }
711}
712
713impl Progress for Live {
714 fn event(&self, event: &Event) {
715 let mut event = event.clone();
716 match &mut event {
718 Event::Usage {
719 input_tokens,
720 output_tokens,
721 cache_read_tokens,
722 cache_write_tokens,
723 usd: price,
724 ..
725 } => {
726 *price = usd(
727 self.prices,
728 *input_tokens,
729 *output_tokens,
730 *cache_read_tokens,
731 *cache_write_tokens,
732 )
733 }
734 Event::TaskFinished {
735 input_tokens,
736 output_tokens,
737 cache_read_tokens,
738 cache_write_tokens,
739 usd: price,
740 ..
741 } => {
742 *price = usd(
743 self.prices,
744 *input_tokens,
745 *output_tokens,
746 *cache_read_tokens,
747 *cache_write_tokens,
748 )
749 }
750 _ => {}
751 }
752 if let Ok(mut s) = self.state.lock() {
753 match &event {
754 Event::TaskStarted {
755 feature,
756 task,
757 title,
758 } => {
759 *s = LiveState {
760 pid: s.pid,
761 feature: feature.clone(),
762 task: task.clone(),
763 title: title.clone(),
764 started_at: now(),
765 ..LiveState::default()
766 };
767 }
768 Event::Stage {
769 stage,
770 state: StageState::Started,
771 detail,
772 ..
773 } => {
774 s.stage = Some(*stage);
775 s.stage_detail.clone_from(detail);
776 }
777 Event::Said { text, .. } => {
778 s.said = text
779 .lines()
780 .find(|l| !l.trim().is_empty())
781 .map(|l| l.chars().take(160).collect());
782 }
783 Event::Reasoning { text, .. } => {
785 s.thinking = text
786 .lines()
787 .rev()
788 .find(|l| !l.trim().is_empty())
789 .map(|l| l.chars().take(160).collect());
790 }
791 Event::AgentStarted {
792 agent,
793 mode,
794 decides_alone,
795 known,
796 ..
797 } => {
798 s.agent = Some(LiveAgent {
799 name: agent.clone(),
800 mode: mode.clone(),
801 decides_alone: *decides_alone,
802 known: *known,
803 ..LiveAgent::default()
804 });
805 }
806 Event::AgentUnasked { calls, .. } => {
807 if let Some(a) = s.agent.as_mut() {
808 a.unasked = *calls;
809 a.decides_alone = true;
810 }
811 }
812 Event::AgentPlan { entries, .. } => {
813 if let Some(a) = s.agent.as_mut() {
814 a.plan.clone_from(entries);
815 }
816 }
817 Event::ApprovalRequested { .. } => {
818 if let Some(a) = s.agent.as_mut() {
819 a.waited += 1;
820 }
821 }
822 Event::Stage { .. } | Event::Resumed { .. } | Event::ApprovalDecided { .. } => {}
823 Event::ToolCall {
824 turn,
825 tool,
826 target,
827 decision,
828 ..
829 } => {
830 if let Some(a) = s.agent.as_mut()
831 && decision != "reported"
832 {
833 a.asked += 1;
834 match decision.as_str() {
835 "allow" => a.allowed += 1,
836 "deny" => a.refused += 1,
837 _ => {}
838 }
839 }
840 let call = format!("{tool} {target} ({decision})");
841 s.recent.push(format!("turn {turn} {call}"));
842 if s.recent.len() > RECENT {
843 s.recent.remove(0);
844 }
845 s.last_call = Some(call);
846 }
847 Event::Usage {
848 input_tokens,
849 output_tokens,
850 usd,
851 ..
852 } => {
853 s.input_tokens = *input_tokens;
854 s.output_tokens = *output_tokens;
855 s.usd = *usd;
856 }
857 Event::TaskFinished {
858 outcome, message, ..
859 } => s.finished = Some(format!("{outcome}: {message}")),
860 Event::Notice { .. }
861 | Event::RunStarted { .. }
862 | Event::Bug { .. }
863 | Event::Oracle { .. }
864 | Event::CollectionChecked { .. }
865 | Event::RegressionBisected { .. }
866 | Event::PlanningStopped { .. }
867 | Event::Project { .. } => {}
868 }
869 s.updated_at = now();
870 self.write(&s);
871 }
872 if let Some(sink) = &self.sink {
873 sink.event(&event);
874 }
875 }
876}
877
878#[must_use]
880pub fn target(tool: &str, args: &serde_json::Value) -> String {
881 let text = match tool {
882 "run_command" => args["argv"]
883 .as_array()
884 .map(|a| {
885 a.iter()
886 .filter_map(|v| v.as_str())
887 .collect::<Vec<_>>()
888 .join(" ")
889 })
890 .unwrap_or_default(),
891 "search_code" => args["pattern"].as_str().unwrap_or_default().to_owned(),
892 name if name.starts_with(crate::mcp::PREFIX) => args
894 .as_object()
895 .and_then(|o| o.values().find_map(|v| v.as_str().map(str::to_owned)))
896 .unwrap_or_default(),
897 _ => args["path"].as_str().unwrap_or_default().to_owned(),
898 };
899 let one: String = text.split_whitespace().collect::<Vec<_>>().join(" ");
900 if one.chars().count() > 60 {
901 format!("{}…", one.chars().take(59).collect::<String>())
902 } else {
903 one
904 }
905}
906
907#[must_use]
911pub fn read_live(path: &Path, state: &Path) -> Option<(LiveState, bool)> {
912 let root = match crate::inspect(path, None).data() {
913 Some(CommandData::Inspect(s)) => PathBuf::from(&s.root),
914 _ => return None,
915 };
916 let text = std::fs::read_to_string(crate::worktree::project_state_dir(state, &root).join(FILE))
917 .ok()?;
918 let live: LiveState = serde_json::from_str(&text).ok()?;
919 let running = live.finished.is_none() && alive(live.pid);
920 Some((live, running))
921}
922
923pub fn alive(pid: u32) -> bool {
925 i32::try_from(pid)
926 .is_ok_and(|p| nix::sys::signal::kill(nix::unistd::Pid::from_raw(p), None).is_ok())
927}
928
929pub fn status_with(path: &Path, state: &Path) -> CommandResult {
932 status_at(path, state, agentplane::core::Timestamp::now_utc())
933}
934
935#[doc(hidden)]
938pub fn status_at(path: &Path, state: &Path, now: agentplane::core::Timestamp) -> CommandResult {
939 let inspected = crate::inspect(path, None);
941 let root = match inspected.data() {
942 Some(CommandData::Inspect(s)) => PathBuf::from(&s.root),
943 _ => return CommandResult::carried("report", &inspected),
944 };
945 let plane_dir = crate::worktree::project_state_dir(state, &root);
946 let res = status_inner(&plane_dir);
947 let lines = waiting_lines(&plane_dir, now);
948 if lines.is_empty() {
949 return res;
950 }
951 let mut next = res.next().to_vec();
952 next.insert(
953 0,
954 speccoder_core::next::NextStep {
955 command: "speccoder report --approvals".to_owned(),
956 why: "see the waiting calls, then approve or reject them".to_owned(),
957 },
958 );
959 let message = format!("{}\n{}", res.message(), lines.join("\n"));
960 let status = res.status();
961 res.reworded(status, &message).with_next(next)
962}
963
964fn waiting_lines(plane_dir: &Path, now: agentplane::core::Timestamp) -> Vec<String> {
968 let unknown = |why: String| vec![format!("waiting for a person: unknown — {why}")];
972 let rt = match crate::tokio_runtime() {
973 Ok(rt) => rt,
974 Err(e) => return unknown(e),
975 };
976 let plane = match crate::substrate::Plane::open_for_reading(plane_dir) {
977 Ok(Some(p)) => p,
978 Ok(None) => return Vec::new(),
979 Err(e) => return unknown(e.0),
980 };
981 let open = match rt.block_on(plane.open_tasks()) {
982 Ok(open) => open,
983 Err(e) => return unknown(e.0),
984 };
985 open.into_iter()
986 .filter(|t| crate::approvals::still_waits(t.due_at, now))
987 .filter_map(|t| {
988 let request = crate::approvals::tasks::request_of(&t.justification.proposed_action)?;
989 let since = (now - t.created_at).whole_minutes().max(0);
990 let until = t
991 .due_at
992 .map(|d| {
993 format!(
994 ", denied as expired in {} h",
995 (d - now).whole_hours().max(0)
996 )
997 })
998 .unwrap_or_default();
999 Some(format!(
1000 "waiting for a person: {} — {} (for {since} min{until})",
1001 request.task,
1002 request.covers()
1003 ))
1004 })
1005 .collect()
1006}
1007
1008fn status_inner(plane_dir: &Path) -> CommandResult {
1009 let plane_dir = plane_dir.to_path_buf();
1010 if let Some(s) = crate::in_place::Session::load(&plane_dir) {
1012 let mode = s.mode_line();
1013 if !alive(s.pid) {
1014 let step = |command: &str, why: &str| speccoder_core::next::NextStep {
1015 command: command.to_owned(),
1016 why: why.to_owned(),
1017 };
1018 return CommandResult::new(
1019 "status",
1020 CommandStatus::Ok,
1021 &format!(
1022 "interrupted in-place run of {} (pid {} is gone): the checkout is still on {}\n{mode}",
1023 s.feature, s.pid, s.branch
1024 ),
1025 )
1026 .with_next(vec![
1027 step("speccoder run --in-place", "resume where it stopped"),
1028 step(
1029 "speccoder run --in-place --return",
1030 "save what it left and switch back without running",
1031 ),
1032 ]);
1033 }
1034 let res = status_live(&plane_dir);
1035 let message = format!("{}\n{mode}", res.message());
1036 let status = res.status();
1037 return res.reworded(status, &message);
1038 }
1039 status_live(&plane_dir)
1040}
1041
1042fn status_live(plane_dir: &Path) -> CommandResult {
1044 let file = plane_dir.join(FILE);
1045 let live: LiveState = match std::fs::read_to_string(&file)
1046 .ok()
1047 .and_then(|t| serde_json::from_str(&t).ok())
1048 {
1049 Some(l) => l,
1050 None => {
1051 return CommandResult::new(
1052 "status",
1053 CommandStatus::Ok,
1054 "no run has been recorded for this project yet",
1055 );
1056 }
1057 };
1058 let ago = now().saturating_sub(live.updated_at);
1059 let tokens = format!(
1060 "{} in · {} out{}",
1061 live.input_tokens,
1062 live.output_tokens,
1063 live.usd.map(|u| format!(" · ${u:.4}")).unwrap_or_default()
1064 );
1065 use speccoder_core::next::{Outcome, next};
1066 let outcome = match (&live.finished, alive(live.pid)) {
1067 (None, true) => Some(Outcome::Running),
1068 (None, false) => Some(Outcome::Interrupted {
1069 task: live.task.clone(),
1070 }),
1071 (Some(_), _) => None,
1072 };
1073 let message = match (&live.finished, alive(live.pid)) {
1074 (None, true) => format!(
1075 "running (pid {}): {} {} — {} for {}s; last: {}; {tokens}{}{}",
1076 live.pid,
1077 live.feature,
1078 live.task,
1079 stage_word(&live),
1080 now().saturating_sub(live.started_at),
1081 live.last_call.as_deref().unwrap_or("-"),
1082 live.thinking
1083 .as_deref()
1084 .map(|s| format!("\nthe model thinks (a claim): {s}"))
1085 .unwrap_or_default(),
1086 live.said
1087 .as_deref()
1088 .map(|s| format!("\nthe model says (a claim): “{s}”"))
1089 .unwrap_or_default(),
1090 ),
1091 (None, false) => format!(
1092 "interrupted: {} {} stopped during {} {ago}s ago (pid {} is gone); the next `speccoder run` resumes it from its journal",
1093 live.feature,
1094 live.task,
1095 stage_word(&live),
1096 live.pid
1097 ),
1098 (Some(outcome), _) => format!(
1099 "last run: {} {} — {outcome} ({ago}s ago); {tokens}",
1100 live.feature, live.task
1101 ),
1102 };
1103 CommandResult::new("status", CommandStatus::Ok, &message)
1104 .with_next(outcome.map(|o| next(&o, None)).unwrap_or_default())
1105}
1106
1107#[must_use]
1109pub fn stage_word(live: &LiveState) -> String {
1110 match (live.stage, &live.stage_detail) {
1111 (Some(st), Some(d)) => format!("{} {d}", st.name()),
1112 (Some(st), None) => st.name().to_owned(),
1113 (None, _) => "starting".to_owned(),
1114 }
1115}
1116
1117#[must_use]
1120pub fn tail(text: &str) -> Vec<String> {
1121 let lines: Vec<String> = text
1122 .lines()
1123 .map(|l| {
1124 l.chars()
1125 .filter(|c| !c.is_control())
1126 .take(200)
1127 .collect::<String>()
1128 })
1129 .filter(|l| !l.trim().is_empty())
1130 .collect();
1131 lines[lines.len().saturating_sub(5)..].to_vec()
1132}
1133
1134#[must_use]
1136pub fn status(path: &Path) -> CommandResult {
1137 match crate::worktree::state_dir() {
1138 Some(state) => status_with(path, &state),
1139 None => CommandResult::new(
1140 "status",
1141 CommandStatus::ConfigError,
1142 "no state directory (set SPECCODER_STATE_DIR or HOME)",
1143 )
1144 .with_code("NO-STATE-DIR"),
1145 }
1146}