From c626c06ebe8eb590a9dce31071a194d70d532ad4 Mon Sep 17 00:00:00 2001 From: zombi3butt Date: Fri, 22 May 2026 14:11:01 +0700 Subject: [PATCH] fix(cli): properly implement --no-context-files flag per maintainer review --- README.md | 16 +++++++ src/cli/args.rs | 3 ++ src/cli/startup.rs | 6 +++ src/prompt.rs | 11 +++++ src/prompt_tests.rs | 104 ++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 140 insertions(+) diff --git a/README.md b/README.md index c904c8065..6fd45996e 100644 --- a/README.md +++ b/README.md @@ -568,6 +568,22 @@ jcode dictate jcode supports interactive TUI use, non-interactive runs, persistent server/client workflows, and hotkey-friendly dictation without requiring a bundled speech-to-text stack. +### Context File Control + +Skip loading project `AGENTS.md` and global `~/.AGENTS.md` context files for a session: + +```bash +# CLI flag (preferred) +jcode --no-context-files + +# Or via environment variable +JCODE_NO_CONTEXT_FILES=1 jcode + +# Both work identically; the CLI flag sets the env var internally +``` + +This is useful when you want to test with a clean context or run sessions without project instructions. +
diff --git a/src/cli/args.rs b/src/cli/args.rs index 1d07efa59..92ab70283 100644 --- a/src/cli/args.rs +++ b/src/cli/args.rs @@ -94,6 +94,9 @@ pub(crate) struct Args { /// Suppress non-error CLI/status output for scripting and wrappers #[arg(long, global = true)] pub(crate) quiet: bool, + /// Skip loading AGENTS.md project/global context files for this session + #[arg(long, global = true)] + pub(crate) no_context_files: bool, /// Resume a session by ID, or list sessions if no ID provided #[arg(long, global = true, num_args = 0..=1, default_missing_value = "")] diff --git a/src/cli/startup.rs b/src/cli/startup.rs index 8b1acae69..3c3643c62 100644 --- a/src/cli/startup.rs +++ b/src/cli/startup.rs @@ -131,6 +131,12 @@ fn parse_and_prepare_args() -> Result { } } + // --no-context-files: translate to JCODE_NO_CONTEXT_FILES so the prompt + // loading helpers can skip AGENTS.md without threading args. + if args.no_context_files { + crate::env::set_var("JCODE_NO_CONTEXT_FILES", "1"); + } + if let Some(ref socket) = args.socket { server::set_socket_path(socket); } diff --git a/src/prompt.rs b/src/prompt.rs index 496df490c..d1ccf6b78 100644 --- a/src/prompt.rs +++ b/src/prompt.rs @@ -3,6 +3,11 @@ use std::path::{Path, PathBuf}; use std::process::Command; +/// Whether context files should be skipped (set by --no-context-files or JCODE_NO_CONTEXT_FILES env var). +pub fn context_files_disabled() -> bool { + std::env::var("JCODE_NO_CONTEXT_FILES").is_ok() +} + /// Default system prompt for jcode (embedded at compile time) pub const DEFAULT_SYSTEM_PROMPT: &str = include_str!("prompt/system_prompt.md"); const SELFDEV_HINT_PROMPT: &str = include_str!("prompt/selfdev_hint.txt"); @@ -378,11 +383,13 @@ fn build_selfdev_hint_prompt() -> String { } /// Build self-dev tools prompt section (static version without dynamic socket path) +#[allow(dead_code)] fn build_selfdev_prompt_static() -> String { build_selfdev_prompt_static_for_context(SelfDevProductContext::Tui) } /// Build self-dev tools prompt section +#[allow(dead_code)] fn build_selfdev_prompt() -> String { build_selfdev_prompt_for_context(SelfDevProductContext::Tui) } @@ -626,6 +633,10 @@ fn gpu_summary() -> Option { /// Load AGENTS.md files from a specific working directory pub fn load_agents_md_files_from_dir(working_dir: Option<&Path>) -> (Option, ContextInfo) { + if context_files_disabled() { + eprintln!("Context files disabled (--no-context-files)"); + return (None, ContextInfo::default()); + } let mut contents = vec![]; let mut info = ContextInfo::default(); diff --git a/src/prompt_tests.rs b/src/prompt_tests.rs index 42c218495..c60d2f66a 100644 --- a/src/prompt_tests.rs +++ b/src/prompt_tests.rs @@ -1,4 +1,6 @@ use super::*; +use crate::cli::args::Args; +use clap::Parser; /// Verify the default system prompt does NOT identify as "Claude Code" /// It's fine to say "powered by Claude" but not "Claude Code" (Anthropic's product) @@ -311,3 +313,105 @@ fn build_system_prompt_full_uses_jcode_system_md_root() { assert!(info.system_prompt_chars < 200); assert!(!prompt.contains(crate::prompt::DEFAULT_SYSTEM_PROMPT)); } + +#[test] +fn test_context_files_disabled_returns_false_by_default() { + let _guard = crate::storage::lock_test_env(); + // Ensure the env var is NOT set + crate::env::remove_var("JCODE_NO_CONTEXT_FILES"); + assert!(!context_files_disabled()); +} + +#[test] +fn test_context_files_disabled_returns_true_when_env_set() { + let _guard = crate::storage::lock_test_env(); + let prev_val = std::env::var("JCODE_NO_CONTEXT_FILES"); + // Ensure the env var is set for this test + crate::env::set_var("JCODE_NO_CONTEXT_FILES", "1"); + assert!(context_files_disabled()); + // Restore previous state + match prev_val { + Ok(val) => crate::env::set_var("JCODE_NO_CONTEXT_FILES", val), + Err(_) => crate::env::remove_var("JCODE_NO_CONTEXT_FILES"), + } +} + +#[test] +fn test_load_agents_md_from_dir_returns_none_when_disabled() { + let _guard = crate::storage::lock_test_env(); + let prev_home = std::env::var_os("JCODE_HOME"); + let temp = tempfile::TempDir::new().unwrap(); + crate::env::set_var("JCODE_HOME", temp.path()); + crate::env::set_var("JCODE_NO_CONTEXT_FILES", "1"); + + // Even with a global AGENTS.md present, loading should be skipped + std::fs::create_dir_all(temp.path()).unwrap(); + std::fs::write(temp.path().join("AGENTS.md"), "global agents instructions").unwrap(); + + let (content, _info) = load_agents_md_files_from_dir(None); + assert!(content.is_none()); + + if let Some(prev_home) = prev_home { + crate::env::set_var("JCODE_HOME", prev_home); + } else { + crate::env::remove_var("JCODE_HOME"); + } + crate::env::remove_var("JCODE_NO_CONTEXT_FILES"); +} + +#[test] +fn test_load_agents_md_from_dir_loads_files_when_not_disabled() { + let _guard = crate::storage::lock_test_env(); + let prev_home = std::env::var_os("JCODE_HOME"); + let temp = tempfile::TempDir::new().unwrap(); + crate::env::set_var("JCODE_HOME", temp.path()); + + // Remove any leftover env var + crate::env::remove_var("JCODE_NO_CONTEXT_FILES"); + + std::fs::create_dir_all(temp.path().join("external")).unwrap(); + std::fs::write( + temp.path().join("external/AGENTS.md"), + "global agents instructions", + ) + .unwrap(); + + let (content, info) = load_agents_md_files_from_dir(None); + assert!(info.has_global_agents_md); + assert!( + content + .as_ref() + .map(|c| c.contains("global agents instructions")) + .unwrap_or(false) + ); + + if let Some(prev_home) = prev_home { + crate::env::set_var("JCODE_HOME", prev_home); + } else { + crate::env::remove_var("JCODE_HOME"); + } +} + +#[test] +fn test_cli_flag_no_short_alias() { + // Verify that -c is NOT a valid alias for --no-context-files + let result = Args::try_parse_from(["jcode", "-c", "--provider", "openai"]); + assert!( + result.is_err(), + "-c should not be a valid short flag for --no-context-files" + ); +} + +#[test] +fn test_cli_flag_no_context_files_parsed() { + let args = Args::parse_from(["jcode", "--no-context-files"]); + assert!(args.no_context_files); + + // Without the flag, should be false + let args2 = Args::parse_from(["jcode"]); + assert!(!args2.no_context_files); + + // With subcommand + let args3 = Args::parse_from(["jcode", "--no-context-files", "run", "hello"]); + assert!(args3.no_context_files); +}