feat(quick-dev): stdout-dispatch SKILL.md shim + TPL-01 validator rule

SKILL.md becomes a two-line shim: run render.py, follow the instruction
it prints to stdout. render.py emits the read-and-follow line on
success, HALT-and-report on failure. Progress chatter moves to stderr.

This removes the skill-dir and project-root tokens from SKILL.md — per
Anthropic skills spec, script output is the defined channel for agent
communication, and no public skill in the reference set uses template
tokens for path resolution.

Adds TPL-01 to the deterministic validator: files whose name contains
template must not contain compile-time double-curly substitutions.
Template files seed durable, version-controlled artifacts (spec files)
that execute on other machines; baking a value at render time would
freeze a machine-local path into every downstream artifact.
This commit is contained in:
Alex Verkhovsky 2026-04-18 14:24:06 -07:00
parent bf30b69757
commit b0d70766f5
4 changed files with 52 additions and 9 deletions

View File

@ -3,10 +3,8 @@ name: bmad-quick-dev
description: 'Implements any user intent, requirement, story, bug fix or change request by producing clean working code artifacts that follow the project''s existing architecture, patterns and conventions. Use when the user wants to build, fix, tweak, refactor, add or modify any code, component or feature.'
---
Render the workflow templates, then load and follow the rendered workflow.
```
python3 {skill-dir}/render.py
python render.py
```
Then read and follow `{project-root}/_bmad/render/bmad-quick-dev/workflow.md`.
Then follow the instruction it prints to stdout.

View File

@ -18,7 +18,8 @@ import sys
def find_project_root():
"""Walk up from cwd until a _bmad/ directory is found. Exit non-zero otherwise."""
"""Walk up from cwd until a _bmad/ directory is found. On failure, print a
HALT instruction to stdout and exit non-zero."""
current = os.path.abspath(os.getcwd())
while True:
candidate = os.path.join(current, "_bmad")
@ -27,8 +28,7 @@ def find_project_root():
parent = os.path.dirname(current)
if parent == current:
print(
f"render.py: no _bmad/ directory found walking up from {os.getcwd()}",
file=sys.stderr,
f"HALT and report to the user: no _bmad/ directory found walking up from {os.getcwd()}"
)
sys.exit(1)
current = parent
@ -119,7 +119,9 @@ def main():
fh.write(render_template(content, vars_))
count += 1
print(f"render.py: rendered {count} files -> {out_dir}")
print(f"render.py: rendered {count} files -> {out_dir}", file=sys.stderr)
workflow_md = os.path.join(out_dir, "workflow.md")
print(f"read and follow {workflow_md}")
if __name__ == "__main__":

View File

@ -10,7 +10,7 @@ Before running inference-based validation, run the deterministic validator:
node tools/validate-skills.js --json path/to/skill-dir
```
This checks 14 rules deterministically: SKILL-01, SKILL-02, SKILL-03, SKILL-04, SKILL-05, SKILL-06, SKILL-07, WF-01, WF-02, PATH-02, STEP-01, STEP-06, STEP-07, SEQ-02.
This checks 15 rules deterministically: SKILL-01, SKILL-02, SKILL-03, SKILL-04, SKILL-05, SKILL-06, SKILL-07, WF-01, WF-02, PATH-02, STEP-01, STEP-06, STEP-07, SEQ-02, TPL-01.
Review its JSON output. For any rule that produced **zero findings** in the first pass, **skip it** during inference-based validation below — it has already been verified. If a rule produced any findings, the inference validator should still review that rule (some rules like SKILL-04 and SKILL-06 have sub-checks that benefit from judgment). Focus your inference effort on the remaining rules that require judgment (PATH-01, PATH-03, PATH-04, PATH-05, WF-03, STEP-02, STEP-03, STEP-04, STEP-05, SEQ-01, REF-01, REF-02, REF-03).
@ -271,6 +271,16 @@ If no findings are generated (from either pass), the skill passes validation.
---
### TPL-01 — Template Files Must Not Contain Compile-Time Substitutions
- **Severity:** HIGH
- **Applies to:** `.md` files whose name contains `template` (case-insensitive)
- **Rule:** Template files seed durable, version-controlled artifacts (e.g. spec files) that execute on other machines. A `{{.var}}` compile-time substitution would be baked at render time and freeze a machine-local value into every artifact produced from the template.
- **Detection:** Regex `\{\{\.\w+\}\}` match anywhere in a file whose basename matches `/template/i`.
- **Fix:** Remove the `{{.var}}` reference. Use single-curly `{var}` if the value should be resolved at LLM runtime by the consumer of the generated artifact.
---
### REF-01 — Variable References Must Be Defined
- **Severity:** HIGH

View File

@ -19,6 +19,7 @@
* - STEP-06: step frontmatter has no name/description
* - STEP-07: step count 2-10
* - SEQ-02: no time estimates
* - TPL-01: template files must not contain compile-time {{.var}} substitutions
*
* Usage:
* node tools/validate-skills.js # All skills, human-readable
@ -45,6 +46,8 @@ const positionalArgs = args.filter((a) => !a.startsWith('--'));
const NAME_REGEX = /^bmad-[a-z0-9]+(-[a-z0-9]+)*$/;
const STEP_FILENAME_REGEX = /^step-\d{2}[a-z]?-[a-z0-9-]+\.md$/;
const TIME_ESTIMATE_PATTERNS = [/takes?\s+\d+\s*min/i, /~\s*\d+\s*min/i, /estimated\s+time/i, /\bETA\b/];
const TEMPLATE_FILENAME_REGEX = /template/i;
const COMPILE_TIME_SUB_REGEX = /\{\{\.\w+\}\}/;
const SEVERITY_ORDER = { CRITICAL: 0, HIGH: 1, MEDIUM: 2, LOW: 3 };
@ -569,6 +572,36 @@ function validateSkill(skillDir) {
}
}
// --- TPL-01: template files must not contain compile-time {{.var}} substitutions ---
// Template files seed durable, version-controlled artifacts (spec files) that
// execute on other machines. Baking a {{.var}} at render time would freeze a
// machine-local value into every downstream artifact.
for (const filePath of allFiles) {
if (path.extname(filePath) !== '.md') continue;
const base = path.basename(filePath);
if (!TEMPLATE_FILENAME_REGEX.test(base)) continue;
const relFile = path.relative(skillDir, filePath);
const content = safeReadFile(filePath, findings, relFile);
if (content === null) continue;
const lines = content.split('\n');
for (const [i, line] of lines.entries()) {
const match = line.match(COMPILE_TIME_SUB_REGEX);
if (match) {
findings.push({
rule: 'TPL-01',
title: 'Template files must not contain compile-time substitutions',
severity: 'HIGH',
file: relFile,
line: i + 1,
detail: `Template file contains compile-time substitution \`${match[0]}\` — this would be baked at render time and leak a machine-local value into every spec produced from the template.`,
fix: 'Remove the `{{.var}}` reference. Use single-curly `{var}` if the value should be resolved at LLM runtime by the consumer of the generated spec.',
});
}
}
}
return findings;
}