# Slash Command Generation for Claude Code and Gemini CLI

**Date:** 2026-03-16
**Status:** Approved

## Summary

Add a CLI command `harness generate-slash-commands` that reads skill metadata (`skill.yaml` + `SKILL.md`) and generates native slash commands for Claude Code (markdown) and Gemini CLI (TOML). Commands are namespaced as `harness:<skill>` (e.g., `/harness:execution`). Generated commands use MCP `run_skill` when available, falling back to inline SKILL.md expansion when it's not.

## Architecture: Two-Phase Generator

### Phase 1 — Normalize

Read `skill.yaml` + `SKILL.md` into an intermediate `SlashCommandSpec`:

```typescript
interface SkillArg {
  name: string;
  description: string;
  required: boolean;
}

interface SlashCommandSpec {
  name: string; // e.g., "execution" (see Name Normalization below)
  namespace: string; // "harness"
  fullName: string; // "harness:execution"
  description: string; // from skill.yaml
  version: string;
  cognitiveMode?: string; // optional — omit context line when absent
  tools: string[]; // allowed tools from skill.yaml
  args: SkillArg[]; // from skill.yaml -> cli.args, defaults to [] when cli is absent
  skillYamlName: string; // original skill.yaml name, used for MCP invocation

  prompt: {
    context: string; // skill metadata, cognitive mode, state info
    objective: string; // from skill.yaml description + phases
    executionContext: string; // @file references to SKILL.md, skill.yaml
    process: string; // MCP-first invocation with SKILL.md fallback
  };
}
```

**Discovery:** Glob `agents/skills/claude-code/*/skill.yaml` (or user-specified path).

**Filter:** Only process skills where `platforms` includes the target platform(s).

**Transform:**

- `name`: see Name Normalization below
- `namespace`: always `harness`
- `tools`: pass through from skill.yaml, always include `Read` (needed for SKILL.md fallback)
- `args`: sourced from `skill.yaml -> cli.args`, defaults to `[]` when `cli` is not defined
- `cognitiveMode`: from `skill.yaml -> cognitive_mode` (optional — omit the "Cognitive mode" line in context block when absent)
- `skillYamlName`: the original `name` from skill.yaml, used verbatim in MCP `run_skill` invocations
- `prompt.context`: generated from cognitive mode (if present), type (rigid/flexible), state persistence info
- `prompt.objective`: skill description + phase names/descriptions
- `prompt.executionContext`: `@` references to the skill's SKILL.md and skill.yaml paths (see Path Resolution)
- `prompt.process`: templated MCP-first logic using `skillYamlName` for the MCP tool call, with args from `mcp.input` keys

### Name Normalization

The `name` field (used as the command name and filename) is derived from the skill.yaml `name` field:

1. Strip leading `harness-` prefix if present: `harness-execution` → `execution`
2. Strip `harness` from interior positions when it adds no meaning: `initialize-harness-project` → `initialize-project`
3. All other names pass through unchanged: `cleanup-dead-code` → `cleanup-dead-code`

This produces commands like `/harness:execution`, `/harness:initialize-project`, `/harness:cleanup-dead-code`.

**Collision detection:** After normalization, if two skills map to the same name, error with a message naming both source skills and their normalized name.

### Argument Hint Derivation

The `argument-hint` in Claude Code frontmatter is built from `args: SkillArg[]`:

- Required args: `--<name> <<name>>`
- Optional args: `[--<name> <<name>>]`
- Joined with spaces: `"--target <target> [--path <path>]"`
- Empty string when `args` is empty (omit the field entirely)

### Phase 2 — Render

Two render functions, each producing platform-specific file content.

Both renderers prepend a generated-file header so the sync logic can distinguish generated files from hand-authored ones:

- Claude Code: `<!-- Generated by harness generate-slash-commands. Do not edit. -->`
- Gemini CLI: `# Generated by harness generate-slash-commands. Do not edit.`

**`renderClaudeCode(spec): string`** — Markdown with YAML frontmatter:

```markdown
## <!-- Generated by harness generate-slash-commands. Do not edit. -->

name: harness:execution
description: Execute implementation plans with state tracking
argument-hint: "[--path <path>]"
allowed-tools:

- Read
- Bash
- Write
- Edit

---

<context>
Cognitive mode: meticulous-implementer
Type: rigid
State: persistent (files: .harness/state.json, .harness/learnings.md)
</context>

<objective>
Execute implementation plans with structured phases...
</objective>

<execution_context>
@agents/skills/claude-code/harness-execution/SKILL.md
@agents/skills/claude-code/harness-execution/skill.yaml
</execution_context>

<process>
1. Try: invoke mcp__harness__run_skill with skill: "harness-execution"
2. If MCP unavailable: read SKILL.md and follow its workflow directly
3. Pass through any arguments provided by the user
</process>
```

