Back to list
cbgbt

script-driven-skill

by cbgbt

prototpe. attempt to get Claude to be more effective at working on Bottlerocket.

4🍴 8📅 Jan 16, 2026

SKILL.md


name: script-driven-skill description: Build multi-step skills using a state-machine pattern with dumb orchestrator and smart phases

Script-Driven Skill Pattern

A pattern for building reliable multi-step skills where a state machine controls flow and phases are self-contained instruction files.

Purpose

Separates concerns in complex skills:

  • Orchestrator: Dumb loop that spawns agents and reports results
  • State machine (next-step.py): Controls flow, validates gates, tracks progress
  • Phase files: Self-contained instructions that travel with subagents

When to Use

SituationUse This Pattern?
3+ phases with dependencies✅ Yes
Risk of skipping validation gates✅ Yes
Context bloat from reading all phases upfront✅ Yes
Simple 1-2 step procedure❌ No, use inline instructions
Phases need orchestrator judgment❌ No, orchestrator must stay dumb

Directory Structure

skills/<skill-name>/
├── SKILL.md              # Human-readable overview (you're reading one)
├── next-step.py          # State machine - returns JSON actions
├── progress.json         # Runtime state (created during execution)
└── phases/
    ├── SCOUT.md          # Phase 1 instructions
    ├── RESEARCH.md       # Phase 2 instructions
    └── ASSEMBLE.md       # Phase 3 instructions

The Orchestrator Loop

The orchestrator is intentionally minimal. It never reads phase files—just passes them to subagents.

import json

workspace = f"planning/{slug}"

while True:
    # Ask state machine what to do
    result = bash(f"python3 skills/{skill}/next-step.py {workspace}", on_error="raise")
    action = json.loads(result)
    
    if action["type"] == "done":
        break
    
    if action["type"] == "spawn":
        r = spawn(
            action["prompt"],
            context_files=action["context_files"],
            context_data=action.get("context_data"),
            allow_tools=True
        )
        # Write result for state machine to read
        write("create", f"{workspace}/{action['output_file']}", file_text=r.response)
    
    if action["type"] == "gate_failed":
        log(f"Gate failed: {action['reason']}")
        break

Key principle: Orchestrator has no knowledge of phases. It just executes actions.

Writing next-step.py

The state machine reads progress.json, checks gates, and returns JSON actions.

#!/usr/bin/env python3
import json
import sys
from pathlib import Path

def main():
    workspace = Path(sys.argv[1])
    progress_file = workspace / "progress.json"
    
    # Load or initialize state
    if progress_file.exists():
        state = json.loads(progress_file.read_text())
    else:
        state = {"phase": "scout", "completed": []}
    
    phase = state["phase"]
    
    # Phase: scout
    if phase == "scout":
        if "scout" not in state["completed"]:
            print(json.dumps({
                "type": "spawn",
                "prompt": "Execute the scout phase for this research task.",
                "context_files": ["skills/my-skill/phases/SCOUT.md"],
                "context_data": {"workspace": str(workspace)},
                "output_file": "00-scout.md"
            }))
            return
        # Gate: scout file must exist
        if not (workspace / "00-scout.md").exists():
            print(json.dumps({"type": "gate_failed", "reason": "Scout output missing"}))
            return
        state["phase"] = "research"
        state["completed"].append("scout")
    
    # Phase: research
    if phase == "research":
        # ... similar pattern
        pass
    
    # Done
    if phase == "done":
        print(json.dumps({"type": "done"}))
        return
    
    # Save state
    progress_file.write_text(json.dumps(state, indent=2))
    
    # Recurse to get next action
    main()

if __name__ == "__main__":
    main()

Action Types

TypeFieldsPurpose
spawnprompt, context_files, context_data, output_fileRun a subagent
gate_failedreasonStop execution, validation failed
done-Skill complete

Writing Phase Files

Phase files are self-contained instructions. The subagent receives ONLY this file (plus context_data).

# Scout Phase

