Ponytail on prod: seat workers carry its rules (#145)
ci / test (push) Successful in 36s

Co-authored-by: teahubxyz <boring+teahub@agentmail.to>
This commit was merged in pull request #145.
This commit is contained in:
teahubxyz authored and time committed 2026-10-18 17:24:33 +00:00
1 parent 4b521f7a86
commit 8ab719e2f2
5 files changed
+74 -1

No files matched your search

+21
View File
@@ -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.
+30
View File
@@ -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: <the limit>, <when to upgrade>`.
Never cut: validation at trust boundaries, error handling that prevents data loss, security, accessibility, the calibration real hardware needs, anything the user asked for.
+6
View File
@@ -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
+16 -1
View File
@@ -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},
+1
View File
@@ -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_<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.