diff --git a/.github/time/test_time.py b/.github/time/test_time.py index 4482f4f..3e4a9d9 100644 --- a/.github/time/test_time.py +++ b/.github/time/test_time.py @@ -1029,6 +1029,13 @@ assert gv.count_signups(w79)[0] == 3, "a 👍 on the signal issue counts; a hear assert gv.signup_for(f79.rows[1], w79) and not gv.signup_for(f79.rows[4], w79) and not gv.signup_for(f79.rows[5], w79) # the field wins over the title: a form answered for another venture never counts here, whatever its title assert not gv.signup_for({"title": "Early access: Star checker", "body": "### Venture\n\nother\n\n### Plan\n\nFree\n\n### What would you use it for?\n\nx"}, w79) +# the field is typed by a person: the name, any spelling of the slug, the home folder or an old slug all count +form79 = lambda v: {"title": "x", "body": f"### Venture\n\n{v}\n\n### Plan\n\nFree\n\n### What would you use it for?\n\nx"} +assert all(gv.signup_for(form79(v), w79) for v in ("Star Checker", "star checker!", "STAR-CHECKER", "starchecker")) +assert gv.signup_for(form79("Cartridge"), {"slug": "box-art-night", "name": "Cartridge"}) +assert gv.signup_for(form79("cartridge"), {"slug": "box-art-night", "name": "Box", "home": "cartridge"}) +assert gv.signup_for(form79("deck preview"), {"slug": "mdecks", "name": ".mdeck", "aliases": ["deck-preview"]}) +assert not gv.signup_for(form79("Star"), w79) and not gv.signup_for(form79("!!"), w79) # the helpers the port leans on: removing a label goes by id; the browser pass clones with the token like every worker gv.tm.unlabel(5, "needs-human") assert f79.calls[-1][:2] == ("DELETE", "/repos/o/r/issues/5/labels/103"), f79.calls[-1] diff --git a/.github/time/venture.py b/.github/time/venture.py index 804bc73..20c94af 100644 --- a/.github/time/venture.py +++ b/.github/time/venture.py @@ -1277,14 +1277,23 @@ def from_form(body: str) -> bool: return all(re.search(rf"^###\s*{re.escape(label)}\s*$", body or "", re.M) for label in FIELDS.values()) +def _key(s) -> str: + return re.sub(r"[^a-z0-9]+", "", _s(s).lower()) + + +def names_of(w: dict) -> set[str]: + """Every way a person may name a venture in the form: its slug, its name, its home folder, its old slugs.""" + return {k for k in map(_key, (w.get("slug"), w.get("name"), w.get("home"), *(w.get("aliases") or []))) if k} + + def signup_for(i: dict, w: dict) -> bool: """Is this early-access issue for venture `w`? The form's Venture field, or, when that was left empty, the title the join link prefilled. The title counts only on a body the form wrote: an issue someone typed by hand with the same title is not a signup. Either way it is one account, counted once, like a 👍 on the signal issue.""" body = i.get("body") or "" f = form_fields(body) - if f["venture"].strip(): - return f["venture"].strip().lower() == w["slug"] + if f["venture"].strip(): # typed by a person: "Cartridge", "Loot Drop" and "loot-drop" all count + return _key(f["venture"]) in names_of(w) return from_form(body) and _s(i.get("title")).lower() == f"early access: {_s(w.get('name'))}".lower() diff --git a/CHANGELOG.md b/CHANGELOG.md index 739e0c6..fd66ff1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ - The second fix round escalates to a different model from the first round: `claude:opus` with the Claude seats (it was `venice:kimi code`, and grok 4.7 before that). `TIME_VENTURE_ESCALATE` overrides it. ### Added +- A signup's Venture field matches the venture's slug, name, home folder or an old slug, ignoring case, spaces and punctuation. The field is typed by a person (Teahub does not prefill it from the link), who writes "Cartridge" or "Loot Drop", not `box-art-night`; those signups were not counted. - A worker's tool call that is cut off at the output limit, or arrives with malformed arguments, is an error the worker reads on its next step instead of a crash that throws the run away. The first Claude venture builds (time#148, #149) died on `'content'`: a whole launch page written in one `write_file` ran past the output limit and arrived with its path and no content. The error says so and asks for smaller files. - 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.