diff --git a/crates/tui/assets/skills/delegate/SKILL.md b/crates/tui/assets/skills/delegate/SKILL.md new file mode 100644 index 00000000..595edb93 --- /dev/null +++ b/crates/tui/assets/skills/delegate/SKILL.md @@ -0,0 +1,102 @@ +--- +name: delegate +description: Strategic delegation for multi-step coding, research, or verification work. Use when a task can be split into parent reasoning plus focused sub-agent execution through agent_open, agent_eval, and agent_close. +metadata: + short-description: Delegate focused work to sub-agents +--- + +# Delegate + +Use sub-agents when they can do focused work in parallel while the parent keeps architectural judgment, integration, and final verification. + +## Keep vs Delegate + +Keep in the parent: + +- Understanding the user's actual request and constraints +- Architecture, security, product, and release-risk decisions +- Cross-module integration +- Final review, test interpretation, and user-facing summary + +Delegate to sub-agents: + +- Read-only exploration over a bounded file set +- Mechanical edits with a clear file ownership boundary +- Focused test or lint runs +- Boilerplate generation from an explicit spec +- Independent checks that can run while parent work continues + +Do not delegate tiny one-step tasks, ambiguous product decisions, destructive operations without a clear acceptance criterion, or final verification. + +## Open Focused Sessions + +Prefer `agent_open` for a named child session, then `agent_eval` to fetch or wait on its result. Open independent children together so they can run in parallel. + +```json +{ + "name": "config_audit", + "prompt": "Inspect crates/tui/src/config.rs and crates/tui/src/settings.rs for duplicate model-default logic. Return file/line findings only; do not edit files.", + "type": "explore", + "model": "deepseek-v4-flash", + "cwd": "." +} +``` + +For code changes, give the child a precise write boundary and tell it not to revert unrelated edits: + +```json +{ + "name": "docs_patch", + "prompt": "Update only docs/configuration.md to document the new [statusline] keys. Match the surrounding style. Do not edit other files.", + "type": "implementer", + "model": "deepseek-v4-flash", + "cwd": "." +} +``` + +Use `fork_context: true` only when the child genuinely needs the current conversation prefix. Leave it omitted for fresh, narrower context. + +## Evaluate and Verify + +Use `agent_eval` with `block: true` when you need the result before continuing: + +```json +{ + "name": "docs_patch", + "block": true, + "timeout_ms": 120000 +} +``` + +Use `block: false` for a quick status projection while other work continues. + +Sub-agent outputs are self-reports. Re-check material claims before relying on them: + +- Read changed files directly. +- Run the relevant tests locally. +- Inspect unexpected diffs before committing. +- Verify externally visible or destructive claims against source data. + +Close sessions that are no longer useful with `agent_close`. + +## Prompt Shape + +A good delegation prompt includes: + +- The exact task +- Files or modules owned by the child +- Files or behavior the child must not touch +- Expected output format +- Acceptance criteria + +Weak prompt: + +```text +Fix the settings bug. +``` + +Strong prompt: + +```text +Own only crates/tui/src/settings.rs and its tests. Preserve existing config key names. Add a regression test showing that provider-specific API key changes do not restart DeepSeek onboarding. Return the changed paths and test command output. +``` diff --git a/crates/tui/src/skills/system.rs b/crates/tui/src/skills/system.rs index 96823888..0c09ebde 100644 --- a/crates/tui/src/skills/system.rs +++ b/crates/tui/src/skills/system.rs @@ -1,65 +1,109 @@ -//! System-skill installer: bundles skill-creator and auto-installs it on first launch. +//! System-skill installer: bundles skill-creator and delegate, auto-installs +//! them on first launch. use std::fs; use std::path::Path; -const BUNDLED_SKILL_VERSION: &str = "1"; +const BUNDLED_SKILL_VERSION: &str = "2"; const SKILL_CREATOR_BODY: &str = include_str!("../../assets/skills/skill-creator/SKILL.md"); +const DELEGATE_BODY: &str = include_str!("../../assets/skills/delegate/SKILL.md"); + +struct BundledSkill { + name: &'static str, + body: &'static str, + introduced_in: u32, +} + +const BUNDLED_SKILLS: &[BundledSkill] = &[ + BundledSkill { + name: "skill-creator", + body: SKILL_CREATOR_BODY, + introduced_in: 1, + }, + BundledSkill { + name: "delegate", + body: DELEGATE_BODY, + introduced_in: 2, + }, +]; + +/// Attempt to install a single bundled skill into `skills_dir`. +/// +/// Returns `true` if installation occurred (fresh install or version bump). +fn install_one( + skills_dir: &Path, + skill: &BundledSkill, + installed_version: Option<&str>, +) -> std::io::Result { + let target_dir = skills_dir.join(skill.name); + let target_file = target_dir.join("SKILL.md"); + let dir_exists = target_dir.exists(); + let installed_number = installed_version.and_then(|value| value.parse::().ok()); + + let should_install = match (installed_version, installed_number, dir_exists) { + // Fresh install: neither marker nor directory. + (None, _, false) => true, + // Newly bundled skill: add it for older system-skill installs. + (Some(_), Some(version), _) if version < skill.introduced_in => true, + // Version bump for an existing skill: refresh only if the user has not + // intentionally deleted that skill directory. + (Some(version), _, true) if version != BUNDLED_SKILL_VERSION => true, + // Every other case: current install, user-deleted dir, or pre-existing + // user-owned skill without our marker. + _ => false, + }; + + if should_install { + fs::create_dir_all(&target_dir)?; + fs::write(&target_file, skill.body)?; + } + Ok(should_install) +} /// Install bundled system skills into `skills_dir`. /// /// Behaviour: -/// - Fresh install (no marker, no dir): installs `skill-creator/SKILL.md` and writes -/// the version marker. -/// - Version bump (marker present with older version, dir present): re-installs. -/// - User deleted the dir while marker still present at same version: leaves it gone. +/// - Fresh install (no marker, no dir): installs `skill-creator/SKILL.md` and +/// `delegate/SKILL.md`, then writes the version marker. +/// - Version bump (marker present with older version): re-installs any existing +/// bundled skill and installs newly introduced bundled skills. +/// - User deleted a skill dir while marker still present at same version: leaves +/// it gone. /// - Idempotent: calling twice with no changes is a no-op. /// /// Errors are I/O errors from the filesystem; the caller should log them but not /// abort startup. pub fn install_system_skills(skills_dir: &Path) -> std::io::Result<()> { let marker = skills_dir.join(".system-installed-version"); - let target_dir = skills_dir.join("skill-creator"); - let target_file = target_dir.join("SKILL.md"); let installed_version = fs::read_to_string(&marker) .ok() .map(|s| s.trim().to_string()); - let dir_exists = target_dir.exists(); - // Re-install only when BOTH conditions hold: - // (a) bundled version is newer than what is recorded in the marker, AND - // (b) the skill directory still exists (user hasn't intentionally deleted it). - // Fresh install (no marker AND no dir) is also handled. - let should_install = match (installed_version.as_deref(), dir_exists) { - // Fresh install: neither marker nor directory. - (None, false) => true, - // Version bump: marker is outdated but directory still present. - (Some(v), true) if v != BUNDLED_SKILL_VERSION => true, - // Every other case: already installed at current version, or user deleted - // the dir (respect that choice). - _ => false, - }; + let mut changed = false; + for skill in BUNDLED_SKILLS { + changed |= install_one(skills_dir, skill, installed_version.as_deref())?; + } - if should_install { + if changed { fs::create_dir_all(skills_dir)?; - fs::create_dir_all(&target_dir)?; - fs::write(&target_file, SKILL_CREATOR_BODY)?; fs::write(&marker, BUNDLED_SKILL_VERSION)?; } Ok(()) } -/// Remove the `skill-creator` system skill and its version marker. +/// Remove all system skills and the version marker. /// /// Intended for tests and `deepseek setup --clean`. Ignores missing files. #[allow(dead_code)] pub fn uninstall_system_skills(skills_dir: &Path) -> std::io::Result<()> { let marker = skills_dir.join(".system-installed-version"); - let target_dir = skills_dir.join("skill-creator"); - if target_dir.exists() { - fs::remove_dir_all(&target_dir)?; + for skill in BUNDLED_SKILLS { + let dir = skills_dir.join(skill.name); + if dir.exists() { + fs::remove_dir_all(&dir)?; + } } if marker.exists() { fs::remove_file(&marker)?; @@ -74,10 +118,22 @@ mod tests { // ── helpers ────────────────────────────────────────────────────────────── - fn skill_file(tmp: &TempDir) -> std::path::PathBuf { + fn sc_file(tmp: &TempDir) -> std::path::PathBuf { tmp.path().join("skill-creator").join("SKILL.md") } + fn dg_file(tmp: &TempDir) -> std::path::PathBuf { + tmp.path().join("delegate").join("SKILL.md") + } + + fn sc_dir(tmp: &TempDir) -> std::path::PathBuf { + tmp.path().join("skill-creator") + } + + fn dg_dir(tmp: &TempDir) -> std::path::PathBuf { + tmp.path().join("delegate") + } + fn marker_file(tmp: &TempDir) -> std::path::PathBuf { tmp.path().join(".system-installed-version") } @@ -85,11 +141,18 @@ mod tests { // ── fresh install ───────────────────────────────────────────────────────── #[test] - fn fresh_install_creates_skill_and_marker() { + fn fresh_install_creates_both_skills_and_marker() { let tmp = TempDir::new().unwrap(); install_system_skills(tmp.path()).unwrap(); - assert!(skill_file(&tmp).exists(), "SKILL.md should be created"); + assert!( + sc_file(&tmp).exists(), + "skill-creator SKILL.md should be created" + ); + assert!( + dg_file(&tmp).exists(), + "delegate SKILL.md should be created" + ); assert!(marker_file(&tmp).exists(), "marker should be created"); let ver = fs::read_to_string(marker_file(&tmp)).unwrap(); @@ -103,78 +166,140 @@ mod tests { let tmp = TempDir::new().unwrap(); install_system_skills(tmp.path()).unwrap(); - // Overwrite SKILL.md with sentinel to detect an undesired second write. - fs::write(skill_file(&tmp), "sentinel").unwrap(); + // Overwrite both SKILL.md files with sentinels to detect undesired writes. + fs::write(sc_file(&tmp), "sc-sentinel").unwrap(); + fs::write(dg_file(&tmp), "dg-sentinel").unwrap(); install_system_skills(tmp.path()).unwrap(); - let contents = fs::read_to_string(skill_file(&tmp)).unwrap(); + let sc = fs::read_to_string(sc_file(&tmp)).unwrap(); + let dg = fs::read_to_string(dg_file(&tmp)).unwrap(); assert_eq!( - contents, "sentinel", - "second install should not overwrite SKILL.md when version is current" + sc, "sc-sentinel", + "second install should not overwrite skill-creator" + ); + assert_eq!( + dg, "dg-sentinel", + "second install should not overwrite delegate" ); } - // ── user deleted the directory ──────────────────────────────────────────── + // ── user deleted a directory ────────────────────────────────────────────── #[test] fn user_deleted_dir_is_not_recreated() { let tmp = TempDir::new().unwrap(); install_system_skills(tmp.path()).unwrap(); - // Simulate user deliberately removing the skill directory. - fs::remove_dir_all(tmp.path().join("skill-creator")).unwrap(); + // Simulate user deliberately removing one skill directory. + fs::remove_dir_all(dg_dir(&tmp)).unwrap(); - // Re-launch must NOT recreate the directory. + // Re-launch must NOT recreate the deleted directory. install_system_skills(tmp.path()).unwrap(); assert!( - !skill_file(&tmp).exists(), - "skill-creator must not be recreated after user deleted it" + !dg_file(&tmp).exists(), + "delegate must not be recreated after user deleted it" ); + assert!( + sc_file(&tmp).exists(), + "skill-creator should still be present (not deleted by user)" + ); + } + + #[test] + fn user_deleted_both_dirs_are_not_recreated() { + let tmp = TempDir::new().unwrap(); + install_system_skills(tmp.path()).unwrap(); + + fs::remove_dir_all(sc_dir(&tmp)).unwrap(); + fs::remove_dir_all(dg_dir(&tmp)).unwrap(); + + install_system_skills(tmp.path()).unwrap(); + + assert!(!sc_file(&tmp).exists()); + assert!(!dg_file(&tmp).exists()); } // ── version bump re-installs ────────────────────────────────────────────── #[test] - fn outdated_marker_triggers_reinstall() { + fn outdated_marker_triggers_reinstall_of_existing_skills() { let tmp = TempDir::new().unwrap(); - // Simulate a previous install at a lower version. - let skill_dir = tmp.path().join("skill-creator"); - fs::create_dir_all(&skill_dir).unwrap(); - fs::write(skill_dir.join("SKILL.md"), "old content").unwrap(); + // Simulate a previous install at a lower version with both skills present. + fs::create_dir_all(sc_dir(&tmp)).unwrap(); + fs::write(sc_file(&tmp), "old-sc").unwrap(); + fs::create_dir_all(dg_dir(&tmp)).unwrap(); + fs::write(dg_file(&tmp), "old-dg").unwrap(); fs::write(marker_file(&tmp), "0").unwrap(); // older than BUNDLED_SKILL_VERSION install_system_skills(tmp.path()).unwrap(); - let contents = fs::read_to_string(skill_file(&tmp)).unwrap(); - assert_ne!( - contents, "old content", - "outdated skill should be overwritten on version bump" - ); - assert_eq!( - contents, SKILL_CREATOR_BODY, - "re-installed file must match the bundled body" - ); + let sc = fs::read_to_string(sc_file(&tmp)).unwrap(); + let dg = fs::read_to_string(dg_file(&tmp)).unwrap(); + assert_ne!(sc, "old-sc", "outdated skill-creator should be overwritten"); + assert_ne!(dg, "old-dg", "outdated delegate should be overwritten"); + assert_eq!(sc, SKILL_CREATOR_BODY); + assert_eq!(dg, DELEGATE_BODY); let ver = fs::read_to_string(marker_file(&tmp)).unwrap(); + assert_eq!(ver.trim(), BUNDLED_SKILL_VERSION); + } + + // ── partial previous install (only skill-creator existed) ───────────────── + + #[test] + fn version_bump_adds_delegate_when_it_was_missing() { + let tmp = TempDir::new().unwrap(); + + // Simulate state from v1: only skill-creator present. + fs::create_dir_all(sc_dir(&tmp)).unwrap(); + fs::write(sc_file(&tmp), "old-sc").unwrap(); + fs::write(marker_file(&tmp), "1").unwrap(); + + install_system_skills(tmp.path()).unwrap(); + + // skill-creator should be updated, delegate should be newly installed. assert_eq!( - ver.trim(), - BUNDLED_SKILL_VERSION, - "marker should be updated" + fs::read_to_string(sc_file(&tmp)).unwrap(), + SKILL_CREATOR_BODY ); + assert_eq!(fs::read_to_string(dg_file(&tmp)).unwrap(), DELEGATE_BODY); + } + + #[test] + fn version_bump_respects_deleted_existing_skill_while_adding_new_skill() { + let tmp = TempDir::new().unwrap(); + + // Simulate v1 where skill-creator had been deliberately removed before + // v2 introduced delegate. + fs::write(marker_file(&tmp), "1").unwrap(); + + install_system_skills(tmp.path()).unwrap(); + + assert!( + !sc_file(&tmp).exists(), + "version bump should not recreate a deleted pre-existing skill" + ); + assert!( + dg_file(&tmp).exists(), + "version bump should install newly introduced bundled skills" + ); + let ver = fs::read_to_string(marker_file(&tmp)).unwrap(); + assert_eq!(ver.trim(), BUNDLED_SKILL_VERSION); } // ── uninstall ───────────────────────────────────────────────────────────── #[test] - fn uninstall_removes_skill_and_marker() { + fn uninstall_removes_both_skills_and_marker() { let tmp = TempDir::new().unwrap(); install_system_skills(tmp.path()).unwrap(); uninstall_system_skills(tmp.path()).unwrap(); - assert!(!skill_file(&tmp).exists(), "SKILL.md should be removed"); + assert!(!sc_file(&tmp).exists(), "skill-creator should be removed"); + assert!(!dg_file(&tmp).exists(), "delegate should be removed"); assert!(!marker_file(&tmp).exists(), "marker should be removed"); }