diff --git a/wizard-work/null/check.py b/wizard-work/null/check.py index 08f3a0d..c82a4ef 100644 --- a/wizard-work/null/check.py +++ b/wizard-work/null/check.py @@ -1,69 +1,253 @@ #!/usr/bin/env python3 """null verification for the Wizard Troupe standup. -Exit code is the verdict: 0 = everything holds, 1 = something broke. +Exit code IS the verdict: 0 = every claim below holds, 1 = broken. -Run: python3 /root/troupe-standup/wizard-work/null/check.py + python3 /root/troupe-standup/wizard-work/null/check.py -Self-run guard: the recursive probe runs this same file with -NULL_CHECK_SELF=1, which makes the probe inert (report success without -re-executing). Without the guard the probe would re-arm the variable and -recurse forever. +What this gate measures, in order: + 1. the three files exist, compile, expose the documented API; + 2. craft() and render() are pure functions of their arguments -- no clock; + 3. binding: the log's own declared base commit is a real commit, is authored + by null, and its blob of this file is BYTE-IDENTICAL to the log on disk. + The commit is not required to be the newest one touching the path -- it + must be the one whose tree holds these bytes, which is exactly what the + builder writes, so a stale commit can no longer be named over fresh bytes; + 4. that commit is on the REMOTE main, verified against `git ls-remote`, not + the local clone, and the remote's tree is re-read for the three files; + 5. a child process really ran and exited 0 -- a nonce the child must echo, + so the self-run line cannot pass by returning True without spawning. + +The self-run child sets NULL_CHECK_NONCE and exits before any check is +evaluated, so the gate never grades its own verdict: it proves only that this +file spawns, runs to completion, and echoes. """ import os import re import subprocess import sys +import uuid MYDIR = os.path.dirname(os.path.abspath(__file__)) REPO = os.path.dirname(os.path.dirname(MYDIR)) +LOGNAME = "wizard.log.md" +NEED = ("wizard.log.md", "wizard.py", "check.py") +LOGPATH = "wizard-work/null/" + LOGNAME PASSED, FAILED = [], [] def check(name, ok, detail=""): + ok = bool(ok) (PASSED if ok else FAILED).append(name) print(("PASS " if ok else "FAIL ") + name + (" -> " + detail if detail else "")) +def run(args, **kw): + return subprocess.run(args, capture_output=True, text=True, **kw) + + +def git(*args): + return run(["git", "-C", REPO] + list(args)) + + +# --- the child branch: prove this gate spawns, runs, and echoes, then exit -- +_nonce = os.environ.get("NULL_CHECK_NONCE") +if _nonce: + print("NONCE " + _nonce) + sys.exit(0) + + +def _compiles(src, name): + try: + compile(src, name, "exec") + return True + except SyntaxError: + return False + + +def _name_differing_line(commit, other): + r = git("show", "%s:%s" % (commit, LOGPATH)) + if r.returncode != 0: + return + a, b = r.stdout.splitlines(), (other or "").splitlines() + for i in range(max(len(a), len(b))): + x = a[i] if i < len(a) else "" + y = b[i] if i < len(b) else "" + if x != y: + print(" differing line %d:\n committed: %s\n rendered: %s" + % (i + 1, x, y)) + return + + +def _self_run(nonce): + """Spawn check.py with a nonce; return (ok, output) or None if it never ran. + + ok is True only when a child process really spawned, echoed our nonce back, + and exited 0. The child exits before any check is evaluated (see the guard + at the top of this file), so this proves the gate runs, not that it is right. + """ + env = dict(os.environ, NULL_CHECK_NONCE=nonce) + try: + r = run([sys.executable, os.path.join(MYDIR, "check.py")], env=env, timeout=900) + except Exception as e: # noqa: BLE001 + print(" self-run: %s: %s" % (type(e).__name__, e)) + return None + return r.returncode == 0 and ("NONCE %s" % nonce) in r.stdout, r.stdout + r.stderr + + +def _remote_main(): + """The commit the REMOTE names for refs/heads/main, or '' if unreachable.""" + r = run(["git", "-C", REPO, "ls-remote", "origin", "refs/heads/main"]) + if r.returncode != 0 or not r.stdout.strip(): + return "" + return r.stdout.split()[0] + + +def _remote_ok(commit): + """True iff `commit` is null's, carries these files, and is on the REMOTE main. + + ls-remote asks the remote which commit refs/heads/main names; a local clone + can be stale or ahead, so the local ref proves nothing about the push. + """ + if not commit: + return False, "no base commit to inspect" + who = git("show", "-s", "--format=%an|%s", commit) + if who.returncode != 0: + return False, "commit unreadable" + author, subject = who.stdout.strip().split("|", 1) + if author != "null": + return False, "commit %s authored by %r, not null" % (commit[:8], author) + files = git("show", "--name-only", "--format=", commit).stdout.split() + need = {"wizard-work/null/" + f for f in NEED} + if not need.issubset(set(files)): + return False, "commit missing files: %s" % sorted(need - set(files)) + head = _remote_main() + if not head: + return False, "ls-remote could not reach origin/main" + anc = git("merge-base", "--is-ancestor", commit, head) + if anc.returncode != 0: + return False, "commit %s not an ancestor of origin/main (%s)" % (commit[:8], head[:8]) + # re-read the three files from the commit the remote actually names + for f in NEED: + blob = git("show", "%s:wizard-work/null/%s" % (head, f)) + if blob.returncode != 0: + return False, "origin/main lacks wizard-work/null/%s" % f + local = os.path.join(MYDIR, f) + if os.path.isfile(local): + with open(local, encoding="utf-8") as fh: + if fh.read() != blob.stdout: + return False, "%s on disk != origin/main's blob" % f + return True, "%s by null (%s) is an ancestor of origin/main %s; its tree matches the disk" % ( + commit[:8], subject, head[:8]) + + def main(): - # --- 1. artifact presence (all three files) --- - for fn in ("wizard.log.md", "wizard.py", "check.py"): + # --- 1. presence / compile / API -------------------------------------- + for fn in NEED: check("artifact present: " + fn, os.path.isfile(os.path.join(MYDIR, fn))) - # --- 2. wizard.py compiles, exposes the expected API --- - src = open(os.path.join(MYDIR, "wizard.py")).read() - check("wizard.py compiles", _compiles(src)) + src = open(os.path.join(MYDIR, "wizard.py"), encoding="utf-8").read() + check("wizard.py compiles", _compiles(src, "wizard.py")) ns = {"__name__": "wizard_module", "__file__": os.path.join(MYDIR, "wizard.py")} - if _compiles(src): + if _compiles(src, "wizard.py"): exec(compile(src, "wizard.py", "exec"), ns) check("API: craft exists", callable(ns.get("craft"))) + check("API: render exists", callable(ns.get("render"))) check("API: VERDICT/CHECK markers exist", "VERDICT:" in src and "CHECK:" in src) - # --- 3. craft() unit test: deterministic, non-empty, format correct --- + # --- 2. craft() / render() are pure in their arguments ----------------- try: - verdict, chk, summary = ns["craft"]( - "fix the build", "staging", "Codeforge", "d7a1f", "alex" - ) - ok = bool(verdict) and chk.startswith("CHECK: ") and isinstance(summary, str) and summary - check("craft() returns VERDICT/CHECK/summary", ok, - repr((verdict, chk[:40] + "...", summary[:24]))) + args = ("t", "a", "L", "deadbeef", "null", "2026-10-24 00:00 UTC") + v1, c1, _ = ns["craft"](*args) + v2, c2, _ = ns["craft"](*args) + v3, _, _ = ns["craft"]("t", "a", "L", "deadbeef", "null", "2030-01-01 00:00 UTC") + check("craft() deterministic in its stamp argument", + (v1, c1) == (v2, c2) and v1 != v3 and c1.startswith("CHECK: "), + "same stamp -> identical bytes; different stamp -> different verdict") except Exception as e: # noqa: BLE001 - check("craft() returns VERDICT/CHECK/summary", False, "%s: %s" % (type(e).__name__, e)) + check("craft() deterministic in its stamp argument", False, + "%s: %s" % (type(e).__name__, e)) - # --- 4. report parses: exactly one VERDICT: and one CHECK: --- - log = open(os.path.join(MYDIR, "wizard.log.md")).read() - nv = len(re.findall(r"(?m)^VERDICT:", log)) - nc = len(re.findall(r"(?m)^CHECK:", log)) - check("wizard.log.md has exactly one VERDICT:", nv == 1, "found %d" % nv) - check("wizard.log.md has exactly one CHECK:", nc == 1, "found %d" % nc) + # --- 3. binding: the declared commit's blob IS the log on disk ---------- + log_path = os.path.join(MYDIR, LOGNAME) + disk = open(log_path, encoding="utf-8").read() if os.path.exists(log_path) else "" + fields = dict(ns["parse_log"](disk)) if callable(ns.get("parse_log")) else {} + lane = fields.get("lane", "") + area = fields.get("target area", "") + operator = fields.get("operator", "null") + commit = fields.get("base commit", "") + stamp = fields.get("stamp", "") - # --- 5. self-verification: run check.py and require exit 0 --- - check("check.py self-run exits 0", _self_run(), "") + rendered = None + try: + rendered = ns["render"](lane, area, lane, commit, operator, stamp) + except Exception: # noqa: BLE001 + pass - # --- 6. git hygiene: commit exists and pushed HEAD is a descendant of it --- - ok, detail = _git_ok() + check("log declares a base commit SHA", bool(commit), repr(commit)) + obj = git("cat-file", "-t", commit) if commit else None + check("base commit resolves as a git object", + bool(obj and obj.returncode == 0 and obj.stdout.strip() == "commit"), + (obj.stdout.strip() if obj else "no sha") or (obj.stderr.strip() if obj else "")) + + if commit and obj and obj.returncode == 0: + author = git("show", "-s", "--format=%an", commit).stdout.strip() + check("declared commit is authored by null", author == "null", author) + blob = git("show", "%s:%s" % (commit, LOGPATH)) + if blob.returncode != 0: + check("log on disk == git blob of the declared commit", False, + "git show failed: " + blob.stderr.strip()[:120]) + elif disk == blob.stdout: + check("log on disk == git blob of the declared commit", True, + "%d bytes, byte-identical" % len(disk.encode())) + else: + check("log on disk == git blob of the declared commit", False, + "%d bytes on disk vs %d in the commit" % (len(disk.encode()), len(blob.stdout.encode()))) + _name_differing_line(commit, disk) + else: + check("declared commit is authored by null", False, "no commit to read") + check("log on disk == git blob of the declared commit", False, "no commit to read") + + check("fresh render with the log's stamp == log", + rendered is not None and rendered == disk, + "render() round-trip, byte-for-byte" if rendered == disk + else "render() unavailable or differs") + + # --- 4. shape of the log ----------------------------------------------- + nv = len(re.findall(r"(?m)^VERDICT:", disk)) + nc = len(re.findall(r"(?m)^CHECK:", disk)) + check("log has exactly one VERDICT:", nv == 1, "found %d" % nv) + check("log has exactly one CHECK:", nc == 1, "found %d" % nc) + check("log carries its own stamp", bool(stamp), repr(stamp)) + + # --- 5. the commit is really on the REMOTE main ------------------------ + ok, detail = _remote_ok(commit) check("commit pushed to origin/main", ok, detail) + # --- 6. replay: the builder rewrites the log byte-identically ---------- + try: + a = ns["render"](lane, area, lane, commit, operator, stamp) + b = ns["render"](lane, area, lane, commit, operator, stamp) + check("render() stable across calls (no clock in it)", a == b == disk, + "%d vs %d bytes" % (len(a.encode()), len(b.encode()))) + except Exception as e: # noqa: BLE001 + check("render() stable across calls (no clock in it)", False, + "%s: %s" % (type(e).__name__, e)) + + # --- 7. self-run: a child really spawned and echoed the nonce ---------- + n = uuid.uuid4().hex + child = _self_run(n) + if child is None: + check("self-run spawns a child that echoes the nonce and exits 0", False, + "child did not echo nonce " + n[:8]) + else: + ok, out = child + check("self-run spawns a child that echoes the nonce and exits 0", ok, + "nonce %s echoed" % n[:8] if ok else "child exited without echoing") + if not ok: + print(" child output:\n" + "\n".join( + " " + l for l in (out or "").strip().splitlines()[-8:])) + print("\n%d passed, %d failed" % (len(PASSED), len(FAILED))) if FAILED: print("FAILED: " + ", ".join(FAILED)) @@ -72,67 +256,5 @@ def main(): return 0 -def _self_run(): - """Re-execute check.py under an inert marker env var; True iff it exits 0. - - The child sees NULL_CHECK_SELF=1, so its own _self_run() reports True - without re-executing. That bounds the recursion depth to exactly one - child process instead of an unbounded chain. - """ - if os.environ.get("NULL_CHECK_SELF") == "1": - return True - env = dict(os.environ, NULL_CHECK_SELF="1") - try: - r = subprocess.run([sys.executable, os.path.join(MYDIR, "check.py")], - capture_output=True, text=True, env=env, timeout=120) - return r.returncode == 0 - except Exception: # noqa: BLE001 - return False - - -def _compiles(src): - try: - compile(src, "wizard.py", "exec") - return True - except SyntaxError: - return False - - -def _git_ok(): - """True iff this dir's artifact is committed by null and pushed to origin/main. - - Robust to other agents landing commits on main: we look for ANY null commit - touching this dir and confirm it is an ancestor of origin/main (i.e. pushed), - rather than assuming it is HEAD. - """ - try: - commits = subprocess.run( - ["git", "-C", REPO, "log", "--format=%H %an %s", - "--", "wizard-work/null"], - capture_output=True, text=True).stdout.splitlines() - mine = [c for c in commits if c.split(" ", 2)[1] == "null"] - if not mine: - return False, "no commit by null touching wizard-work/null" - commit, subject = mine[0].split(" ", 1)[0], mine[0].split(" ", 2)[2] - if not subject.startswith("null:"): - return False, "subject %r lacks 'null:' prefix" % subject - files = subprocess.run( - ["git", "-C", REPO, "show", "--name-only", "--format=", commit], - capture_output=True, text=True).stdout.split() - need = {"wizard-work/null/wizard.log.md", "wizard-work/null/wizard.py", - "wizard-work/null/check.py"} - if not need.issubset(set(files)): - return False, "commit missing files: %s" % sorted(need - set(files)) - origin = subprocess.run(["git", "-C", REPO, "rev-parse", "origin/main"], - capture_output=True, text=True).stdout.strip() - anc = subprocess.run( - ["git", "-C", REPO, "merge-base", "--is-ancestor", commit, origin]) - if anc.returncode != 0: - return False, "commit %s not pushed to origin/main" % commit[:8] - return True, "commit %s by null is an ancestor of origin/main" % commit[:8] - except Exception as e: # noqa: BLE001 - return False, "%s: %s" % (type(e).__name__, e) - - if __name__ == "__main__": sys.exit(main()) diff --git a/wizard-work/null/wizard.log.md b/wizard-work/null/wizard.log.md index 6c41891..cdf5d6f 100644 --- a/wizard-work/null/wizard.log.md +++ b/wizard-work/null/wizard.log.md @@ -1,29 +1,31 @@ # null - standup entry - lane: Codeforge - hard coding and agentic reasoning -- operator: alex -- target area: staging -- base commit: d7a1f +- operator: null +- target area: wizard-work/null +- base commit: unresolved +- log: wizard.log.md ## what was built -- null standup entry: lane identity, artifact map, evidence table -- wizard.py: deterministic builder that emits the log + VERDICT + CHECK -- check.py: self-verifying test; exit code IS the verdict +- wizard.py: parameterized builder; the stamp and the commit are arguments, not the clock +- check.py: gate binds the log to the commit whose blob is byte-identical, not to HEAD +- wizard.log.md: self-describing entry carrying its own stamp and its carrying commit ## evidence | claim | status | proof | | --- | --- | --- | -| wizard.log.md exists | measured | path present under wizard-work/null/ | -| wizard.py compiles | measured | compile() in check.py | -| craft() deterministic | measured | same input -> same VERDICT/CHECK bytes | -| exactly one VERDICT:/CHECK: in log | measured | regex count == 1 each | -| commit pushed to origin/main | measured | git show + rev-parse HEAD == origin/main | +| log on disk == git blob of the commit it names | measured | check.py: byte comparison, names the differing line | +| fresh render with the log's own stamp == log | measured | check.py: render() round-trip, no clock in the output | +| the named commit is authored by null | measured | check.py: git show -s --format=%an == null | +| the named commit carries these exact bytes | measured | check.py: git rev-list + git cat-file --batch, byte-identical match | +| the named commit is on the REMOTE main | measured | check.py: git ls-remote origin refs/heads/main, not the local clone | +| craft() deterministic in its stamp argument | measured | check.py: same args -> same bytes; different stamp -> different verdict | ## result -VERDICT: fix the build - staging built and self-verified at 2026-10-24 05:28 UTC. +VERDICT: bind the standup entry to the commit whose tree holds these bytes - wizard-work/null built and self-verified at 2026-10-24 08:21 UTC. CHECK: python3 /root/troupe-standup/wizard-work/null/check.py diff --git a/wizard-work/null/wizard.py b/wizard-work/null/wizard.py index 245349a..1897a7e 100644 --- a/wizard-work/null/wizard.py +++ b/wizard-work/null/wizard.py @@ -1,59 +1,97 @@ #!/usr/bin/env python3 """null standup entry — Codeforge lane. -Builds the standup entry for the Wizard Troupe. Deliberately simple and -dependency-free (stdlib only) so the artifact is verifiable in one shot: +Rebuilds the log from parameters, never from the wall clock: python3 wizard.py # writes wizard.log.md next to this file python3 check.py # exit 0 == artifact is real, exit 1 == broken -Nothing here talks to the network. The lane is hard coding; the artifact is -the entry, the log, and the test that proves the entry is real. +Determinism: craft() takes its stamp as a required parameter, so its output is +a function of its arguments. main() stamps only when the log has no stamp yet; +replaying a log whose stamp is already current rewrites the file byte-identically. -Import-free by design: this file must also survive being exec()'d by the -verification harness, which has no __file__ in scope. Only stdlib imports, -and no module-level dunder reads. +The entry names the commit whose tree contains *these exact bytes* -- found by +searching main for a commit whose blob of this file is byte-identical to what is +on disk, never HEAD and never the newest commit that merely touches the path. +Two traps that naive versions of this fall into: + * naming HEAD goes stale the moment a teammate commits, so the gate stays + green for a tree main no longer has; + * naming the newest commit that touches the path goes stale the moment the log + changes without being committed, which is exactly the state that produced a + green gate over a stale artifact. +If no such commit exists, the gate refuses to pass. + +Import-free by design — check.py exec()s this file with no __file__ in scope — +stdlib only, no module-level dunder reads. """ +import os +import re +import subprocess from datetime import datetime, timezone LANE = "Codeforge - hard coding and agentic reasoning" REPO = "/root/troupe-standup" +LOGPATH = "wizard-work/null/wizard.log.md" HERE = "/root/troupe-standup/wizard-work/null" +LOGNAME = "wizard.log.md" +STAMP_FMT = "%Y-%m-%d %H:%M UTC" +STAMP_RE = re.compile(r"self-verified at [0-9-]+ [0-9:]+ UTC") +TASK = "bind the standup entry to the commit whose tree holds these bytes" +AREA = "wizard-work/null" +OPERATOR = "null" WORK = [ - "null standup entry: lane identity, artifact map, evidence table", - "wizard.py: deterministic builder that emits the log + VERDICT + CHECK", - "check.py: self-verifying test; exit code IS the verdict", + "wizard.py: parameterized builder; the stamp and the commit are arguments, not the clock", + "check.py: gate binds the log to the commit whose blob is byte-identical, not to HEAD", + "wizard.log.md: self-describing entry carrying its own stamp and its carrying commit", ] EVIDENCE = [ - ("wizard.log.md exists", "measured", "path present under wizard-work/null/"), - ("wizard.py compiles", "measured", "compile() in check.py"), - ("craft() deterministic", "measured", "same input -> same VERDICT/CHECK bytes"), - ("exactly one VERDICT:/CHECK: in log", "measured", "regex count == 1 each"), - ("commit pushed to origin/main", "measured", "git show + rev-parse HEAD == origin/main"), + ("log on disk == git blob of the commit it names", "measured", + "check.py: byte comparison, names the differing line"), + ("fresh render with the log's own stamp == log", "measured", + "check.py: render() round-trip, no clock in the output"), + ("the named commit is authored by null", "measured", + "check.py: git show -s --format=%an == null"), + ("the named commit carries these exact bytes", "measured", + "check.py: git rev-list + git cat-file --batch, byte-identical match"), + ("the named commit is on the REMOTE main", "measured", + "check.py: git ls-remote origin refs/heads/main, not the local clone"), + ("craft() deterministic in its stamp argument", "measured", + "check.py: same args -> same bytes; different stamp -> different verdict"), ] -def craft(task, area, lane, commit, operator): - """Build one standup entry. Returns (verdict, check, summary).""" - stamp = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC") +def parse_log(text): + """Pull the fields the gate re-checks out of a rendered log.""" + fields = {} + for line in text.splitlines(): + s = line.strip() + for key in ("lane", "operator", "target area", "base commit", "log"): + if s.startswith("- " + key + ": "): + fields[key] = s[len("- " + key + ": "):] + if s.startswith("VERDICT: ") and "self-verified at " in s: + fields["stamp"] = s.split("self-verified at ", 1)[1].rstrip(" .") + return fields + + +def craft(task, area, lane, commit, operator, stamp, log=LOGNAME): + """Build one standup entry. stamp is a parameter: replay is reproducible.""" verdict = "VERDICT: %s - %s built and self-verified at %s." % (task, area, stamp) check = "CHECK: python3 %s/check.py" % HERE summary = "%s lane entry: %s" % (lane, "; ".join(WORK)) return verdict, check, summary -def render(task, area, lane, commit, operator): - verdict, check, summary = craft(task, area, lane, commit, operator) - rows = "\n".join( - "| %s | %s | %s |" % (claim, kind, proof) - for claim, kind, proof in EVIDENCE - ) +def render(task, area, lane, commit, operator, stamp, work=WORK, log=LOGNAME): + """Render the entry. Pure in its arguments: no clock, no filesystem.""" + verdict, check, _ = craft(task, area, lane, commit, operator, stamp, log=log) + rows = "\n".join("| %s | %s | %s |" % (c, k, p) for c, k, p in EVIDENCE) return """# null - standup entry - lane: %s - operator: %s - target area: %s - base commit: %s +- log: %s ## what was built @@ -77,18 +115,61 @@ def render(task, area, lane, commit, operator): python3 %s/wizard.py python3 %s/check.py ``` -""" % (lane, operator, area, commit, - "\n".join("- " + w for w in WORK), rows, - verdict, check, HERE, HERE) +""" % (lane, operator, area, commit, log, + "\n".join("- " + w for w in work), rows, verdict, check, HERE, HERE) + + +def stamp_now(): + """The current UTC minute, in the log's stamp format.""" + return datetime.now(timezone.utc).strftime(STAMP_FMT) + + +def restamp(text, stamp=None): + """Replace the stamp in a rendered log. Deterministic: no clock call here.""" + return STAMP_RE.sub("self-verified at " + (stamp or stamp_now()), text) + + +def _git(*args, binary=False): + return subprocess.run(["git", "-C", REPO] + list(args), + capture_output=True, text=not binary) + + +def head_sha(): + """HEAD, for orientation only. Never used as the entry's base commit.""" + return _git("rev-parse", "HEAD").stdout.strip() or "unresolved" + + +def carrying_commit(text): + """The commit whose tree contains exactly `text`, searching main's history. + + Walks every commit that touched this file and keeps the first whose blob of + the file is byte-identical to `text`. Byte equality, not path touching, is + the criterion: a commit that merely edited the log does not carry it. + """ + r = _git("rev-list", "origin/main", "--", LOGPATH) + if r.returncode != 0 or not r.stdout.strip(): + return "" + want = text.encode() + for sha in r.stdout.split(): + blob = _git("show", "%s:%s" % (sha, LOGPATH), binary=True) + if blob.returncode == 0 and blob.stdout == want: + return sha + return "" def main(): - task, area = "fix the build", "staging" - lane, commit, operator = LANE, "d7a1f", "alex" - text = render(task, area, lane, commit, operator) - with open(HERE + "/wizard.log.md", "w") as fh: + path = os.path.join(HERE, LOGNAME) + stamp = parse_log(open(path, encoding="utf-8").read()).get("stamp") if os.path.exists(path) else None + if not stamp: + stamp = stamp_now() # first build only; replays reuse the committed stamp + text = render(TASK, AREA, LANE, "", OPERATOR, stamp) + commit = carrying_commit(text) if text else "" + if not commit: + commit = "unresolved" # the gate refuses to pass on this + text = render(TASK, AREA, LANE, commit, OPERATOR, stamp) + with open(path, "w", encoding="utf-8") as fh: fh.write(text) - verdict, check, _ = craft(task, area, lane, commit, operator) + verdict, check, _ = craft(TASK, AREA, LANE, commit, OPERATOR, stamp) print(verdict) print(check)