380 lines
24 KiB
Go
380 lines
24 KiB
Go
package skill
|
|
|
|
import (
|
|
"sort"
|
|
"strings"
|
|
|
|
"reasonix/internal/tool"
|
|
)
|
|
|
|
// Built-in skills ship with Reasonix and back the dedicated subagent tools
|
|
// (explore / research / review / security_review) plus inline playbooks such as
|
|
// test. A user/project file with the same name overrides the
|
|
// built-in (see Store.List / Store.Read). Tool names in the bodies match
|
|
// internal/tool/builtin.
|
|
|
|
// negativeClaimRule keeps subagents honest about "found nothing" answers.
|
|
const negativeClaimRule = `When you claim something does NOT exist (no caller, no usage, not implemented), say which searches you ran to reach that conclusion — a negative claim is only as trustworthy as the search behind it.`
|
|
|
|
// tuiFormatting nudges concise, terminal-friendly output.
|
|
const tuiFormatting = `Keep the final answer compact and terminal-friendly: short paragraphs or bullets, no walls of text, no restating the question.`
|
|
|
|
const optionalCodeGraphHint = `Optional installed code graph MCP tools are available in this session. Choose the semantic tool that fits the task: use LSP for language semantics (definitions, references, hover, diagnostics), use code graph tools first for call graph, impact analysis, and architecture relationships, use code_index only as the built-in outline/definition-candidate fallback, and verify textual or negative claims with read_file or grep.`
|
|
|
|
const builtinExploreBody = `You are running as an exploration subagent. Investigate the codebase the parent pointed you at, then return one focused, distilled answer.
|
|
|
|
How to operate:
|
|
- For code intelligence, choose the best semantic tool for the task. Prefer LSP for language semantics (definitions, references, hover, diagnostics). If LSP is unavailable or insufficient, use code_index for file outlines and symbol definition candidates, then verify important claims with read_file or grep. Stay read-only.
|
|
- For "how does X work" / architecture questions, start with the strongest available structure tool, then read the key files in full.
|
|
- For "find all places that call / reference / use X" questions: use LSP references when available or ` + "`grep`" + ` (content search) — NOT ` + "`glob`" + ` (which only matches file names). code_index finds definitions/candidates, not full textual references.
|
|
- Cast a wide net first (LSP/code_index for symbols, grep for references, ls/glob for structure) to map the territory; then read the 3-10 most relevant files in full.
|
|
- Don't read every file — be selective. Breadth on the first pass, depth only where the question demands it.
|
|
- Stop exploring as soon as you can answer. The parent doesn't see your tool calls, so over-exploration is pure waste.
|
|
|
|
Your final answer:
|
|
- One paragraph (or a few short bullets). Lead with the conclusion.
|
|
- Cite specific file paths + line ranges when they support the answer.
|
|
- If the question can't be answered from what you found, say so plainly and suggest where to look next.
|
|
|
|
` + negativeClaimRule + `
|
|
|
|
` + tuiFormatting + `
|
|
|
|
The 'task' the parent gave you is the question you must answer. Treat any other reading of it as scope creep.`
|
|
|
|
const builtinResearchBody = `You are running as a research subagent. Gather information from code AND the web, synthesize it, and return one focused conclusion.
|
|
|
|
How to operate:
|
|
- Combine code reading (LSP for language semantics; code_index as the local symbol fallback; read_file, grep, glob for verification) with web_fetch as appropriate. (There is no dedicated web-search tool — fetch the canonical doc/spec URL directly when you know it.)
|
|
- For "how does X work" questions: use symbol/reference lookup first when available; otherwise use code_index, then read_file for full context.
|
|
- For "is Y supported" questions: fetch the canonical reference, then verify against the local code.
|
|
- For "what's our policy on Z" / "where do we use Q": local code first, web only to compare against external standards.
|
|
- Cap yourself at ~10 tool calls. If you can't converge, return what you have plus a note on what's missing.
|
|
|
|
Your final answer:
|
|
- One paragraph (or short bullets). Lead with the conclusion.
|
|
- Cite both code (file:line) AND web sources (URL) when they back the answer.
|
|
- Distinguish "I verified this in code" from "I read this on a docs page" — the parent trusts the former more.
|
|
- If the answer is uncertain, say so. Don't invent confidence.
|
|
|
|
` + negativeClaimRule + `
|
|
|
|
` + tuiFormatting + `
|
|
|
|
The 'task' the parent gave you is the research question. Stay on it.`
|
|
|
|
const builtinInstallCapabilityBody = `This skill is INLINED. Use it when the user asks to install a Reasonix MCP server or skill from a URL, local file, local folder, .mcp.json, or package name. For removing a previously installed skill or MCP server, follow the "Uninstall" rules at the bottom — same tool, different op.
|
|
|
|
Operate as an installer, not as a shell-script guesser:
|
|
1. Extract the source string exactly from the user's request. It may be an https URL, GitHub URL, local path, .mcp.json, executable path, or npm package name.
|
|
2. Decide kind only when it is explicit. Use kind="auto" when unsure.
|
|
3. First call install_source with apply=false. Include scope when the user says project/global. Include mode when they say copy/link/register; otherwise leave mode="auto".
|
|
4. Read the returned plan. If status is blocked or failed, report the concrete next step. Do not invent a command from a README when the tool could not identify a manifest.
|
|
5. Inspect the plan's actions. Each one carries a riskLevel:
|
|
- low → safe to apply without asking.
|
|
- medium → safe to apply, but mention what was written.
|
|
- high → ask the user to confirm in one short question before apply=true. High actions include MCP installs that send auth headers, eager-tier servers, link targets that are absolute paths outside the project/home root, and any replace=true on an existing entry.
|
|
6. If the plan is acceptable and any needed user confirmation has happened, call install_source again with apply=true and echo back the same planId you got from the planning call. The tool refuses to apply when the planId does not match, so always re-fetch by running apply=false again if the user changed their mind about the source. Host permissions may still deny the apply call.
|
|
7. After apply=true, report what was installed, where it was persisted, and whether it is usable in the current session. For skills, prefer actions[].canonicalPath, actions[].installRoot, actions[].discoverable, and actions[].indexed over guessing from the source path. The plan's kinds field tells you how many skills vs MCP servers were touched.
|
|
|
|
Defaults:
|
|
- MCP installs default to global so the server is available in every project; use scope="project" only for project-specific servers, tokens, or commands. A project-root .mcp.json import stays project-scoped by default.
|
|
- A folder containing many skills should be registered as a skill root, not copied.
|
|
- A single SKILL.md, <name>.md, or <name>/SKILL.md should be copied unless the user asked to link/register. The installer writes canonical <skill-name>/SKILL.md paths by default; flat <name>.md is compatibility input, not the preferred output.
|
|
- A local SKILL.md source may have references/, scripts/, assets/, or other sibling files. Treat its parent directory as the skill package so those files remain available after install.
|
|
- Local skill folders may contain grouped skills up to a bounded depth. Let install_source decide which roots to register instead of telling the user to manually split every nested folder first.
|
|
- Remote MCP URLs should use http unless the endpoint is explicitly SSE.
|
|
- Package-name MCP installs should default to npx -y <package>.
|
|
- Never put raw tokens in headers or config. Prefer ${VAR} placeholders and tell the user which env var to set.
|
|
|
|
Uninstall (op=uninstall):
|
|
- Use op=uninstall with the same name and scope as the original install. Source is ignored.
|
|
- Skill and MCP server matching happen in the chosen scope's active config; if you don't know where the entry lives, ask the user. Removal is destructive but symmetric with a previously approved install, so it is applied directly (no approval step).
|
|
|
|
Stop rather than guessing when the source is only a documentation page, README without a manifest, or a repo whose install command cannot be determined.`
|
|
|
|
const builtinReviewBody = `You are running as a code-review subagent. Inspect the changes the user is about to ship — usually the current git branch vs its upstream — and produce a focused review the parent can hand back.
|
|
|
|
How to operate:
|
|
- Default scope: the current branch's diff vs the default branch. If the task names a specific commit range or files, honor that instead.
|
|
- Discover scope first: ` + "`bash git status`" + `, ` + "`git diff --stat`" + `, ` + "`git log --oneline`" + `. Then ` + "`git diff`" + ` (or ` + "`git diff <base>...HEAD`" + `) for the hunks.
|
|
- Read touched files (read_file) when the diff alone lacks context — signatures, surrounding invariants, callers.
|
|
- For "any callers depending on this?" questions: use LSP references/call hierarchy when available or grep the symbol BEFORE asserting impact. Use code_index only to find definition candidates/outline, not as proof of no callers.
|
|
- Stay read-only. Never commit, never write files, never propose edits as applied changes. The parent decides whether to act.
|
|
- Cap yourself at ~12 tool calls. If the diff is too big, pick the riskiest 2-3 files and say so.
|
|
|
|
What to look for, in priority order:
|
|
1. Correctness bugs — off-by-one, nil handling, races, wrong operator, unhandled edge cases.
|
|
2. Security — injection (SQL, shell, path traversal), secrets, missing authz, unsafe deserialization.
|
|
3. Behavior changes the diff hides — renames missing callers, removed load-bearing branches, error-handling that now swallows what used to surface.
|
|
4. Tests — does the change have tests for the new behavior? Are existing tests still meaningful?
|
|
5. Style + consistency — only flag deviations that matter; don't pile on cosmetic nits if the substance is clean.
|
|
|
|
Your final answer:
|
|
- Lead with a one-sentence verdict: "ship as-is" / "minor nits, OK to ship after" / "blocking issues, do not ship".
|
|
- Then a short bulleted list, each with file:line + the problem in one sentence + what to change.
|
|
- Group by severity if more than 4 items: Blocking, Should-fix, Nits.
|
|
- If everything looks clean, say so plainly. Don't manufacture concerns.
|
|
|
|
` + negativeClaimRule + `
|
|
|
|
` + tuiFormatting + `
|
|
|
|
The 'task' names WHAT to review (a branch, a file set, or "the pending changes"). Stay on it; don't redesign the feature.`
|
|
|
|
const builtinSecurityReviewBody = `You are running as a security-review subagent. Inspect the changes the user is about to ship — usually the current git branch vs its upstream — through a security lens specifically, and report exploitable issues.
|
|
|
|
How to operate:
|
|
- Default scope: the current branch's diff vs the default branch. Honor a named range or directory if given.
|
|
- Discover scope first: ` + "`bash git status`" + `, ` + "`git diff --stat`" + `, ` + "`git diff <base>...HEAD`" + `. Read touched files (read_file) when the diff lacks security context — auth checks, input validation, the handler that calls the changed code.
|
|
- Use LSP references/call hierarchy when available or grep to verify "is this user-controlled input ever sanitized later?" / "what other call sites depend on this validation?" before asserting impact. Use code_index only to find definition candidates/outline, not as proof of no callers.
|
|
- Stay read-only. Never write, never run destructive commands. The parent decides what to act on.
|
|
- Cap yourself at ~12 tool calls. If the diff is too big, focus on the riskiest 2-3 files and say so.
|
|
|
|
Threat model — flag with severity:
|
|
|
|
CRITICAL (do-not-ship): SQL/NoSQL/shell/template injection; path traversal; missing authn/authz; hardcoded secrets; deserialization of untrusted input; cryptographic mistakes (homemade crypto, MD5/SHA-1 for passwords, ECB, predictable nonces).
|
|
HIGH: XSS; SSRF; TOCTOU on auth/file checks; open redirects.
|
|
MEDIUM: verbose errors leaking internals; missing rate limiting on credential endpoints; missing cookie flags (Secure/HttpOnly/SameSite).
|
|
|
|
Out of scope here (regular review covers them): style, naming, performance, non-security test gaps, "extract this helper".
|
|
|
|
Your final answer:
|
|
- Lead with a one-sentence verdict: "no security issues found", "minor concerns", or "blocking issues".
|
|
- Then a list grouped by severity. Each item: file:line + 1-sentence threat + 1-sentence fix direction.
|
|
- If clean, say so plainly. Don't manufacture findings.
|
|
|
|
` + negativeClaimRule + `
|
|
|
|
` + tuiFormatting + `
|
|
|
|
The 'task' names what to review. Stay on it; don't redesign the feature.`
|
|
|
|
const builtinTestBody = `This skill is INLINED — you run in the parent loop. The user asked you to run the tests and fix failures. Run the project's test suite, diagnose any failure, propose and apply fixes, then re-run. Repeat until green or you hit a wall worth escalating.
|
|
|
|
How to operate:
|
|
1. Detect the test command. Look at the project: go.mod → ` + "`go test ./...`" + `; package.json scripts.test → ` + "`npm test`" + ` (or pnpm/yarn); pyproject.toml/requirements.txt → ` + "`pytest`" + `; Cargo.toml → ` + "`cargo test`" + `. If you can't tell, ASK — don't guess.
|
|
2. Run it via bash. Capture stdout + stderr; for intentionally long-running commands, start them in the background and use wait/bash_output.
|
|
3. Read the failures: which tests failed, the actual error, the file + line that threw. Locate the exact assertion or stack frame.
|
|
4. Fix each distinct failure:
|
|
- Production bug (test caught a real defect) → fix the production code.
|
|
- Test bug (test is wrong, code is right) → fix the test, and say so explicitly.
|
|
- Environmental (missing dep, wrong toolchain, missing fixture) → say so and stop; don't install packages or change config without checking.
|
|
5. Apply the edit and re-run. Iterate.
|
|
6. Stop conditions: all green → report what changed; same test still failing after 2 attempts on the same line → STOP and explain; 3+ unrelated failures → fix one at a time, smallest first.
|
|
|
|
Don't: install/update dependencies without asking; skip/delete/disable failing tests to force green; edit the test runner config to silence failures.
|
|
|
|
Lead each turn with a one-line status (e.g. "▸ running go test ./… ", "▸ 2 failures in foo_test.go — first is …") so the user always knows where you are.`
|
|
|
|
const builtinInitBody = `This skill is INLINED — you run in the parent loop. The user invoked /init: bootstrap (or refresh) this project's AGENTS.md — the durable memory file folded into every future session. Analyze the codebase, then write a concise, high-signal AGENTS.md.
|
|
|
|
How to operate:
|
|
1. Check for an existing memory doc first: list the project root and look for AGENTS.md / REASONIX.md / CLAUDE.md. If one exists, read it and IMPROVE it in place (fix stale facts, fill gaps) — write back to that same filename, don't clobber it wholesale or create a second file.
|
|
2. Explore enough to be accurate, not exhaustive:
|
|
- Project shape: ls / directory listing, the manifest (go.mod, package.json, pyproject.toml, Cargo.toml, …), the README.
|
|
- Build / test / run commands: derive them from the manifest + scripts and verify the exact names — don't guess.
|
|
- Architecture: the main packages/modules and how they fit; the entry point(s).
|
|
- Conventions: formatting, naming, error handling, testing patterns — infer from real code (read a few representative files), not assumptions.
|
|
3. Write AGENTS.md with write_file (default filename AGENTS.md, unless an existing doc uses another name), each section terse:
|
|
- Title + one-line description of the project.
|
|
- ## Project — what it is, the stack, where the entry point lives.
|
|
- ## Commands — the exact build / test / run / lint commands.
|
|
- ## Architecture — the 3-7 load-bearing modules and their roles.
|
|
- ## Conventions — only rules an agent must follow (style, patterns, do/don't).
|
|
- ## Notes — leave an empty stub for later quick-adds.
|
|
4. Keep it tight — it loads into every session's prompt, so every line costs context. Prefer specifics (file paths, command names) over prose. Never include secrets.
|
|
|
|
Rules:
|
|
- Verify commands and paths against the actual files before writing them — a wrong build command is worse than none.
|
|
- Don't fabricate conventions the code doesn't demonstrate.
|
|
- After writing, summarize in one or two lines what you captured and tell the user to review and edit it.`
|
|
|
|
// CodeGraphReadTools returns read-only tool names that look like an installed
|
|
// codegraph MCP surface. Writable or untrusted tools stay out of subagents.
|
|
func CodeGraphReadTools(reg *tool.Registry) []string {
|
|
if reg == nil {
|
|
return nil
|
|
}
|
|
var names []string
|
|
for _, name := range reg.Names() {
|
|
if !looksLikeCodeGraphTool(name) {
|
|
continue
|
|
}
|
|
tl, ok := reg.Get(name)
|
|
if !ok || !tl.ReadOnly() {
|
|
continue
|
|
}
|
|
names = append(names, name)
|
|
}
|
|
return normalizeExtraToolNames(names)
|
|
}
|
|
|
|
func looksLikeCodeGraphTool(name string) bool {
|
|
return strings.HasPrefix(name, "codegraph_") ||
|
|
strings.HasPrefix(name, tool.MCPNamePrefix+"codegraph__")
|
|
}
|
|
|
|
func normalizeExtraToolNames(names []string) []string {
|
|
seen := map[string]bool{}
|
|
out := make([]string, 0, len(names))
|
|
for _, name := range names {
|
|
name = strings.TrimSpace(name)
|
|
if name == "" || seen[name] {
|
|
continue
|
|
}
|
|
seen[name] = true
|
|
out = append(out, name)
|
|
}
|
|
sort.Strings(out)
|
|
return out
|
|
}
|
|
|
|
func withOptionalCodeGraphHint(body string, enabled bool) string {
|
|
if !enabled {
|
|
return body
|
|
}
|
|
if strings.Contains(body, optionalCodeGraphHint) {
|
|
return body
|
|
}
|
|
return body + "\n\n" + optionalCodeGraphHint
|
|
}
|
|
|
|
// WithCodeGraphTools enables user-installed codegraph MCP tools for built-in
|
|
// code-reading subagent skills. The caller passes names discovered from its live
|
|
// registry so desktop tabs/sessions never share mutable skill state.
|
|
func WithCodeGraphTools(sk Skill, names []string) Skill {
|
|
names = normalizeExtraToolNames(names)
|
|
if len(names) == 0 || sk.Scope != ScopeBuiltin || !codeReadingBuiltin(sk.Name) {
|
|
return sk
|
|
}
|
|
sk.AllowedTools = appendUniqueToolNames(sk.AllowedTools, names...)
|
|
sk.Body = withOptionalCodeGraphHint(sk.Body, true)
|
|
return sk
|
|
}
|
|
|
|
func codeReadingBuiltin(name string) bool {
|
|
switch name {
|
|
case "explore", "research", "review", "security-review":
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func appendUniqueToolNames(base []string, extra ...string) []string {
|
|
out := append([]string(nil), base...)
|
|
seen := make(map[string]bool, len(out)+len(extra))
|
|
for _, name := range out {
|
|
seen[name] = true
|
|
}
|
|
for _, name := range extra {
|
|
if seen[name] {
|
|
continue
|
|
}
|
|
seen[name] = true
|
|
out = append(out, name)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// builtinSkills returns the shipped skills. A fresh slice each call so callers
|
|
// can't mutate the shared set.
|
|
func builtinSkills() []Skill {
|
|
readCodeTools := []string{"read_file", "ls", "glob", "grep", "code_index"}
|
|
reviewTools := append(append([]string(nil), readCodeTools...), "bash")
|
|
base := []Skill{
|
|
{
|
|
Name: "init",
|
|
Description: "Bootstrap or refresh this project's AGENTS.md — analyze the codebase (structure, build/test commands, architecture, conventions) and write a concise memory file loaded into every future session. Inlined — runs in the main loop so you see and approve the write.",
|
|
Body: builtinInitBody,
|
|
Scope: ScopeBuiltin,
|
|
Path: "(builtin)",
|
|
RunAs: RunInline,
|
|
Triggers: []string{"agents.md", "initialize project", "bootstrap project", "初始化项目", "项目记忆", "生成 agents.md"},
|
|
AutoUse: "suggest",
|
|
},
|
|
{
|
|
Name: "explore",
|
|
Description: "Explore the codebase in an isolated subagent — wide-net read-only investigation that returns one distilled answer. Best for: 'find all places that...', 'how does X work across the project', 'survey the code for Y'.",
|
|
Body: builtinExploreBody,
|
|
Scope: ScopeBuiltin,
|
|
Path: "(builtin)",
|
|
RunAs: RunSubagent,
|
|
AllowedTools: append([]string(nil), readCodeTools...),
|
|
Triggers: []string{"how does", "find all", "architecture", "callers", "references", "impact analysis", "代码架构", "怎么实现", "如何实现", "调用链", "所有引用", "影响范围", "分析代码"},
|
|
AutoUse: "suggest",
|
|
},
|
|
{
|
|
Name: "research",
|
|
Description: "Research a question by combining web_fetch + code reading in an isolated subagent. Best for: 'is X supported by lib Y', 'what's the canonical way to do Z', 'compare our impl against the spec'.",
|
|
Body: builtinResearchBody,
|
|
Scope: ScopeBuiltin,
|
|
Path: "(builtin)",
|
|
RunAs: RunSubagent,
|
|
AllowedTools: append(append([]string(nil), readCodeTools...), "web_fetch"),
|
|
Triggers: []string{"canonical", "documentation", "specification", "compare against", "is supported", "official docs", "官方文档", "规范", "是否支持", "对比实现", "外部资料", "最新文档"},
|
|
AutoUse: "suggest",
|
|
NeedsFreshData: true,
|
|
},
|
|
{
|
|
Name: "install-capability",
|
|
Description: "Install or uninstall Reasonix MCP servers and skills from a URL, GitHub/raw file, local path/folder, .mcp.json, executable, or package name. Plans with install_source (op=install or op=uninstall) before applying, surfacing per-action riskLevel.",
|
|
Body: builtinInstallCapabilityBody,
|
|
Scope: ScopeBuiltin,
|
|
Path: "(builtin)",
|
|
RunAs: RunInline,
|
|
Triggers: []string{"install skill", "install mcp", "install plugin", "安装 skill", "安装 mcp", "安装插件"},
|
|
AutoUse: "suggest",
|
|
},
|
|
{
|
|
Name: "review",
|
|
Description: "Review the pending changes (current branch diff by default) in an isolated subagent — flags correctness, security, missing tests, hidden behavior changes; reports a verdict + per-issue file:line. Read-only.",
|
|
Body: builtinReviewBody,
|
|
Scope: ScopeBuiltin,
|
|
Path: "(builtin)",
|
|
RunAs: RunSubagent,
|
|
AllowedTools: append([]string(nil), reviewTools...),
|
|
ReadOnly: true,
|
|
Triggers: []string{"review changes", "review diff", "code review", "评审变更", "审查代码", "检查改动"},
|
|
AutoUse: "suggest",
|
|
},
|
|
{
|
|
Name: "security-review",
|
|
Description: "Security-focused review of the current branch diff in an isolated subagent — flags injection/authz/secrets/deserialization/path-traversal/crypto issues, severity-tagged. Read-only.",
|
|
Body: builtinSecurityReviewBody,
|
|
Scope: ScopeBuiltin,
|
|
Path: "(builtin)",
|
|
RunAs: RunSubagent,
|
|
AllowedTools: append([]string(nil), reviewTools...),
|
|
ReadOnly: true,
|
|
Triggers: []string{"security review", "authentication", "authorization", "token handling", "injection", "安全评审", "安全审查", "鉴权", "权限", "令牌", "注入", "漏洞"},
|
|
AutoUse: "suggest",
|
|
},
|
|
{
|
|
Name: "test",
|
|
Description: "Run the project's test suite, diagnose failures, propose+apply fixes, re-run until green (or stop after 2 attempts on the same failure). Inlined — runs in the parent loop. Detects go/npm/pnpm/yarn/pytest/cargo.",
|
|
Body: builtinTestBody,
|
|
Scope: ScopeBuiltin,
|
|
Path: "(builtin)",
|
|
RunAs: RunInline,
|
|
Triggers: []string{"run tests", "test failure", "failing test", "ci failure", "运行测试", "测试失败", "修复测试", "ci 失败"},
|
|
AutoUse: "suggest",
|
|
},
|
|
}
|
|
// Embedded directory skills (reasonix-guide, …) append after the const
|
|
// playbooks so the index order stays deterministic and bodies stay on-demand.
|
|
return append(base, loadEmbeddedBuiltins()...)
|
|
}
|
|
|
|
// BuiltinNames returns the built-in skill names, used by callers that wire
|
|
// dedicated subagent tools for the subagent built-ins.
|
|
func BuiltinNames() []string {
|
|
skills := builtinSkills()
|
|
names := make([]string, len(skills))
|
|
for i, s := range skills {
|
|
names[i] = s.Name
|
|
}
|
|
return names
|
|
}
|