This document describes the fest plugin system for external commands and the extension system for methodology packs.
Fest supports two types of extensibility:
| System | Purpose | Discovery | Format |
|---|---|---|---|
| Plugins | External CLI commands | PATH + manifest |
Executables (fest-*) |
| Extensions | Methodology packs | .festival/extensions/ |
Directories with extension.yml |
Plugins are external executables that extend fest with new commands.
Plugins are discovered from three sources (in priority order):
- User manifest:
~/.config/fest-repos/<active>/plugins/manifest.yml - User bin directory:
~/.config/fest-repos/<active>/plugins/bin/ - System PATH: Any executable matching
fest-*
Plugin executables must be named with the fest- prefix:
fest-<group>-<name> # Two-part command
fest-<name> # Single command
Examples:
fest-export-jira # Command: "export jira"
fest-import-confluence # Command: "import confluence"
fest-stats # Command: "stats"
The manifest file (manifest.yml) provides metadata for plugins:
version: 1
plugins:
- command: "export jira"
exec: "fest-export-jira"
summary: "Export festival to Jira"
description: |
Export festival phases and tasks to Jira issues.
Creates epics for phases and stories for sequences.
when_to_use:
- "Syncing work to issue tracker"
- "Generating Jira backlog from festival"
examples:
- "fest export jira --project PROJ"
- "fest export jira --project PROJ --dry-run"
version: "1.2.0"
- command: "import confluence"
exec: "fest-import-confluence"
summary: "Import requirements from Confluence"
description: "Parse Confluence pages and generate task files"
version: "1.0.0"| Field | Required | Description |
|---|---|---|
command |
Yes | The fest subcommand (e.g., "export jira") |
exec |
Yes | Executable filename |
summary |
Yes | One-line description |
description |
No | Full description |
when_to_use |
No | Usage hints for AI agents |
examples |
No | Example command invocations |
version |
No | Plugin version |
#!/bin/bash
# fest-export-jira
echo "Exporting to Jira..."
# Implementation hereOr in Go:
package main
import (
"fmt"
"os"
)
func main() {
// args[0] = executable name
// args[1:] = user-provided arguments
args := os.Args[1:]
fmt.Printf("Exporting to Jira with args: %v\n", args)
// Implementation
}chmod +x fest-export-jiraOption A: Add to PATH
mv fest-export-jira /usr/local/bin/Option B: Add to config repo
mv fest-export-jira ~/.config/fest-repos/default/plugins/bin/# ~/.config/fest-repos/default/plugins/manifest.yml
version: 1
plugins:
- command: "export jira"
exec: "fest-export-jira"
summary: "Export festival to Jira"When fest receives an unknown command, it checks for plugins:
// Internal dispatch logic
discovery := plugins.NewPluginDiscovery()
discovery.DiscoverAll()
plugin := discovery.FindByArgs(os.Args[1:])
if plugin != nil {
// Dispatch to external executable
return dispatch.Run(plugin, remainingArgs)
}fest understand plugins # List all discovered pluginsExtensions are methodology packs that provide templates, agents, or workflow configurations.
Extensions are loaded from three locations (in priority order):
| Priority | Location | Source Name |
|---|---|---|
| 1 (highest) | .festival/extensions/ (project) |
project |
| 2 | User config repo .festival/extensions/ |
user |
| 3 (lowest) | ~/.config/fest/festivals/.festival/extensions/ |
built-in |
Higher priority sources override extensions with the same name.
my-extension/
├── extension.yml # Manifest (optional)
├── templates/ # Custom templates
│ ├── TASK_TEMPLATE.md
│ └── PHASE_GOAL_TEMPLATE.md
├── agents/ # AI agent prompts
│ └── reviewer.md
└── README.md # Documentation
The extension.yml file provides metadata:
name: api-workflow
version: 1.0.0
description: Templates and agents for API development
author: Team Backend
type: workflow
tags:
- api
- backend
- rest
files:
- path: templates/API_TASK_TEMPLATE.md
description: Task template for API endpoints
type: template
- path: agents/api-reviewer.md
description: API code review agent prompt
type: agent| Field | Required | Description |
|---|---|---|
name |
Yes | Extension identifier |
version |
No | Semantic version |
description |
No | Extension description |
author |
No | Author or team name |
type |
No | Extension type (workflow, template, agent) |
tags |
No | Searchable tags |
files |
No | File listing with metadata |
| Type | Purpose |
|---|---|
workflow |
Complete methodology pack |
template |
Custom templates only |
agent |
AI agent prompts |
config |
Configuration presets |
mkdir -p .festival/extensions/my-extension
cd .festival/extensions/my-extension# extension.yml
name: my-extension
version: 1.0.0
description: Custom templates for my team
author: My Team
type: template# templates/CUSTOM_TASK.md
---
template_id: CUSTOM_TASK
description: Custom task template
---
# Task: {{.TaskName}}
## Team-Specific Section
[FILL: Custom content here]
## Standard Sections
...fest understand extensions # List loaded extensions# List all extensions
fest understand extensions
# Output includes:
# - Extension name
# - Version
# - Description
# - Source (project/user/built-in)
# - File countimport "github.com/lancekrogers/festival-methodology/fest/internal/extensions"
loader := extensions.NewExtensionLoader()
loader.LoadAll("/path/to/festival")
// Get extension by name
ext := loader.Get("my-extension")
// List all extensions
all := loader.List()
// Filter by source
projectExts := loader.ListBySource("project")
// Filter by type
workflowExts := loader.ListByType("workflow")// Check if file exists in extension
if ext.HasFile("templates/TASK.md") {
path := ext.GetFile("templates/TASK.md")
// Use file
}
// List all files
files, _ := ext.ListFiles()import "github.com/lancekrogers/festival-methodology/fest/internal/plugins"
discovery := plugins.NewPluginDiscovery()
discovery.DiscoverAll()
// Get all plugins
all := discovery.Plugins()
// Find by command
plugin := discovery.FindByCommand("export jira")
// Find by CLI args
plugin := discovery.FindByArgs([]string{"export", "jira", "--project", "PROJ"})import "github.com/lancekrogers/festival-methodology/fest/internal/plugins"
// Run plugin with arguments
err := plugins.Dispatch(plugin, []string{"--project", "PROJ"})- Follow naming conventions: Use
fest-<group>-<name>format - Handle errors gracefully: Exit with non-zero codes on failure
- Support --help: Provide usage information
- Use manifest: Include metadata for better discovery
- Version your plugins: Track compatibility
- Include manifest: Always provide
extension.yml - Use semantic versioning: Track changes properly
- Document files: Use the
filesarray in manifest - Test templates: Verify variable rendering
- Keep focused: One extension per concern
# Plugins
fest understand plugins # List discovered plugins
# Extensions
fest understand extensions # List loaded extensions
fest extension list # Alternative: list extensions
fest extension enable <name> # Enable an extension
fest extension disable <name> # Disable an extension