From 23d63830603e9ce37afa353e694abd935c819e55 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 11 Oct 2026 16:59:41 -0700 Subject: [PATCH] Worker: runs can keep the files they generate grok's Docscope round generated a real sample .docx and the finished page with scripts inside run, then tried to cp them into place: each run starts from a fresh checkout, so none of it reached the branch and the round burned its steps. The run tool now takes keep: [paths] and commits exactly those files afterwards (inside the checkout, not workflows, up to 2 MB, at most ten), and the rules say plainly that everything else a run does is thrown away. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01UpT7eWzeLhRwuYmsSQnbRb --- .github/time/test_time.py | 15 +++++++++++++++ .github/time/time.py | 37 ++++++++++++++++++++++++++++++++----- 2 files changed, 47 insertions(+), 5 deletions(-) diff --git a/.github/time/test_time.py b/.github/time/test_time.py index 5793770..81e1b82 100644 --- a/.github/time/test_time.py +++ b/.github/time/test_time.py @@ -736,3 +736,18 @@ assert any("below the first screen" in p for p in vt.layout_problems({"phone": { b31 = vt.build_body({**w16, "prior": "ventures/mdecks/v0.html", "prior_note": "Keep its look and layout."}, plan16) assert "ventures/mdecks/v0.html" in b31 and "Keep its look and layout." in b31 print("ALL OK 23") + +# 32 a run can land the files it generated (keep), and nothing outside the checkout or in workflows +import tempfile as _tf +d32 = _tf.mkdtemp() +os.makedirs(os.path.join(d32, "ventures/x")) +W32 = World([card(1, D)]) +tm.http = W32.http +h = tm.Hands.__new__(tm.Hands) +h.n, h.branch, h.pr, h.steps, h.checkout = 1, "gh-pages", None, 0, None +h._checkout = lambda: d32 +out = h.call("run", {"command": "printf 'const SAMPLE=1;' > ventures/x/sample.js && echo ok > junk.txt", + "keep": ["ventures/x/sample.js", "../../etc/passwd", ".github/workflows/x.yml"]}) +assert "ventures/x/sample.js: kept" in out and "refused or missing" in out, out +assert W32.files.get("ventures/x/sample.js") == "const SAMPLE=1;" and "junk.txt" not in W32.files +print("ALL OK 24") diff --git a/.github/time/time.py b/.github/time/time.py index 25571d5..75dade5 100644 --- a/.github/time/time.py +++ b/.github/time/time.py @@ -400,8 +400,13 @@ WORKER_TOOLS = [ {"type": "function", "function": {"name": "write_file", "description": "Create or replace one file on the branch with the full new content (commits it).", "parameters": {"type": "object", "properties": {"path": {"type": "string"}, "content": {"type": "string"}, "message": {"type": "string"}}, "required": ["path", "content"]}}}, - {"type": "function", "function": {"name": "run", "description": "Run a shell command on a checkout of the branch (tests, lint). Returns exit code and output.", - "parameters": {"type": "object", "properties": {"command": {"type": "string"}}, "required": ["command"]}}}, + {"type": "function", "function": {"name": "run", "description": "Run a shell command on a fresh checkout of the branch (tests, checks, " + "generators). Returns exit code and output. Whatever the command creates or changes is thrown away, except the paths " + "listed in `keep`, which are committed to the branch afterwards.", + "parameters": {"type": "object", "properties": {"command": {"type": "string"}, + "keep": {"type": "array", "items": {"type": "string"}, + "description": "Repo-relative paths the command created or changed that should land on the branch."}}, + "required": ["command"]}}}, {"type": "function", "function": {"name": "open_pr", "description": "Open the pull request when the work is done and checked. Call this exactly once, last.", "parameters": {"type": "object", "properties": {"title": {"type": "string"}, "summary": {"type": "string", "description": "What changed and how you checked it."}}, "required": ["title", "summary"]}}}, @@ -411,8 +416,9 @@ WORKER_RULES = """You are the worker for card #{n} in {repo}: "{title}". {body} -You have hands: list_files, read_file, write_file (one file per call, full content), run (shell on a checkout of your -branch), open_pr. Work on the branch `{branch}`; everything you write lands there. Keep the change small and focused. +You have hands: list_files, read_file, write_file (one file per call, full content), run (shell on a fresh checkout of +your branch), open_pr. Work on the branch `{branch}`. Only two things reach it: write_file, and files a run lists in +`keep` (for generated files, e.g. a sample built by a script). Anything else a run does is thrown away. Keep the change small and focused. Read before you write. Always `run` the project's tests or checks before opening the PR (that run is the PR's check). When it is done and checked, call open_pr once with a short summary; the PR must close the card, and Time adds that line for you. Do not touch .github/workflows. The card's finish line is: DONE: {done}""" @@ -472,12 +478,33 @@ class Hands: except subprocess.TimeoutExpired: # a server or watcher never exits: tell the model, don't crash the card return (f"timed out after {limit}s and was stopped. Servers and watchers never exit; run one-shot commands " f"(a parser, a test script, node -e '...') instead") + kept = [] + root = os.path.realpath(self.checkout) + for rel in (a.get("keep") or [])[:10]: + full = os.path.realpath(os.path.join(root, str(rel))) + rel = os.path.relpath(full, root) + if not full.startswith(root + os.sep) or rel.startswith(".github/workflows") or not os.path.isfile(full): + kept.append(f"{rel}: refused or missing") + continue + data = open(full, "rb").read() + if len(data) > 2_000_000: + kept.append(f"{rel}: too big to keep") + continue + body = {"message": f"card #{self.n}: {rel} (generated)", "branch": self.branch, "content": base64.b64encode(data).decode()} + try: + body["sha"] = gh("GET", f"/repos/{REPO}/contents/{rel}?ref={self.branch}")["sha"] + except RuntimeError: + pass + gh("PUT", f"/repos/{REPO}/contents/{rel}", body) + kept.append(f"{rel}: kept") sha = subprocess.run(["git", "rev-parse", "HEAD"], cwd=self.checkout, capture_output=True, text=True).stdout.strip() # GitHub does not run pull_request workflows for commits made with the Actions token, so the worker's own # test run is recorded as a commit status; land() counts statuses as checks. gh("POST", f"/repos/{REPO}/statuses/{sha}", {"state": "success" if p.returncode == 0 else "failure", "context": "time/worker-run", "description": a["command"][:100]}) - return f"exit {p.returncode}\n{(p.stdout + p.stderr)[-6000:]}" + self.checkout = None if kept else self.checkout + note = ("\n" + "; ".join(kept)) if kept else "" + return f"exit {p.returncode}\n{(p.stdout + p.stderr)[-6000:]}{note}" if name == "open_pr": pr = gh("POST", f"/repos/{REPO}/pulls", {"title": a["title"][:200], "head": self.branch, "base": default_branch(), "body": f"{a['summary']}\n\nCloses #{self.n}\n\nworker: Venice seat via Time"})