You are executing the scout phase of a research task.

## Your Goal

Discover what exists and formulate sub-questions. Do NOT answer them yet.

## Inputs

- Workspace: `{{workspace}}` (from context_data)
- Question: Read from `{{workspace}}/question.txt`

## Procedure

1. Run broad searches:
   ```bash
   crumbly search "topic overview"
  1. Skim top 2-3 results for structure only

  2. Write findings to {{workspace}}/00-scout.md

Output Format

# Scout: <Question>

## Key Concepts
- [Concept]: [Brief note]

## Sub-Questions
1. [Question] - Type: fact-find
2. [Question] - Type: research

Completion

Call respond_to_leader("success", "Scout complete") when done.


**Key principles:**
- Self-contained: agent doesn't need other files
- Explicit inputs/outputs: what to read, what to write
- Clear completion signal

## Validation Gates

Gates prevent phase skipping. Check in `next-step.py` before advancing:

```python
# Gate: all sub-questions answered
scout = json.loads((workspace / "00-scout.md").read_text())
expected = len(scout["sub_questions"])
actual = len(list(workspace.glob("[0-9][0-9]-*.md"))) - 1  # exclude 00-scout
if actual < expected:
    print(json.dumps({
        "type": "gate_failed",
        "reason": f"Only {actual}/{expected} sub-questions answered"
    }))
    return

Example: Minimal 2-Phase Skill

skills/simple-research/
├── SKILL.md
├── next-step.py
└── phases/
    ├── GATHER.md
    └── SYNTHESIZE.md

next-step.py:

#!/usr/bin/env python3
import json, sys
from pathlib import Path

workspace = Path(sys.argv[1])
progress = workspace / "progress.json"
state = json.loads(progress.read_text()) if progress.exists() else {"phase": "gather"}

if state["phase"] == "gather":
    if not (workspace / "notes.md").exists():
        print(json.dumps({
            "type": "spawn",
            "prompt": "Gather information per the phase instructions.",
            "context_files": ["skills/simple-research/phases/GATHER.md"],
            "context_data": {"workspace": str(workspace)},
            "output_file": "notes.md"
        }))
    else:
        state["phase"] = "synthesize"
        progress.write_text(json.dumps(state))
        print(json.dumps({"type": "continue"}))

elif state["phase"] == "synthesize":
    if not (workspace / "FINAL.md").exists():
        print(json.dumps({
            "type": "spawn",
            "prompt": "Synthesize findings per the phase instructions.",
            "context_files": ["skills/simple-research/phases/SYNTHESIZE.md", f"{workspace}/notes.md"],
            "context_data": {"workspace": str(workspace)},
            "output_file": "FINAL.md"
        }))
    else:
        state["phase"] = "done"
        progress.write_text(json.dumps(state))
        print(json.dumps({"type": "done"}))

else:
    print(json.dumps({"type": "done"}))

Anti-Patterns

❌ Don't✅ Do
Orchestrator reads phase filesPass phase files via context_files
Orchestrator decides what phase is nextState machine decides
Skip gates "because it looks done"Always validate gates
Hardcode phase count in orchestratorState machine knows phases
Store state in orchestrator variablesStore state in progress.json

Benefits

  • Testable: Run next-step.py directly to verify state transitions
  • Resumable: progress.json survives context resets
  • Debuggable: Each phase's output is a file you can inspect
  • Maintainable: Change phases without touching orchestrator

Score

Total Score

50/100

Based on repository quality metrics

SKILL.md

SKILL.mdファイルが含まれている

+20
LICENSE

ライセンスが設定されている

0/10
説明文

100文字以上の説明がある

0/10
人気

GitHub Stars 100以上

0/15
最近の活動

3ヶ月以内に更新がある

0/10
フォーク

10回以上フォークされている

0/5
Issue管理

オープンIssueが50未満

+5
言語

プログラミング言語が設定されている

+5
タグ

1つ以上のタグが設定されている

0/5

Reviews

💬

Reviews coming soon