Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Changelog

## 0.9.0
- Add /release command to guide release workflow
- Replace code-release skill with ask-questions skill
- Show a toast when loading a skill

## 0.8.0
- Add pdf-to-markdown tool for PDF conversion

Expand Down
89 changes: 81 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<a href="https://www.npmjs.com/package/opencode-froggy"><img src="https://badge.fury.io/js/opencode-froggy.svg" alt="npm version"></a>
</p>

OpenCode plugin providing hooks, specialized agents (architect, doc-writer, rubber-duck, partner, code-reviewer, code-simplifier), skills (code-release), and tools (gitingest, pdf-to-markdown, blockchain queries, agent-promote).
OpenCode plugin providing hooks, specialized agents (architect, doc-writer, rubber-duck, partner, code-reviewer, code-simplifier), skills (ask-questions-if-underspecified), and tools (gitingest, pdf-to-markdown, blockchain queries, agent-promote).

---

Expand All @@ -17,7 +17,11 @@ OpenCode plugin providing hooks, specialized agents (architect, doc-writer, rubb
- [Commands](#commands)
- [Agents](#agents)
- [Skills](#skills)
- [code-release](#code-release)
- [Overview](#overview)
- [Available Skills](#available-skills)
- [Discovery Locations](#discovery-locations)
- [Creating a Skill](#creating-a-skill)
- [Automatic Activation](#automatic-activation)
- [Tools](#tools)
- [gitingest](#gitingest)
- [prompt-session](#prompt-session)
Expand Down Expand Up @@ -117,15 +121,84 @@ Shows stats overview, commits, files changed, and full diff between branches.

## Skills

Skills are loaded on-demand to provide specialized capabilities during a session.
Skills are contextual instructions loaded on demand via the `skill` tool. The agent invokes `skill({ name: "skill-name" })` to load the instructions when needed.

### code-release
### Overview

Prepare releases with version bumps, changelog updates, and tags.
- Skills provide specialized guidance for specific tasks
- Instructions are loaded only when explicitly requested
- Multiple skills can exist with the same name; the highest-priority location wins

- **Purpose**: Guide release preparation steps and require confirmation before changing release artifacts
- **Activation**: On user request to prepare or perform a release
- **Constraints**: Avoid changing versions, tags, or changelogs without explicit confirmation
### Available Skills

| Skill | Description |
|-------|-------------|
| `ask-questions-if-underspecified` | Clarify requirements before implementing. Use when serious doubts arise. |

### Discovery Locations

Skills are discovered from the following locations, in order of increasing priority:

| Priority | Scope | Location |
|----------|-------|----------|
| 1 (lowest) | plugin | `<plugin>/skill/` |
| 2 | global | `~/.config/opencode/skill/` |
| 3 (highest) | project | `<project>/.opencode/skill/` |

If multiple skills share the same name, the one from the highest-priority location takes precedence.

### Creating a Skill

Each skill lives in its own directory with a `SKILL.md` file:

```
skill/
└── my-skill/
└── SKILL.md
```

The `SKILL.md` file uses YAML frontmatter for metadata:

```markdown
---
name: my-skill
description: Short description of the skill (required)
use_when: >
Condition for automatic activation (optional).
---

# Detailed Instructions

Markdown content with step-by-step guidance...
```

#### Frontmatter Fields

| Field | Required | Description |
|-------|----------|-------------|
| `name` | Yes | Unique identifier for the skill |
| `description` | Yes | Short description (displayed in skill listings) |
| `use_when` | No | Condition for automatic activation |

A skill without `name` or `description` will be ignored.

### Automatic Activation

If a skill defines `use_when`, a directive is injected into the system prompt:

```
MANDATORY: Call skill({ name: "my-skill" }) <use_when content>
```

This instructs the agent to load the skill when the specified condition is met.

**What is injected:**
- The skill `name`
- The `use_when` text (normalized: multiple spaces collapsed to single space)

**What is NOT injected:**
- The `description`
- The markdown content (loaded only when the skill is invoked)

---

Expand Down
2 changes: 1 addition & 1 deletion command/commit-push.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ agent: build

## Your task

1. Run `/diff-summary` to analyze all working tree changes
1. /diff-summary to analyze all working tree changes
2. Present a summary to the user:
- Files modified/added/deleted with stats
- Proposed commit message based on the changes
Expand Down
23 changes: 12 additions & 11 deletions skill/code-release/SKILL.md → command/release.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
---
name: code-release
description: >
Prepare and execute a release with version bumping, changelog updates, and tags.
use_when: >
REQUIRED: When the user asks to prepare or perform a release,
call skill({ name: "code-release" }) before changing any release artifacts.
description: Prepare and execute a release with version bumping, changelog updates, and tags
---

# Code Release Skill
## Context

- Current version: !`node -p "require('./package.json').version"`
- Latest tag: !`git describe --tags --abbrev=0 2>/dev/null || echo "none"`
- Commits since last tag: !`git log $(git describe --tags --abbrev=0 2>/dev/null || echo "HEAD~10")..HEAD --oneline 2>/dev/null || git log --oneline -10`

## CRITICAL CONSTRAINT

Expand All @@ -17,13 +16,15 @@ Any destructive or remote action requires confirmation, including:
- `git tag`
- `git push`

## Step 1: Determine last released version
## Your task

### Step 1: Determine last released version

1. Prefer the latest Git tag that matches `v<semver>`.
2. If no matching tag exists, use the version in `package.json`.
3. Collect commits since the last version (tag to `HEAD`).

## Step 2: Propose version bump
### Step 2: Propose version bump

Analyze commits since the last version and recommend a semver bump:
- **major**: breaking changes or incompatible behavior changes.
Expand All @@ -32,14 +33,14 @@ Analyze commits since the last version and recommend a semver bump:

Present the recommendation and ask the user to confirm before changing any files.

## Step 3: Update release artifacts (after confirmation)
### Step 3: Update release artifacts (after confirmation)

- Update the version in `package.json`.
- Update `CHANGELOG` with a new section for the version.
- Summarize changes based on the commit range.
- Preserve the existing changelog format.

## Step 4: Tag and publish (after confirmation)
### Step 4: Tag and publish (after confirmation)

- Commit release changes with a clear release message.
- Create an annotated tag (for example, `vX.Y.Z`).
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "opencode-froggy",
"version": "0.8.0",
"version": "0.9.0",
"description": "OpenCode plugin with a hook layer (tool.before.*, session.idle...), agents (code-reviewer, doc-writer), and commands (/review-pr, /commit)",
"main": "dist/index.js",
"types": "dist/index.d.ts",
Expand Down
59 changes: 59 additions & 0 deletions skill/ask-questions-if-underspecified/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
---
name: ask-questions-if-underspecified
description: Clarify requirements before implementing. Use when serious doubts arise.
use_when: >
When a request has multiple plausible interpretations or key details
(objective, scope, constraints, environment, or safety) are unclear,
load this skill before starting implementation.
Do NOT use when the request is already clear or when a quick, low-risk
discovery read can answer the missing details.
---

# Ask Questions If Underspecified

## Goal

Ask the minimum set of clarifying questions needed to avoid wrong work; do not start implementing until the must-have questions are answered (or the user explicitly approves proceeding with stated assumptions).

## Workflow

### 1) Decide whether the request is underspecified

Treat a request as underspecified if after exploring how to perform the work, some or all of the following are not clear:
- Define the objective (what should change vs stay the same)
- Define "done" (acceptance criteria, examples, edge cases)
- Define scope (which files/components/users are in/out)
- Define constraints (compatibility, performance, style, deps, time)
- Identify environment (language/runtime versions, OS, build/test runner)
- Clarify safety/reversibility (data migration, rollout/rollback, risk)

If multiple plausible interpretations exist, assume it is underspecified.

### 2) Ask must-have questions first (keep it small)

Ask 1-5 questions in the first pass. Prefer questions that eliminate whole branches of work.

Use the `question` tool to present structured choices:
- Offer multiple-choice options with clear labels
- Mark recommended defaults with "(Recommended)" in the label
- Use `multiple: true` when the user can select several options
- The user can always select "Other" for custom input

### 3) Pause before acting

Until must-have answers arrive:
- Do not run commands, edit files, or produce a detailed plan that depends on unknowns
- Do perform a clearly labeled, low-risk discovery step only if it does not commit you to a direction (e.g., inspect repo structure, read relevant config files)

If the user explicitly asks you to proceed without answers:
- State your assumptions as a short numbered list
- Ask for confirmation; proceed only after they confirm or correct them

### 4) Confirm interpretation, then proceed

Once you have answers, restate the requirements in 1-3 sentences (including key constraints and what success looks like), then start work.

## Anti-patterns

- Don't ask questions you can answer with a quick, low-risk discovery read (e.g., configs, existing patterns, docs).
- Don't ask open-ended questions if a tight multiple-choice or yes/no would eliminate ambiguity faster.
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ const SmartfrogPlugin: Plugin = async (ctx) => {
const skillTool = createSkillTool({
pluginSkills: skills,
pluginDir: PLUGIN_ROOT,
client: ctx.client,
})

log("[init] Plugin loaded", {
Expand Down
20 changes: 19 additions & 1 deletion src/tools/skill.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ import { tool, type ToolContext } from "@opencode-ai/plugin"
import { existsSync, readdirSync, readFileSync } from "node:fs"
import { join, dirname } from "node:path"
import { homedir } from "node:os"
import type { createOpencodeClient } from "@opencode-ai/sdk"
import { parseFrontmatter, type LoadedSkill } from "../loaders"
import { log } from "../logger"

type Client = ReturnType<typeof createOpencodeClient>

export interface SkillInfo {
name: string
Expand Down Expand Up @@ -118,18 +122,20 @@ function loadSkillContent(location: string): string {
export interface CreateSkillToolOptions {
pluginSkills: LoadedSkill[]
pluginDir: string
client: Client
}

export function createSkillTool(options: CreateSkillToolOptions) {
let cachedSkills: SkillInfo[] | null = null
let cachedDescription: string | null = null
const { client, pluginDir, pluginSkills } = options

const getSkills = (cwd: string): SkillInfo[] => {
if (cachedSkills) return cachedSkills

// Merge order: plugin defaults < global < project (later entries override earlier on name collision)
const allSkills = [
...pluginSkillsToInfo(options.pluginSkills, options.pluginDir),
...pluginSkillsToInfo(pluginSkills, pluginDir),
...discoverClaudeGlobalSkills(),
...discoverOpencodeGlobalSkills(),
...discoverClaudeProjectSkills(cwd),
Expand Down Expand Up @@ -183,6 +189,18 @@ export function createSkillTool(options: CreateSkillToolOptions) {
const body = loadSkillContent(skill.location)
const dir = dirname(skill.location)

try {
await client.tui.showToast({
body: {
message: `Skill "${skill.name}" loaded`,
variant: "info",
duration: 3000,
},
})
} catch (error) {
log("[skill] Failed to show toast", { error: String(error) })
}

return [
`## Skill: ${skill.name}`,
"",
Expand Down