diff --git a/docs/changes/unreleased/1938-skill-manual-answers.md b/docs/changes/unreleased/1938-skill-manual-answers.md new file mode 100644 index 0000000000..1435c180ed --- /dev/null +++ b/docs/changes/unreleased/1938-skill-manual-answers.md @@ -0,0 +1,13 @@ +--- +kind: fixed +title: Skill questions reach a definition, capability answer and troubleshooting +pr: 1938 +surface: [chat, docs] +invalidates: + - "Asking what a skill is led with a folder list; the manual now returns a direct definition first." + - "The four original skill capability questions had no explicit retrieval regression coverage; they now check their answer sections and memory-independent behavior." +--- + +The manual explains how to choose a skill, why one may be unavailable, and that +turning automatic memory saving off does not disable skills. Retrieval and +live-terminal tests cover the four questions from issue #1937. diff --git a/internal/e2e/skill_manual_e2e_test.go b/internal/e2e/skill_manual_e2e_test.go new file mode 100644 index 0000000000..683a01df31 --- /dev/null +++ b/internal/e2e/skill_manual_e2e_test.go @@ -0,0 +1,66 @@ +//go:build e2e + +package e2e + +import ( + "fmt" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/Agent-Field/codeaf/internal/session" +) + +// Read the actual tool result from a fresh terminal conversation for each +// question: an answer or a tool-call argument alone cannot prove retrieval. +func TestSkillManualOnTheTerminal(t *testing.T) { + requireTmuxAndKey(t) + const model = "deepseek/deepseek-v4.1-flash" + if e2eModel != model { + t.Fatalf("requires CODEAF_E2E_MODEL=%s", model) + } + for _, memory := range []string{"on", "off"} { + for index, question := range []struct{ query, heading string }{ + {"what is a skill", "What is a skill"}, + {"can you use skills", "Can you use skills"}, + {"do you have skills", "Can you use skills"}, + {"why can't you use my skills", "Why can't you use my skills"}, + } { + t.Run(memory+"/"+question.query, func(t *testing.T) { + home := newHome(t, map[string]any{"memory.enabled": memory, "models.fallbacks": "", "model_pool": "off"}) + workspace := newWorkspace(t, "skill-manual", false) + stopRecoveryEngineOnCleanup(t, nil, home, workspace) + stage := fmt.Sprintf("skill-manual-%s-%d", memory, index) + r := startDefault(t, stage, home, workspace, 140, 42, + "chat", "--model", model, "--one-model", "--debug") + r.lit(fmt.Sprintf("Look up this question with the manual tool, setting query to exactly %q, then answer it briefly in your own words. Explain whether automatic memory saving being off disables skills. Finish with SKILL_MANUAL_CHECK_DONE. Do not change files.", question.query)) + r.keys("Enter") + cleanChatWaitAnswer(t, r, "SKILL_MANUAL_CHECK_DONE", 2*time.Minute) + places, err := filepath.Glob(filepath.Join(home, "v3", "projects", "*", "*")) + if err != nil || len(places) != 1 { + t.Fatalf("want one conversation place, got %v: %v", places, err) + } + matched := false + for _, call := range manualCalls(t, session.Place{Dir: places[0]}) { + if call.Query != question.query { + continue + } + for at, section := range renderedSections(call.Output) { + if strings.HasPrefix(section.Title, question.heading) && strings.Contains(section.Body, "memory.enabled") { + matched = true + if index == 0 && at != 0 { + t.Error("the definition was not the first returned section") + } + } + } + } + if !matched { + t.Fatal("the exact manual query did not deliver its answer and memory independence to the model") + } + cleanChatCapture(t, r, stage) + cleanChatAuditModels(t, home, model, stage) + }) + } + } +} diff --git a/internal/manual/chat/skills-from-other-tools.md b/internal/manual/chat/skills-from-other-tools.md index c88ca202d5..3f77f51959 100644 --- a/internal/manual/chat/skills-from-other-tools.md +++ b/internal/manual/chat/skills-from-other-tools.md @@ -1,19 +1,27 @@ # Skills from Claude Code, Codex, OpenCode, Goose and other tools -## Can you use my Claude Code, Codex, OpenCode or Goose skills - -Yes, directly. Any skill you already installed for Claude Code, Codex, Cursor, -Gemini, OpenCode, Goose or another agentskills.io tool — a folder holding a `SKILL.md` whose -frontmatter has a `name` and a `description` — is read where it lives. Nothing -is copied or reinstalled. Each launch reads the folders again before the first -message, so a skill you add or edit shows up the next time you open codeaf. - -Once found, a skill is used two ways. Automatically: the model is shown every -skill's name and what it is for, and when a request fits one it opens it with -`use_skill` and follows it, even when your words share none with the skill's -description. A message whose words do match a skill's description also carries -that skill with it, and a dim `skills ·` line in the turn names it. By hand: `/skill` puts one in front of the conversation until you take it -off. +## What is a skill — skills in codeaf + +A skill is a reusable set of instructions for a kind of work, such as reviewing +code or extracting PDF tables. Its folder holds a `SKILL.md` with a name, +description and instructions, plus any scripts or references it needs. + +Type `/skill` to choose a skill for this conversation, or `/skill ` to +attach one. The model can also open a relevant skill through `use_skill`. +Skills work with automatic memory saving off: `memory.enabled` does not turn +skills off. + +## Can you use skills — do you have skills from other tools + +Yes. codeaf can use skills installed for Claude Code, Codex, Cursor, Gemini, +OpenCode, Goose and other agentskills.io tools. A skill folder holds a `SKILL.md` +with a name and description; it is read in place, never copied or reinstalled. +New and edited skills are discovered at the next launch. + +Use `/skill` to choose a skill for this conversation. The model can also use +`use_skill` to open one that fits your request. Matching words in its description +carry the skill automatically, and the dim `skills ·` line names it. Automatic +memory saving can be off: `memory.enabled` does not disable skills. ## Which folders are read diff --git a/internal/manual/chat/use-skill.md b/internal/manual/chat/use-skill.md index c6cd16139f..df23cc81b7 100644 --- a/internal/manual/chat/use-skill.md +++ b/internal/manual/chat/use-skill.md @@ -50,6 +50,24 @@ and `/skill` still attaches them. The procedures codeaf saved after watching them run live in that database, so without it only the skills in folders are on the shelf. +## Why can't you use my skills + +Skills work with automatic memory saving off: `memory.enabled` does not disable +them. If a skill is missing, open `/skill` and check its row for a warning, or +type `/skill ` to attach it directly. The folder needs a readable +`SKILL.md` with a name and description. A missing file or invalid frontmatter +can keep it off the shelf. After adding or editing a skill in a scanned folder, +open a new conversation so discovery reads it again. + +For a Claude Code plugin skill, the plugin must be installed and enabled in +Claude Code; browsing a marketplace is not installation. If two skills share a +name, the higher-precedence copy wins. The page `skills-from-other-tools` +explains those folders and precedence rules. + +A message with a picture does not automatically carry skill instructions, even +for an attached skill. Send the picture, then send the instruction in a message +of its own; the model can still open the skill with `use_skill`. + ## Where the shelf comes from Every launch reads the skill folders before the first message is built and diff --git a/internal/manual/chat_test.go b/internal/manual/chat_test.go index 24b6f6229d..471f65bbe4 100644 --- a/internal/manual/chat_test.go +++ b/internal/manual/chat_test.go @@ -4439,6 +4439,38 @@ func TestTheCrossComputerQuestionsReachTheFactThatAnswersThem(t *testing.T) { } } +// These are the four original capability questions from #1378 and #1937. +func TestSkillCapabilityQuestionsReachTheirAnswer(t *testing.T) { + for _, row := range []struct { + question, page, heading string + facts []string + }{ + {"what is a skill", "skills-from-other-tools", "What is a skill", []string{"SKILL.md", "/skill"}}, + {"can you use skills", "skills-from-other-tools", "Can you use skills", []string{"Yes", "SKILL.md", "/skill"}}, + {"do you have skills", "skills-from-other-tools", "Can you use skills", []string{"Yes", "SKILL.md", "/skill"}}, + {"why can't you use my skills", "use-skill", "Why can't you use my skills", []string{"memory.enabled", "SKILL.md", "/skill"}}, + } { + t.Run(row.question, func(t *testing.T) { + found := Chat().Search(row.question, DefaultResults) + for index, section := range found { + if section.Page != row.page || !strings.HasPrefix(section.Title, row.heading) { + continue + } + if row.question == "what is a skill" && index != 0 { + t.Errorf("the definition is answer %d, want the first answer", index+1) + } + for _, fact := range row.facts { + if !strings.Contains(section.Body, fact) { + t.Errorf("answer does not explain %q", fact) + } + } + return + } + t.Fatalf("question did not reach its %q answer in %d results", row.heading, DefaultResults) + }) + } +} + // N4 pins all shared refusals quoted by the Windows manual sections. func TestWindowsManualQuotesTheShellRefusal(t *testing.T) { page, ok := Chat().Page("running-commands-on-windows")