use std::collections::BTreeSet;
use std::path::{Path, PathBuf};
use anyhow::{bail, Result};
use rayon::prelude::*;
use crate::cli::{OutputFormat, PlanArgs};
use crate::parse::mmap_bytes;
use crate::path;
mod audit;
mod slug;
pub(crate) use audit::*;
use slug::*;
pub const PLAN_SIGIL: &str = "@plan";
#[derive(Debug, Clone)]
pub struct PlanRef {
pub session_id: String,
pub is_subagent: bool,
pub parent_session_id: String,
pub plan_file: String,
pub plan_exists: bool,
pub line_no: usize,
pub slug: Option<String>,
pub binding_source: &'static str,
pub minted_at_compaction: bool,
}
fn line_is_plan_candidate(line: &[u8]) -> bool {
static PLAN_MODE: std::sync::LazyLock<memchr::memmem::Finder<'static>> =
std::sync::LazyLock::new(|| memchr::memmem::Finder::new(b"plan_mode"));
PLAN_MODE.find(line).is_some()
}
pub fn resolve_session_plan(path: &Path) -> Result<Option<PlanRef>> {
let session_id = crate::subagent::session_id_from_path(path);
let is_subagent = crate::subagent::is_subagent_path(path);
let parent_session_id =
crate::subagent::parent_session_id_from_path(path).unwrap_or_else(|| session_id.clone());
let Some(mmap) = mmap_bytes(path)? else {
return Ok(None);
};
let bytes: &[u8] = &mmap;
let (records, _skipped) =
crate::parse::parse_candidates_parallel(bytes, line_is_plan_candidate);
let mut latest: Option<PlanRef> = None;
for (line_no, rec) in &records {
let Some(att) = rec.attachment_value() else {
continue;
};
if att.get("type").and_then(serde_json::Value::as_str) != Some("plan_mode") {
continue;
}
let Some(plan_file) = att.get("planFilePath").and_then(serde_json::Value::as_str) else {
continue;
};
let plan_is_subagent = att
.get("isSubAgent")
.and_then(serde_json::Value::as_bool)
.unwrap_or(is_subagent);
latest = Some(PlanRef {
session_id: session_id.clone(),
is_subagent: plan_is_subagent,
parent_session_id: parent_session_id.clone(),
plan_file: plan_file.to_string(),
plan_exists: Path::new(plan_file).is_file(),
line_no: *line_no,
slug: rec.slug.clone(),
binding_source: "plan_mode",
minted_at_compaction: false,
});
}
if latest.is_none() {
if let Some((line_no, rec)) = first_slug_record(bytes) {
if let Some(slug) = rec.slug.as_deref().filter(|s| slug_is_valid(s)) {
let root_owned = first_cwd(bytes).or_else(|| rec.cwd.clone());
let root = root_owned.as_deref().map(Path::new);
let plan_file = plans_dir(root).join(format!("{slug}.md"));
latest = Some(PlanRef {
session_id: session_id.clone(),
is_subagent,
parent_session_id: parent_session_id.clone(),
plan_exists: plan_file.is_file(),
plan_file: plan_file.display().to_string(),
line_no,
slug: Some(slug.to_string()),
binding_source: "slug-only",
minted_at_compaction: rec.is_compact_boundary(),
});
}
}
}
Ok(latest)
}
pub fn resolve_plan_target(session_files: &[PathBuf]) -> Result<PlanRef> {
let mut refs: Vec<PlanRef> = Vec::new();
for p in session_files {
if let Some(r) = resolve_session_plan(p)? {
refs.push(r);
}
}
if refs.is_empty() {
bail!(
"--file {PLAN_SIGIL}: no plan file is bound to the target session(s) — no \
`plan_mode` attachment found (a session has a bound plan only if it entered \
Plan Mode). To recover an ordinary file, pass its path to --file instead."
);
}
let top_level: Vec<&PlanRef> = refs.iter().filter(|r| !r.is_subagent).collect();
let pool: Vec<&PlanRef> = if top_level.is_empty() {
refs.iter().collect()
} else {
top_level
};
let distinct: BTreeSet<&str> = pool.iter().map(|r| r.plan_file.as_str()).collect();
if distinct.len() > 1 {
let mut paths: Vec<&str> = distinct.into_iter().collect();
paths.sort_unstable();
bail!(
"--file {PLAN_SIGIL}: the target spans sessions with different bound plan files \
({}). Pass `@<uuid>` to select one.",
paths.join(", ")
);
}
let chosen = distinct.into_iter().next().unwrap_or_default();
Ok(pool
.into_iter()
.find(|r| r.plan_file == chosen)
.cloned()
.expect("chosen path came from pool"))
}
fn run_plan_reverse(args: &PlanArgs, plan_file: &Path) -> Result<()> {
let want = path::absolutize(plan_file)?;
let session_files = path::resolve_targets_with_session_list(
&args.paths,
args.sessions_from.as_deref(),
args.want_subagents().into(),
path::Caller::Other,
)?;
let mut hits: Vec<PlanRef> = session_files
.par_iter()
.map(|p| -> Result<Option<PlanRef>> {
Ok(resolve_session_plan(p)?.filter(|r| {
path::absolutize(Path::new(&r.plan_file))
.map(|a| a == want)
.unwrap_or(false)
}))
})
.collect::<Result<Vec<_>>>()?
.into_iter()
.flatten()
.collect();
hits.sort_by(|a, b| {
a.is_subagent
.cmp(&b.is_subagent)
.then_with(|| a.session_id.cmp(&b.session_id))
});
match args.format {
OutputFormat::Text => render_reverse_text(&want, &hits),
OutputFormat::Json => render_reverse_json(&want, &hits)?,
}
Ok(())
}
fn render_reverse_text(plan_file: &Path, hits: &[PlanRef]) {
println!("plan {}", plan_file.display());
if hits.is_empty() {
eprintln!("note: no session in scope is bound to this plan file (no `plan_mode` binding).");
return;
}
for r in hits {
let tag = if r.is_subagent { " (subagent)" } else { "" };
println!("session {}{}", r.session_id, tag);
if r.is_subagent {
println!("parent {}", r.parent_session_id);
}
if let Some(slug) = &r.slug {
println!("slug {slug}");
}
println!("bound at jsonl L{}", r.line_no);
}
}
fn render_reverse_json(plan_file: &Path, hits: &[PlanRef]) -> Result<()> {
let _ = plan_file; println!(
"{}",
crate::text::envelope_header("plan", serde_json::json!({}))
);
for r in hits {
let obj = serde_json::json!({
"kind": "plan",
"plan_file": r.plan_file,
"session_id": r.session_id,
"is_subagent": r.is_subagent,
"parent_session_id": r.parent_session_id,
"line": r.line_no,
"slug": r.slug,
"binding_source": r.binding_source,
"minted_at_compaction": r.minted_at_compaction,
});
println!("{}", serde_json::to_string(&obj)?);
}
println!(
"{}",
crate::text::envelope_summary(serde_json::json!({"plans": hits.len()}))
);
Ok(())
}
pub fn run_plan(args: &PlanArgs) -> Result<()> {
if let Some(plan_file) = &args.reverse {
return run_plan_reverse(args, plan_file);
}
let session_paths: Vec<PathBuf> = if args.paths.is_empty() && args.sessions_from.is_none() {
match crate::whoami::detect_session_id() {
Some(id) => vec![PathBuf::from(format!("@{id}"))],
None => bail!("{}", crate::whoami::AMBIGUOUS_GUIDANCE),
}
} else {
args.paths.clone()
};
let session_files = path::resolve_targets_with_session_list(
&session_paths,
args.sessions_from.as_deref(),
args.want_subagents().into(),
path::Caller::Other,
)?;
if args.audit {
return run_plan_audit(args, &session_files);
}
let mut refs: Vec<PlanRef> = session_files
.par_iter()
.map(|p| resolve_session_plan(p))
.collect::<Result<Vec<_>>>()?
.into_iter()
.flatten()
.collect();
refs.sort_by(|a, b| {
a.is_subagent
.cmp(&b.is_subagent)
.then_with(|| a.session_id.cmp(&b.session_id))
});
match args.format {
OutputFormat::Text => render_text(&refs),
OutputFormat::Json => render_json(&refs)?,
}
Ok(())
}
fn render_text(refs: &[PlanRef]) {
if refs.is_empty() {
eprintln!("note: no plan file is bound to the resolved session(s) (no Plan Mode).");
return;
}
for r in refs {
let tag = if r.is_subagent { " (subagent)" } else { "" };
println!("session {}{}", r.session_id, tag);
println!(
"plan {} [{}]",
r.plan_file,
if r.plan_exists { "exists" } else { "missing" }
);
if let Some(slug) = &r.slug {
println!("slug {slug}");
}
if r.binding_source == "slug-only" {
println!(
"binding slug only — no plan_mode attachment; Claude Code binds by the \
first slug-carrying record{}",
if r.minted_at_compaction {
" (slug MINTED at a compaction boundary — Plan Mode never ran; CC \
still injects/rebuilds this file)"
} else {
""
}
);
}
if r.is_subagent {
println!("parent {}", r.parent_session_id);
}
println!("line L{}", r.line_no);
}
}
fn render_json(refs: &[PlanRef]) -> Result<()> {
use serde_json::json;
println!("{}", crate::text::envelope_header("plan", json!({})));
for r in refs {
let obj = json!({
"kind": "plan",
"session_id": r.session_id,
"is_subagent": r.is_subagent,
"parent_session_id": r.parent_session_id,
"plan_file": r.plan_file,
"plan_exists": r.plan_exists,
"line": r.line_no,
"slug": r.slug,
"binding_source": r.binding_source,
"minted_at_compaction": r.minted_at_compaction,
});
println!("{}", serde_json::to_string(&obj)?);
}
println!(
"{}",
crate::text::envelope_summary(json!({"plans": refs.len()}))
);
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn resolve_plan_target_prefers_the_top_level_lane() {
let dir = std::env::temp_dir().join(format!("csift-plan-lane-{}", std::process::id()));
let sub_dir = dir.join("11111111-2222-4333-8444-555555555555/subagents");
std::fs::create_dir_all(&sub_dir).unwrap();
let top = dir.join("11111111-2222-4333-8444-555555555555.jsonl");
std::fs::write(
&top,
"{\"type\":\"attachment\",\"attachment\":{\"type\":\"plan_mode\",\"planFilePath\":\"/plans/top-level-plan.md\",\"isSubAgent\":false},\"uuid\":\"a1\",\"timestamp\":\"2026-06-07T05:00:01.000Z\"}\n",
)
.unwrap();
let sub = sub_dir.join("agent-abcdef0123456789.jsonl");
std::fs::write(
&sub,
"{\"type\":\"attachment\",\"attachment\":{\"type\":\"plan_mode\",\"planFilePath\":\"/plans/subagent-plan.md\",\"isSubAgent\":true},\"uuid\":\"a2\",\"timestamp\":\"2026-06-07T05:01:01.000Z\"}\n",
)
.unwrap();
let r = resolve_plan_target(&[top.clone(), sub.clone()]).unwrap();
std::fs::remove_file(&top).ok();
std::fs::remove_file(&sub).ok();
assert_eq!(r.plan_file, "/plans/top-level-plan.md");
assert!(!r.is_subagent);
}
#[test]
fn plan_sigil_is_bash_safe_and_at_prefixed() {
assert_eq!(PLAN_SIGIL, "@plan");
assert!(PLAN_SIGIL.starts_with('@'));
assert!(!PLAN_SIGIL
.chars()
.any(|c| matches!(c, '$' | '!' | '*' | '`' | ' ' | '"' | '\'' | '\\')));
}
#[test]
fn prefilter_matches_only_plan_mode_lines() {
assert!(line_is_plan_candidate(
br#"{"attachment":{"type":"plan_mode","planFilePath":"/x.md"}}"#
));
assert!(!line_is_plan_candidate(
br#"{"message":{"content":[{"type":"tool_use","name":"Edit","input":{"file_path":"/Users/x/.claude/plans/foo.md"}}]}}"#
));
}
#[test]
fn resolve_session_plan_binds_only_on_a_real_plan_mode_attachment() {
let dir = std::env::temp_dir().join(format!("csift-plan-ut-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let write = |name: &str, body: &str| {
let p = dir.join(name);
std::fs::write(&p, body).unwrap();
p
};
let empty = write("empty.jsonl", "");
assert!(resolve_session_plan(&empty).unwrap().is_none());
let decoy = write(
"decoy.jsonl",
"{\"type\":\"user\",\"timestamp\":\"2026-06-07T05:00:00Z\",\"message\":{\"role\":\"user\",\"content\":\"what does plan_mode do\"}}\n",
);
assert!(resolve_session_plan(&decoy).unwrap().is_none());
let other_att = write(
"other.jsonl",
"{\"type\":\"attachment\",\"attachment\":{\"type\":\"file\",\"note\":\"see plan_mode\"},\"timestamp\":\"2026-06-07T05:00:00Z\"}\n",
);
assert!(resolve_session_plan(&other_att).unwrap().is_none());
let no_path = write(
"nopath.jsonl",
"{\"type\":\"attachment\",\"attachment\":{\"type\":\"plan_mode\",\"isSubAgent\":false,\"planExists\":false},\"timestamp\":\"2026-06-07T05:00:00Z\"}\n",
);
assert!(resolve_session_plan(&no_path).unwrap().is_none());
let real = write(
"real.jsonl",
"{\"type\":\"attachment\",\"slug\":\"quiet-harbor-relay\",\"attachment\":{\"type\":\"plan_mode\",\"isSubAgent\":false,\"planFilePath\":\"/x/p.md\",\"planExists\":false},\"timestamp\":\"2026-06-07T05:00:00Z\"}\n",
);
let got = resolve_session_plan(&real)
.unwrap()
.expect("a real plan_mode binds");
assert_eq!(got.plan_file, "/x/p.md");
assert!(!got.is_subagent);
assert_eq!(got.slug.as_deref(), Some("quiet-harbor-relay"));
let no_slug = write(
"noslug.jsonl",
"{\"type\":\"attachment\",\"attachment\":{\"type\":\"plan_mode\",\"isSubAgent\":false,\"planFilePath\":\"/x/q.md\",\"planExists\":false},\"timestamp\":\"2026-06-07T05:00:00Z\"}\n",
);
assert_eq!(resolve_session_plan(&no_slug).unwrap().unwrap().slug, None);
let _ = std::fs::remove_dir_all(&dir);
}
}