Skip to content
This repository was archived by the owner on Apr 11, 2026. It is now read-only.

Commit 68d9d60

Browse files
committed
feat(skills): add template generation system with placeholder resolution
- Create gen-skill-docs.sh to resolve {{PLACEHOLDERS}} in .tmpl files - Create check-skill-freshness.sh for CI validation of generated files - Convert flow-code-debug skill to use template as proof of concept - Document template system in docs/skills.md Task: fn-23-gstack-inspired-plugin-optimizations.4
1 parent c26d087 commit 68d9d60

5 files changed

Lines changed: 403 additions & 4 deletions

File tree

docs/skills.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,3 +122,47 @@ Skills that make final judgments, complete workflows, or serve as primary manage
122122
| `flow-code` | Task and epic management entry point |
123123
| `flow-code-retro` | Post-epic retrospective and lessons learned |
124124
| `flow-code-autoplan` | Multi-perspective auto-review pipeline |
125+
126+
## Template Generation
127+
128+
Skills can be generated from `.tmpl` template files with `{{PLACEHOLDER}}` markers that resolve to shared content. This keeps common patterns (preamble, flowctl path, review protocols) in sync across all skills.
129+
130+
### Creating a Template
131+
132+
1. Copy the existing `SKILL.md` to `SKILL.md.tmpl` in the same directory
133+
2. Replace shared content with placeholder markers (see table below)
134+
3. Add `{{GENERATED_NOTICE}}` after the frontmatter to mark the file as auto-generated
135+
4. Run the generation script to produce the `SKILL.md`
136+
137+
### Available Placeholders
138+
139+
| Placeholder | Resolves To |
140+
|-------------|-------------|
141+
| `{{GENERATED_NOTICE}}` | `<!-- AUTO-GENERATED from SKILL.md.tmpl — DO NOT EDIT DIRECTLY -->` |
142+
| `{{FLOWCTL_PATH}}` | `FLOWCTL="$HOME/.flow/bin/flowctl"` |
143+
| `{{SKILL_NAME}}` | Extracted from the template's frontmatter `name:` field |
144+
| `{{PREAMBLE}}` | Contents of `skills/_shared/preamble.md` |
145+
| `{{RP_REVIEW_PROTOCOL}}` | Contents of `skills/_shared/rp-review-protocol.md` |
146+
147+
### Running Generation
148+
149+
```bash
150+
# Generate all SKILL.md files from their .tmpl sources
151+
bash scripts/gen-skill-docs.sh
152+
153+
# Preview what would change without writing
154+
bash scripts/gen-skill-docs.sh --dry-run
155+
156+
# Check if generated files are up to date (for CI)
157+
bash scripts/gen-skill-docs.sh --check
158+
```
159+
160+
### CI Freshness Check
161+
162+
Add to your CI pipeline to ensure generated files stay in sync:
163+
164+
```bash
165+
bash scripts/check-skill-freshness.sh
166+
```
167+
168+
This exits with code 1 if any `SKILL.md` does not match its `.tmpl` source, preventing stale generated files from being committed.

scripts/check-skill-freshness.sh

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
#!/usr/bin/env bash
2+
# CI check: ensures generated SKILL.md files match their .tmpl sources
3+
set -euo pipefail
4+
bash "$(dirname "$0")/gen-skill-docs.sh" --check

