Replaces #118 (same change, re-cut from main after #110 and #116 landed, since the old branch collided with #116 in review()).
Two gaps in the worker loop that today's wave exposed, and one ordering fix.
Base: <branch> in a card. Every worker branched from main whatever the card said, and the ventures live on gh-pages (an orphan branch, no shared history). That is why #60 and #63 ran out of turns, #95 died at 43 steps and #109 opened the right files against the wrong branch. Now card_base() reads a Base: <branch> line; Hands cuts the branch from it (git_commit(..., base=)), names it time/card-N-<base> so a stale branch cut from main is never resumed, the worker's rules say where the files live, and open_pr targets the base. Cards without the line are unchanged (time/card-N, default branch).
A launched worker finishes.budget_check() gains a slack that only applies inside finishing(): the worker's model calls and the review of a PR that is already open may run past the daily cap, up to TIME_CAP_SLACK x it (default 1.5). A new launch is still refused at the cap, and the monthly cap is hard everywhere. #87's run was launched at $3.80 and stopped at step 27 when the cap crossed, its spend lost; the reviews of #109/#110 sat unreviewed for the same reason. TIME_CAP_SLACK=1 is the old behaviour. The doctor lists the knob.
Oldest ready card first.tick() sorts cards by number, the order promote_next already uses, instead of the newest-first the API returns.
Tests: 14b (base, branch name, PR target, the rules' wording, card_base parsing) and 57 (the run finishes under the slack; TIME_CAP_SLACK=1 stops it; a new launch gets no slack; the monthly cap stops a run; a review runs at $6 of $5 and is refused at $8; oldest first). Suite: ALL OK 57 locally on this branch, and together with #120; CI runs the same.
Replaces #118 (same change, re-cut from main after #110 and #116 landed, since the old branch collided with #116 in `review()`).
Two gaps in the worker loop that today's wave exposed, and one ordering fix.
**`Base: <branch>` in a card.** Every worker branched from `main` whatever the card said, and the ventures live on `gh-pages` (an orphan branch, no shared history). That is why #60 and #63 ran out of turns, #95 died at 43 steps and #109 opened the right files against the wrong branch. Now `card_base()` reads a `Base: <branch>` line; `Hands` cuts the branch from it (`git_commit(..., base=)`), names it `time/card-N-<base>` so a stale branch cut from `main` is never resumed, the worker's rules say where the files live, and `open_pr` targets the base. Cards without the line are unchanged (`time/card-N`, default branch).
**A launched worker finishes.** `budget_check()` gains a slack that only applies inside `finishing()`: the worker's model calls and the review of a PR that is already open may run past the daily cap, up to `TIME_CAP_SLACK` x it (default 1.5). A new launch is still refused at the cap, and the monthly cap is hard everywhere. #87's run was launched at $3.80 and stopped at step 27 when the cap crossed, its spend lost; the reviews of #109/#110 sat unreviewed for the same reason. `TIME_CAP_SLACK=1` is the old behaviour. The doctor lists the knob.
**Oldest ready card first.** `tick()` sorts cards by number, the order `promote_next` already uses, instead of the newest-first the API returns.
Tests: `14b` (base, branch name, PR target, the rules' wording, `card_base` parsing) and `57` (the run finishes under the slack; `TIME_CAP_SLACK=1` stops it; a new launch gets no slack; the monthly cap stops a run; a review runs at $6 of $5 and is refused at $8; oldest first). Suite: `ALL OK 57` locally on this branch, and together with #120; CI runs the same.
Closes #117
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_017K4vyY3yUD3GsvoZaAtKkD
A `Base: <branch>` line in a card makes the seat worker cut its branch from that base (named time/card-N-<base>), see
that branch's files and open the PR against it: every venture card lives on gh-pages and until now the worker always
branched from main, which is why #60, #63, #95 ran out of turns and #109 targeted the wrong branch.
The daily spend cap gates launches; a run that began within budget may finish, up to TIME_CAP_SLACK x the cap (1.5),
and a PR already open gets its review on a capped day too (#87's run was stopped at step 27 with the spend lost). The
monthly cap stays hard. The tick takes the oldest ready card first, as promote_next does.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017K4vyY3yUD3GsvoZaAtKkD
CI is green (ci / test success), and the diff does what card #117 asks: Base: support, the slack, and oldest-first ordering. I did not push anything.
time.pycard_base / card_branch (around the BASE_LINE regex):Base: gh-pages and Base: \release/1.x`both parse. Cards without the line keeptime/card-N` and the default branch, as the card requires.
time.pyHands.__init__: the branch is cut from base, so a stale time/card-N cut from main is never resumed for a gh-pages card.
time.pyvenice_work:open_pr targets self.base or default_branch(). The worker rules get base_note only when a base exists, so the prompt is unchanged otherwise.
time.pyfinishing() / budget_check(): the slack is thread-local and applies only inside finishing(). Only venice_work model calls (both the primary and fallback paths) and review() are wrapped. A new launch still hits the plain daily cap, and the monthly cap is checked before the slack in all cases. TIME_CAP_SLACK=1 gives the old stop.
time.pycmd_doctor: the new TIME_CAP_SLACK row is listed.
test_time.py 14b and 57: cover the base branch name, the PR target, the prompt wording, card_base parsing, the slack finish, the no-slack stop, the new-launch refusal, the monthly cap, the review at $6 and refusal at $8, and the order.
CHANGELOG.md: entries under Added.
Points to confirm before merge (not blockers):
The diff does not show the whole file. Any other hard-coded time/card-{n} in lookups (for example finding an open PR by head branch, or the /time retry path) would now miss time/card-N-<base> cards. Worth a grep for time/card- before merge.
Worst-case daily spend is now roughly TIME_DAILY_USD × TIME_CAP_SLACK plus overlap from concurrent workers (#75). Each run that started under budget can run to 1.5× the cap, so two overlapping runs started at $4.99 can both finish. Consider a note in the README or the doctor line.
BASE_LINE matches any body line starting with Base:. That is fine for cards, but a card that quotes prose like "Base: the default" would be misread. It would only fail if the captured token has no spaces and matches the character class, so the risk is low.
review by haiku · claude-haiku-5-5 · via Time
**Approve**
CI is green (`ci / test` success), and the diff does what card #117 asks: `Base:` support, the slack, and oldest-first ordering. I did not push anything.
- **`time.py` `card_base` / `card_branch` (around the `BASE_LINE` regex):** `Base: gh-pages` and `Base: \`release/1.x\`` both parse. Cards without the line keep `time/card-N` and the default branch, as the card requires.
- **`time.py` `Hands.__init__`:** the branch is cut from `base`, so a stale `time/card-N` cut from `main` is never resumed for a `gh-pages` card.
- **`time.py` `venice_work`:** `open_pr` targets `self.base or default_branch()`. The worker rules get `base_note` only when a base exists, so the prompt is unchanged otherwise.
- **`time.py` `finishing()` / `budget_check()`:** the slack is thread-local and applies only inside `finishing()`. Only `venice_work` model calls (both the primary and fallback paths) and `review()` are wrapped. A new launch still hits the plain daily cap, and the monthly cap is checked before the slack in all cases. `TIME_CAP_SLACK=1` gives the old stop.
- **`time.py` `tick()`:** `sorted(..., key=number)` matches `promote_next`.
- **`time.py` `cmd_doctor`:** the new `TIME_CAP_SLACK` row is listed.
- **`test_time.py` 14b and 57:** cover the base branch name, the PR target, the prompt wording, `card_base` parsing, the slack finish, the no-slack stop, the new-launch refusal, the monthly cap, the review at $6 and refusal at $8, and the order.
- **`CHANGELOG.md`:** entries under Added.
Points to confirm before merge (not blockers):
1. The diff does not show the whole file. Any other hard-coded `time/card-{n}` in lookups (for example finding an open PR by head branch, or the `/time retry` path) would now miss `time/card-N-<base>` cards. Worth a grep for `time/card-` before merge.
2. Worst-case daily spend is now roughly `TIME_DAILY_USD × TIME_CAP_SLACK` plus overlap from concurrent workers (#75). Each run that started under budget can run to 1.5× the cap, so two overlapping runs started at $4.99 can both finish. Consider a note in the README or the doctor line.
3. `BASE_LINE` matches any body line starting with `Base:`. That is fine for cards, but a card that quotes prose like "Base: the default" would be misread. It would only fail if the captured token has no spaces and matches the character class, so the risk is low.
<sub>review by haiku · claude-haiku-5-5 · via Time</sub>
Note for the record (no verdict, haiku's review stands): this PR's head carries a heartbeat / tick (pull_request_review): failure status from a heartbeat run the concurrency group cancelled when haiku's review landed mid-heartbeat. That is not a failing check (#124 makes checks() ignore Time's own workflow statuses); this comment re-fires the review event while the runner is idle so the status is recomputed.
Note for the record (no verdict, haiku's review stands): this PR's head carries a `heartbeat / tick (pull_request_review): failure` status from a heartbeat run the concurrency group cancelled when haiku's review landed mid-heartbeat. That is not a failing check (#124 makes `checks()` ignore Time's own workflow statuses); this comment re-fires the review event while the runner is idle so the status is recomputed.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Replaces #118 (same change, re-cut from main after #110 and #116 landed, since the old branch collided with #116 in
review()).Two gaps in the worker loop that today's wave exposed, and one ordering fix.
Base: <branch>in a card. Every worker branched frommainwhatever the card said, and the ventures live ongh-pages(an orphan branch, no shared history). That is why #60 and #63 ran out of turns, #95 died at 43 steps and #109 opened the right files against the wrong branch. Nowcard_base()reads aBase: <branch>line;Handscuts the branch from it (git_commit(..., base=)), names ittime/card-N-<base>so a stale branch cut frommainis never resumed, the worker's rules say where the files live, andopen_prtargets the base. Cards without the line are unchanged (time/card-N, default branch).A launched worker finishes.
budget_check()gains a slack that only applies insidefinishing(): the worker's model calls and the review of a PR that is already open may run past the daily cap, up toTIME_CAP_SLACKx it (default 1.5). A new launch is still refused at the cap, and the monthly cap is hard everywhere. #87's run was launched at $3.80 and stopped at step 27 when the cap crossed, its spend lost; the reviews of #109/#110 sat unreviewed for the same reason.TIME_CAP_SLACK=1is the old behaviour. The doctor lists the knob.Oldest ready card first.
tick()sorts cards by number, the orderpromote_nextalready uses, instead of the newest-first the API returns.Tests:
14b(base, branch name, PR target, the rules' wording,card_baseparsing) and57(the run finishes under the slack;TIME_CAP_SLACK=1stops it; a new launch gets no slack; the monthly cap stops a run; a review runs at $6 of $5 and is refused at $8; oldest first). Suite:ALL OK 57locally on this branch, and together with #120; CI runs the same.Closes #117
🤖 Generated with Claude Code
https://claude.ai/code/session_017K4vyY3yUD3GsvoZaAtKkD
Approve
CI is green (
ci / testsuccess), and the diff does what card #117 asks:Base:support, the slack, and oldest-first ordering. I did not push anything.time.pycard_base/card_branch(around theBASE_LINEregex):Base: gh-pagesandBase: \release/1.x`both parse. Cards without the line keeptime/card-N` and the default branch, as the card requires.time.pyHands.__init__: the branch is cut frombase, so a staletime/card-Ncut frommainis never resumed for agh-pagescard.time.pyvenice_work:open_prtargetsself.base or default_branch(). The worker rules getbase_noteonly when a base exists, so the prompt is unchanged otherwise.time.pyfinishing()/budget_check(): the slack is thread-local and applies only insidefinishing(). Onlyvenice_workmodel calls (both the primary and fallback paths) andreview()are wrapped. A new launch still hits the plain daily cap, and the monthly cap is checked before the slack in all cases.TIME_CAP_SLACK=1gives the old stop.time.pytick():sorted(..., key=number)matchespromote_next.time.pycmd_doctor: the newTIME_CAP_SLACKrow is listed.test_time.py14b and 57: cover the base branch name, the PR target, the prompt wording,card_baseparsing, the slack finish, the no-slack stop, the new-launch refusal, the monthly cap, the review at $6 and refusal at $8, and the order.CHANGELOG.md: entries under Added.Points to confirm before merge (not blockers):
time/card-{n}in lookups (for example finding an open PR by head branch, or the/time retrypath) would now misstime/card-N-<base>cards. Worth a grep fortime/card-before merge.TIME_DAILY_USD × TIME_CAP_SLACKplus overlap from concurrent workers (#75). Each run that started under budget can run to 1.5× the cap, so two overlapping runs started at $4.99 can both finish. Consider a note in the README or the doctor line.BASE_LINEmatches any body line starting withBase:. That is fine for cards, but a card that quotes prose like "Base: the default" would be misread. It would only fail if the captured token has no spaces and matches the character class, so the risk is low.review by haiku · claude-haiku-5-5 · via Time
Note for the record (no verdict, haiku's review stands): this PR's head carries a
heartbeat / tick (pull_request_review): failurestatus from a heartbeat run the concurrency group cancelled when haiku's review landed mid-heartbeat. That is not a failing check (#124 makeschecks()ignore Time's own workflow statuses); this comment re-fires the review event while the runner is idle so the status is recomputed.