**`renderGemini(spec): string`** — TOML:

Gemini CLI does not natively expand `@file` references in TOML prompt fields. The Gemini renderer handles this by inlining the SKILL.md content directly into the prompt's `<execution_context>` block, wrapped in a labeled section so the agent knows its source. The skill.yaml content is also inlined as YAML.

```toml
# Generated by harness generate-slash-commands. Do not edit.
description = "Execute implementation plans with state tracking"
prompt = """
<context>
Cognitive mode: meticulous-implementer
Type: rigid
State: persistent (files: .harness/state.json, .harness/learnings.md)
</context>

<objective>
Execute implementation plans with structured phases...
</objective>

<execution_context>
--- SKILL.md (agents/skills/claude-code/harness-execution/SKILL.md) ---
[inlined SKILL.md content]

--- skill.yaml (agents/skills/claude-code/harness-execution/skill.yaml) ---
[inlined skill.yaml content]
</execution_context>

<process>
1. Try: invoke mcp__harness__run_skill with skill: "harness-execution"
2. If MCP unavailable: follow the SKILL.md workflow provided above directly
3. Pass through any arguments provided by the user
</process>
"""
```

The Claude Code renderer uses `@file` references (natively supported). The Gemini renderer inlines the content. Both achieve the same result — the agent has access to the full skill definition at invocation time.

## CLI Command

```
harness generate-slash-commands [options]

Options:
  --platforms <list>     claude-code,gemini-cli (default: both)
  --global               Write to ~/.claude/commands/ and ~/.gemini/commands/
  --output <dir>         Custom output directory
  --skills-dir <path>    Skills directory to scan (default: agents/skills/claude-code)
  --dry-run              Show what would change without writing files
  --yes                  Skip deletion confirmation prompts
  --json                 Output results as JSON (inherited from global CLI options)
```

### Output Directory Structure

**Project-local (default):**

```
agents/commands/
  claude-code/harness/
    execution.md
    debugging.md
    code-review.md
    ...
  gemini-cli/harness/
    execution.toml
    debugging.toml
    code-review.toml
    ...
```

**Global (`--global`):**

```
~/.claude/commands/harness/
  execution.md
  ...
~/.gemini/commands/harness/
  execution.toml
  ...
```

### Sync Behavior (Incremental with Cleanup)

1. Generate all specs from skill.yaml files
2. Compare against existing files in the output directory using **string equality** of rendered output vs. existing file content
3. Report a summary:

   ```
   Slash commands sync:
     + 3 new:     harness:brainstorming, harness:onboarding, harness:diagnostics
     ~ 2 updated: harness:execution, harness:code-review
     - 1 removed: harness:old-skill

   Remove 1 orphaned command? (y/N)
   ```

4. Write new/updated files, delete orphans only after confirmation (or with `--yes`)

## Codebase Location

**New files in `packages/cli/src/`:**

```
packages/cli/src/
  slash-commands/
    types.ts              # SlashCommandSpec interface, Platform type
    normalize.ts          # skill.yaml + SKILL.md → SlashCommandSpec[]
    render-claude-code.ts # spec → markdown string
    render-gemini.ts      # spec → TOML string
    sync.ts               # diff existing files, report changes, write/delete
  commands/
    generate-slash-commands.ts  # CLI command wiring
```

**Modifications to existing files:**

- `packages/cli/src/index.ts` — register the new command
- `packages/mcp-server/src/server.ts` — expose as `generate_slash_commands` MCP tool

## Edge Cases and Error Handling

**Skill validation:**

- Skip skills missing required fields (`name`, `description`, `platforms`) with a warning
- Skip skills where `platforms` doesn't include the target platform
- Warn on skills with no `tools` array (generates with empty `allowed-tools`)

**Path resolution:**

- Project-local: `@` references use paths relative to project root
- Global (`--global`): `@` references use absolute paths (SKILL.md lives in the project, not in `~/.claude/`)

**Conflict handling:**

- Content changes in previously generated files are treated as updates (no manual override tracking)
- Collision detection is handled during name normalization (see Name Normalization above)

**Missing SKILL.md:**

- Generate the command without `execution_context` file references (Claude Code) or inlined content (Gemini); log a warning

**MCP tool derivation:**

- The MCP tool name in the process template is derived from `skill.yaml -> mcp.tool` (typically `run_skill`)
- The MCP input keys are derived from `skill.yaml -> mcp.input` (e.g., `{ skill: "harness-execution", path: string }`)
- If `mcp` is not defined in skill.yaml, the process section omits the MCP-first step and uses SKILL.md directly

## Testing

- **Unit — normalize:** skill.yaml → spec mapping, prefix stripping, platform filtering
- **Unit — renderers:** spec → expected output string for each platform
- **Unit — sync:** additions, updates, deletions detection
- **Integration:** end-to-end from fixture skill.yaml to generated output files