scripts/gen-skill-docs.sh

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
#!/usr/bin/env bash
2+
# gen-skill-docs.sh — Resolve {{PLACEHOLDERS}} in .tmpl files to generate SKILL.md
3+
# Usage: bash scripts/gen-skill-docs.sh [--dry-run] [--check]
4+
# --dry-run: show what would change without writing
5+
# --check: exit 1 if any generated file is stale (for CI)
6+
7+
set -euo pipefail
8+
9+
SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
10+
SHARED_DIR="$SCRIPT_DIR/skills/_shared"
11+
12+
DRY_RUN=false
13+
CHECK_MODE=false
14+
15+
for arg in "$@"; do
16+
case "$arg" in
17+
--dry-run) DRY_RUN=true ;;
18+
--check) CHECK_MODE=true ;;
19+
-h|--help)
20+
echo "Usage: bash scripts/gen-skill-docs.sh [--dry-run] [--check]"
21+
echo " --dry-run Show what would change without writing"
22+
echo " --check Exit 1 if any generated file is stale (for CI)"
23+
exit 0
24+
;;
25+
*)
26+
echo "Unknown argument: $arg" >&2
27+
exit 1
28+
;;
29+
esac
30+
done
31+
32+
STALE_FILE="/tmp/gen-skill-docs-stale.$$"
33+
rm -f "$STALE_FILE"
34+
35+
# extract_frontmatter_name <file>
36+
# Reads the YAML frontmatter and returns the name: value
37+
extract_frontmatter_name() {
38+
local file="$1"
39+
awk '
40+
BEGIN { in_fm=0 }
41+
NR==1 && /^---$/ { in_fm=1; next }
42+
in_fm && /^---$/ { exit }
43+
in_fm && /^name:/ { sub(/^name:[[:space:]]*/, ""); print; exit }
44+
' "$file"
45+
}
46+
47+
# resolve_placeholders <tmpl_file> <skill_name>
48+
# Reads template, resolves all {{PLACEHOLDER}} markers, outputs to stdout
49+
resolve_placeholders() {
50+
local tmpl_file="$1"
51+
local skill_name="$2"
52+
53+
local generated_notice="<!-- AUTO-GENERATED from SKILL.md.tmpl — DO NOT EDIT DIRECTLY -->"
54+
local flowctl_path='FLOWCTL="$HOME/.flow/bin/flowctl"'
55+
56+
# Use python3 for reliable multiline placeholder replacement
57+
python3 - "$tmpl_file" "$skill_name" "$generated_notice" "$flowctl_path" "$SHARED_DIR" <<'PYEOF'
58+
import sys, os
59+
60+
tmpl_file = sys.argv[1]
61+
skill_name = sys.argv[2]
62+
generated_notice = sys.argv[3]
63+
flowctl_path = sys.argv[4]
64+
shared_dir = sys.argv[5]
65+
66+
with open(tmpl_file, 'r') as f:
67+
content = f.read()
68+
69+
# Simple string placeholders
70+
content = content.replace('{{GENERATED_NOTICE}}', generated_notice)
71+
content = content.replace('{{FLOWCTL_PATH}}', flowctl_path)
72+
content = content.replace('{{SKILL_NAME}}', skill_name)
73+
74+
# File-content placeholders
75+
file_placeholders = {
76+
'{{PREAMBLE}}': os.path.join(shared_dir, 'preamble.md'),
77+
'{{RP_REVIEW_PROTOCOL}}': os.path.join(shared_dir, 'rp-review-protocol.md'),
78+
}
79+
80+
for placeholder, filepath in file_placeholders.items():
81+
if placeholder in content:
82+
if os.path.isfile(filepath):
83+
with open(filepath, 'r') as f:
84+
replacement = f.read()
85+
content = content.replace(placeholder, replacement)
86+
else:
87+
print(f"Warning: {placeholder} used but {filepath} not found", file=sys.stderr)
88+
89+
# Check for unresolved placeholders
90+
import re
91+
unresolved = set(re.findall(r'\{\{[A-Z_]+\}\}', content))
92+
for p in sorted(unresolved):
93+
print(f"Warning: Unresolved placeholder {p} in {tmpl_file}", file=sys.stderr)
94+
95+
sys.stdout.write(content)
96+
PYEOF
97+
}
98+
99+
# Find all .tmpl files under skills/
100+
find "$SCRIPT_DIR/skills" -name "*.md.tmpl" -type f | sort | while read -r tmpl_file; do
101+
# Output path: strip .tmpl suffix
102+
output_file="${tmpl_file%.tmpl}"
103+
skill_dir="$(dirname "$tmpl_file")"
104+
skill_dirname="$(basename "$skill_dir")"
105+
106+
# Extract SKILL_NAME from frontmatter name: field
107+
SKILL_NAME=$(extract_frontmatter_name "$tmpl_file")
108+
# Fallback to directory name if no frontmatter name
109+
if [ -z "$SKILL_NAME" ]; then
110+
SKILL_NAME="$skill_dirname"
111+
fi
112+
113+
# Resolve placeholders
114+
resolved=$(resolve_placeholders "$tmpl_file" "$SKILL_NAME")
115+
116+
# Relative paths for display
117+
rel_tmpl="${tmpl_file#$SCRIPT_DIR/}"
118+
rel_out="${output_file#$SCRIPT_DIR/}"
119+
120+
if $CHECK_MODE; then
121+
# Compare with existing output
122+
if [ -f "$output_file" ]; then
123+
existing=$(cat "$output_file")
124+
if [ "$existing" != "$resolved" ]; then
125+
echo "STALE: $rel_out (does not match $rel_tmpl)"
126+
echo "stale" >> "$STALE_FILE"
127+
else
128+
echo "OK: $rel_out"
129+
fi
130+
else
131+
echo "MISSING: $rel_out (not yet generated from $rel_tmpl)"
132+
echo "stale" >> "$STALE_FILE"
133+
fi
134+
elif $DRY_RUN; then
135+
echo "Would generate: $rel_out (from $rel_tmpl)"
136+
if [ -f "$output_file" ]; then
137+
existing=$(cat "$output_file")
138+
if [ "$existing" != "$resolved" ]; then
139+
echo " -> Content would change"
140+
else
141+
echo " -> No changes"
142+
fi
143+
else
144+
echo " -> New file"
145+
fi
146+
else
147+
# Write the resolved content
148+
printf '%s' "$resolved" > "$output_file"
149+
echo "Generated: $rel_out"
150+
fi
151+
done
152+
153+
# Check for staleness in check mode
154+
if $CHECK_MODE; then
155+
if [ -f "$STALE_FILE" ]; then
156+
count=$(wc -l < "$STALE_FILE" | tr -d ' ')
157+
rm -f "$STALE_FILE"
158+
echo ""
159+
echo "ERROR: $count generated file(s) are stale. Run: bash scripts/gen-skill-docs.sh"
160+
exit 1
161+
else
162+
echo ""
163+
echo "All generated files are up to date."
164+
fi
165+
fi

