From 4f22b8f7b1fac63d31d196c6e01cd9ee96fa44c0 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 21 Feb 2026 11:20:50 +0000 Subject: [PATCH 01/15] docs: add skill writer design doc Design for agent self-authoring skills with hot-reload (Approach A): - write_skill_file + reload_skills tools in agent.rs - RwLock for live updates - creating-skills SKILL.md as the instruction layer https://claude.ai/code/session_01V5qMEXBhQQuKHeot52gxEh --- docs/plans/2026-02-21-skill-writer-design.md | 196 +++++++++++++++++++ 1 file changed, 196 insertions(+) create mode 100644 docs/plans/2026-02-21-skill-writer-design.md diff --git a/docs/plans/2026-02-21-skill-writer-design.md b/docs/plans/2026-02-21-skill-writer-design.md new file mode 100644 index 0000000..81deb6e --- /dev/null +++ b/docs/plans/2026-02-21-skill-writer-design.md @@ -0,0 +1,196 @@ +# Design: Skill Writer — Agent Self-Authoring Skills with Hot-Reload + +**Date:** 2026-02-21 +**Status:** Approved + +--- + +## Goal + +Enable the RustFox Telegram bot to: +1. Interactively author new agent skills in the correct format at a user's request +2. Write multi-file skill directories (SKILL.md + supporting reference/template/script files) +3. Hot-reload skills into memory immediately — no bot restart required + +--- + +## Architecture Overview + +Three components work together: + +``` +skills/creating-skills/SKILL.md + (new instruction file) + • Teaches agent the SKILL.md format and best practices + • Guides agent to call write_skill_file() for each file + • Tells agent to call reload_skills() when done + │ + │ injected into system prompt + ▼ +Agent (src/agent.rs) + skills: RwLock ◄──┐ + │ + write_skill_file(skill_name, │ + relative_path, │ + content) │ + → creates skills// │ + │ + reload_skills() │ + → reloads dir → writes RwLock ─────┘ + + process_message() + → always rebuilds messages[0] + from current live registry + (in-memory only, DB keeps historical system message) +``` + +**Hot-reload flow:** +1. User asks agent to create a skill +2. Agent calls `write_skill_file` one or more times (building the directory) +3. Agent calls `reload_skills` → registry updated in RwLock +4. `process_message` rebuilds system prompt from live registry before each LLM call +5. Skill is immediately active in the same conversation — no restart needed + +--- + +## Chosen Approach: Approach A (Dedicated Skill Management Tools) + +Rejected alternatives: +- **Approach B** (extend `write_file` sandbox): Muddles security model; sandbox designed for one root +- **Approach C** (pure SKILL.md, no Rust changes): `execute_command` is also sandboxed; can't reach `skills/` + +--- + +## Rust Changes — `src/agent.rs` only + +### 1. Field type change + +```rust +// before +pub skills: SkillRegistry, + +// after +pub skills: tokio::sync::RwLock, +``` + +`Agent::new` wraps: `skills: tokio::sync::RwLock::new(skills)` + +### 2. `build_system_prompt` becomes async + +```rust +async fn build_system_prompt(&self) -> String { + let skills = self.skills.read().await; + let skill_context = skills.build_context(); + // ... same body as before +} +``` + +All call sites in `process_message` already `await`. + +### 3. `process_message` — always-fresh system prompt + +```rust +let current_system_prompt = self.build_system_prompt().await; +if messages.is_empty() { + // First message: save to DB as before + let system_msg = ChatMessage { + role: "system".to_string(), + content: Some(current_system_prompt), + tool_calls: None, + tool_call_id: None, + }; + self.memory.save_message(&conversation_id, &system_msg).await?; + messages.push(system_msg); +} else { + // Existing conversation: refresh in-memory only + // DB retains historical system message; live calls always use fresh one + messages[0].content = Some(current_system_prompt); +} +``` + +### 4. Two new tools + +Added to a new `skill_tool_definitions()` method, `all_tool_definitions()`, the agentic loop tool list, and `execute_tool()`: + +#### `write_skill_file` + +| Parameter | Type | Description | +|-----------|------|-------------| +| `skill_name` | string | Skill directory name: lowercase letters, numbers, hyphens only (max 64 chars) | +| `relative_path` | string | Path within skill dir, e.g. `SKILL.md`, `reference.md`, `scripts/helper.py` | +| `content` | string | Full file content to write | + +Behaviour: +- Validates `skill_name`: regex `^[a-z0-9-]{1,64}$` +- Validates `relative_path`: no `..` components, forward slashes only +- Creates `config.skills.directory / skill_name / relative_path` +- Creates parent directories automatically +- Returns path written on success + +#### `reload_skills` + +No parameters. Calls `load_skills_from_dir(&self.config.skills.directory)`, acquires write lock, replaces registry, returns count of loaded skills. + +--- + +## New Skill File — `skills/creating-skills/SKILL.md` + +Named `creating-skills` (gerund form per official best practices). + +### YAML frontmatter + +```yaml +--- +name: creating-skills +description: Use when the user asks to create, write, or add a new bot skill, or wants to teach the bot a new behavior or capability. +tags: [skills, meta] +--- +``` + +### Instruction body — what the agent does + +1. **Gather** — Ask user: skill name (slug), what it does, when it should trigger, whether supporting reference/template/script files are needed +2. **Design** — Plan the file structure: + - `SKILL.md` always (main entry point) + - Optional: `reference.md`, `examples.md`, `templates/`, `scripts/` — one level deep per best practices +3. **Write** — Call `write_skill_file` once per file +4. **Activate** — Call `reload_skills` immediately after +5. **Confirm** — Report to user: skill is live, list files created + +### SKILL.md content the agent writes follows official best practices + +- YAML frontmatter: `name` + `description` only +- `description`: starts with "Use when...", third person, no workflow summary, max 1024 chars +- `name`: lowercase letters, numbers, hyphens, max 64 chars, no reserved words +- Body under 500 lines; heavy content split into separate reference files +- References kept one level deep from SKILL.md (no nested chains) +- No time-sensitive content +- Consistent terminology throughout + +--- + +## Files Touched + +| File | Change | +|------|--------| +| `src/agent.rs` | RwLock wrapping, async build_system_prompt, always-fresh messages[0], two new tools + handlers | +| `skills/creating-skills/SKILL.md` | New skill file (no code changes needed) | + +No other files require modification. + +--- + +## Security Notes + +- `write_skill_file` validates `skill_name` with strict regex (no traversal possible via name) +- `relative_path` is checked for `..` components before joining +- Skills directory is separate from the user sandbox — no cross-contamination +- `reload_skills` only reads from the configured `skills.directory` + +--- + +## Open Questions (resolved) + +- **Single vs multi-file tool**: Chose single `write_skill_file(skill_name, relative_path, content)` — more extensible, handles full directory structures +- **Hot-reload scope**: In-memory only; DB keeps historical system message intact +- **Skill name for this feature**: `creating-skills` (gerund, follows official naming convention) From 055fbb7114d366b96875adae75aded28763030fe Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 21 Feb 2026 11:25:32 +0000 Subject: [PATCH 02/15] docs: add skill writer implementation plan 7-task TDD plan covering: - Validation helpers + tests (Task 1) - RwLock + async build_system_prompt (Task 2) - Always-fresh system prompt in process_message (Task 3) - skill_tool_definitions() method (Task 4) - write_skill_file + reload_skills handlers (Task 5) - creating-skills SKILL.md (Task 6) - Lint, format, push (Task 7) https://claude.ai/code/session_01V5qMEXBhQQuKHeot52gxEh --- docs/plans/2026-02-21-skill-writer.md | 701 ++++++++++++++++++++++++++ 1 file changed, 701 insertions(+) create mode 100644 docs/plans/2026-02-21-skill-writer.md diff --git a/docs/plans/2026-02-21-skill-writer.md b/docs/plans/2026-02-21-skill-writer.md new file mode 100644 index 0000000..d269e5e --- /dev/null +++ b/docs/plans/2026-02-21-skill-writer.md @@ -0,0 +1,701 @@ +# Skill Writer Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Let the RustFox agent write multi-file skill directories and hot-reload them instantly — no bot restart required. + +**Architecture:** Wrap `SkillRegistry` in `tokio::sync::RwLock` so `reload_skills` can swap it at runtime. Add `write_skill_file` + `reload_skills` as built-in tools in `agent.rs`. Rebuild the system prompt from the live registry on every `process_message` call so new skills take effect immediately in the same conversation. + +**Tech Stack:** Rust 2021, Tokio async runtime, `anyhow`, `serde_json`, `tracing` + +--- + +## Task 1: Add validation helpers + tests (TDD — RED then GREEN) + +**Files:** +- Modify: `src/agent.rs` + +### Step 1: Write failing tests + +At the bottom of `src/agent.rs`, inside the existing `#[cfg(test)] mod tests` block, add: + +```rust + #[test] + fn test_validate_skill_name_valid() { + assert!(validate_skill_name("creating-skills").is_ok()); + assert!(validate_skill_name("my-skill-123").is_ok()); + assert!(validate_skill_name("a").is_ok()); + } + + #[test] + fn test_validate_skill_name_empty() { + assert!(validate_skill_name("").is_err()); + } + + #[test] + fn test_validate_skill_name_too_long() { + let long = "a".repeat(65); + assert!(validate_skill_name(&long).is_err()); + } + + #[test] + fn test_validate_skill_name_invalid_chars() { + assert!(validate_skill_name("My-Skill").is_err()); // uppercase + assert!(validate_skill_name("my skill").is_err()); // space + assert!(validate_skill_name("my_skill").is_err()); // underscore + assert!(validate_skill_name("my/skill").is_err()); // slash + } + + #[test] + fn test_validate_skill_path_valid() { + assert!(validate_skill_path("SKILL.md").is_ok()); + assert!(validate_skill_path("reference.md").is_ok()); + assert!(validate_skill_path("scripts/helper.py").is_ok()); + assert!(validate_skill_path("scripts/sub/tool.sh").is_ok()); + } + + #[test] + fn test_validate_skill_path_traversal() { + assert!(validate_skill_path("../other-skill/SKILL.md").is_err()); + assert!(validate_skill_path("scripts/../../../etc/passwd").is_err()); + assert!(validate_skill_path("..").is_err()); + } + + #[test] + fn test_validate_skill_path_empty() { + assert!(validate_skill_path("").is_err()); + } +``` + +### Step 2: Run tests — expect compile failure (functions don't exist yet) + +```bash +cargo test -p rustfox 2>&1 | head -30 +``` + +Expected: `error[E0425]: cannot find function 'validate_skill_name'` + +### Step 3: Add the validation functions + +Just above the `#[cfg(test)]` block (at module level, after the `split_response_chunks` function), add: + +```rust +/// Validate skill directory name: lowercase letters, numbers, hyphens, 1–64 chars. +fn validate_skill_name(name: &str) -> Result<(), String> { + if name.is_empty() { + return Err("Skill name must not be empty".to_string()); + } + if name.len() > 64 { + return Err(format!("Skill name too long ({} chars, max 64)", name.len())); + } + if !name + .chars() + .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-') + { + return Err( + "Skill name must contain only lowercase letters, numbers, and hyphens".to_string(), + ); + } + Ok(()) +} + +/// Validate a relative path within a skill directory: no '..' components, non-empty. +fn validate_skill_path(path: &str) -> Result<(), String> { + if path.is_empty() { + return Err("Relative path must not be empty".to_string()); + } + if path.split('/').any(|c| c == "..") { + return Err("Path traversal ('..') is not allowed".to_string()); + } + Ok(()) +} +``` + +### Step 4: Run tests — expect GREEN + +```bash +cargo test -p rustfox 2>&1 | tail -20 +``` + +Expected: all new tests pass. + +### Step 5: Commit + +```bash +git add src/agent.rs +git commit -m "feat: add skill_name and skill_path validation helpers with tests" +``` + +--- + +## Task 2: Wrap SkillRegistry in RwLock and make build_system_prompt async + +**Files:** +- Modify: `src/agent.rs` + +### Step 1: Change the `skills` field type + +In the `Agent` struct (around line 33), change: + +```rust +// BEFORE +pub skills: SkillRegistry, + +// AFTER +pub skills: tokio::sync::RwLock, +``` + +### Step 2: Update `Agent::new` to wrap in RwLock + +In `Agent::new`, in the `Self { ... }` block (around line 58), change: + +```rust +// BEFORE + skills, + +// AFTER + skills: tokio::sync::RwLock::new(skills), +``` + +### Step 3: Make `build_system_prompt` async and acquire read lock + +Replace the entire `build_system_prompt` function (currently around line 73–92): + +```rust +/// Build the system prompt, incorporating loaded skills +async fn build_system_prompt(&self) -> String { + let mut prompt = self.config.openrouter.system_prompt.clone(); + + let skills = self.skills.read().await; + let skill_context = skills.build_context(); + if !skill_context.is_empty() { + prompt.push_str("\n\n# Available Skills\n\n"); + prompt.push_str(&skill_context); + } + drop(skills); // release read lock before further work + + // Append current timestamp and optional location + let now = chrono::Utc::now() + .format("%Y-%m-%d %H:%M:%S UTC") + .to_string(); + prompt.push_str(&format!("\n\nCurrent date and time: {}", now)); + if let Some(loc) = self.config.user_location() { + prompt.push_str(&format!("\nUser location: {}", loc)); + } + + prompt +} +``` + +### Step 4: Verify the project compiles + +```bash +cargo check 2>&1 +``` + +Expected: no errors. (The call site in `process_message` will be updated in the next step.) + +### Step 5: Commit + +```bash +git add src/agent.rs +git commit -m "feat: wrap SkillRegistry in RwLock, make build_system_prompt async" +``` + +--- + +## Task 3: Always-fresh system prompt in `process_message` + +**Files:** +- Modify: `src/agent.rs` + +### Step 1: Replace the system-prompt block in `process_message` + +Find this block (around line 110–121): + +```rust + // If no messages yet, add system prompt + if messages.is_empty() { + let system_msg = ChatMessage { + role: "system".to_string(), + content: Some(self.build_system_prompt()), + tool_calls: None, + tool_call_id: None, + }; + self.memory + .save_message(&conversation_id, &system_msg) + .await?; + messages.push(system_msg); + } +``` + +Replace it with: + +```rust + // Always build the system prompt from the live registry. + // For new conversations: save to DB and push. + // For existing conversations: refresh messages[0] in-memory only + // (DB keeps the historical system message intact). + let current_system_prompt = self.build_system_prompt().await; + if messages.is_empty() { + let system_msg = ChatMessage { + role: "system".to_string(), + content: Some(current_system_prompt), + tool_calls: None, + tool_call_id: None, + }; + self.memory + .save_message(&conversation_id, &system_msg) + .await?; + messages.push(system_msg); + } else { + // Refresh in-memory: new skills loaded by reload_skills take effect + // on the very next message without restarting the bot. + messages[0].content = Some(current_system_prompt); + } +``` + +### Step 2: Verify compilation + +```bash +cargo check 2>&1 +``` + +Expected: no errors. + +### Step 3: Run existing tests + +```bash +cargo test 2>&1 +``` + +Expected: all existing tests pass. + +### Step 4: Commit + +```bash +git add src/agent.rs +git commit -m "feat: always rebuild system prompt from live registry on each process_message" +``` + +--- + +## Task 4: Add skill tool definitions + +**Files:** +- Modify: `src/agent.rs` + +### Step 1: Add `skill_tool_definitions()` method + +Add this method to the `impl Agent` block, right after `scheduling_tool_definitions()` (before `execute_tool`): + +```rust + /// Skill management tool definitions exposed to the LLM + fn skill_tool_definitions(&self) -> Vec { + use serde_json::json; + + vec![ + ToolDefinition { + tool_type: "function".to_string(), + function: FunctionDefinition { + name: "write_skill_file".to_string(), + description: concat!( + "Write a file into a skill directory under the configured skills folder. ", + "Use this to create SKILL.md and any supporting files (reference docs, templates, scripts). ", + "Call reload_skills after ALL files for the skill are written." + ).to_string(), + parameters: json!({ + "type": "object", + "properties": { + "skill_name": { + "type": "string", + "description": "Skill directory name: lowercase letters, numbers, hyphens only, max 64 chars (e.g. 'creating-reports')" + }, + "relative_path": { + "type": "string", + "description": "Path within the skill directory, e.g. 'SKILL.md', 'reference.md', 'scripts/helper.py'" + }, + "content": { + "type": "string", + "description": "Full file content to write" + } + }, + "required": ["skill_name", "relative_path", "content"] + }), + }, + }, + ToolDefinition { + tool_type: "function".to_string(), + function: FunctionDefinition { + name: "reload_skills".to_string(), + description: concat!( + "Reload all skills from the skills directory into memory. ", + "Call this after writing skill files to make the new skill immediately active ", + "without restarting the bot." + ).to_string(), + parameters: json!({ "type": "object", "properties": {} }), + }, + }, + ] + } +``` + +### Step 2: Add to `all_tool_definitions()` + +In `all_tool_definitions()` (around line 324), add: + +```rust +// BEFORE (last line of the method): + all_tools +// AFTER: + all_tools.extend(self.skill_tool_definitions()); + all_tools +``` + +### Step 3: Add to the agentic loop's tool list in `process_message` + +In `process_message`, find the block that builds `all_tools` (around line 136–139): + +```rust + // Gather all tool definitions + let mut all_tools: Vec = tools::builtin_tool_definitions(); + all_tools.extend(self.mcp.tool_definitions()); + all_tools.extend(self.memory_tool_definitions()); + all_tools.extend(self.scheduling_tool_definitions()); +``` + +Add one line after the last `extend`: + +```rust + all_tools.extend(self.skill_tool_definitions()); +``` + +### Step 4: Verify compilation + +```bash +cargo check 2>&1 +``` + +Expected: no errors. + +### Step 5: Commit + +```bash +git add src/agent.rs +git commit -m "feat: add write_skill_file and reload_skills tool definitions" +``` + +--- + +## Task 5: Handle write_skill_file and reload_skills in execute_tool + +**Files:** +- Modify: `src/agent.rs` + +### Step 1: Add handlers in `execute_tool` + +In `execute_tool`, find the arm: + +```rust + _ if self.mcp.is_mcp_tool(name) => match self.mcp.call_tool(name, arguments).await { +``` + +Insert the two new arms **immediately before** that line: + +```rust + "write_skill_file" => { + let skill_name = match arguments["skill_name"].as_str() { + Some(n) => n.to_string(), + None => return "Missing skill_name".to_string(), + }; + let relative_path = match arguments["relative_path"].as_str() { + Some(p) => p.to_string(), + None => return "Missing relative_path".to_string(), + }; + let content = arguments["content"].as_str().unwrap_or("").to_string(); + + if let Err(e) = validate_skill_name(&skill_name) { + return format!("Invalid skill_name: {}", e); + } + if let Err(e) = validate_skill_path(&relative_path) { + return format!("Invalid relative_path: {}", e); + } + + let target = self + .config + .skills + .directory + .join(&skill_name) + .join(&relative_path); + + if let Some(parent) = target.parent() { + if let Err(e) = tokio::fs::create_dir_all(parent).await { + return format!("Failed to create directories: {}", e); + } + } + + match tokio::fs::write(&target, &content).await { + Ok(()) => { + info!("Skill file written: {}", target.display()); + format!("Written: {}", target.display()) + } + Err(e) => format!("Failed to write skill file: {}", e), + } + } + "reload_skills" => { + use crate::skills::loader::load_skills_from_dir; + match load_skills_from_dir(&self.config.skills.directory).await { + Ok(new_registry) => { + let count = new_registry.len(); + let mut skills = self.skills.write().await; + *skills = new_registry; + info!("Skills reloaded: {} skill(s) active", count); + format!("Skills reloaded. {} skill(s) now active.", count) + } + Err(e) => format!("Failed to reload skills: {}", e), + } + } +``` + +### Step 2: Verify compilation + +```bash +cargo check 2>&1 +``` + +Expected: no errors. + +### Step 3: Run all tests + +```bash +cargo test 2>&1 +``` + +Expected: all tests pass (including the validation tests from Task 1). + +### Step 4: Commit + +```bash +git add src/agent.rs +git commit -m "feat: handle write_skill_file and reload_skills in execute_tool" +``` + +--- + +## Task 6: Create skills/creating-skills/SKILL.md + +**Files:** +- Create: `skills/creating-skills/SKILL.md` + +### Step 1: Create the directory + +```bash +mkdir -p skills/creating-skills +``` + +### Step 2: Create the skill file + +Create `skills/creating-skills/SKILL.md` with this exact content: + +````markdown +--- +name: creating-skills +description: Use when the user asks to create, add, or write a new bot skill, or wants to teach the bot a new behavior, capability, or workflow. +tags: [skills, meta] +--- + +# Creating Skills + +Writes new bot skills as properly-formatted multi-file directories in `skills/` and activates them immediately without restarting the bot. + +## When to Use + +- "Create a skill for X" +- "Teach the bot to Y" +- "Add a skill that does Z" +- "Write a skill for [topic]" + +## Process + +### 1. Gather Requirements + +Ask the user (one question at a time if unclear): +- **Name**: Slug for the skill directory — lowercase letters, numbers, hyphens, e.g. `processing-reports` +- **Trigger**: When should this activate? (becomes the `description` field's "Use when...") +- **Behavior**: What should the agent do step-by-step? +- **Files**: Does it need supporting files (reference docs, templates, scripts)? + +### 2. Design the Structure + +Plan the directory before writing: + +``` +skills// +├── SKILL.md # Always required — main entry point +├── reference.md # Optional: heavy reference content (100+ lines) +├── examples.md # Optional: input/output examples +└── scripts/ # Optional: utility scripts + └── helper.py +``` + +Rules from official best practices: +- References must be **one level deep** from SKILL.md — no chained references +- Split into separate files only when SKILL.md would exceed ~500 lines +- SKILL.md body should be concise — it loads into context on every trigger + +### 3. Write SKILL.md + +SKILL.md must follow this format: + +```markdown +--- +name: skill-name-with-hyphens +description: Use when [specific triggering conditions — third person, no workflow summary] +tags: [optional, tags] +--- + +# Skill Title + +Brief overview (1-2 sentences). + +## When to Use + +- Trigger condition 1 +- Trigger condition 2 + +## [Core Instructions] + +[Clear, action-oriented, imperative instructions] + +## Supporting Files (if any) + +**Topic A**: See [reference.md](reference.md) +**Topic B**: See [examples.md](examples.md) +``` + +**Frontmatter rules:** +- `name`: lowercase letters, numbers, hyphens only; max 64 chars; avoid "anthropic" / "claude" +- `description`: starts with "Use when..."; third person; no workflow summary; max 1024 chars +- `tags`: optional list + +**Body rules:** +- Under 500 lines total +- Action-oriented and imperative +- Consistent terminology throughout +- No time-sensitive information + +### 4. Write Files + +Call `write_skill_file` once per file. Always write `SKILL.md` first: + +``` +write_skill_file(skill_name="my-skill", relative_path="SKILL.md", content="---\nname: ...") +write_skill_file(skill_name="my-skill", relative_path="reference.md", content="# Reference\n...") +``` + +### 5. Activate + +Call `reload_skills` immediately after all files are written. + +Tell the user: +- The skill is now live (no restart needed) +- Which files were created +- What trigger phrase activates it + +## Description Writing Guide + +```yaml +# ✅ Good — triggering conditions only, third person +description: Use when the user asks to generate weekly reports, export data summaries, or create formatted output from raw data. + +# ✅ Good — specific triggers +description: Use when analyzing code for bugs, reviewing pull requests, or the user asks for a code review. + +# ❌ Bad — summarizes workflow (causes Claude to skip reading the skill body) +description: Use when creating reports — reads data, formats it, writes to file. + +# ❌ Bad — first person +description: I help users create reports from their data. +``` +```` + +### Step 3: Verify the skill loads + +```bash +cargo check 2>&1 +``` + +The skill is a plain file — no compilation needed. The bot will load it at startup. + +### Step 4: Commit + +```bash +git add skills/creating-skills/SKILL.md +git commit -m "feat: add creating-skills skill for agent self-authoring" +``` + +--- + +## Task 7: Lint, format, and push + +**Files:** none (verification only) + +### Step 1: Format + +```bash +cargo fmt +``` + +Expected: no output (or only whitespace changes). + +### Step 2: Clippy + +```bash +cargo clippy -- -D warnings 2>&1 +``` + +Expected: no warnings. If any warnings appear, fix them before continuing. + +Common clippy issues to watch for: +- `dead_code` on `validate_skill_name` / `validate_skill_path` (they're used in `execute_tool`, so should be fine) +- If `validate_skill_name` is only used in tests + execute_tool, clippy may flag it — suppress with `#[allow(dead_code)]` only if needed + +### Step 3: Run all tests + +```bash +cargo test 2>&1 +``` + +Expected: all tests pass. + +### Step 4: Commit any fmt changes + +```bash +git add -u +git diff --cached --quiet || git commit -m "style: cargo fmt" +``` + +### Step 5: Push branch + +```bash +git push -u origin claude/agent-skill-writer-SJS5d +``` + +Expected: push succeeds. If network failure, retry with exponential backoff (2s, 4s, 8s, 16s). + +--- + +## Verification Checklist + +After all tasks complete: +- [ ] `cargo check` passes with no errors +- [ ] `cargo clippy -- -D warnings` passes with no warnings +- [ ] `cargo fmt --check` passes +- [ ] `cargo test` passes (all tests including the new validation tests) +- [ ] `skills/creating-skills/SKILL.md` exists and has valid YAML frontmatter +- [ ] `src/agent.rs` has `skills: tokio::sync::RwLock` +- [ ] `src/agent.rs` has `build_system_prompt` as `async fn` +- [ ] `process_message` refreshes `messages[0]` for existing conversations +- [ ] `write_skill_file` and `reload_skills` are handled in `execute_tool` +- [ ] Both tools appear in `all_tool_definitions()` and the agentic loop +- [ ] Branch pushed to `claude/agent-skill-writer-SJS5d` From 98e9705f1e0ddcae8964ca548c63785a79388b94 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 21 Feb 2026 13:57:45 +0000 Subject: [PATCH 03/15] feat: add skill_name and skill_path validation helpers with tests --- src/agent.rs | 76 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/src/agent.rs b/src/agent.rs index 1e9de58..28c1279 100644 --- a/src/agent.rs +++ b/src/agent.rs @@ -766,6 +766,36 @@ pub fn split_response_chunks(text: &str, max_len: usize) -> Vec { chunks } +/// Validate skill directory name: lowercase letters, numbers, hyphens, 1–64 chars. +fn validate_skill_name(name: &str) -> Result<(), String> { + if name.is_empty() { + return Err("Skill name must not be empty".to_string()); + } + if name.len() > 64 { + return Err(format!("Skill name too long ({} chars, max 64)", name.len())); + } + if !name + .chars() + .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-') + { + return Err( + "Skill name must contain only lowercase letters, numbers, and hyphens".to_string(), + ); + } + Ok(()) +} + +/// Validate a relative path within a skill directory: no '..' components, non-empty. +fn validate_skill_path(path: &str) -> Result<(), String> { + if path.is_empty() { + return Err("Relative path must not be empty".to_string()); + } + if path.split('/').any(|c| c == "..") { + return Err("Path traversal ('..') is not allowed".to_string()); + } + Ok(()) +} + #[cfg(test)] mod tests { use super::*; @@ -800,4 +830,50 @@ mod tests { assert!(validate_cron_expr("0 9 * * *").is_err()); // 5 fields assert!(validate_cron_expr("0 0 9 1 * * MON").is_err()); // 7 fields } + + #[test] + fn test_validate_skill_name_valid() { + assert!(validate_skill_name("creating-skills").is_ok()); + assert!(validate_skill_name("my-skill-123").is_ok()); + assert!(validate_skill_name("a").is_ok()); + } + + #[test] + fn test_validate_skill_name_empty() { + assert!(validate_skill_name("").is_err()); + } + + #[test] + fn test_validate_skill_name_too_long() { + let long = "a".repeat(65); + assert!(validate_skill_name(&long).is_err()); + } + + #[test] + fn test_validate_skill_name_invalid_chars() { + assert!(validate_skill_name("My-Skill").is_err()); // uppercase + assert!(validate_skill_name("my skill").is_err()); // space + assert!(validate_skill_name("my_skill").is_err()); // underscore + assert!(validate_skill_name("my/skill").is_err()); // slash + } + + #[test] + fn test_validate_skill_path_valid() { + assert!(validate_skill_path("SKILL.md").is_ok()); + assert!(validate_skill_path("reference.md").is_ok()); + assert!(validate_skill_path("scripts/helper.py").is_ok()); + assert!(validate_skill_path("scripts/sub/tool.sh").is_ok()); + } + + #[test] + fn test_validate_skill_path_traversal() { + assert!(validate_skill_path("../other-skill/SKILL.md").is_err()); + assert!(validate_skill_path("scripts/../../../etc/passwd").is_err()); + assert!(validate_skill_path("..").is_err()); + } + + #[test] + fn test_validate_skill_path_empty() { + assert!(validate_skill_path("").is_err()); + } } From 8ac260cece65d165ca54e62db43dc5a0f85e1997 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 21 Feb 2026 14:01:59 +0000 Subject: [PATCH 04/15] fix: suppress dead_code for validation helpers, reject absolute paths in validate_skill_path https://claude.ai/code/session_01V5qMEXBhQQuKHeot52gxEh --- src/agent.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/agent.rs b/src/agent.rs index 28c1279..a12a315 100644 --- a/src/agent.rs +++ b/src/agent.rs @@ -767,6 +767,7 @@ pub fn split_response_chunks(text: &str, max_len: usize) -> Vec { } /// Validate skill directory name: lowercase letters, numbers, hyphens, 1–64 chars. +#[allow(dead_code)] fn validate_skill_name(name: &str) -> Result<(), String> { if name.is_empty() { return Err("Skill name must not be empty".to_string()); @@ -786,10 +787,14 @@ fn validate_skill_name(name: &str) -> Result<(), String> { } /// Validate a relative path within a skill directory: no '..' components, non-empty. +#[allow(dead_code)] fn validate_skill_path(path: &str) -> Result<(), String> { if path.is_empty() { return Err("Relative path must not be empty".to_string()); } + if path.starts_with('/') { + return Err("Relative path must not be absolute".to_string()); + } if path.split('/').any(|c| c == "..") { return Err("Path traversal ('..') is not allowed".to_string()); } @@ -876,4 +881,10 @@ mod tests { fn test_validate_skill_path_empty() { assert!(validate_skill_path("").is_err()); } + + #[test] + fn test_validate_skill_path_absolute() { + assert!(validate_skill_path("/etc/passwd").is_err()); + assert!(validate_skill_path("/SKILL.md").is_err()); + } } From fbf416cebc5e13100380552e1125d15379ad6815 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 21 Feb 2026 14:06:19 +0000 Subject: [PATCH 05/15] feat: wrap SkillRegistry in RwLock, make build_system_prompt async https://claude.ai/code/session_01V5qMEXBhQQuKHeot52gxEh --- src/agent.rs | 10 ++++++---- src/platform/telegram.rs | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/agent.rs b/src/agent.rs index a12a315..9da35ab 100644 --- a/src/agent.rs +++ b/src/agent.rs @@ -30,7 +30,7 @@ pub struct Agent { pub config: Config, pub mcp: McpManager, pub memory: MemoryStore, - pub skills: SkillRegistry, + pub skills: tokio::sync::RwLock, // Fields used by scheduling / job closures pub task_store: ScheduledTaskStore, pub scheduler: Arc, @@ -60,7 +60,7 @@ impl Agent { config, mcp, memory, - skills, + skills: tokio::sync::RwLock::new(skills), task_store, scheduler, bot, @@ -70,14 +70,16 @@ impl Agent { } /// Build the system prompt, incorporating loaded skills - fn build_system_prompt(&self) -> String { + async fn build_system_prompt(&self) -> String { let mut prompt = self.config.openrouter.system_prompt.clone(); - let skill_context = self.skills.build_context(); + let skills = self.skills.read().await; + let skill_context = skills.build_context(); if !skill_context.is_empty() { prompt.push_str("\n\n# Available Skills\n\n"); prompt.push_str(&skill_context); } + drop(skills); // release read lock before further work // Append current timestamp and optional location let now = chrono::Utc::now() diff --git a/src/platform/telegram.rs b/src/platform/telegram.rs index ecf5377..642fc2e 100644 --- a/src/platform/telegram.rs +++ b/src/platform/telegram.rs @@ -128,7 +128,7 @@ async fn handle_message(bot: Bot, msg: Message, agent: Arc) -> ResponseRe } if text == "/skills" { - let skills = agent.skills.list(); + let skills = agent.skills.read().await.list(); if skills.is_empty() { bot.send_message(msg.chat.id, "No skills loaded.").await?; } else { From 3d255b207dd25a7c96e870cdf951f836fba613e4 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 21 Feb 2026 14:06:55 +0000 Subject: [PATCH 06/15] feat: wrap SkillRegistry in RwLock, make build_system_prompt async Also fix temporary borrow in telegram.rs /skills handler caused by the new RwLock wrapper requiring an explicit guard binding before calling list(). https://claude.ai/code/session_01V5qMEXBhQQuKHeot52gxEh --- src/platform/telegram.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/platform/telegram.rs b/src/platform/telegram.rs index 642fc2e..ef5028a 100644 --- a/src/platform/telegram.rs +++ b/src/platform/telegram.rs @@ -128,7 +128,8 @@ async fn handle_message(bot: Bot, msg: Message, agent: Arc) -> ResponseRe } if text == "/skills" { - let skills = agent.skills.read().await.list(); + let skills_guard = agent.skills.read().await; + let skills = skills_guard.list(); if skills.is_empty() { bot.send_message(msg.chat.id, "No skills loaded.").await?; } else { From cd1e0bf553b524fe81ade3e47f5d516ada10d049 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 21 Feb 2026 14:09:56 +0000 Subject: [PATCH 07/15] feat: always rebuild system prompt from live registry on each process_message https://claude.ai/code/session_01V5qMEXBhQQuKHeot52gxEh --- src/agent.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/agent.rs b/src/agent.rs index 9da35ab..ee5010c 100644 --- a/src/agent.rs +++ b/src/agent.rs @@ -108,11 +108,15 @@ impl Agent { // Load existing messages from memory let mut messages = self.memory.load_messages(&conversation_id).await?; - // If no messages yet, add system prompt + // Always build the system prompt from the live registry. + // For new conversations: save to DB and push. + // For existing conversations: refresh messages[0] in-memory only + // (DB keeps the historical system message intact). + let current_system_prompt = self.build_system_prompt().await; if messages.is_empty() { let system_msg = ChatMessage { role: "system".to_string(), - content: Some(self.build_system_prompt()), + content: Some(current_system_prompt), tool_calls: None, tool_call_id: None, }; @@ -120,6 +124,10 @@ impl Agent { .save_message(&conversation_id, &system_msg) .await?; messages.push(system_msg); + } else { + // Refresh in-memory: new skills loaded by reload_skills take effect + // on the very next message without restarting the bot. + messages[0].content = Some(current_system_prompt); } // Add user message From 5ba840aa9dac31c3e7b6fd685d4fec8c6c85a9d5 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 21 Feb 2026 14:13:18 +0000 Subject: [PATCH 08/15] fix: find system message by role before refreshing prompt, not by index https://claude.ai/code/session_01V5qMEXBhQQuKHeot52gxEh --- src/agent.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/agent.rs b/src/agent.rs index ee5010c..7c284c6 100644 --- a/src/agent.rs +++ b/src/agent.rs @@ -127,7 +127,10 @@ impl Agent { } else { // Refresh in-memory: new skills loaded by reload_skills take effect // on the very next message without restarting the bot. - messages[0].content = Some(current_system_prompt); + // Find the system message by role (defensive: don't assume messages[0] is system). + if let Some(system_msg) = messages.iter_mut().find(|m| m.role == "system") { + system_msg.content = Some(current_system_prompt); + } } // Add user message From 6ccc049f249e8028a1493ee3791728e9847b9515 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 21 Feb 2026 14:16:37 +0000 Subject: [PATCH 09/15] feat: add write_skill_file and reload_skills tool definitions --- src/agent.rs | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/src/agent.rs b/src/agent.rs index 7c284c6..db7265a 100644 --- a/src/agent.rs +++ b/src/agent.rs @@ -150,6 +150,7 @@ impl Agent { all_tools.extend(self.mcp.tool_definitions()); all_tools.extend(self.memory_tool_definitions()); all_tools.extend(self.scheduling_tool_definitions()); + all_tools.extend(self.skill_tool_definitions()); // Agentic loop — keep calling LLM until we get a non-tool response let max_iterations = self.config.max_iterations(); @@ -339,6 +340,7 @@ impl Agent { all_tools.extend(self.mcp.tool_definitions()); all_tools.extend(self.memory_tool_definitions()); all_tools.extend(self.scheduling_tool_definitions()); + all_tools.extend(self.skill_tool_definitions()); all_tools } @@ -460,6 +462,56 @@ impl Agent { } /// Execute a tool call by routing to the right handler + + /// Skill management tool definitions exposed to the LLM + fn skill_tool_definitions(&self) -> Vec { + use serde_json::json; + + vec![ + ToolDefinition { + tool_type: "function".to_string(), + function: FunctionDefinition { + name: "write_skill_file".to_string(), + description: concat!( + "Write a file into a skill directory under the configured skills folder. ", + "Use this to create SKILL.md and any supporting files (reference docs, templates, scripts). ", + "Call reload_skills after ALL files for the skill are written." + ).to_string(), + parameters: json!({ + "type": "object", + "properties": { + "skill_name": { + "type": "string", + "description": "Skill directory name: lowercase letters, numbers, hyphens only, max 64 chars (e.g. 'creating-reports')" + }, + "relative_path": { + "type": "string", + "description": "Path within the skill directory, e.g. 'SKILL.md', 'reference.md', 'scripts/helper.py'" + }, + "content": { + "type": "string", + "description": "Full file content to write" + } + }, + "required": ["skill_name", "relative_path", "content"] + }), + }, + }, + ToolDefinition { + tool_type: "function".to_string(), + function: FunctionDefinition { + name: "reload_skills".to_string(), + description: concat!( + "Reload all skills from the skills directory into memory. ", + "Call this after writing skill files to make the new skill immediately active ", + "without restarting the bot." + ).to_string(), + parameters: json!({ "type": "object", "properties": {} }), + }, + }, + ] + } + async fn execute_tool( &self, name: &str, From 07f85df394a62a0b4b3d0b417fb1bf07963051a2 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 21 Feb 2026 14:20:32 +0000 Subject: [PATCH 10/15] fix: move skill_tool_definitions before execute_tool to fix clippy doc comment lint https://claude.ai/code/session_01V5qMEXBhQQuKHeot52gxEh --- src/agent.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/agent.rs b/src/agent.rs index db7265a..02c6eaf 100644 --- a/src/agent.rs +++ b/src/agent.rs @@ -461,8 +461,6 @@ impl Agent { ] } - /// Execute a tool call by routing to the right handler - /// Skill management tool definitions exposed to the LLM fn skill_tool_definitions(&self) -> Vec { use serde_json::json; @@ -512,6 +510,7 @@ impl Agent { ] } + /// Execute a tool call by routing to the right handler async fn execute_tool( &self, name: &str, From cf1bcb2366dc52c306d99165b06b46ef0e16db1e Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 21 Feb 2026 14:23:23 +0000 Subject: [PATCH 11/15] feat: handle write_skill_file and reload_skills in execute_tool https://claude.ai/code/session_01V5qMEXBhQQuKHeot52gxEh --- src/agent.rs | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 52 insertions(+), 2 deletions(-) diff --git a/src/agent.rs b/src/agent.rs index 02c6eaf..b76ac39 100644 --- a/src/agent.rs +++ b/src/agent.rs @@ -745,6 +745,58 @@ impl Agent { Err(e) => format!("Failed to update task status: {}", e), } } + "write_skill_file" => { + let skill_name = match arguments["skill_name"].as_str() { + Some(n) => n.to_string(), + None => return "Missing skill_name".to_string(), + }; + let relative_path = match arguments["relative_path"].as_str() { + Some(p) => p.to_string(), + None => return "Missing relative_path".to_string(), + }; + let content = arguments["content"].as_str().unwrap_or("").to_string(); + + if let Err(e) = validate_skill_name(&skill_name) { + return format!("Invalid skill_name: {}", e); + } + if let Err(e) = validate_skill_path(&relative_path) { + return format!("Invalid relative_path: {}", e); + } + + let target = self + .config + .skills + .directory + .join(&skill_name) + .join(&relative_path); + + if let Some(parent) = target.parent() { + if let Err(e) = tokio::fs::create_dir_all(parent).await { + return format!("Failed to create directories: {}", e); + } + } + + match tokio::fs::write(&target, &content).await { + Ok(()) => { + info!("Skill file written: {}", target.display()); + format!("Written: {}", target.display()) + } + Err(e) => format!("Failed to write skill file: {}", e), + } + } + "reload_skills" => { + use crate::skills::loader::load_skills_from_dir; + match load_skills_from_dir(&self.config.skills.directory).await { + Ok(new_registry) => { + let count = new_registry.len(); + let mut skills = self.skills.write().await; + *skills = new_registry; + info!("Skills reloaded: {} skill(s) active", count); + format!("Skills reloaded. {} skill(s) now active.", count) + } + Err(e) => format!("Failed to reload skills: {}", e), + } + } _ if self.mcp.is_mcp_tool(name) => match self.mcp.call_tool(name, arguments).await { Ok(result) => result, Err(e) => format!("MCP tool error: {}", e), @@ -831,7 +883,6 @@ pub fn split_response_chunks(text: &str, max_len: usize) -> Vec { } /// Validate skill directory name: lowercase letters, numbers, hyphens, 1–64 chars. -#[allow(dead_code)] fn validate_skill_name(name: &str) -> Result<(), String> { if name.is_empty() { return Err("Skill name must not be empty".to_string()); @@ -851,7 +902,6 @@ fn validate_skill_name(name: &str) -> Result<(), String> { } /// Validate a relative path within a skill directory: no '..' components, non-empty. -#[allow(dead_code)] fn validate_skill_path(path: &str) -> Result<(), String> { if path.is_empty() { return Err("Relative path must not be empty".to_string()); From 785f06bc67fd4d48afed1173f4969b713f2318fe Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 21 Feb 2026 14:24:43 +0000 Subject: [PATCH 12/15] feat: add creating-skills skill for agent self-authoring https://claude.ai/code/session_01V5qMEXBhQQuKHeot52gxEh --- skills/creating-skills/SKILL.md | 119 ++++++++++++++++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 skills/creating-skills/SKILL.md diff --git a/skills/creating-skills/SKILL.md b/skills/creating-skills/SKILL.md new file mode 100644 index 0000000..4a2bf22 --- /dev/null +++ b/skills/creating-skills/SKILL.md @@ -0,0 +1,119 @@ +--- +name: creating-skills +description: Use when the user asks to create, add, or write a new bot skill, or wants to teach the bot a new behavior, capability, or workflow. +tags: [skills, meta] +--- + +# Creating Skills + +Writes new bot skills as properly-formatted multi-file directories in `skills/` and activates them immediately without restarting the bot. + +## When to Use + +- "Create a skill for X" +- "Teach the bot to Y" +- "Add a skill that does Z" +- "Write a skill for [topic]" + +## Process + +### 1. Gather Requirements + +Ask the user (one question at a time if unclear): +- **Name**: Slug for the skill directory — lowercase letters, numbers, hyphens, e.g. `processing-reports` +- **Trigger**: When should this activate? (becomes the `description` field's "Use when...") +- **Behavior**: What should the agent do step-by-step? +- **Files**: Does it need supporting files (reference docs, templates, scripts)? + +### 2. Design the Structure + +Plan the directory before writing: + +``` +skills// +├── SKILL.md # Always required — main entry point +├── reference.md # Optional: heavy reference content (100+ lines) +├── examples.md # Optional: input/output examples +└── scripts/ # Optional: utility scripts + └── helper.py +``` + +Rules from official best practices: +- References must be **one level deep** from SKILL.md — no chained references +- Split into separate files only when SKILL.md would exceed ~500 lines +- SKILL.md body should be concise — it loads into context on every trigger + +### 3. Write SKILL.md + +SKILL.md must follow this format: + +```markdown +--- +name: skill-name-with-hyphens +description: Use when [specific triggering conditions — third person, no workflow summary] +tags: [optional, tags] +--- + +# Skill Title + +Brief overview (1-2 sentences). + +## When to Use + +- Trigger condition 1 +- Trigger condition 2 + +## [Core Instructions] + +[Clear, action-oriented, imperative instructions] + +## Supporting Files (if any) + +**Topic A**: See [reference.md](reference.md) +**Topic B**: See [examples.md](examples.md) +``` + +**Frontmatter rules:** +- `name`: lowercase letters, numbers, hyphens only; max 64 chars; avoid "anthropic" / "claude" +- `description`: starts with "Use when..."; third person; no workflow summary; max 1024 chars +- `tags`: optional list + +**Body rules:** +- Under 500 lines total +- Action-oriented and imperative +- Consistent terminology throughout +- No time-sensitive information + +### 4. Write Files + +Call `write_skill_file` once per file. Always write `SKILL.md` first: + +``` +write_skill_file(skill_name="my-skill", relative_path="SKILL.md", content="---\nname: ...") +write_skill_file(skill_name="my-skill", relative_path="reference.md", content="# Reference\n...") +``` + +### 5. Activate + +Call `reload_skills` immediately after all files are written. + +Tell the user: +- The skill is now live (no restart needed) +- Which files were created +- What trigger phrase activates it + +## Description Writing Guide + +```yaml +# ✅ Good — triggering conditions only, third person +description: Use when the user asks to generate weekly reports, export data summaries, or create formatted output from raw data. + +# ✅ Good — specific triggers +description: Use when analyzing code for bugs, reviewing pull requests, or the user asks for a code review. + +# ❌ Bad — summarizes workflow (causes Claude to skip reading the skill body) +description: Use when creating reports — reads data, formats it, writes to file. + +# ❌ Bad — first person +description: I help users create reports from their data. +``` From 529ac247f4bfea1cec18428070c55c25494de7cf Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 21 Feb 2026 14:28:58 +0000 Subject: [PATCH 13/15] style: cargo fmt --- src/agent.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/agent.rs b/src/agent.rs index b76ac39..4002966 100644 --- a/src/agent.rs +++ b/src/agent.rs @@ -888,7 +888,10 @@ fn validate_skill_name(name: &str) -> Result<(), String> { return Err("Skill name must not be empty".to_string()); } if name.len() > 64 { - return Err(format!("Skill name too long ({} chars, max 64)", name.len())); + return Err(format!( + "Skill name too long ({} chars, max 64)", + name.len() + )); } if !name .chars() @@ -970,7 +973,7 @@ mod tests { #[test] fn test_validate_skill_name_invalid_chars() { - assert!(validate_skill_name("My-Skill").is_err()); // uppercase + assert!(validate_skill_name("My-Skill").is_err()); // uppercase assert!(validate_skill_name("my skill").is_err()); // space assert!(validate_skill_name("my_skill").is_err()); // underscore assert!(validate_skill_name("my/skill").is_err()); // slash From c61e2bb152a1fd3835bf067b557b83fe39d9b61c Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 21 Feb 2026 16:26:00 +0000 Subject: [PATCH 14/15] docs: mark write_skill_file and reload_skills as done in roadmap --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index cf6c2f3..b343117 100644 --- a/README.md +++ b/README.md @@ -204,6 +204,8 @@ src/ - [x] Vector embedding search (`qwen/qwen3-embedding-8b`) - [x] Scheduling tools (`schedule_task`, `list_scheduled_tasks`, `cancel_scheduled_task`) - [x] Bot skills (folder-based, auto-loaded at startup) +- [x] Agent skill writer (`write_skill_file` tool — creates/updates skill files from within the agent) +- [x] Agent skill reload (`reload_skills` tool — hot-reloads skill registry without restart) ### Planned From 456dbac593e51935514f22630cd88fa52a565d76 Mon Sep 17 00:00:00 2001 From: "chinkan.ai" Date: Sun, 22 Feb 2026 00:27:33 +0800 Subject: [PATCH 15/15] docs: add setup wizard to roadmap in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b343117..4440f7f 100644 --- a/README.md +++ b/README.md @@ -204,6 +204,7 @@ src/ - [x] Vector embedding search (`qwen/qwen3-embedding-8b`) - [x] Scheduling tools (`schedule_task`, `list_scheduled_tasks`, `cancel_scheduled_task`) - [x] Bot skills (folder-based, auto-loaded at startup) +- [x] Setup wizard (web UI + CLI) for guided `config.toml` creation - [x] Agent skill writer (`write_skill_file` tool — creates/updates skill files from within the agent) - [x] Agent skill reload (`reload_skills` tool — hot-reloads skill registry without restart) @@ -212,7 +213,6 @@ src/ - [ ] Image upload support - [ ] Google integration tools (Calendar, Email, Drive) - [ ] Event trigger framework (e.g., on email receive) -- [x] Setup wizard (web UI + CLI) for guided `config.toml` creation - [ ] WhatsApp support - [ ] Webhook mode (in addition to polling) - [ ] And more…