@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
@@ -150,8 +150,11 @@ feeds results back. Neither the loop nor ACP/TUI surfaces depend on a concrete b
150 - **`src/models.rs`** — model-picker types shared across platforms.
151
152 Slash commands (`/help`, `/models`, `/skills`, `/mcp`, `/login`, `/logout`, `/whoami`, `/reload`,
153 -`/plan`, `/permissions`, `/clear`, `/status`) are advertised via `advertise_commands` in `main.rs`
154 -and handled in both the TUI and ACP sessions.
153 +`/plan`, `/permissions`, `/init`, `/clear`, `/status`) are advertised via `advertise_commands` in
154 +`main.rs` and handled in both the TUI and ACP sessions. `/init` is special: instead of replying
155 +directly it substitutes `instructions::INIT_PROMPT` for the user text and runs a normal agent
156 +turn that explores the repo and writes (or improves) `AGENTS.md` through the ordinary tools and
157 +permission checks.
158
159 ## Model cache (macOS)
160
src/chat.rs
+27 -4
@@ -1316,6 +1316,9 @@ mod tui {
1316 Compact,
1317 /// Restore the saved TUI session from disk.
1318 Resume,
1319 + /// Generate (or improve) the repo's AGENTS.md by running an agent
1320 + /// turn with [`crate::instructions::INIT_PROMPT`] as the user text.
1321 + Init,
1322 Exit,
1323 Unknown(String),
1324 }
@@ -1345,6 +1348,7 @@ mod tui {
1348 "/tools" => SlashCommand::Tools(parse_on_off(arg)),
1349 "/compact" => SlashCommand::Compact,
1350 "/resume" => SlashCommand::Resume,
1351 + "/init" => SlashCommand::Init,
1352 "/exit" | "/quit" | "/q" => SlashCommand::Exit,
1353 other => SlashCommand::Unknown(other.to_string()),
1354 })
@@ -2024,6 +2028,7 @@ mod tui {
2028 /tools [on|off]— expand or collapse tool-call details\n\
2029 /compact — summarize and shrink conversation history\n\
2030 /resume — restore the saved session from disk\n\
2031 + /init — generate or improve the repo's AGENTS.md\n\
2032 /clear — wipe conversation history\n\
2033 /status — show engine status\n\
2034 /exit — quit chat",
@@ -2232,6 +2237,12 @@ mod tui {
2237 let message = crate::account::status_line().await;
2238 app.messages.push(ChatMessage::system(message));
2239 }
2240 + SlashCommand::Init => {
2241 + // Handled in the input loop (it runs a real turn); reaching
2242 + // this arm means a caller forgot that special case.
2243 + app.messages
2244 + .push(ChatMessage::system("Type /init in the chat to run it."));
2245 + }
2246 SlashCommand::Exit => {
2247 app.quit = true;
2248 }
@@ -2901,9 +2912,21 @@ mod tui {
2912 }
2913
2914 if let Some(text) = handle_key(&mut app, key) {
2904 - if let Some(cmd) = parse_slash(&text) {
2905 - exec_slash(&mut app, cmd, Arc::clone(&engine), terminal).await;
2906 - continue;
2915 + // `/init` runs a real agent turn: the transcript
2916 + // shows the command the user typed, but the model
2917 + // receives the canned AGENTS.md-generation prompt.
2918 + let mut inference_text = text.clone();
2919 + match parse_slash(&text) {
2920 + Some(SlashCommand::Init) => {
2921 + inference_text =
2922 + crate::instructions::INIT_PROMPT.to_string();
2923 + }
2924 + Some(cmd) => {
2925 + exec_slash(&mut app, cmd, Arc::clone(&engine), terminal)
2926 + .await;
2927 + continue;
2928 + }
2929 + None => {}
2930 }
2931
2932 // On-device inference needs a model in memory, and we
@@ -2929,7 +2952,7 @@ mod tui {
2952 app.inference_rx = Some(rx);
2953
2954 let backend_handle = Arc::clone(&app.backend);
2932 - let user_text = text.clone();
2955 + let user_text = inference_text;
2956 let tools_enabled = app.tool_calling;
2957 tokio::spawn(async move {
2958 run_inference_task(backend_handle, user_text, tx, tools_enabled).await;
src/instructions.rs
+34
@@ -24,6 +24,33 @@ use std::path::{Path, PathBuf};
24 /// Only the first match in a given directory is loaded.
25 const INSTRUCTION_FILE_NAMES: &[&str] = &["AGENTS.md", "CLAUDE.md"];
26
27 +/// The prompt behind the `/init` slash command. Both the TUI and the ACP
28 +/// server substitute it for the user's input and run a normal agent turn, so
29 +/// generation goes through the ordinary tools and permission checks. It lives
30 +/// here, next to the loader that consumes the file it produces.
31 +pub const INIT_PROMPT: &str = "\
32 +Analyze this repository and write an AGENTS.md instruction file for AI coding \
33 +agents working in it.
34 +
35 +First explore, then write. Read the manifest/build files (Cargo.toml, \
36 +package.json, pyproject.toml, Makefile, or equivalents), the CI configuration, \
37 +the README, and the top one or two levels of the directory tree. Read broadly \
38 +but shallowly; do not descend into every subdirectory.
39 +
40 +Then create AGENTS.md at the repository root covering, briefly:
41 +- what the project is and does (a sentence or two)
42 +- how to build, test, and lint it (the exact commands)
43 +- the architecture: main modules/directories and what each owns
44 +- conventions an agent must follow (branch naming, commit rules, code style, \
45 +platform constraints) that the repository itself shows evidence of
46 +
47 +Keep it compact — aim for under 60 lines. State only what this repository \
48 +actually supports; no generic boilerplate.
49 +
50 +If AGENTS.md or CLAUDE.md already exists, read it first and improve it in \
51 +place (fix what is stale, add what is missing) instead of replacing it. \
52 +Always target AGENTS.md, never CLAUDE.md.";
53 +
54 /// Per-file and total caps so an oversized file can't blow up the context window.
55 const MAX_FILE_BYTES: usize = 32 * 1024;
56 const MAX_TOTAL_BYTES: usize = 64 * 1024;
@@ -193,6 +220,13 @@ mod tests {
220 std::env::temp_dir().join(format!("sigit-instr-test-{name}-{nanos}"))
221 }
222
223 + #[test]
224 + fn init_prompt_targets_agents_md_and_preserves_existing_files() {
225 + assert!(INIT_PROMPT.contains("AGENTS.md"));
226 + assert!(INIT_PROMPT.contains("improve it in place"));
227 + assert!(INIT_PROMPT.contains("never CLAUDE.md"));
228 + }
229 +
230 #[test]
231 fn none_when_no_files() {
232 let root = unique_dir("empty");
src/main.rs
+29 -3
@@ -798,6 +798,7 @@ impl SiGitAgent {
798 ),
799 AvailableCommand::new("permissions", "Show the tool permission policy"),
800 AvailableCommand::new("compact", "Summarize and shrink the conversation history"),
801 + AvailableCommand::new("init", "Generate or improve the repo's AGENTS.md"),
802 AvailableCommand::new("clear", "Wipe the conversation history"),
803 AvailableCommand::new("status", "Show engine status"),
804 ];
@@ -1268,9 +1269,15 @@ impl SiGitAgent {
1269 return Ok(PromptResponse::new(StopReason::EndTurn));
1270 }
1271
1271 - if let Some(command) = parse_slash(&user_text) {
1272 - return exec_slash_acp(self, cx, session_id, command).await;
1273 - }
1272 + let user_text = match parse_slash(&user_text) {
1273 + // `/init` is not a status command: it substitutes the canned
1274 + // AGENTS.md-generation prompt and runs a normal agent turn, so the
1275 + // exploration and file write go through the ordinary tools and
1276 + // permission checks.
1277 + Some(SlashCommand::Init) => instructions::INIT_PROMPT.to_string(),
1278 + Some(command) => return exec_slash_acp(self, cx, session_id, command).await,
1279 + None => user_text,
1280 + };
1281
1282 log::info!(
1283 "prompt({}): \"{}\"",
@@ -2234,6 +2241,9 @@ enum SlashCommand {
2241 Permissions,
2242 /// Summarize-and-shrink the conversation history on demand.
2243 Compact,
2244 + /// Generate (or improve) the repo's AGENTS.md by running an agent turn
2245 + /// with [`instructions::INIT_PROMPT`] as the user text.
2246 + Init,
2247 Exit,
2248 Unknown(String),
2249 }
@@ -2262,6 +2272,7 @@ fn parse_slash(input: &str) -> Option<SlashCommand> {
2272 "/plan" => SlashCommand::Plan(parse_on_off(argument)),
2273 "/permissions" => SlashCommand::Permissions,
2274 "/compact" => SlashCommand::Compact,
2275 + "/init" => SlashCommand::Init,
2276 "/exit" | "/quit" | "/q" => SlashCommand::Exit,
2277 other => SlashCommand::Unknown(other.to_string()),
2278 })
@@ -2373,6 +2384,7 @@ async fn exec_slash_acp(
2384 /plan [on|off] - plan mode: research only, no edits or commands\n\
2385 /permissions - show the tool permission policy\n\
2386 /compact - summarize and shrink conversation history\n\
2387 + /init - generate or improve the repo's AGENTS.md\n\
2388 /clear - wipe conversation history\n\
2389 /status - show engine status\n\
2390 /exit - end this turn",
@@ -2642,6 +2654,13 @@ async fn exec_slash_acp(
2654 )
2655 .ok();
2656 }
2657 + SlashCommand::Init => {
2658 + // Handled in the prompt path (it runs a real turn); reaching this
2659 + // arm means a caller forgot that special case.
2660 + agent
2661 + .send_assistant_message(cx, session_id, "Send /init as a prompt to run it.")
2662 + .ok();
2663 + }
2664 SlashCommand::Unknown(command) => {
2665 agent
2666 .send_assistant_message(cx, session_id, format!("unknown command: {command}"))
@@ -3223,6 +3242,13 @@ mod tests {
3242 );
3243 }
3244
3245 + #[test]
3246 + fn parse_slash_maps_init() {
3247 + assert!(matches!(parse_slash("/init"), Some(SlashCommand::Init)));
3248 + // Trailing whitespace comes from editors that submit the raw line.
3249 + assert!(matches!(parse_slash(" /init "), Some(SlashCommand::Init)));
3250 + }
3251 +
3252 #[test]
3253 fn ascii_safe_replaces_multibyte_chars() {
3254 // The exact label that crashed Zed: the cloud tier name plus the old