skills/flow-code-debug/SKILL.md

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,17 @@ name: flow-code-debug
33
description: Use when encountering any bug, test failure, or unexpected behavior, before proposing fixes
44
tier: 2
55
---
6+
<!-- AUTO-GENERATED from SKILL.md.tmpl — DO NOT EDIT DIRECTLY -->
67

78
# Systematic Debugging
89

910
> **Startup:** Follow [Startup Sequence](../_shared/preamble.md) before proceeding.
10-
<!-- SKILL_TAGS: debugging,testing,errors -->
11+
12+
## flowctl Setup
13+
14+
```bash
15+
FLOWCTL="$HOME/.flow/bin/flowctl"
16+
```
1117

1218
## The Iron Law
1319

@@ -38,7 +44,7 @@ If you haven't completed Phase 1, you cannot propose fixes.
3844

3945
4. **Run guards to establish baseline:**
4046
```bash
41-
<FLOWCTL> guard
47+
$FLOWCTL guard
4248
```
4349

4450
5. **Gather evidence in multi-component systems:**
@@ -98,7 +104,7 @@ END
98104
2. **Confirm RED** — run the test, verify it actually fails. If it passes, your test doesn't reproduce the bug
99105
3. **Fix root cause** — implement the fix (not a workaround)
100106
4. **Confirm GREEN** — run the test, verify it now passes
101-
5. **Run full suite** — check for regressions: `<FLOWCTL> guard`
107+
5. **Run full suite** — check for regressions: `$FLOWCTL guard`
102108

103109
**If the test passes on step 2, your test is wrong.** Go back to Phase 1 and refine your understanding of the bug.
104110

@@ -171,4 +177,4 @@ END
171177
Bug fixed. Next:
172178
1) Review the fix: `/flow-code:impl-review --base <pre-fix-commit>`
173179
2) Continue current work: `/flow-code:work <epic-id>`
174-
```
180+
```

0 commit comments

Comments
 (0)