diff --git a/CLAUDE.md b/CLAUDE.md index 6df8612..ea62970 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -79,7 +79,7 @@ Read `docs/architecture.md` first. Key points: messages when context is built. - **System prompt** pulls project context from the working directory: `AGENTS.md`, or `CLAUDE.md` if there is no `AGENTS.md` (never both), plus `PERSONA.md` and `MEMORY.md`. File names match case-insensitively. `SkillCatalog` - lists the cwd's Agent Skills (`.claude/skills//SKILL.md`, frontmatter `name`/`description`) in a `` + lists the cwd's Agent Skills (`.volcanic/skills//SKILL.md`, frontmatter `name`/`description`) in a `` section on every run; the model reads a SKILL.md with `read`. Only the cwd, so chats never see each other's skills. The Claude Code provider keeps the CLI's own skills off (`--setting-sources ""`, `--disable-slash-commands`). - **Extension discovery**: host code, then `/.volcanic/settings.json` / `.volcanic/extensions/*.dll`, then the same diff --git a/README.md b/README.md index 908e93e..e6e0524 100644 --- a/README.md +++ b/README.md @@ -105,7 +105,7 @@ event model; slash commands; provider registration; API key resolution; built-in Beyond pi: the working directory may also hold `PERSONA.md` (the agent's personality) and `MEMORY.md` (memories the agent maintains itself with `edit`/`write`); both are appended to the system prompt when present. File names -match case-insensitively. Agent Skills (Anthropic's `SKILL.md` format) in `/.claude/skills//SKILL.md` are +match case-insensitively. Agent Skills (Anthropic's `SKILL.md` format) in `/.volcanic/skills//SKILL.md` are listed by name and description in the system prompt on every run; the model reads a skill's file when it needs it. Not ported (by design): the terminal UI, themes, keybindings, renderers, prompt templates, diff --git a/docs/docker.md b/docs/docker.md index d02e6d7..40b2f24 100644 --- a/docs/docker.md +++ b/docs/docker.md @@ -146,7 +146,7 @@ The compose file binds them to directories under `/media/disk1/containers/volcan | Volume | Path | Contents | |---|---|---| -| `workspaces` | `/data/workspaces` | One directory per chat (`/data/workspaces/`): `AGENTS.md` or `CLAUDE.md`, `PERSONA.md`, `MEMORY.md`, skills (`.claude/skills//SKILL.md`), `inbox/` (received files), `browser/` (screenshots, downloads), project-level `.volcanic/extensions/`, the Google Calendar token (`.volcanic/google-calendar/token.json`), scheduled jobs (`.volcanic/scheduler/jobs.json`), A2A callers and tasks, remote agents and watches (`.volcanic/a2a/`, [a2a.md](a2a.md#files), [a2a.md](a2a.md#client)) | +| `workspaces` | `/data/workspaces` | One directory per chat (`/data/workspaces/`): `AGENTS.md` or `CLAUDE.md`, `PERSONA.md`, `MEMORY.md`, skills (`.volcanic/skills//SKILL.md`), `inbox/` (received files), `browser/` (screenshots, downloads), project-level `.volcanic/extensions/`, the Google Calendar token (`.volcanic/google-calendar/token.json`), scheduled jobs (`.volcanic/scheduler/jobs.json`), A2A callers and tasks, remote agents and watches (`.volcanic/a2a/`, [a2a.md](a2a.md#files), [a2a.md](a2a.md#client)) | | `agent` | `/data/agent` | Sessions (`sessions/telegram-/`), `settings.json`, `extensions/*.dll`, chat time zones (`telegram/chat-settings.json`); a `scheduler/jobs.json` from before jobs were per chat is moved into the workspaces on start | | `claude` | `/data/claude` | The Claude login (credentials and onboarding state) | diff --git a/docs/telegram-bot.md b/docs/telegram-bot.md index 118aac8..987566f 100644 --- a/docs/telegram-bot.md +++ b/docs/telegram-bot.md @@ -31,7 +31,7 @@ settings use the `GoogleCalendar` section, the scheduler the `Scheduler` section | `TELEGRAM_BOT_TOKEN` / `Telegram:BotToken` | — | Required | | `TELEGRAM_ALLOWED_USER_IDS` / `Telegram:AllowedUserIds` | — | Required, comma separated. The bot refuses to start without it | | `ADMIN_USER_ID` / `Telegram:AdminUserId` | — (off) | The Telegram user id that may use `/impersonate` (see [Impersonation](#impersonation)). Need not be on the allow-list. Must be positive | -| `Telegram:WorkspaceRoot` | `~/.volcanic/telegram-workspaces` | Chat `` works in `/`; put `AGENTS.md`, `PERSONA.md`, `MEMORY.md` and `.claude/skills/` there | +| `Telegram:WorkspaceRoot` | `~/.volcanic/telegram-workspaces` | Chat `` works in `/`; put `AGENTS.md`, `PERSONA.md`, `MEMORY.md` and `.volcanic/skills/` there | | `Telegram:Model` | `claude-code/claude-opus-5-5` | Any `provider/model` the Claude Code provider serves | | `Telegram:EditInterval` | `00:00:01.5` | Minimum time between edits of the "Working..." status message | | `Telegram:InputDebounce` | `00:00:00.6` | How long the bot waits after a message for more before sending them to the agent as one request. `0` disables the wait; messages still being read together (e.g. a photo still downloading and a text after it) are still sent as one request. Must not be negative | @@ -182,7 +182,7 @@ chat's workspace and delivers only to that chat. ### Skills Each chat can have its own [Agent Skills](https://docs.claude.com/en/docs/agents-and-tools/agent-skills/overview) in its -workspace: `/.claude/skills//SKILL.md`, a YAML frontmatter with `name` and `description` followed by +workspace: `/.volcanic/skills//SKILL.md`, a YAML frontmatter with `name` and `description` followed by the instructions, with scripts or reference files next to it if needed. The system prompt lists every skill's name, description and path, re-read on every run, so a skill the agent (or you) just wrote is used from the next message on, no `/new` needed. The model reads the full SKILL.md with `read` when a task matches. Skills without a diff --git a/src/Volcanic.AgenticCore/Features/SystemPrompt/SkillCatalog.cs b/src/Volcanic.AgenticCore/Features/SystemPrompt/SkillCatalog.cs index 3384eb7..270ee13 100644 --- a/src/Volcanic.AgenticCore/Features/SystemPrompt/SkillCatalog.cs +++ b/src/Volcanic.AgenticCore/Features/SystemPrompt/SkillCatalog.cs @@ -6,7 +6,7 @@ namespace Volcanic.AgenticCore.Features.SystemPrompt; public sealed record Skill(string Name, string Description, string Path); /// -/// Agent Skills (Anthropic's format) from the working directory only: <cwd>/.claude/skills/<dir>/SKILL.md, +/// Agent Skills (Anthropic's format) from the working directory only: <cwd>/.volcanic/skills/<dir>/SKILL.md, /// a YAML frontmatter with name and description, then the instructions. Scanned on every call, so a skill /// written by the agent shows up in the next run's system prompt. Skills without a description are skipped; the name /// defaults to the directory name. The model only sees the list and reads a SKILL.md itself (progressive disclosure). @@ -17,7 +17,7 @@ public static class SkillCatalog public const int MaxDescriptionLength = 1024; private const int MaxFrontmatterBytes = 16 * 1024; - public static string Directory(string cwd) => System.IO.Path.Combine(cwd, ".claude", "skills"); + public static string Directory(string cwd) => System.IO.Path.Combine(cwd, ".volcanic", "skills"); public static IReadOnlyList Load(string cwd) { diff --git a/tests/Volcanic.AgenticCore.Telegram.Tests/SkillsChatTests.cs b/tests/Volcanic.AgenticCore.Telegram.Tests/SkillsChatTests.cs index 2116fc2..3f98524 100644 --- a/tests/Volcanic.AgenticCore.Telegram.Tests/SkillsChatTests.cs +++ b/tests/Volcanic.AgenticCore.Telegram.Tests/SkillsChatTests.cs @@ -7,7 +7,7 @@ public sealed class SkillsChatTests { private static void WriteSkill(string workspace, string dir, string name, string description) { - var path = Path.Combine(workspace, ".claude", "skills", dir); + var path = Path.Combine(workspace, ".volcanic", "skills", dir); Directory.CreateDirectory(path); File.WriteAllText(Path.Combine(path, "SKILL.md"), $"---\nname: {name}\ndescription: {description}\n---\nInstructions"); } @@ -17,13 +17,13 @@ public sealed class SkillsChatTests { await using var host = new ChatHost(); await host.SendAsync("/skills"); - Assert.Equal("No skills yet. Ask me to write one, or put it in .claude/skills//SKILL.md.", host.LastReply()); + Assert.Equal("No skills yet. Ask me to write one, or put it in .volcanic/skills//SKILL.md.", host.LastReply()); WriteSkill(host.WorkspaceOf(), "trips", "trip-planner", "Plans trips"); WriteSkill(host.WorkspaceOf(2002), "secret", "other-chat", "Belongs to another chat"); await host.SendAsync("/skills"); - Assert.Equal("Skills (.claude/skills//SKILL.md)\n• trip-planner – Plans trips", host.LastReply()); + Assert.Equal("Skills (.volcanic/skills//SKILL.md)\n• trip-planner – Plans trips", host.LastReply()); } [Fact] @@ -42,7 +42,7 @@ public sealed class SkillsChatTests var prompt = host.Faux.Requests.Last().SystemPrompt!; Assert.Contains("trip-planner", prompt); - Assert.Contains(Path.Combine(host.WorkspaceOf(), ".claude", "skills", "trips", "SKILL.md"), prompt); + Assert.Contains(Path.Combine(host.WorkspaceOf(), ".volcanic", "skills", "trips", "SKILL.md"), prompt); Assert.DoesNotContain("other-chat", prompt); } } diff --git a/tests/Volcanic.AgenticCore.Tests/Features/PromptAndCompactionTests.cs b/tests/Volcanic.AgenticCore.Tests/Features/PromptAndCompactionTests.cs index 1585baa..7b309c0 100644 --- a/tests/Volcanic.AgenticCore.Tests/Features/PromptAndCompactionTests.cs +++ b/tests/Volcanic.AgenticCore.Tests/Features/PromptAndCompactionTests.cs @@ -100,7 +100,7 @@ public class SkillCatalogTests public void Lists_workspace_skills_and_picks_up_new_ones_on_the_next_render() { var dir = Path.Combine(Path.GetTempPath(), "volcanic-prompt", Guid.NewGuid().ToString("N")); - var skills = Path.Combine(dir, ".claude", "skills"); + var skills = Path.Combine(dir, ".volcanic", "skills"); Directory.CreateDirectory(Path.Combine(skills, "pdf")); Directory.CreateDirectory(Path.Combine(skills, "no-description")); Directory.CreateDirectory(Path.Combine(skills, "folded")); @@ -141,7 +141,7 @@ public class SkillCatalogTests { Assert.Empty(SkillCatalog.Load(dir)); var section = SkillCatalog.Render(dir, []); - Assert.Contains(Path.Combine(dir, ".claude", "skills"), section); + Assert.Contains(Path.Combine(dir, ".volcanic", "skills"), section); Assert.EndsWith("No skills yet.", section); } finally { Directory.Delete(dir, true); }