From 8ab719e2f23ffde5dbe8cb2bc1428df61d9da51c Mon Sep 17 00:00:00 2001 From: teahubxyz Date: Sun, 18 Oct 2026 17:24:33 +0000 Subject: [PATCH] Ponytail on prod: seat workers carry its rules (#145) Co-authored-by: teahubxyz --- .github/time/rules/PONYTAIL-LICENSE | 21 ++++++++++++++++++++ .github/time/rules/ponytail.md | 30 +++++++++++++++++++++++++++++ .github/time/test_time.py | 6 ++++++ .github/time/time.py | 17 +++++++++++++++- CHANGELOG.md | 1 + 5 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 .github/time/rules/PONYTAIL-LICENSE create mode 100644 .github/time/rules/ponytail.md diff --git a/.github/time/rules/PONYTAIL-LICENSE b/.github/time/rules/PONYTAIL-LICENSE new file mode 100644 index 0000000..715d483 --- /dev/null +++ b/.github/time/rules/PONYTAIL-LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 DietrichGebert + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/.github/time/rules/ponytail.md b/.github/time/rules/ponytail.md new file mode 100644 index 0000000..b314fa6 --- /dev/null +++ b/.github/time/rules/ponytail.md @@ -0,0 +1,30 @@ +# Ponytail, lazy senior dev mode + +You are a lazy senior developer. The best code is the code never written. You solve the whole problem with the least new code. End your reply with one or two lines: what you skipped or did not check, and any risk the user must know. + +## Before you write + +Read the task and the code it touches. List every place your change must reach: callers, tests, fixtures, config, exports. Check what your change could break for users: data it would destroy or expose, callers that stop working. That is scope. Extra features are not. + +## The smallest complete change + +Take the first option that fully works: + +1. Does it need to exist? Skip features, options and flexibility nobody asked for, and name them in one line. A vague request ("build me X") gets the smallest version that does the core job. +2. Already in this codebase (a helper, component, service, pattern)? Use it the way the surrounding code does. +3. Standard library or a platform feature? Use it, unless the project has its own. A house component beats a native widget. +4. An installed dependency? Use it. Never add a dependency for a few lines. +5. Can it be one line a reader gets at a glance? One line. +6. Otherwise: the minimum code that works. + +- Be lazy about the solution, never about the change itself: finish every part the task needs, including the callers, tests and fixtures your change breaks. +- No abstraction, wrapper, type conversion, option, config, boilerplate or "for later" code nobody asked for. Keep values in the form the platform already gives you. Deletion beats addition. Keep the structure the codebase already has: its layers, interfaces and conventions. +- The shortest working diff wins, once you know everything it must touch. A one-liner that needs decoding is not short. +- Comment only the why the code cannot show, in one line. +- Bug fix: before you edit, grep every caller of the function you touch, then fix the root cause once in the shared code. +- Code you move or merge keeps its error handling and validation. +- Between options of equal size, take the one that is correct on edge cases. +- Lazy code without its check is unfinished: new non-trivial logic (a branch, a loop, a parser, money or security, or a whole new script or app) leaves one small test or an assert-based self-check. Trivial changes need none. +- A shortcut with a known limit gets a code comment in this form: `shortcut: , `. + +Never cut: validation at trust boundaries, error handling that prevents data loss, security, accessibility, the calibration real hardware needs, anything the user asked for. diff --git a/.github/time/test_time.py b/.github/time/test_time.py index 9b91bef..c771397 100644 --- a/.github/time/test_time.py +++ b/.github/time/test_time.py @@ -249,6 +249,7 @@ run(World([card(1, D)], {1: [beat("dispatched", launched=True)]}, [prh], None, c assert "# Checks\n- ci: pending\n- lint: success\n\n# Diff" in seen["user"], seen["user"] assert "means that context reported success" in seen["system"] and "tests or builds passed" not in seen["system"] assert "evidence you can rely on" in seen["system"] and "pending check is not yet evidence" in seen["system"] +assert "lazy senior developer" not in seen["system"], "a reviewer never gets Ponytail: it reviews, it does not write the diff" run(World([card(1, D)], {1: [beat("dispatched", launched=True)]}, [prh]), {"TIME_MODE": "live", "CURSOR_API_KEY": "ck", "TIME_REVIEW": "venice:kimi"}) assert "# Checks\n(checks unavailable)\n\n# Diff" in seen["user"], seen["user"] @@ -383,6 +384,11 @@ w = run(World([card(1, "Base: gh-pages\n\n" + D)], {1: [beat("would-dispatch")]} assert w.branches == ["time/card-1-gh-pages"] and w.bases == {"time/card-1-gh-pages": "gh-pages"}, (w.branches, getattr(w, "bases", None)) assert w.opened[0]["head"] == "time/card-1-gh-pages" and w.opened[0]["base"] == "gh-pages", w.opened assert "cut from `gh-pages`" in w.venice_system and "targets `gh-pages`" in w.venice_system +# 14c the worker carries Ponytail's rules (it writes code); TIME_PONYTAIL=off leaves them out +assert "You are a lazy senior developer" in w.venice_system and "Never cut: validation at trust boundaries" in w.venice_system +w_off = run(World([card(1, D)], {1: [beat("would-dispatch")]}), {**K, "TIME_WORKER": "venice:kimi code", "VENICE_API_KEY": "vk", "TIME_PONYTAIL": "off"}) +assert "lazy senior developer" not in w_off.venice_system and w_off.opened, "off: the same build, without the rules" +os.environ.pop("TIME_PONYTAIL", None) assert "on branch `time/card-1-gh-pages`" in w.posted[0][1], w.posted[0] assert tm.card_base({"body": "Base: `release/1.x`\nDONE: x"}) == "release/1.x" and tm.card_base({"body": "rebase: no\nDONE: x"}) is None assert tm.card_branch({"number": 5, "body": D}) == "time/card-5" and w.bases.get("time/card-1") is None diff --git a/.github/time/time.py b/.github/time/time.py index 57c3bc1..0431b78 100644 --- a/.github/time/time.py +++ b/.github/time/time.py @@ -1108,6 +1108,21 @@ def worker_seat_model(spec: str) -> tuple[str, str, str | None]: return seat, model or SEATS[seat].get("aliases", {}).get("code") or SEATS[seat]["default"], effort +PONYTAIL = HERE / "rules" / "ponytail.md" # vendored from DietrichGebert/ponytail (MIT, rules/PONYTAIL-LICENSE) + + +def ponytail() -> str: + """Ponytail's rules (the smallest complete change; say what you skipped) for a seat that writes code: the worker, + and the venture builder through it. Reviewers, the planner and the board never get them: it is a coding ruleset, + and its closing "what I skipped" line distorts an answer that isn't a diff. TIME_PONYTAIL=off turns it off.""" + if (os.environ.get("TIME_PONYTAIL") or "on").strip().lower() == "off": + return "" + try: + return "\n\n" + PONYTAIL.read_text() + except OSError: # a checkout without the rules still builds + return "" + + def venice_work(card: dict, done: str, spec: str, hands_cls=None, review: str | None = None, steps: int | None = None) -> dict: """Run one card with a seat (Venice or Claude) as the worker. Returns {pr, steps, seat, model}. The name is historical; venture.py and the tests call it. TIME_WORKER_FALLBACK (default `grok build` for a Venice worker, @@ -1122,7 +1137,7 @@ def venice_work(card: dict, done: str, spec: str, hands_cls=None, review: str | base_note = (f" It is cut from `{base}` and your pull request targets `{base}`, not the default branch: that is where " "this card's files live.") if base else "" system = WORKER_RULES.format(n=card["number"], repo=REPO, title=card["title"], - body=(card.get("body") or "")[:20000], branch=hands.branch, done=done, base_note=base_note) + body=(card.get("body") or "")[:20000], branch=hands.branch, done=done, base_note=base_note) + ponytail() if review is not None: # answering a review: same branch, and the reviewer's words go in marked as theirs system += REVISE_RULES.format(branch=hands.branch, review=review or "(no review comments were found; check the PR)") msgs = [{"role": "system", "content": system}, diff --git a/CHANGELOG.md b/CHANGELOG.md index 041a6c0..c5fd174 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ - The second fix round escalates to Kimi's code model (`venice:kimi code`) instead of grok 4.7: still a different model from the first round, at a fraction of the cost. `TIME_VENTURE_ESCALATE` overrides it. ### Added +- Ponytail on prod: a seat worker (and so the venture builder) carries Ponytail's rules in its system prompt: the smallest complete change, reuse before new code, a check for new logic, never cut validation, security or accessibility, and one closing line on what it skipped. Vendored from DietrichGebert/ponytail (MIT) at `.github/time/rules/ponytail.md`, upstream commit 09c434a. Reviewers, Astra and the venture board never get it (a coding ruleset distorts answers that aren't a diff). `TIME_PONYTAIL=off` turns it off. It ran on hb-5's seats since 2026-10-12; prod had never had it. - Time retries its own parked cards. A card parked on `error` or `stuck` with no open PR is dispatched again on the next beat, up to `TIME_AUTO_RETRY` times (default 1; 0 leaves it to a human), instead of waiting for a human `/time retry`; the beats carry `auto_retry`, a seat worker resumes the card's branch with half as many turns again (`TIME_WORKER_STEPS` x 1.5), and a card with an open PR still goes to review. Dry-run retries nothing. The `stuck` and `error` comments say what Time will do next. - Council verdict: `TIME_REVIEW` takes a comma-separated list of seats (`claude:haiku, claude:opus`); every seat reviews each PR and posts as itself, and each identity's latest vote counts once (Time's own comments are one identity). `approve` when approves minus objects reaches `TIME_REVIEW_QUORUM` (default 1), `object` when objects exceed approves. `TIME_REVIEW_TIEBREAK` names one seat (e.g. `venice:kimi`) called only when every standing seat has voted and they split; if it doesn't settle it either, one "the council is split N–N; over to a human" comment with a `needs-human` beat. One seat and quorum 1 behave as before. Codex's ladder (`luna`, `terra`, `sol`, `astra`) is in `seats.json` on the CLI route (its review run lands with #79; the `astra` login is the Codex seat, not the planning tick). - Seats review as themselves: with a `TIME_SEAT_` token (one Gitea user per seat, e.g. `TIME_SEAT_KIMI`) the reviewer's verdict is a real Gitea review by that user (Approve → approved, Object → changes requested, prose → comment), so reviews carry an identity and can count. Without a token, or if Gitea refuses (a seat on its own PR), it is Time's comment as before.