@setoelkahfi / sigit / commits / d12b0b8

Add /init to generate AGENTS.md from the repo

/init is not a status command: both surfaces substitute a canned prompt (instructions::INIT_PROMPT, next to the loader that consumes the file it produces) and run a normal agent turn, so exploration and the file write go through the ordinary tools and permission checks. The TUI transcript shows the /init the user typed while the model receives the full prompt; the ACP server advertises the command so editors autocomplete it. The prompt targets AGENTS.md (the cross-tool standard), tells the model to explore manifests, CI config, and the top of the tree before writing, and to improve an existing AGENTS.md or CLAUDE.md in place rather than replacing it. Spec: sigit-si docs/product/sigit-code-init-command.md.

paydii committed Jul 6, 2026 at 20:51 UTC d12b0b806da613bb3fcf29786b1d25833b9a68bf
4 files changed +95 -9
CLAUDE.md
+5 -2
index 7490f38..44b480f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -150,8 +150,11 @@ feeds results back. Neither the loop nor ACP/TUI surfaces depend on a concrete b - **`src/models.rs`** — model-picker types shared across platforms. Slash commands (`/help`, `/models`, `/skills`, `/mcp`, `/login`, `/logout`, `/whoami`, `/reload`, -`/plan`, `/permissions`, `/clear`, `/status`) are advertised via `advertise_commands` in `main.rs` -and handled in both the TUI and ACP sessions. +`/plan`, `/permissions`, `/init`, `/clear`, `/status`) are advertised via `advertise_commands` in +`main.rs` and handled in both the TUI and ACP sessions. `/init` is special: instead of replying +directly it substitutes `instructions::INIT_PROMPT` for the user text and runs a normal agent +turn that explores the repo and writes (or improves) `AGENTS.md` through the ordinary tools and +permission checks. ## Model cache (macOS)
src/chat.rs
+27 -4
index 8573317..953e5f7 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -1316,6 +1316,9 @@ mod tui { Compact, /// Restore the saved TUI session from disk. Resume, + /// Generate (or improve) the repo's AGENTS.md by running an agent + /// turn with [`crate::instructions::INIT_PROMPT`] as the user text. + Init, Exit, Unknown(String), } @@ -1345,6 +1348,7 @@ mod tui { "/tools" => SlashCommand::Tools(parse_on_off(arg)), "/compact" => SlashCommand::Compact, "/resume" => SlashCommand::Resume, + "/init" => SlashCommand::Init, "/exit" | "/quit" | "/q" => SlashCommand::Exit, other => SlashCommand::Unknown(other.to_string()), }) @@ -2024,6 +2028,7 @@ mod tui { /tools [on|off]— expand or collapse tool-call details\n\ /compact — summarize and shrink conversation history\n\ /resume — restore the saved session from disk\n\ + /init — generate or improve the repo's AGENTS.md\n\ /clear — wipe conversation history\n\ /status — show engine status\n\ /exit — quit chat", @@ -2232,6 +2237,12 @@ mod tui { let message = crate::account::status_line().await; app.messages.push(ChatMessage::system(message)); } + SlashCommand::Init => { + // Handled in the input loop (it runs a real turn); reaching + // this arm means a caller forgot that special case. + app.messages + .push(ChatMessage::system("Type /init in the chat to run it.")); + } SlashCommand::Exit => { app.quit = true; } @@ -2901,9 +2912,21 @@ mod tui { } if let Some(text) = handle_key(&mut app, key) { - if let Some(cmd) = parse_slash(&text) { - exec_slash(&mut app, cmd, Arc::clone(&engine), terminal).await; - continue; + // `/init` runs a real agent turn: the transcript + // shows the command the user typed, but the model + // receives the canned AGENTS.md-generation prompt. + let mut inference_text = text.clone(); + match parse_slash(&text) { + Some(SlashCommand::Init) => { + inference_text = + crate::instructions::INIT_PROMPT.to_string(); + } + Some(cmd) => { + exec_slash(&mut app, cmd, Arc::clone(&engine), terminal) + .await; + continue; + } + None => {} } // On-device inference needs a model in memory, and we @@ -2929,7 +2952,7 @@ mod tui { app.inference_rx = Some(rx); let backend_handle = Arc::clone(&app.backend); - let user_text = text.clone(); + let user_text = inference_text; let tools_enabled = app.tool_calling; tokio::spawn(async move { run_inference_task(backend_handle, user_text, tx, tools_enabled).await;
src/instructions.rs
+34
index b7f6814..78835cd 100644 --- a/src/instructions.rs +++ b/src/instructions.rs @@ -24,6 +24,33 @@ use std::path::{Path, PathBuf}; /// Only the first match in a given directory is loaded. const INSTRUCTION_FILE_NAMES: &[&str] = &["AGENTS.md", "CLAUDE.md"]; +/// The prompt behind the `/init` slash command. Both the TUI and the ACP +/// server substitute it for the user's input and run a normal agent turn, so +/// generation goes through the ordinary tools and permission checks. It lives +/// here, next to the loader that consumes the file it produces. +pub const INIT_PROMPT: &str = "\ +Analyze this repository and write an AGENTS.md instruction file for AI coding \ +agents working in it. + +First explore, then write. Read the manifest/build files (Cargo.toml, \ +package.json, pyproject.toml, Makefile, or equivalents), the CI configuration, \ +the README, and the top one or two levels of the directory tree. Read broadly \ +but shallowly; do not descend into every subdirectory. + +Then create AGENTS.md at the repository root covering, briefly: +- what the project is and does (a sentence or two) +- how to build, test, and lint it (the exact commands) +- the architecture: main modules/directories and what each owns +- conventions an agent must follow (branch naming, commit rules, code style, \ +platform constraints) that the repository itself shows evidence of + +Keep it compact — aim for under 60 lines. State only what this repository \ +actually supports; no generic boilerplate. + +If AGENTS.md or CLAUDE.md already exists, read it first and improve it in \ +place (fix what is stale, add what is missing) instead of replacing it. \ +Always target AGENTS.md, never CLAUDE.md."; + /// Per-file and total caps so an oversized file can't blow up the context window. const MAX_FILE_BYTES: usize = 32 * 1024; const MAX_TOTAL_BYTES: usize = 64 * 1024; @@ -193,6 +220,13 @@ mod tests { std::env::temp_dir().join(format!("sigit-instr-test-{name}-{nanos}")) } + #[test] + fn init_prompt_targets_agents_md_and_preserves_existing_files() { + assert!(INIT_PROMPT.contains("AGENTS.md")); + assert!(INIT_PROMPT.contains("improve it in place")); + assert!(INIT_PROMPT.contains("never CLAUDE.md")); + } + #[test] fn none_when_no_files() { let root = unique_dir("empty");
src/main.rs
+29 -3
index bd7e7e2..f022bb0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -798,6 +798,7 @@ impl SiGitAgent { ), AvailableCommand::new("permissions", "Show the tool permission policy"), AvailableCommand::new("compact", "Summarize and shrink the conversation history"), + AvailableCommand::new("init", "Generate or improve the repo's AGENTS.md"), AvailableCommand::new("clear", "Wipe the conversation history"), AvailableCommand::new("status", "Show engine status"), ]; @@ -1268,9 +1269,15 @@ impl SiGitAgent { return Ok(PromptResponse::new(StopReason::EndTurn)); } - if let Some(command) = parse_slash(&user_text) { - return exec_slash_acp(self, cx, session_id, command).await; - } + let user_text = match parse_slash(&user_text) { + // `/init` is not a status command: it substitutes the canned + // AGENTS.md-generation prompt and runs a normal agent turn, so the + // exploration and file write go through the ordinary tools and + // permission checks. + Some(SlashCommand::Init) => instructions::INIT_PROMPT.to_string(), + Some(command) => return exec_slash_acp(self, cx, session_id, command).await, + None => user_text, + }; log::info!( "prompt({}): \"{}\"", @@ -2234,6 +2241,9 @@ enum SlashCommand { Permissions, /// Summarize-and-shrink the conversation history on demand. Compact, + /// Generate (or improve) the repo's AGENTS.md by running an agent turn + /// with [`instructions::INIT_PROMPT`] as the user text. + Init, Exit, Unknown(String), } @@ -2262,6 +2272,7 @@ fn parse_slash(input: &str) -> Option<SlashCommand> { "/plan" => SlashCommand::Plan(parse_on_off(argument)), "/permissions" => SlashCommand::Permissions, "/compact" => SlashCommand::Compact, + "/init" => SlashCommand::Init, "/exit" | "/quit" | "/q" => SlashCommand::Exit, other => SlashCommand::Unknown(other.to_string()), }) @@ -2373,6 +2384,7 @@ async fn exec_slash_acp( /plan [on|off] - plan mode: research only, no edits or commands\n\ /permissions - show the tool permission policy\n\ /compact - summarize and shrink conversation history\n\ + /init - generate or improve the repo's AGENTS.md\n\ /clear - wipe conversation history\n\ /status - show engine status\n\ /exit - end this turn", @@ -2642,6 +2654,13 @@ async fn exec_slash_acp( ) .ok(); } + SlashCommand::Init => { + // Handled in the prompt path (it runs a real turn); reaching this + // arm means a caller forgot that special case. + agent + .send_assistant_message(cx, session_id, "Send /init as a prompt to run it.") + .ok(); + } SlashCommand::Unknown(command) => { agent .send_assistant_message(cx, session_id, format!("unknown command: {command}")) @@ -3223,6 +3242,13 @@ mod tests { ); } + #[test] + fn parse_slash_maps_init() { + assert!(matches!(parse_slash("/init"), Some(SlashCommand::Init))); + // Trailing whitespace comes from editors that submit the raw line. + assert!(matches!(parse_slash(" /init "), Some(SlashCommand::Init))); + } + #[test] fn ascii_safe_replaces_multibyte_chars() { // The exact label that crashed Zed: the cloud tier name plus the old