diff --git a/.gitignore b/.gitignore index f18b4fd..5637145 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ npm/dist/ # Wisp runtime data (user-specific, created by `wisp init`) **/.wisp/sessions/ +**/.wisp/settings.toml wisp.toml # OS @@ -19,3 +20,6 @@ Thumbs.db .claude .npm-cache/ + +# QA-issues-reports +Wisp-QA-issues.txt \ No newline at end of file diff --git a/.wisp/settings.toml b/.wisp/settings.toml deleted file mode 100644 index 1770e3b..0000000 --- a/.wisp/settings.toml +++ /dev/null @@ -1 +0,0 @@ -execute_agents = true diff --git a/npm/README.md b/npm/README.md index 6bf8628..6d75e38 100644 --- a/npm/README.md +++ b/npm/README.md @@ -20,5 +20,5 @@ current platform. If the binary is unavailable, build from source in the repo. ```sh wisp init -wisp run +wisp summon "" ``` diff --git a/npm/scripts/postinstall.js b/npm/scripts/postinstall.js index 706d7cc..2badf64 100644 --- a/npm/scripts/postinstall.js +++ b/npm/scripts/postinstall.js @@ -39,6 +39,8 @@ const arch = ARCH_MAP[process.arch]; if (!plat || !arch) { warn(`Unsupported platform: ${process.platform}-${process.arch}`); + warn('No binary will be installed — the `wisp` command will not be runnable on this platform.'); + warn('Build from source: https://github.com/LKA09/Wisp#build-from-source'); process.exit(0); } @@ -61,7 +63,16 @@ get(downloadUrl, (err, data) => { warn(`Expected release asset: ${assetName}`); warn(`Release URL checked: ${downloadUrl}`); warn(''); - warn('The GitHub Release asset may be missing for this version/platform.'); + const isHttpError = err.message && /^HTTP \d+/.test(err.message); + const is404 = err.message && err.message.includes('HTTP 404'); + if (is404) { + warn('The GitHub Release asset may be missing for this version/platform.'); + } else if (isHttpError) { + warn(`HTTP error during download. The release asset may be missing or the release URL is incorrect.`); + } else { + warn('This appears to be a network or access error (not a missing release asset).'); + warn('Check your internet connection, firewall, or proxy settings and retry.'); + } warn(''); warn('Build from source (Windows PowerShell):'); warn(' cd wisp'); diff --git a/wisp/src/cli.rs b/wisp/src/cli.rs index 07a1058..c92fce9 100644 --- a/wisp/src/cli.rs +++ b/wisp/src/cli.rs @@ -226,6 +226,13 @@ fn parse_execute_command(task: &str, permission_mode: PermissionMode) -> Interac } pub fn interactive() { + use std::io::IsTerminal; + if !std::io::stdin().is_terminal() { + eprintln!( + "wisp: interactive TUI requires a terminal. Use `wisp summon \"\"` for non-interactive use." + ); + std::process::exit(1); + } if let Err(e) = crate::tui::run() { eprintln!("Error starting TUI: {e}"); } diff --git a/wisp/src/display.rs b/wisp/src/display.rs index 52ef687..91777af 100644 --- a/wisp/src/display.rs +++ b/wisp/src/display.rs @@ -163,10 +163,15 @@ pub fn agent_display(name: &str) -> String { pub fn header(task: &str, branch: &str, mode: &str, instruction_files: usize) { let rule = heavy_rule(); + let subtitle = if mode == "dry-run" { + "preview workflow" + } else { + "implement · patch · review · ship" + }; emit(&rule); emit(&format!( - " {ACCENT}✦{RESET} {BOLD}{WHITE}Wisp{RESET} {GRAY}—{RESET} implement · patch · review · ship" + " {ACCENT}✦{RESET} {BOLD}{WHITE}Wisp{RESET} {GRAY}—{RESET} {subtitle}" )); emit(&rule); emit(""); @@ -412,7 +417,7 @@ pub fn mode_status(execute_agents: bool) { {GRAY}— bare tasks invoke agents{RESET}" )); emit(&format!( - " {GRAY}Use {WHITE}/mode dry-run{GRAY} to switch to preview-only.{RESET}" + " {GRAY}Use {WHITE}wisp mode dry-run{GRAY} or {WHITE}/mode dry-run{GRAY} to switch to preview-only.{RESET}" )); } else { emit(&format!( @@ -420,7 +425,7 @@ pub fn mode_status(execute_agents: bool) { {GRAY}— bare tasks show a preview only (default){RESET}" )); emit(&format!( - " {GRAY}Use {WHITE}/mode execute{GRAY} to invoke agents for bare tasks.{RESET}" + " {GRAY}Use {WHITE}wisp mode execute{GRAY} or {WHITE}/mode execute{GRAY} to invoke agents for bare tasks.{RESET}" )); } emit(""); diff --git a/wisp/src/policy.rs b/wisp/src/policy.rs index 14c4915..8119b05 100644 --- a/wisp/src/policy.rs +++ b/wisp/src/policy.rs @@ -32,9 +32,7 @@ pub fn is_protected_path(path: &str, config: &Config) -> bool { let normalized = normalize_path(path); config.policy.protected_paths.iter().any(|candidate| { let candidate = normalize_path(candidate); - normalized == candidate - || normalized.starts_with(&(candidate.clone() + "/")) - || normalized.contains(&candidate) + normalized == candidate || normalized.starts_with(&(candidate.clone() + "/")) }) || matches_sensitive_path(&normalized) } diff --git a/wisp/src/tui.rs b/wisp/src/tui.rs index 2892d63..05617ac 100644 --- a/wisp/src/tui.rs +++ b/wisp/src/tui.rs @@ -132,7 +132,7 @@ impl App { return; } // Skip redundant workflow title - if t.contains("implement · patch · review · ship") { + if t.contains("implement · patch · review · ship") || t.contains("preview workflow") { return; } // Skip session path (too technical for TUI) @@ -449,7 +449,18 @@ fn on_submit(input: String, app: &mut App) -> anyhow::Result<()> { }, InteractiveAction::EnterPasteMode => { app.push(format!(" > {input}"), LAVENDER); - app.push(" Type your task in the input bar and press Enter.", DIM); + app.push( + " Multi-line tasks: type your task, then end with a trailing command:", + DIM, + ); + app.push(" /run — execute workflow", DIM); + app.push(" /dry — dry-run preview", DIM); + app.push(" /claude — run Claude directly", DIM); + app.push(" /codex — run Codex directly", DIM); + app.push( + " Or type a task alone and press Enter (respects current mode).", + DIM, + ); } InteractiveAction::ModeAction { arg } => { app.push(format!(" > {input}"), LAVENDER); diff --git a/wisp/src/workflow.rs b/wisp/src/workflow.rs index f9095be..802ba1f 100644 --- a/wisp/src/workflow.rs +++ b/wisp/src/workflow.rs @@ -105,6 +105,7 @@ pub fn summon(args: SummonArgs) -> Result<()> { // ── Step 1: implement (always once) ─────────────────────────────────────── step += 1; let impl_prompt = build_implement_prompt(&normalized_task_en, &args.task, &instructions_text); + let impl_display_role = display_role_label("implement", args.execute_agents); run_workflow_step( &config, &session, @@ -112,6 +113,7 @@ pub fn summon(args: SummonArgs) -> Result<()> { &cwd, config.workflow.implementer.as_str(), "implement", + &impl_display_role, step, total_steps, "prompts/implementer.en.md", @@ -141,7 +143,7 @@ pub fn summon(args: SummonArgs) -> Result<()> { display::wisp_note(&handoff_note( handoff_from, config.workflow.patcher.as_str(), - "patch", + &display_role_label("patch", args.execute_agents), &args.lang, )); @@ -153,6 +155,7 @@ pub fn summon(args: SummonArgs) -> Result<()> { "patch".to_string() }; let patch_prompt = build_patch_prompt(&normalized_task_en, &args.task, &instructions_text); + let patch_display_role = display_role_label(&patch_role, args.execute_agents); run_workflow_step( &config, &session, @@ -160,6 +163,7 @@ pub fn summon(args: SummonArgs) -> Result<()> { &cwd, config.workflow.patcher.as_str(), &patch_role, + &patch_display_role, step, total_steps, &format!("prompts/patcher{suffix}.en.md"), @@ -172,7 +176,7 @@ pub fn summon(args: SummonArgs) -> Result<()> { display::wisp_note(&handoff_note( config.workflow.patcher.as_str(), config.workflow.reviewer.as_str(), - "review", + &display_role_label("review", args.execute_agents), &args.lang, )); @@ -183,6 +187,7 @@ pub fn summon(args: SummonArgs) -> Result<()> { } else { "review".to_string() }; + let review_display_role = display_role_label(&review_role, args.execute_agents); let review_prompt = build_review_prompt(&normalized_task_en, &args.task); let review_output = run_workflow_step( &config, @@ -191,6 +196,7 @@ pub fn summon(args: SummonArgs) -> Result<()> { &cwd, config.workflow.reviewer.as_str(), &review_role, + &review_display_role, step, total_steps, &format!("prompts/reviewer{suffix}.en.md"), @@ -256,12 +262,13 @@ pub fn summon(args: SummonArgs) -> Result<()> { display::wisp_note(&handoff_note( config.workflow.reviewer.as_str(), config.workflow.shipper.as_str(), - "ship", + &display_role_label("ship", args.execute_agents), &args.lang, )); step += 1; let ship_prompt = build_ship_prompt(&normalized_task_en, &args.task); + let ship_display_role = display_role_label("ship", args.execute_agents); run_workflow_step( &config, &session, @@ -269,6 +276,7 @@ pub fn summon(args: SummonArgs) -> Result<()> { &cwd, config.workflow.shipper.as_str(), "ship", + &ship_display_role, step, total_steps, "prompts/shipper.en.md", @@ -403,7 +411,25 @@ pub fn run_single_agent(args: SingleAgentArgs) -> Result<()> { write_step_diffs(&session, "direct", &before_step, &after_step)?; match handle_policy_violations(&violations, &config, &args.lang, &args.agent, "direct")? { - true => display::agent_end(&args.agent, output.status == 0), + true => { + let succeeded = output.status == 0 || !args.execute_agents; + display::agent_end(&args.agent, succeeded); + if args.execute_agents && output.status != 0 { + bail!(msg( + &args.lang, + &format!( + "{} failed with exit code {}.", + display::agent_display(&args.agent), + output.status + ), + &format!( + "{}이(가) 종료 코드 {}로 실패했습니다.", + display::agent_display(&args.agent), + output.status + ), + )); + } + } false => { display::agent_end(&args.agent, false); bail!(format_policy_violation_error( @@ -438,6 +464,7 @@ fn run_workflow_step( cwd: &std::path::Path, agent: &str, role: &str, + display_role: &str, step_num: usize, total_steps: usize, prompt_file: &str, @@ -447,7 +474,7 @@ fn run_workflow_step( ) -> Result { session.write(prompt_file, prompt)?; - display::agent_start(agent, role, step_num, total_steps); + display::agent_start(agent, display_role, step_num, total_steps); let cfg = config .agents .get(agent) @@ -516,7 +543,25 @@ fn run_workflow_step( write_step_diffs(session, role, &before_step, &after_step)?; match handle_policy_violations(&violations, config, &args.lang, agent, role)? { - true => display::agent_end(agent, output.status == 0), + true => { + let succeeded = output.status == 0 || !args.execute_agents; + display::agent_end(agent, succeeded); + if args.execute_agents && output.status != 0 { + bail!(msg( + &args.lang, + &format!( + "{} ({role}) failed with exit code {}.", + display::agent_display(agent), + output.status + ), + &format!( + "{} ({role})이(가) 종료 코드 {}로 실패했습니다.", + display::agent_display(agent), + output.status + ), + )); + } + } false => { display::agent_end(agent, false); bail!(format_policy_violation_error( @@ -881,6 +926,31 @@ fn handoff_note(from: &str, to: &str, role: &str, lang: &Language) -> String { } } +fn display_role_label(role: &str, execute_agents: bool) -> String { + if execute_agents { + return role.to_string(); + } + + let suffix = role + .find('[') + .map(|idx| format!(" {}", role[idx..].trim())) + .unwrap_or_default(); + + let base = if role.starts_with("implement") { + "task analysis" + } else if role.starts_with("patch") { + "change preview" + } else if role.starts_with("review") { + "review check" + } else if role.starts_with("ship") { + "summary" + } else { + role + }; + + format!("{base}{suffix}") +} + // ── Task normalization ───────────────────────────────────────────────────────── fn normalize_task_en(task: &str, lang: &Language) -> String { @@ -1099,7 +1169,9 @@ fn finalize_workflow_summary( ## Security Note\n\n\ Wisp is not a security sandbox. Agents run with your full user permissions. \ The policy layer blocks specific commands and paths configured in wisp.toml, \ - but cannot prevent all unsafe actions. Review agent output before approving commits.\n", + but cannot prevent all unsafe actions. Review agent output before approving commits.\n\n\ + Session logs in `.wisp/sessions/` contain task text, prompts, instructions, \ + and git diffs verbatim. Review and delete sessions that contain sensitive data.\n", session.path().display(), instructions.files.len(), instructions.total_bytes, @@ -1137,7 +1209,9 @@ fn finalize_single_agent_summary( - Session: {}\n\ - Instructions loaded: {} ({} bytes{})\n\n\ ## Security Note\n\n\ - Wisp is not a security sandbox. Agents run with your full user permissions.\n", + Wisp is not a security sandbox. Agents run with your full user permissions.\n\ + Session logs in `.wisp/sessions/` contain task text, prompts, and git diffs verbatim. \ + Review and delete sessions that contain sensitive data.\n", session.path().display(), instructions.files.len(), instructions.total_bytes,