diff --git a/README.md b/README.md index 7e7be81..51b7fa7 100644 --- a/README.md +++ b/README.md @@ -1,28 +1,30 @@ # Pyro -Pyro is an open-source renderer for ***plain specifications. It's packaged as an agentic skill -that renders a `.plain` spec into working and tested code. +Pyro is an open-source renderer for ***plain specifications. It's packaged as an +agentic skill that renders a `.plain` spec into working and tested code. Invoke it from your agent with: ``` -/pyro:render-spec # Claude Code plugin install -/render-spec # npx skills install or manual copy +/pyro:render-spec # if installed as Claude Code plugin +/render-spec # if installed via npx skills or manual copy ``` The skill resolves the modules the target spec requires or imports, renders them in dependency order, and writes: -* intermediate implementation code to `plain_modules/` -* conformance tests to `conf_tests/` +* intermediate implementation code to `plain_module/code/` +* conformance tests to `plain_module/tests/` * the target module's output to `dist/` ## Installation ### Claude Code plugin -``` -/plugin marketplace add plainlang/pyro -/plugin install pyro@pyro +Run these two commands in your terminal: + +```bash +claude plugin marketplace add plainlang/pyro +claude plugin install pyro@pyro ``` Plugin skills are namespaced by their plugin, so invoke this one as @@ -50,8 +52,7 @@ claude plugin marketplace update pyro claude plugin update pyro ``` -Both also live in the in-session `/plugin` menu. A restart is needed either way -before the new version loads. +A restart is needed either way before the new version loads. ### Installation with npx skills @@ -59,7 +60,7 @@ If you have Node available, install the skill with the `npx skills` tool: ```bash cd project -npx skills add https://github.com/plainlang/pyro --agent claude-code +npx skills add https://github.com/plainlang/pyro --agent codex ``` This copies the skill straight into your agent's skills folder, so it is not diff --git a/skills/render-spec/SKILL.md b/skills/render-spec/SKILL.md index 9d3311a..129e9c8 100644 --- a/skills/render-spec/SKILL.md +++ b/skills/render-spec/SKILL.md @@ -22,16 +22,16 @@ Do every step yourself. Never hand any step to a subagent, Agent/Task tool or wo * On Claude Code it is `${CLAUDE_SKILL_DIR}`; on other agents derive it from the location this skill was loaded from. * Never reach a file inside this skill by a relative path. -`` is required Python 3.8 or newer. Resolve it now and reuse it everywhere in this skill where `` is used. Try these in order, keep the first that prints `ok`: -* `py -3 "/scripts/check_version.py" --check` -* `python3 "/scripts/check_version.py" --check` -* `python "/scripts/check_version.py" --check` +`` is required Python 3.8 or newer. Resolve it now and reuse it everywhere in this skill where `` is used. Run all three of these as independent tool calls in one single turn, in parallel - do not run them one after another: +* `python3 "/scripts/check_version.py" check` +* `py -3 "/scripts/check_version.py" check` +* `python "/scripts/check_version.py" check` -Report the Python tool you settled on. If none prints `ok`, Python is a missing dependency: abort the rendering and report it. +Among the commands whose output contains a `current:` line, keep the first one in the order listed above: its interpreter is ``. Report the Python tool you settled on. If no command prints a `current:` line, Python is a missing dependency: abort the rendering and report it. -Run ` "/scripts/check_version.py" check` and report with a message "Running pyro: ", where and the other values are read from the command's output. +From that same command's output report with a message "Running pyro: ". * If the output shows `status: update-available`, warn with a message: "A newer pyro release is available (this is )". -* On any other status, or if the command fails, say nothing about updates and continue. +* On any other status, say nothing about updates and continue. # Step 1: Introduction, inputs and outputs diff --git a/skills/render-spec/scripts/check_version.py b/skills/render-spec/scripts/check_version.py index 4a2a884..2cf193c 100644 --- a/skills/render-spec/scripts/check_version.py +++ b/skills/render-spec/scripts/check_version.py @@ -5,7 +5,6 @@ # Usage: check_version.py current [--skill-md ] # check_version.py latest [--url ] # check_version.py check [--skill-md ] [--url ] -# check_version.py --check # # current prints the version this copy of the skill ships with, read from # the `metadata: version:` field of the SKILL.md next to this script. @@ -32,10 +31,6 @@ # current and latest versions are read from; the tests use them with fixture # files and file:// URLs. # -# --check takes no other arguments: it verifies the interpreter is a usable -# Python 3, prints "ok" and exits 0, so a caller can test-drive this script -# before relying on it. -# # Versions are `X.Y.Z` with an optional `-rc.N` prerelease, and a release # outranks its own release candidates: 0.1.0 < 0.2.0-rc.1 < 0.2.0. # @@ -62,7 +57,7 @@ PROG = os.path.basename(sys.argv[0]) LATEST_URL = "https://raw.githubusercontent.com/plainlang/pyro/main/pyproject.toml" -FETCH_TIMEOUT = 10 # seconds +FETCH_TIMEOUT = 3 # seconds SKILL_MD = os.path.normpath( os.path.join(os.path.dirname(os.path.abspath(__file__)), os.pardir, "SKILL.md") ) @@ -72,7 +67,6 @@ USAGE = """usage: %(prog)s current [--skill-md ] %(prog)s latest [--url ] %(prog)s check [--skill-md ] [--url ] - %(prog)s --check current print this skill's own version (from SKILL.md metadata) latest print the latest published stable version (pyproject.toml on main) @@ -175,7 +169,7 @@ def parse_args(argv): usage_error("%s needs a value" % name) value = argv[index] options[name] = value - elif name in ("current", "latest", "check", "--check") and not equals: + elif name in ("current", "latest", "check") and not equals: if command is not None: usage_error("only one command allowed, got %r after %r" % (name, command)) command = name @@ -195,10 +189,6 @@ def usage_error(message): def main(argv): command, skill_md, url = parse_args(argv) - if command == "--check": - emit("ok\n") - return 0 - if command == "latest": latest = fetch_latest(url) if latest is None: diff --git a/skills/render-spec/scripts/plain_sections.py b/skills/render-spec/scripts/plain_sections.py index 294ea45..b94b91a 100644 --- a/skills/render-spec/scripts/plain_sections.py +++ b/skills/render-spec/scripts/plain_sections.py @@ -2,7 +2,6 @@ # Extract one section from .plain specs and print the bodies to stdout. # # Usage: plain_sections.py [--include-filename] [--output ]
[spec.plain ...] -# plain_sections.py --check # # Sections (case-insensitive, `-` and `_` interchangeable): # definitions defs :plainDefinitions: @@ -21,10 +20,6 @@ # `>>`, which corrupts files that later steps read back, so redirecting here rather than in # the calling shell keeps the output UTF-8/LF on every platform. Warnings stay on stderr. # -# --check takes no other arguments: it verifies the interpreter is a usable Python 3, prints -# "ok" and exits 0, so a caller can test-drive this script before relying on it. It returns -# before the --output redirect, so --output is ignored with --check. -# # Portability: the standard library only, Python 3.8+, no shell and no awk - so the same # command line works from sh, PowerShell and cmd alike. Everything is written as bytes to # keep the output UTF-8 with LF endings even on a Windows console, whose text streams would @@ -50,7 +45,6 @@ PROG = os.path.basename(sys.argv[0]) USAGE = """usage: %(prog)s [--include-filename] [--output ]
[spec.plain ...] - %(prog)s --check sections (case-insensitive, - and _ interchangeable): definitions defs :plainDefinitions: @@ -265,7 +259,6 @@ def parse_args(argv): """Options are recognized wherever they appear, including after the positionals.""" show_path = False out = None - check_only = False positional = [] args = list(argv) @@ -273,8 +266,6 @@ def parse_args(argv): arg = args.pop(0) if arg == "--include-filename": show_path = True - elif arg == "--check": - check_only = True elif arg == "--output": if not args: error("--output requires a path") @@ -290,17 +281,11 @@ def parse_args(argv): else: positional.append(arg) - return show_path, out, check_only, positional + return show_path, out, positional def main(argv): - show_path, out, check_only, positional = parse_args(argv) - - # --check proves the interpreter is a usable Python 3; anything older already exited at - # the version guard above, so reaching here is the answer. - if check_only: - write_text(byte_sink(sys.stdout), "ok\n") - return 0 + show_path, out, positional = parse_args(argv) if len(positional) < 2: usage() diff --git a/tests/run_check_version_tests.py b/tests/run_check_version_tests.py index e3311bb..289ad69 100644 --- a/tests/run_check_version_tests.py +++ b/tests/run_check_version_tests.py @@ -82,10 +82,7 @@ def run_suite(tmp): url_garbage = write_pyproject(tmp, "py-garbage.toml", "not.a.version") url_missing = pathlib.Path(os.path.join(tmp, "no-such-file.toml")).as_uri() - # 1. Interpreter probe. - assert_stdout("--check prints ok", b"ok\n", "--check") - - # 2. current: fixture and the real SKILL.md this repo ships. + # 1. current: fixture and the real SKILL.md this repo ships. assert_stdout("current from fixture", b"0.1.0\n", "current", "--skill-md", skill_010) assert_stdout("current, = spelling", b"0.1.0\n", "current", "--skill-md=" + skill_010) rc, out, err = run("current") @@ -97,13 +94,13 @@ def run_suite(tmp): "exit %d, stdout %r, stderr %r" % (rc, out, err), ) - # 3. latest: scoped to [project], loud when unavailable. + # 2. latest: scoped to [project], loud when unavailable. assert_stdout("latest from fixture", b"0.2.0\n", "latest", "--url", url_020) assert_rc("latest with unreachable url exits 1", 1, "latest", "--url", url_missing) assert_rc("latest without [project] exits 1", 1, "latest", "--url", url_no_project) assert_rc("latest with unparseable version exits 1", 1, "latest", "--url", url_garbage) - # 4. check: every status. + # 3. check: every status. assert_stdout( "check update-available", check_wants("0.1.0", "0.2.0", "update-available"), @@ -125,7 +122,7 @@ def run_suite(tmp): "check", "--skill-md", skill_rc, "--url", url_020, ) - # 5. check degrades softly: still exit 0, status unknown. + # 4. check degrades softly: still exit 0, status unknown. assert_stdout( "check with unreachable url degrades to unknown", check_wants("0.1.0", "unknown", "unknown"), @@ -142,7 +139,7 @@ def run_suite(tmp): "check", "--skill-md", skill_010, "--url", url_garbage, ) - # 6. A broken own version is a hard error. + # 5. A broken own version is a hard error. assert_rc( "missing SKILL.md exits 1", 1, @@ -155,12 +152,12 @@ def run_suite(tmp): "check", "--skill-md", no_version, "--url", url_010, ) - # 7. Usage errors. + # 6. Usage errors. assert_rc("no command exits 2", 2) assert_rc("unknown command exits 2", 2, "frobnicate") assert_rc("two commands exit 2", 2, "check", "latest") assert_rc("--skill-md without value exits 2", 2, "current", "--skill-md") - assert_rc("--check with a command exits 2", 2, "check", "--check") + assert_rc("unknown flag exits 2", 2, "check", "--frobnicate") if __name__ == "__main__": diff --git a/tests/run_plain_sections_tests.py b/tests/run_plain_sections_tests.py index f1efa35..d08774c 100644 --- a/tests/run_plain_sections_tests.py +++ b/tests/run_plain_sections_tests.py @@ -106,20 +106,7 @@ def run_suite(tmp): want_two = expect("defs") + b"\n" + b"- :Gadget: is another thing.\n" assert_stdout("two specs, one blank line between", want_two, "defs", SAMPLE, SECOND) - # 7. --check probes the interpreter and says so. - assert_rc("--check exits 0", 0, "--check") - if run("--check")[1] == b"ok\n": - ok("--check prints ok") - else: - bad("--check prints ok") - unused = os.path.join(tmp, "unused") - assert_rc("--check ignores a trailing --output", 0, "--check", "--output", unused) - if os.path.exists(unused): - bad("--check leaves --output path untouched") - else: - ok("--check leaves --output path untouched") - - # 8. Unreadable inputs exit 1. + # 7. Unreadable inputs exit 1. assert_rc("missing spec exits 1", 1, "defs", os.path.join(FIXTURES, "does-not-exist.plain")) assert_rc("directory argument exits 1", 1, "defs", FIXTURES) # ... but a readable spec later in the list is still printed. @@ -129,14 +116,14 @@ def run_suite(tmp): else: bad("unreadable spec does not suppress the readable one") - # 9. Usage errors exit 2. + # 8. Usage errors exit 2. assert_rc("unknown section exits 2", 2, "nonsense", SAMPLE) assert_rc("no arguments exits 2", 2) assert_rc("section without a spec exits 2", 2, "defs") assert_rc("--output without a path exits 2", 2, "defs", SAMPLE, "--output") assert_rc("--help exits 2", 2, "--help") - # 10. A missing section is a warning, not an error. + # 9. A missing section is a warning, not an error. assert_rc("missing section exits 0", 0, "defs", EMPTY) rc, out, err = run("defs", EMPTY) if out == b"" and err != b"": @@ -144,7 +131,7 @@ def run_suite(tmp): else: bad("missing section warns on stderr, stdout empty") - # 11. Flags are accepted after the positional arguments too. + # 10. Flags are accepted after the positional arguments too. assert_stdout("flag after positionals", want_filename, "defs", SAMPLE, "--include-filename")