Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/claude.rs
Original file line number Diff line number Diff line change
Expand Up @@ -303,7 +303,7 @@ pub fn parse_lterm_input(raw: &str) -> ClaudeInput {
session_id: session_key,
// lterm 세션/페인 표시 라벨(status row에 cwd 앞 표시용).
session_label,
// codex enrich는 호출부(main.rs)에서 Source::Lterm 한정으로 별도 수행한다(초기 None).
// codex enrich는 호출부(main.rs)에서 Source::Lterm·Codex 한정으로 별도 수행한다(초기 None).
codex: None,
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/codex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -759,7 +759,7 @@ fn is_codex_model(model: &str) -> bool {
/// Codex 세션을 판독해 [`ClaudeInput`]을 in-place로 enrich한다(spec §7 게이팅).
///
/// # 게이팅(이중 + observability)
/// - 호출부([`crate::main`])에서 **`Source::Lterm`으로 한정**해 호출한다(Claude 경로 오발동 차단).
/// - 호출부([`crate::main`])에서 **`Source::Lterm`·`Source::Codex`로 한정**해 호출한다(Claude 경로 오발동 차단).
/// - 추가로 `cfg.codex.enabled` && model이 codex 계열 && `input.cwd=Some` && `codex_home()` 존재.
///
/// 단일 해소면 `model_display_name`/`context_used_percentage`/`codex`를 설정한다. 모호/없음/실패
Expand Down
70 changes: 60 additions & 10 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,16 +85,21 @@ fn has_extra_args(args: &[String]) -> bool {
args.len() > 2
}

/// 렌더 입력 소스(spec §6.1). `--source <claude|lterm>`로 선택하며 기본은 claude.
/// 렌더 입력 소스(spec §6.1). `--source <claude|lterm|codex>`로 선택하며 기본은 claude.
///
/// - `Claude`: 기존 동작(Claude Code stdin JSON 파싱 + chain 가능).
/// - `Lterm`: lterm 합성 JSON 파싱(git 비활성, chain 기본 off).
/// - `Codex`: `Lterm`과 동일한 합성 JSON 파서를 공유하되, lterm 데몬 없이
/// tmux status-line 등에서 직접 호출하는 용도다. codex enrich(`~/.codex` 직접 판독)가
/// `Lterm`과 동일하게 활성화되어 model/ctx%/rate-limit를 채운다(stdin에 `agent`/`cwd`만 주면 됨).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Source {
/// Claude Code stdin JSON(기본값).
Claude,
/// lterm 합성 JSON(`--source lterm`).
Lterm,
/// lterm-free 직접 호출(`--source codex`). `Lterm`과 파서·enrich를 공유한다.
Codex,
}

/// 렌더 출력 표면(surface) 형식. `--surface-format <oneline|cmux-status>`로 선택하며 기본 Oneline.
Expand All @@ -117,7 +122,7 @@ enum SurfaceFormat {

/// render 경로의 파싱된 플래그.
struct RenderArgs {
/// `--source <claude|lterm>`. 미지정 시 [`Source::Claude`].
/// `--source <claude|lterm|codex>`. 미지정 시 [`Source::Claude`].
source: Source,
/// `--oneline`. true면 chain 미수행 + 후행 개행 없이 1행 출력(spec §6.3).
oneline: bool,
Expand Down Expand Up @@ -154,9 +159,9 @@ fn parse_render_args(args: &[String]) -> Result<RenderArgs, String> {
while index < args.len() {
match args[index].as_str() {
"--source" => {
let value = args
.get(index + 1)
.ok_or_else(|| "--source 뒤에 값이 필요합니다(claude|lterm).".to_string())?;
let value = args.get(index + 1).ok_or_else(|| {
"--source 뒤에 값이 필요합니다(claude|lterm|codex).".to_string()
})?;
source = parse_source(value)?;
index += 2;
}
Expand Down Expand Up @@ -204,12 +209,23 @@ fn parse_source(value: &str) -> Result<Source, String> {
match value {
"claude" => Ok(Source::Claude),
"lterm" => Ok(Source::Lterm),
"codex" => Ok(Source::Codex),
other => Err(format!(
"알 수 없는 source '{other}'. 사용 가능: claude|lterm."
"알 수 없는 source '{other}'. 사용 가능: claude|lterm|codex."
)),
}
}

/// 해당 소스가 Codex 세션 심층판독(`~/.codex` 직접 판독) enrich를 수행해야 하는지 판정한다.
///
/// `Lterm`·`Codex`만 `true`다(둘 다 `parse_lterm_input` 기반 lterm 합성 JSON 경로).
/// `Claude`는 `false` — Claude payload의 모델 별칭이 우연히 codex 계열이어도 `~/.codex`를
/// 읽지 않도록 차단해 기존 Claude 경로를 **비트 단위로 보존**한다(회귀 0). 인라인 `matches!`를
/// 순수 함수로 추출해 게이트 불변식을 단위 테스트로 고정한다(`should_enrich_codex_gate`).
fn should_enrich_codex(source: Source) -> bool {
matches!(source, Source::Lterm | Source::Codex)
}

/// 설치 가능한 테마 기본값(미지정 + 비TTY/`--yes` 폴백).
const DEFAULT_THEME: &str = "calm";
/// 설치 가능한 갱신 주기 기본값(초).
Expand Down Expand Up @@ -641,7 +657,8 @@ fn run_render_pipeline(source: Source, oneline: bool, surface_format: SurfaceFor
// (2) 소스별 세션 정보 파싱(누락/null/깨진 JSON 안전). lterm은 git 비활성.
let mut claude_input = match source {
Source::Claude => claude::parse_claude_input(&raw_stdin),
Source::Lterm => claude::parse_lterm_input(&raw_stdin),
// lterm·codex는 동일 합성 JSON 파서를 공유한다(codex는 lterm 데몬 없이 tmux 등에서 직접 호출).
Source::Lterm | Source::Codex => claude::parse_lterm_input(&raw_stdin),
};

// 세션 캐시 격리 키를 한 곳에서 1회 살균한다(§11.3). session_id 부재/빈 값은 "default"로 폴백.
Expand All @@ -650,10 +667,11 @@ fn run_render_pipeline(source: Source, oneline: bool, surface_format: SurfaceFor
// (5) 설정 로드(부재/깨짐 시 기본값).
let cfg = config::load_config();

// (5') Codex 세션 심층판독 enrich(spec §7). **Source::Lterm 한정**: Claude 경로에서 모델
// (5') Codex 세션 심층판독 enrich(spec §7). **Source::Lterm·Codex 한정**: Claude 경로에서 모델
// 별칭이 우연히 codex 계열이어도 ~/.codex를 읽지 않도록 여기서 게이팅한다(비트 동일 보존).
// `--source codex`는 lterm 데몬 없이 직접 호출하는 경로로, 같은 파서·enrich를 공유한다.
// enrich는 session_id를 바꾸지 않으므로 위 session_key 도출/이후 파이프라인에 영향 없다.
if source == Source::Lterm {
if should_enrich_codex(source) {
codex::maybe_enrich(&mut claude_input, &cfg);
}

Expand Down Expand Up @@ -776,7 +794,7 @@ fn print_help() {
\x20 understatus --version 버전 출력\n\
\n\
render 옵션(understatus render 뒤에 사용):\n\
\x20 --source <s> 입력 소스(claude|lterm). 미지정 시 claude.\n\
\x20 --source <s> 입력 소스(claude|lterm|codex). 미지정 시 claude.\n\
\x20 --oneline chain 없이 코어 한 줄만 후행 개행 없이 출력(terse, status row용).\n\
\x20 --surface-format <f> 출력 표면(oneline|cmux-status). 미지정 시 oneline.\n\
\x20 (--surface-format은 표면 선택, --oneline은 terse 여부 — 직교)\n\
Expand Down Expand Up @@ -861,12 +879,44 @@ mod tests {
assert!(!parsed.oneline);
}

/// `--source codex` → Codex 소스 + oneline 동시 지정 진입(lterm-free tmux 경로).
#[test]
fn parse_render_args_source_codex() {
let parsed = parse_render_args(&render_argv(&["--source", "codex", "--oneline"]))
.expect("파싱 성공");
assert_eq!(parsed.source, Source::Codex);
assert!(parsed.oneline);
}

/// `--source` 마지막 값이 codex여도 last-wins 계약이 유지되어야 한다.
#[test]
fn parse_render_args_duplicate_source_codex_last_wins() {
let parsed = parse_render_args(&render_argv(&["--source", "lterm", "--source", "codex"]))
.expect("파싱 성공");
assert_eq!(parsed.source, Source::Codex);
}

/// 미지 source 값은 에러(ExitCode::FAILURE로 이어짐).
#[test]
fn parse_render_args_rejects_unknown_source() {
assert!(parse_render_args(&render_argv(&["--source", "bogus"])).is_err());
}

/// codex enrich 게이트 불변식(회귀 0 잠금): Lterm·Codex만 발화하고 Claude는 no-op.
///
/// `run_render_pipeline`이 stdin/`~/.codex` I/O를 타 직접 테스트가 어려우므로, 게이트를
/// 순수 함수로 추출해 여기서 잠근다. 향후 게이트 리팩토링이 Claude 경로를 오발동시키면
/// (예: `_ => true`) 이 테스트가 즉시 실패한다.
#[test]
fn should_enrich_codex_gate() {
assert!(should_enrich_codex(Source::Lterm), "Lterm은 enrich 발화");
assert!(should_enrich_codex(Source::Codex), "Codex는 enrich 발화");
assert!(
!should_enrich_codex(Source::Claude),
"Claude는 enrich no-op(회귀 0)"
);
}

/// `--source` 값 누락은 에러여야 한다.
#[test]
fn parse_render_args_rejects_missing_source_value() {
Expand Down