feat(ce-demo-reel): add demo reel skill with Python capture pipeline (#541)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
---
|
||||
title: "Prefer Python over bash for multi-step pipeline scripts"
|
||||
date: 2026-04-09
|
||||
category: best-practices
|
||||
module: "skill scripting / ce-demo-reel"
|
||||
problem_type: best_practice
|
||||
component: tooling
|
||||
severity: medium
|
||||
applies_when:
|
||||
- Script orchestrates 2+ external CLI tools (ffmpeg, curl, silicon, vhs)
|
||||
- Script needs retry logic or graceful degradation on tool failure
|
||||
- Script will run on macOS where bash 3.2 is the default
|
||||
- Script needs to be tested from a non-shell test runner (Bun, Jest, pytest)
|
||||
- Script has conditional failure paths where some errors should be caught and others should abort
|
||||
tags:
|
||||
- bash-vs-python
|
||||
- pipeline-scripts
|
||||
- skill-scripting
|
||||
- set-e-footguns
|
||||
- error-handling
|
||||
- ce-demo-reel
|
||||
---
|
||||
|
||||
# Prefer Python over bash for multi-step pipeline scripts
|
||||
|
||||
## Context
|
||||
|
||||
When building the `ce-demo-reel` skill, the initial implementation used a bash script (`capture-evidence.sh`) to orchestrate ffmpeg stitching, frame normalization, and catbox.moe upload. Over 4 review rounds, the script hit 4 distinct bug classes that are inherent to bash's execution model rather than simple coding mistakes.
|
||||
|
||||
## Guidance
|
||||
|
||||
Use Python for agent pipeline scripts that chain multiple CLI tools with error handling. Bash `set -euo pipefail` works for simple sequential scripts but becomes a footgun when you need controlled failure paths.
|
||||
|
||||
**Python subprocess model (explicit error handling):**
|
||||
```python
|
||||
result = subprocess.run(
|
||||
["curl", "-s", "-F", f"fileToUpload=@{file_path}", url],
|
||||
capture_output=True, text=True, timeout=30, check=False
|
||||
)
|
||||
if result.returncode != 0:
|
||||
# Retry logic runs normally
|
||||
attempts += 1
|
||||
continue
|
||||
```
|
||||
|
||||
**Python timeout handling (explicit catch):**
|
||||
```python
|
||||
try:
|
||||
result = subprocess.run(cmd, timeout=60)
|
||||
except subprocess.TimeoutExpired:
|
||||
# Controlled failure, not a crash
|
||||
return subprocess.CompletedProcess(cmd, returncode=1, stdout="", stderr="Timed out")
|
||||
```
|
||||
|
||||
**Bash equivalent (the footgun):**
|
||||
```bash
|
||||
set -euo pipefail
|
||||
|
||||
# Exits the entire script before retry logic runs
|
||||
url=$(curl -s -F "fileToUpload=@${file}" "$endpoint")
|
||||
# Never reaches here on curl failure
|
||||
|
||||
# Workaround: || true on every line that might fail
|
||||
url=$(curl -s -F "fileToUpload=@${file}" "$endpoint") || true
|
||||
# Works but fragile and easy to forget
|
||||
```
|
||||
|
||||
## Why This Matters
|
||||
|
||||
Agent pipeline scripts run in environments the skill author does not control: different macOS versions (bash 3.2 vs 5.x), CI containers, worktrees. Each bash portability issue requires a non-obvious workaround that reviewers must catch. Python's subprocess model makes error handling explicit and testable rather than implicit and version-dependent.
|
||||
|
||||
The 4 bugs found were not unusual. They are the predictable consequence of using bash for scripts that exceed its sweet spot.
|
||||
|
||||
## When to Apply
|
||||
|
||||
Use Python when:
|
||||
- The script orchestrates 2+ external CLI tools
|
||||
- The script needs retry logic or graceful degradation on tool failure
|
||||
- The script will run on macOS where bash 3.2 is the default
|
||||
- The script needs to be tested from a non-shell test runner
|
||||
- The script has more than ~3 subcommands
|
||||
|
||||
Bash is still the right choice when:
|
||||
- Simple sequential scripts with no error recovery (set -e is fine)
|
||||
- One-liner wrappers around a single tool
|
||||
- Scripts using only POSIX features with no array manipulation
|
||||
- Git hooks and CI steps where the only failure mode is "abort the pipeline"
|
||||
|
||||
## Examples
|
||||
|
||||
**Before (bash, 4 bugs across 4 review rounds):**
|
||||
|
||||
| Bug | Cause | Workaround needed |
|
||||
|---|---|---|
|
||||
| `url=$(curl ...)` exits on network failure | `set -e` + command substitution | `\|\| true` on every line |
|
||||
| `${array[-1]}` fails | Bash 3.2 lacks negative indexing | `${array[${#array[@]}-1]}` |
|
||||
| Frame reduction keeps all frames for n=3,4 | Integer math: `step=(n-1)/2` with min 1 | Minimum step of 2 |
|
||||
| `command -v ffmpeg` in Bun tests | `command` is a shell builtin, not spawnable | Use `which` instead |
|
||||
|
||||
**After (Python, all 4 bug classes eliminated):**
|
||||
|
||||
```python
|
||||
# Negative indexing just works
|
||||
last = frames[-1]
|
||||
|
||||
# Timeout handling is explicit
|
||||
try:
|
||||
result = subprocess.run(cmd, timeout=30)
|
||||
except subprocess.TimeoutExpired:
|
||||
return None
|
||||
|
||||
# Tool detection is a regular function
|
||||
if not shutil.which("ffmpeg"):
|
||||
sys.exit("ffmpeg not found")
|
||||
|
||||
# Math is straightforward
|
||||
step = max(2, (len(frames) - 1) // 2)
|
||||
```
|
||||
|
||||
## Related
|
||||
|
||||
- `docs/solutions/skill-design/script-first-skill-architecture.md`: covers when to use scripts vs agent logic (complementary: that doc answers "should a script do this?", this doc answers "which language?")
|
||||
- `docs/solutions/agent-friendly-cli-principles.md`: CLI design from the consumer side (overlaps on exit code and stderr patterns)
|
||||
Reference in New Issue
Block a user