| 1 | package skill |
| 2 | |
| 3 | import ( |
| 4 | "sort" |
| 5 | "strings" |
| 6 | |
| 7 | "reasonix/internal/tool" |
| 8 | ) |
| 9 | |
| 10 | // Built-in skills ship with Reasonix and back the dedicated subagent tools |
| 11 | // (explore / research / review / security_review) plus inline playbooks such as |
| 12 | // test. A user/project file with the same name overrides the |
| 13 | // built-in (see Store.List / Store.Read). Tool names in the bodies match |
| 14 | // internal/tool/builtin. |
| 15 | |
| 16 | // negativeClaimRule keeps subagents honest about "found nothing" answers. |
| 17 | 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.` |
| 18 | |
| 19 | 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.` |
| 20 | |
| 21 | const builtinExploreBody = `You are a read-only exploration subagent. Answer the parent's question from the relevant code and report file:line evidence. |
| 22 | |
| 23 | Use LSP for definitions, references, and call hierarchy when available; code_index supplies outlines/definition candidates, not proof of all callers. Use content search for textual references and read the context needed to verify the mechanism. Let the question determine breadth and depth. |
| 24 | |
| 25 | Finish when the requested scope is supported by evidence. If an essential source is unavailable or further searches add no evidence, report the uncovered scope and what would resolve it. |
| 26 | |
| 27 | ` + negativeClaimRule |
| 28 | |
| 29 | const builtinResearchBody = `You are a read-only research subagent. Resolve the parent's question using repository code and primary external sources as needed. |
| 30 | |
| 31 | Verify external claims against the relevant version and local implementation. Use available search/fetch tools rather than assuming a fixed tool set. Distinguish source documentation from observed code behavior and cite both where relevant. |
| 32 | |
| 33 | Continue until the question is answered with evidence, or identify the concrete missing source or access that prevents an answer. Respect the task's time, cost, and permission limits. |
| 34 | |
| 35 | ` + negativeClaimRule |
| 36 | |
| 37 | 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. |
| 38 | |
| 39 | Operate as an installer, not as a shell-script guesser: |
| 40 | 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. |
| 41 | 2. Decide kind only when it is explicit. Use kind="auto" when unsure. |
| 42 | 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". |
| 43 | 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. |
| 44 | 5. Inspect the plan's actions. Each one carries a riskLevel: |
| 45 | - low → safe to apply without asking. |
| 46 | - medium → safe to apply, but mention what was written. |
| 47 | - 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. |
| 48 | 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. |
| 49 | 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. |
| 50 | |
| 51 | Defaults: |
| 52 | - 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. |
| 53 | - A folder containing many skills should be registered as a skill root, not copied. |
| 54 | - 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. |
| 55 | - 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. |
| 56 | - 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. |
| 57 | - Remote MCP URLs should use http unless the endpoint is explicitly SSE. |
| 58 | - Package-name MCP installs should default to npx -y <package>. |
| 59 | - Never put raw tokens in headers or config. Prefer ${VAR} placeholders and tell the user which env var to set. |
| 60 | |
| 61 | Uninstall (op=uninstall): |
| 62 | - Use op=uninstall with the same name and scope as the original install. Source is ignored. |
| 63 | - 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). |
| 64 | |
| 65 | 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.` |
| 66 | |
| 67 | const builtinReviewBody = `You are a read-only code-review subagent. Assess the working-tree diff or the range and files named by the parent. Verify supplied context against current code; inspect history only when needed to establish a regression or requested explicitly. |
| 68 | |
| 69 | Trace correctness, security, behavior changes, and missing regression coverage through affected callers and state owners. Use references/content search to establish impact; code_index alone cannot prove absence of callers. Report material findings, not cosmetic preferences. |
| 70 | |
| 71 | Cover the requested scope. If a concrete access or execution limit prevents coverage, name what remains unreviewed. Do not write files, commit, or present proposed fixes as applied. |
| 72 | |
| 73 | Return verdict, blocking_findings, non_blocking, and required_changes with file:line evidence and concise fix directions. |
| 74 | |
| 75 | ` + negativeClaimRule |
| 76 | |
| 77 | const builtinSecurityReviewBody = `You are a read-only security-review subagent. Review the named diff/range, or the current branch against its verified base, for exploitable regressions. |
| 78 | |
| 79 | Trace untrusted input and privileges through callers, validation, and sinks. Examine auth/authz, secrets, command execution, file paths, network egress, dependencies, and persistence where the diff affects them. Establish the threat, preconditions, impact, and existing mitigations before assigning severity. |
| 80 | |
| 81 | Cover the requested scope; report any concrete access or execution limit and the remaining coverage. Do not modify files or run destructive commands. General style and unrelated performance work are outside this review. |
| 82 | |
| 83 | Lead with the verdict and group findings by severity. Each finding needs file:line evidence, an exploitation scenario, and a repair direction. A clean report states what was checked without manufacturing findings. |
| 84 | |
| 85 | ` + negativeClaimRule |
| 86 | |
| 87 | const builtinTestBody = `Run and repair the tests requested by the user in the parent loop. Discover the project's actual test commands and relevant modules from its manifests, scripts, and instructions. Use isolated fixtures and honor existing session authorization. |
| 88 | |
| 89 | Diagnose each failure against the tested contract. Fix production defects in production code; correct invalid tests with an explanation. Never skip, delete, or weaken valid checks to manufacture green results. |
| 90 | |
| 91 | Re-run affected checks after changes and broaden for shared impact. Continue while new evidence supports a next repair; passing one package does not complete a broader requested suite. When repeated attempts produce no new evidence, revisit the root cause instead of blindly retrying. Finish independent work before reporting a concrete remaining blocker. |
| 92 | |
| 93 | Resolve environment issues within the authorized reversible scope. Ask only when a necessary dependency, configuration, credential, or external action falls outside that scope. Report commands, results, repairs, and any checks still blocked.` |
| 94 | |
| 95 | const builtinInitBody = `The user invoked /init to create or improve durable project instructions. |
| 96 | |
| 97 | Find existing AGENTS.md, REASONIX.md, or CLAUDE.md and follow their imports to the authoritative document. Preserve thin wrappers and unrelated user guidance; update the owning file rather than creating a competing instruction source. Use AGENTS.md for a new project with no existing instruction document. |
| 98 | |
| 99 | Inspect enough code, manifests, and scripts to establish non-obvious project contracts and verified commands. Keep always-loaded guidance concise: ownership boundaries, important defaults, safe local workflows, and concrete completion criteria. Link task-specific procedures only where they are needed. |
| 100 | |
| 101 | Do not prescribe a fixed section list, restate ordinary coding advice, invent conventions, copy secrets, or turn one past failure into a universal rule. Remove stale or duplicate instructions when supported by current evidence. Validate paths and commands that the document recommends, then summarize what changed.` |
| 102 | |
| 103 | // CodeGraphReadTools returns read-only tool names that look like an installed |
| 104 | // codegraph MCP surface. Writable or untrusted tools stay out of subagents. |
| 105 | func CodeGraphReadTools(reg *tool.Registry) []string { |
| 106 | if reg == nil { |
| 107 | return nil |
| 108 | } |
| 109 | var names []string |
| 110 | for _, name := range reg.Names() { |
| 111 | if !looksLikeCodeGraphTool(name) { |
| 112 | continue |
| 113 | } |
| 114 | tl, ok := reg.Get(name) |
| 115 | if !ok || !tl.ReadOnly() { |
| 116 | continue |
| 117 | } |
| 118 | names = append(names, name) |
| 119 | } |
| 120 | return normalizeExtraToolNames(names) |
| 121 | } |
| 122 | |
| 123 | func looksLikeCodeGraphTool(name string) bool { |
| 124 | return strings.HasPrefix(name, "codegraph_") || |
| 125 | strings.HasPrefix(name, tool.MCPNamePrefix+"codegraph__") |
| 126 | } |
| 127 | |
| 128 | func normalizeExtraToolNames(names []string) []string { |
| 129 | seen := map[string]bool{} |
| 130 | out := make([]string, 0, len(names)) |
| 131 | for _, name := range names { |
| 132 | name = strings.TrimSpace(name) |
| 133 | if name == "" || seen[name] { |
| 134 | continue |
| 135 | } |
| 136 | seen[name] = true |
| 137 | out = append(out, name) |
| 138 | } |
| 139 | sort.Strings(out) |
| 140 | return out |
| 141 | } |
| 142 | |
| 143 | func withOptionalCodeGraphHint(body string, enabled bool) string { |
| 144 | if !enabled { |
| 145 | return body |
| 146 | } |
| 147 | if strings.Contains(body, optionalCodeGraphHint) { |
| 148 | return body |
| 149 | } |
| 150 | return body + "\n\n" + optionalCodeGraphHint |
| 151 | } |
| 152 | |
| 153 | // WithCodeGraphTools enables user-installed codegraph MCP tools for built-in |
| 154 | // code-reading subagent skills. The caller passes names discovered from its live |
| 155 | // registry so desktop tabs/sessions never share mutable skill state. |
| 156 | func WithCodeGraphTools(sk Skill, names []string) Skill { |
| 157 | names = normalizeExtraToolNames(names) |
| 158 | if len(names) == 0 || sk.Scope != ScopeBuiltin || !codeReadingBuiltin(sk.Name) { |
| 159 | return sk |
| 160 | } |
| 161 | sk.AllowedTools = appendUniqueToolNames(sk.AllowedTools, names...) |
| 162 | sk.Body = withOptionalCodeGraphHint(sk.Body, true) |
| 163 | return sk |
| 164 | } |
| 165 | |
| 166 | func codeReadingBuiltin(name string) bool { |
| 167 | switch name { |
| 168 | case "explore", "research", "review", "security-review": |
| 169 | return true |
| 170 | default: |
| 171 | return false |
| 172 | } |
| 173 | } |
| 174 | |
| 175 | func appendUniqueToolNames(base []string, extra ...string) []string { |
| 176 | out := append([]string(nil), base...) |
| 177 | seen := make(map[string]bool, len(out)+len(extra)) |
| 178 | for _, name := range out { |
| 179 | seen[name] = true |
| 180 | } |
| 181 | for _, name := range extra { |
| 182 | if seen[name] { |
| 183 | continue |
| 184 | } |
| 185 | seen[name] = true |
| 186 | out = append(out, name) |
| 187 | } |
| 188 | return out |
| 189 | } |
| 190 | |
| 191 | // builtinSkills returns the shipped skills. A fresh slice each call so callers |
| 192 | // can't mutate the shared set. |
| 193 | func builtinSkills() []Skill { |
| 194 | readCodeTools := []string{"read_file", "ls", "glob", "grep", "code_index"} |
| 195 | // use_capability is the stable MCP proxy for strict review children: they |
| 196 | // never inherit direct mcp__* schemas, so the proxy must be allowlisted or |
| 197 | // review/security-review cannot discover authorized read-only MCP readers. |
| 198 | reviewTools := append(append([]string(nil), readCodeTools...), "bash", "use_capability") |
| 199 | base := []Skill{ |
| 200 | { |
| 201 | Name: "init", |
| 202 | Description: "Create or update concise project instructions from verified repository contracts and commands.", |
| 203 | Body: builtinInitBody, |
| 204 | Scope: ScopeBuiltin, |
| 205 | Path: "(builtin)", |
| 206 | RunAs: RunInline, |
| 207 | Triggers: []string{"agents.md", "initialize project", "bootstrap project", "初始化项目", "项目记忆", "生成 agents.md"}, |
| 208 | AutoUse: "suggest", |
| 209 | }, |
| 210 | { |
| 211 | Name: "explore", |
| 212 | Description: "Investigate repository structure or behavior in a read-only subagent.", |
| 213 | Body: builtinExploreBody, |
| 214 | Scope: ScopeBuiltin, |
| 215 | Path: "(builtin)", |
| 216 | RunAs: RunSubagent, |
| 217 | AllowedTools: append([]string(nil), readCodeTools...), |
| 218 | Triggers: []string{"how does", "find all", "architecture", "callers", "references", "impact analysis", "代码架构", "怎么实现", "如何实现", "调用链", "所有引用", "影响范围", "分析代码"}, |
| 219 | AutoUse: "suggest", |
| 220 | }, |
| 221 | { |
| 222 | Name: "research", |
| 223 | Description: "Research a technical question using code and primary sources in a read-only subagent.", |
| 224 | Body: builtinResearchBody, |
| 225 | Scope: ScopeBuiltin, |
| 226 | Path: "(builtin)", |
| 227 | RunAs: RunSubagent, |
| 228 | AllowedTools: append(append([]string(nil), readCodeTools...), "web_fetch"), |
| 229 | Triggers: []string{"canonical", "documentation", "specification", "compare against", "is supported", "official docs", "官方文档", "规范", "是否支持", "对比实现", "外部资料", "最新文档"}, |
| 230 | AutoUse: "suggest", |
| 231 | NeedsFreshData: true, |
| 232 | }, |
| 233 | { |
| 234 | Name: "install-capability", |
| 235 | Description: "Install or uninstall Reasonix skills and MCP servers through install_source plans.", |
| 236 | Body: builtinInstallCapabilityBody, |
| 237 | Scope: ScopeBuiltin, |
| 238 | Path: "(builtin)", |
| 239 | RunAs: RunInline, |
| 240 | Triggers: []string{"install skill", "install mcp", "install plugin", "安装 skill", "安装 mcp", "安装插件"}, |
| 241 | AutoUse: "suggest", |
| 242 | }, |
| 243 | { |
| 244 | Name: "review", |
| 245 | Description: "Review the requested code changes for correctness, security, and regression risks in a read-only subagent.", |
| 246 | Body: builtinReviewBody, |
| 247 | Scope: ScopeBuiltin, |
| 248 | Path: "(builtin)", |
| 249 | RunAs: RunSubagent, |
| 250 | AllowedTools: append([]string(nil), reviewTools...), |
| 251 | ReadOnly: true, |
| 252 | Triggers: []string{"review changes", "review diff", "code review", "评审变更", "审查代码", "检查改动"}, |
| 253 | AutoUse: "suggest", |
| 254 | }, |
| 255 | { |
| 256 | Name: "security-review", |
| 257 | Description: "Assess code changes for exploitable security regressions in a read-only subagent.", |
| 258 | Body: builtinSecurityReviewBody, |
| 259 | Scope: ScopeBuiltin, |
| 260 | Path: "(builtin)", |
| 261 | RunAs: RunSubagent, |
| 262 | AllowedTools: append([]string(nil), reviewTools...), |
| 263 | ReadOnly: true, |
| 264 | Triggers: []string{"security review", "authentication", "authorization", "token handling", "injection", "安全评审", "安全审查", "鉴权", "权限", "令牌", "注入", "漏洞"}, |
| 265 | AutoUse: "suggest", |
| 266 | }, |
| 267 | { |
| 268 | Name: "test", |
| 269 | Description: "Run requested tests, diagnose failures, and complete authorized repairs.", |
| 270 | Body: builtinTestBody, |
| 271 | Scope: ScopeBuiltin, |
| 272 | Path: "(builtin)", |
| 273 | RunAs: RunInline, |
| 274 | Triggers: []string{"run tests", "test failure", "failing test", "ci failure", "运行测试", "测试失败", "修复测试", "ci 失败"}, |
| 275 | AutoUse: "suggest", |
| 276 | }, |
| 277 | } |
| 278 | // Embedded directory skills (reasonix-guide, …) append after the const |
| 279 | // playbooks so the index order stays deterministic and bodies stay on-demand. |
| 280 | return append(base, loadEmbeddedBuiltins()...) |
| 281 | } |
| 282 | |
| 283 | // BuiltinNames returns the built-in skill names, used by callers that wire |
| 284 | // dedicated subagent tools for the subagent built-ins. |
| 285 | func BuiltinNames() []string { |
| 286 | skills := builtinSkills() |
| 287 | names := make([]string, len(skills)) |
| 288 | for i, s := range skills { |
| 289 | names[i] = s.Name |
| 290 | } |
| 291 | return names |
| 292 | } |
| 293 |