
Wave Executor
- 39 installs
- 36 repo stars
- Updated July 14, 2026
- oimiragieo/agent-studio
Helps with ai & agent building tasks.
About
wave-executor is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.
- wave-executor
- AI & Agent Building
- AI-coding skill
Wave Executor by the numbers
- 39 all-time installs (skills.sh)
- Ranked #8,302 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/oimiragieo/agent-studio --skill wave-executorAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 39 |
|---|---|
| repo stars | ★ 36 |
| Last updated | July 14, 2026 |
| Repository | oimiragieo/agent-studio ↗ |
What it does
Helps with ai & agent building tasks.
Files
Wave Executor
Overview
Wave Executor runs EPIC-tier batch pipelines by spawning a fresh Claude Code process per wave via the Claude Agent SDK. Each wave gets a clean Bun runtime with zero accumulated spawn() or abort_signal state, preventing the JSC garbage collector use-after-free crash (oven-sh/bun, anthropics/claude-code#21875, #27003) that occurs when a single Bun process handles thousands of concurrent subagent spawns.
This is the framework's implementation of the Ralph Wiggum pattern: iteration over fresh processes with file-based coordination.
When to Use
Use this skill when:
- EPIC-tier batch work: >10 artifacts, >5 waves
- Multi-wave skill updates, bundle generation, or mass refactoring
- Any pipeline expected to run >30 minutes with parallel subagents
- Work that previously crashed due to Bun segfaults
Do NOT use for:
- Simple 1-3 skill updates (use
skill-updaterdirectly) - Single-skill work (use
Task()subagent) - Work that fits in one context window (just do it inline)
How It Works
Router invokes wave-executor via Bash
│
└─ node .claude/tools/cli/wave-executor.mjs --plan <path>
│ (runs on system Node.js — NOT Bun)
│
├─ Reads plan.json with wave definitions
├─ Reads inventory.json for resume state
│
├─ For each pending wave:
│ ├─ SDK query() → NEW Bun process (fresh GC)
│ ├─ Claude executes wave tasks
│ ├─ Streams output to stdout
│ ├─ Bun process exits → memory freed
│ ├─ Updates inventory.json
│ └─ Sleeps → next wave
│
└─ Returns JSON summaryKey invariant: no single Bun process accumulates more than ~100 spawns.
Invocation
Via Bash (agents):
node .claude/tools/cli/wave-executor.mjs --plan <path> --jsonVia slash command (users):
/wave-executor --plan .claude/context/plans/my-plan.jsonCLI flags:
| Flag | Default | Description |
|---|---|---|
--plan <path> | required | Path to wave plan JSON |
--model <model> | claude-sonnet-4-6 | Model for wave execution |
--max-turns <n> | 50 | Max conversation turns per wave |
--start-from <n> | 1 | Resume from wave N |
--dry-run | false | Preview without executing |
--json | false | Machine-readable output |
Plan File Format
{
"name": "enterprise-bundle-generation",
"waves": [
{
"id": 1,
"skills": ["rust-expert", "python-backend-expert", "typescript-expert"],
"domain": "language",
"promptTemplate": "Update enterprise bundle files for skills: {skills}. Read each SKILL.md and .claude/rules/ file. Do 3-5 WebSearch queries for current {domain} tools and patterns. Generate domain-specific bundle files (append-only, never overwrite non-stubs). Validate JSON schemas and Node.js syntax. Commit results."
},
{
"id": 2,
"skills": ["nextjs-expert", "react-expert", "svelte-expert"],
"domain": "web-framework"
}
],
"config": {
"model": "claude-sonnet-4-6",
"maxTurnsPerWave": 50,
"sleepBetweenWaves": 3000,
"inventoryPath": ".claude/context/runtime/wave-inventory.json"
}
}Each wave must have id (number) and skills (non-empty array). Optional: domain, promptTemplate.
Inventory Tracking
The executor maintains an inventory file at the configured path (default .claude/context/runtime/wave-inventory.json). This enables:
- Resume from crash:
--start-from Npicks up where a failed run left off - Progress monitoring: read the inventory file to see completed waves
- Cost tracking: each wave records its cost
Integration with Router
The router should use this skill when the planner classifies work as EPIC-tier:
1. Planner creates a plan file with wave definitions 2. Router invokes: Skill({ skill: 'wave-executor' }) 3. Agent runs: node .claude/tools/cli/wave-executor.mjs --plan <path> --json 4. Router reads JSON result for success/failure
The router's Bun process stays idle during execution (single Bash call) — no subagent spawning, no hook accumulation.
Iron Laws
1. ALWAYS spawn each wave in a fresh Bun process to prevent GC-related crashes in long-running sessions 2. NEVER batch more concurrent waves than the configured MAX_PARALLEL_WAVES limit 3. ALWAYS await wave completion acknowledgment before spawning the next wave 4. NEVER proceed to the next wave if the current wave has any failed or incomplete agents 5. ALWAYS log wave metadata (wave number, agent count, duration) for pipeline observability
Anti-Patterns
| Anti-Pattern | Why It Fails | Correct Approach |
|---|---|---|
| Reusing the same process across waves | GC pressure causes crashes in long pipelines | Spawn a fresh Bun process per wave |
| Exceeding MAX_PARALLEL_WAVES | Resource exhaustion and flaky failures | Respect the configured concurrency limit |
| Starting next wave before current completes | Race conditions and incomplete pipeline state | Await wave completion signal before advancing |
| Ignoring failed agents in a wave | Partial state propagates incorrect data forward | Halt and surface failures before continuing |
| No wave metadata logging | Can't diagnose which wave caused issues | Log wave number, agents, and duration to context |
Memory Protocol (MANDATORY)
Before starting:
- Read
.claude/context/memory/learnings.mdfor prior wave execution learnings - Check inventory file for resume state
After completing:
- Append wave execution summary to
.claude/context/memory/learnings.md - Record any errors to
.claude/context/memory/issues.md - Record architecture decisions to
.claude/context/memory/decisions.md
ASSUME INTERRUPTION: Your context may reset. If it's not in memory, it didn't happen.
Invoke the wave-executor skill and follow it exactly as presented to you
#!/usr/bin/env node
'use strict';
/**
* wave-executor — Post-Execute Hook
* Logs wave execution summary to memory.
*/
const fs = require('node:fs');
const path = require('node:path');
const { safeParseJSON } = require('../../../lib/utils/safe-json.cjs');
function findProjectRoot() {
let dir = __dirname;
const root = path.parse(dir).root;
while (dir && dir !== root) {
if (fs.existsSync(path.join(dir, '.claude'))) return dir;
dir = path.dirname(dir);
}
return process.cwd();
}
function parseInput() {
const raw = process.argv.length > 2 ? process.argv.slice(2).join(' ') : '{}';
try {
return safeParseJSON(raw);
} catch {
return {};
}
}
const input = parseInput();
const projectRoot = findProjectRoot();
// Record execution in learnings
try {
const learningsPath = path.join(projectRoot, '.claude', 'context', 'memory', 'learnings.md');
if (fs.existsSync(learningsPath)) {
const timestamp = new Date().toISOString().slice(0, 10);
const wavesCompleted = input.wavesCompleted || 0;
const skillsProcessed = input.skillsProcessed || 0;
const totalCost = input.totalCost || 'N/A';
const entry = `\n## Wave Executor Run (${timestamp})\n- Waves completed: ${wavesCompleted}\n- Skills processed: ${skillsProcessed}\n- Cost: ${totalCost}\n`;
fs.appendFileSync(learningsPath, entry, 'utf8');
}
} catch {
// Non-fatal — memory logging is best-effort
}
console.log('[wave-executor] Post-execute: execution logged');
#!/usr/bin/env node
'use strict';
/**
* wave-executor — Pre-Execute Hook
* Validates that plan file is provided and the SDK is available.
*/
const fs = require('node:fs');
const path = require('node:path');
const { safeParseJSON } = require('../../../lib/utils/safe-json.cjs');
function parseInput() {
const raw = process.argv.length > 2 ? process.argv.slice(2).join(' ') : '{}';
try {
return safeParseJSON(raw);
} catch {
return {};
}
}
function findProjectRoot() {
let dir = __dirname;
const root = path.parse(dir).root;
while (dir && dir !== root) {
if (fs.existsSync(path.join(dir, '.claude'))) return dir;
dir = path.dirname(dir);
}
return process.cwd();
}
function validateInput(input) {
const errors = [];
const warnings = [];
if (input && typeof input !== 'object') {
errors.push('Input must be an object');
return { errors, warnings };
}
// Validate plan file
if (input.plan) {
const resolved = path.resolve(input.plan);
if (!fs.existsSync(resolved)) {
errors.push(`Plan file not found: ${resolved}`);
} else {
try {
const content = safeParseJSON(fs.readFileSync(resolved, 'utf8'));
if (!content.waves || !Array.isArray(content.waves)) {
errors.push('Plan file must contain a "waves" array');
} else if (content.waves.length === 0) {
errors.push('Plan file "waves" array is empty');
} else {
warnings.push(
`Plan loaded: ${content.waves.length} waves, ${content.waves.reduce((sum, w) => sum + (w.skills?.length || 0), 0)} total skills`
);
}
} catch (err) {
errors.push(`Invalid plan file: ${err.message}`);
}
}
}
// Check SDK availability
try {
require.resolve('@anthropic-ai/claude-agent-sdk');
} catch {
errors.push('Claude Agent SDK not installed. Run: pnpm add @anthropic-ai/claude-agent-sdk');
}
// Check CLI tool exists
const projectRoot = findProjectRoot();
const cliTool = path.join(projectRoot, '.claude', 'tools', 'cli', 'wave-executor.mjs');
if (!fs.existsSync(cliTool)) {
errors.push(`CLI tool not found: ${cliTool}`);
}
return { errors, warnings };
}
const input = parseInput();
const { errors, warnings } = validateInput(input);
if (warnings.length > 0) {
console.log('[wave-executor] Pre-execute:');
for (const w of warnings) console.log(` - ${w}`);
}
if (errors.length > 0) {
console.error('[wave-executor] Pre-execute validation failed:');
for (const e of errors) console.error(` - ${e}`);
process.exit(1);
}
console.log('[wave-executor] Pre-execute validation passed');
Wave Executor Research Requirements
Generated: 2026-02-19
Problem Statement
Bun's JSC garbage collector has a use-after-free race condition triggered by high spawn/abort_signal accumulation in long-running processes. This crashes Claude Code during EPIC-tier multi-agent orchestration.
Root Cause Research
- anthropics/claude-code#21875 — 78 documented crashes, root cause analysis with ProcDump/WinDbg
- anthropics/claude-code#27003 — Confirms Bun 1.3.10 still affected
- oven-sh/bun#26153 — JSC GC corruption in MarkedBlock sweep
- oven-sh/bun#26853 — Segfault at 0xFFFFFFFFFFFFFFFF on Windows x64
Solution Research
- Ralph Wiggum pattern: https://paddo.dev/blog/ralph-wiggum-autonomous-loops/
- Ralph loop quickstart: https://github.com/coleam00/ralph-loop-quickstart
- Claude Agent SDK sessions: https://platform.claude.com/docs/en/agent-sdk/sessions
- Claude Agent SDK TypeScript ref: https://platform.claude.com/docs/en/agent-sdk/typescript
Key Design Decisions
1. Use Claude Agent SDK query() instead of raw spawnSync('claude', ...) — typed messages, session management, streaming 2. Run wave-executor on system Node.js (not Bun) — the outer loop must not be subject to the same GC bug 3. File-based coordination (plan.json + inventory.json) instead of in-memory state — survives process death 4. Each query() call = new Bun subprocess = fresh GC state
Wave Executor Rules
When to Use
- EPIC-tier batch work only: >10 artifacts, >5 waves
- Multi-wave pipelines expected to run >30 minutes
- Any work that previously crashed due to Bun segfaults under heavy subagent load
When NOT to Use
- Simple 1-3 skill updates — use
skill-updaterdirectly - Single-skill work — use
Task()subagent - Work that fits in one context window — just do it inline
- Non-batch work (debugging, code review, single features)
Routing Decision
The router should prefer wave-executor over Task() subagent swarms when:
1. Planner classifies work as EPIC complexity 2. Plan has >5 waves defined 3. Previous attempt crashed with Bun segfault 4. Total expected spawns >500 (waves x skills x hooks)
Plan File Requirements
- Must be valid JSON with a
wavesarray - Each wave must have
id(number) andskills(non-empty array) - Create the plan file BEFORE invoking wave-executor
- Use the planner agent to generate the plan
Anti-Patterns
- Running wave-executor for a single wave (use skill-updater instead)
- Skipping the plan file and trying to pass tasks inline
- Setting maxTurnsPerWave too low (<10) — waves need room to work
- Setting maxTurnsPerWave too high (>100) — defeats the purpose of fresh processes
- Not checking the inventory file after a crash before resuming
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "wave-executor Input Schema",
"description": "Input contract for wave-executor skill — fresh-process batch orchestration",
"type": "object",
"required": ["plan"],
"properties": {
"plan": {
"type": "string",
"minLength": 1,
"description": "Path to wave plan JSON file containing wave definitions."
},
"model": {
"type": "string",
"default": "claude-sonnet-4-6",
"description": "Claude model to use for wave execution."
},
"maxTurnsPerWave": {
"type": "integer",
"default": 50,
"minimum": 5,
"maximum": 200,
"description": "Maximum conversation turns per wave before forcing completion."
},
"dryRun": {
"type": "boolean",
"default": false,
"description": "Preview plan without executing waves."
},
"startFromWave": {
"type": "integer",
"minimum": 1,
"description": "Resume execution from a specific wave ID."
}
},
"additionalProperties": false
}
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "wave-executor Output Schema",
"description": "Output contract for wave-executor skill execution",
"type": "object",
"required": ["success", "wavesCompleted", "wavesTotal"],
"properties": {
"success": {
"type": "boolean",
"description": "True when all waves completed without errors."
},
"wavesCompleted": {
"type": "integer",
"minimum": 0,
"description": "Number of waves successfully completed."
},
"wavesTotal": {
"type": "integer",
"minimum": 0,
"description": "Total number of waves in the plan."
},
"skillsProcessed": {
"type": "integer",
"minimum": 0,
"description": "Total number of skills processed across all waves."
},
"totalCost": {
"type": "string",
"description": "Total cost in USD format (e.g. '$4.20')."
},
"inventoryPath": {
"type": "string",
"description": "Path to the inventory JSON file for resume support."
},
"errors": {
"type": "array",
"items": { "type": "string" },
"description": "List of error messages from failed waves."
}
},
"additionalProperties": false
}
#!/usr/bin/env node
'use strict';
/**
* wave-executor — Skill Entry Point
* Validates inputs and delegates to the CLI tool.
*/
const fs = require('node:fs');
const path = require('node:path');
const { spawnSync } = require('node:child_process');
function findProjectRoot() {
let dir = __dirname;
while (dir !== path.parse(dir).root) {
if (fs.existsSync(path.join(dir, '.claude'))) return dir;
dir = path.dirname(dir);
}
return process.cwd();
}
function parseArgs(argv) {
const options = {};
for (let i = 0; i < argv.length; i++) {
if (!argv[i].startsWith('--')) continue;
const key = argv[i].slice(2);
const value = argv[i + 1] && !argv[i + 1].startsWith('--') ? argv[++i] : true;
options[key] = value;
}
return options;
}
function main() {
const options = parseArgs(process.argv.slice(2));
if (options.help) {
console.log(`
wave-executor — Fresh-process orchestration for EPIC-tier batch pipelines
Usage:
node main.cjs --plan <path> [--model <model>] [--dry-run] [--json]
node main.cjs --help
`);
process.exit(0);
}
const projectRoot = findProjectRoot();
const planPath = options.plan;
// Validate plan file exists
if (!planPath) {
console.error('Missing required --plan argument');
process.exit(1);
}
const resolvedPlan = path.resolve(planPath);
if (!fs.existsSync(resolvedPlan)) {
console.error(`Plan file not found: ${resolvedPlan}`);
process.exit(1);
}
// Build args for the CLI tool
const cliTool = path.join(projectRoot, '.claude', 'tools', 'cli', 'wave-executor.mjs');
const args = [cliTool, '--plan', resolvedPlan];
if (options.model) args.push('--model', options.model);
if (options['dry-run']) args.push('--dry-run');
if (options.json) args.push('--json');
if (options['start-from']) args.push('--start-from', options['start-from']);
if (options['max-turns']) args.push('--max-turns', options['max-turns']);
const result = spawnSync(process.execPath, args, {
cwd: projectRoot,
encoding: 'utf8',
stdio: 'inherit',
shell: false,
windowsHide: true,
});
process.exitCode = result.status || 0;
}
main();
Wave Plan Template
Use this template to create a wave plan JSON file for the wave-executor.
Plan Structure
{
"name": "<pipeline-name>",
"waves": [
{
"id": 1,
"skills": ["<skill-a>", "<skill-b>", "<skill-c>"],
"domain": "<domain-category>",
"promptTemplate": "<custom prompt with {skills} and {domain} placeholders>"
}
],
"config": {
"model": "claude-sonnet-4-6",
"maxTurnsPerWave": 50,
"sleepBetweenWaves": 3000,
"inventoryPath": ".claude/context/runtime/wave-inventory.json"
}
}Wave Design Guidelines
1. Group skills by domain (language, framework, devops, security, etc.) 2. Keep 2-4 skills per wave for manageable scope 3. Put dependent skills in later waves 4. Add a promptTemplate for domain-specific instructions 5. Use {skills}, {domain}, {waveId} placeholders in templates
Execution
# Preview
node .claude/tools/cli/wave-executor.mjs --plan <path> --dry-run
# Execute
node .claude/tools/cli/wave-executor.mjs --plan <path> --json
# Resume from wave 5 after a crash
node .claude/tools/cli/wave-executor.mjs --plan <path> --start-from 5 --json