"""spec-first-prompting-coding-agent.py — lint a SPEC.md before handing it to a coding agent. What it does: reads a Markdown spec and checks that the sections an agent needs are present and non-empty (Goal, Non-goals, Interfaces, Files, Verification, Budget), that Verification contains at least one runnable command in backticks, and that the spec does not lean on vague verbs ("improve", "clean up", "make better", "polish", "optimize") without a measurable check somewhere in the text. Input: path to a spec file (Markdown, `## Section` headings). Run: python spec-first-prompting-coding-agent.py SPEC.md Exit: 0 when no errors, 1 when any error (warnings never fail). Python 3.13, standard library only. """ from __future__ import annotations import re import sys REQUIRED = ["Goal", "Non-goals", "Interfaces", "Files", "Verification", "Budget"] VAGUE = ["improve", "clean up", "cleanup", "make better", "polish", "optimize", "optimise", "modernize", "refactor"] MEASURABLE = re.compile(r"(\d|exit code|passes|fails|returns|prints|assert|under \d|within \d|<=|>=)", re.I) COMMAND = re.compile(r"`([^`\n]+)`") LOOKS_RUNNABLE = re.compile(r"^(python|python3|pytest|npm|npx|node|go|cargo|make|bash|sh|git|uv|pip)\b") def sections(text: str) -> dict[str, str]: """Split Markdown into {heading: body} for every `## Heading` block.""" out: dict[str, str] = {} current = None for line in text.splitlines(): m = re.match(r"^##\s+(.+?)\s*$", line) if m: current = m.group(1).strip() out[current] = "" elif current is not None: out[current] += line + "\n" return out def lint(path: str) -> tuple[list[str], list[str]]: text = open(path, encoding="utf-8").read() secs = sections(text) errors: list[str] = [] warnings: list[str] = [] lower = {k.lower(): v for k, v in secs.items()} for name in REQUIRED: body = lower.get(name.lower()) if body is None: errors.append(f"missing section: ## {name}") elif not body.strip(): errors.append(f"empty section: ## {name}") ver = lower.get("verification", "") cmds = [c for c in COMMAND.findall(ver) if LOOKS_RUNNABLE.match(c.strip())] if ver.strip() and not cmds: errors.append("Verification has no runnable command in backticks (e.g. `python -m unittest`)") for c in cmds: if not MEASURABLE.search(ver): warnings.append(f"Verification names `{c}` but never says what a pass looks like") break nongoals = lower.get("non-goals", "") if nongoals.strip() and len(nongoals.strip()) < 20: warnings.append("Non-goals is very short; list at least one thing the agent must not touch") body_text = "\n".join(v for k, v in secs.items() if k.lower() != "verification") for verb in VAGUE: for m in re.finditer(r"\b" + re.escape(verb) + r"\b", body_text, re.I): line_no = body_text[: m.start()].count("\n") + 1 window = body_text[max(0, m.start() - 120): m.end() + 120] if not MEASURABLE.search(window): warnings.append(f"vague verb '{m.group(0)}' near line {line_no} with no measurable check nearby") break budget = lower.get("budget", "") if budget.strip() and not re.search(r"\d", budget): warnings.append("Budget has no number (turns, dollars, minutes, or files)") return errors, warnings def main(argv: list[str]) -> int: if len(argv) != 2: print(__doc__) return 2 errors, warnings = lint(argv[1]) for e in errors: print(f"ERROR {e}") for w in warnings: print(f"WARNING {w}") print(f"{argv[1]}: {len(errors)} error(s), {len(warnings)} warning(s)") return 1 if errors else 0 if __name__ == "__main__": sys.exit(main(sys.argv))