feat(commands): add handoffs support for speckit compatibility (#410)

* feat(commands): add handoffs support for speckit compatibility

- Upgrade frontmatter parser to use js-yaml for complex YAML support
- Add HandoffDefinition interface for speckit-style workflow transitions
- Update CommandFrontmatter and CommandDefinition to include handoffs
- Add comprehensive tests for backward compatibility and complex YAML
- Fix type parameters in auto-slash-command and slashcommand tools

Closes #407

* fix(frontmatter): use JSON_SCHEMA for security and add extra fields tolerance tests

- Use JSON_SCHEMA in yaml.load() to prevent code execution via YAML tags
- Add tests to verify extra fields in frontmatter don't cause failures
- Address Greptile security review comment

---------

Co-authored-by: sisyphus-dev-ai <sisyphus-dev-ai@users.noreply.github.com>
This commit is contained in:
Sisyphus
2026-01-02 15:11:14 +09:00
committed by GitHub
parent 6eaa96f421
commit 99711dacc1
7 changed files with 302 additions and 22 deletions

View File

@@ -78,6 +78,7 @@ $ARGUMENTS
model: sanitizeModelField(data.model, isOpencodeSource ? "opencode" : "claude-code"),
subtask: data.subtask,
argumentHint: data["argument-hint"],
handoffs: data.handoffs,
}
commands.push({

View File

@@ -1,5 +1,21 @@
export type CommandScope = "user" | "project" | "opencode" | "opencode-project"
/**
* Handoff definition for command workflows.
* Based on speckit's handoff pattern for multi-agent orchestration.
* @see https://github.com/github/spec-kit
*/
export interface HandoffDefinition {
/** Human-readable label for the handoff action */
label: string
/** Target agent/command identifier (e.g., "speckit.tasks") */
agent: string
/** Pre-filled prompt text for the handoff */
prompt: string
/** If true, automatically executes after command completion; if false, shows as suggestion */
send?: boolean
}
export interface CommandDefinition {
name: string
description?: string
@@ -8,6 +24,8 @@ export interface CommandDefinition {
model?: string
subtask?: boolean
argumentHint?: string
/** Handoff definitions for workflow transitions */
handoffs?: HandoffDefinition[]
}
export interface CommandFrontmatter {
@@ -16,6 +34,8 @@ export interface CommandFrontmatter {
agent?: string
model?: string
subtask?: boolean
/** Handoff definitions for workflow transitions */
handoffs?: HandoffDefinition[]
}
export interface LoadedCommand {

View File

@@ -134,7 +134,7 @@ Skill with env vars.
})
it("handles malformed YAML gracefully", async () => {
// #given
// #given - malformed YAML causes entire frontmatter to fail parsing
const skillContent = `---
name: bad-yaml
mcp: [this is not valid yaml for mcp
@@ -150,9 +150,9 @@ Skill body.
try {
const skills = discoverSkills({ includeClaudeCodePaths: false })
const skill = skills.find(s => s.name === "bad-yaml")
// #then - when YAML fails, skill uses directory name as fallback
const skill = skills.find(s => s.name === "bad-yaml-skill")
// #then - should still load skill but without MCP config
expect(skill).toBeDefined()
expect(skill?.mcpConfig).toBeUndefined()
} finally {