Skip to main content

csd/detect/
plan.rs

1//! Plan-file signal. In plan mode `claude` writes the plan to a markdown file under
2//! `~/.claude/plans/` before the approval gate, and the plan *content* lives in that file rather
3//! than the transcript message (PoC §3.3).
4//!
5//! NOTE: the filename is NOT `plan-*.md` (the PoC's guess) — on claude v2.1.158 it is a slug of the
6//! prompt plus random words, e.g. `make-a-trivial-plan-parsed-whisper.md`. So we match any `*.md`.
7//!
8//! The plans dir is GLOBAL (not per-session), so we correlate by time: a `*.md` modified at or after
9//! the session's spawn epoch is attributed to it. This is a documented heuristic — the combiner
10//! additionally requires the capture-pane plan marker before declaring `plan_ready`, which makes a
11//! stale-file false positive harmless.
12
13use std::path::PathBuf;
14
15use crate::error::Result;
16use crate::session::{mtime_epoch, plans_dir};
17
18/// The newest `plan-*.md` modified at/after `since_epoch`, with its content if readable.
19pub fn latest_plan_file(since_epoch: u64) -> Result<Option<(PathBuf, Option<String>)>> {
20    let dir = plans_dir()?;
21    if !dir.exists() {
22        return Ok(None);
23    }
24
25    let mut best: Option<(u64, PathBuf)> = None;
26    for entry in std::fs::read_dir(&dir).map_err(|e| crate::error::Error::io(&dir, e))? {
27        let Ok(entry) = entry else { continue };
28        let path = entry.path();
29        let is_plan = path
30            .extension()
31            .and_then(|e| e.to_str())
32            .map(|e| e.eq_ignore_ascii_case("md"))
33            .unwrap_or(false);
34        if !is_plan {
35            continue;
36        }
37        let Some(mtime) = mtime_epoch(&path) else { continue };
38        if mtime < since_epoch {
39            continue;
40        }
41        if best.as_ref().map(|(t, _)| mtime > *t).unwrap_or(true) {
42            best = Some((mtime, path));
43        }
44    }
45
46    Ok(best.map(|(_, path)| {
47        let content = std::fs::read_to_string(&path).ok();
48        (path, content)
49    }))
50}