Router: a new venture starts only when the router picks it (TIME_VENTURE=router) #294

Closed
time wants to merge 0 commits from claude/integrate-batch-5-1p4lwf into main
pull from: claude/integrate-batch-5-1p4lwf
Collaborator

From github: claude/integrate-batch-5-1p4lwf (git@github.com:teahubxyz/time.git), brought in by sync.py. Time's council reviews it like any PR.

Commits:

Router: a new venture starts only when the router picks it (TIME_VENTURE=router)

The venture loop had its own clock: one card a day whatever else the board
needed, which is why it sits paused at TIME_VENTURE=off. A new venture costs
what a card costs, so the router should choose it against everything else.

venture.py: TIME_VENTURE=router is a third mode beside on / off. No daily card
on the schedule; a card starts only when asked (TIME_VENTURE_START, the
dispatch input start=true). The 6-hour tick still advances and measures the
open venture cards, as it does under off, and under off the start input does
nothing, as before.

router.py: new_venture(mode) makes "start a new venture" actionable once Astra
is told the loop is in router mode, so its size and cost come from the seat
like any card's; its why-not says what's missing otherwise. v2_candidates(reg)
adds a "v2 of " for every live venture in ventures/ventures.json
(Pages branch) with signups > 0, shown with its signups. Both have no card
here (n is None), so no pick, no cloud pick and no promotion order reaches
one.

astra.py, live: a picked new venture dispatches venture.yml with start=true
through the Actions API (the call the heartbeat already makes to run CI) and
is never queued as a card. A picked v2 posts the venture's own open v2 card if
the loop wrote one, else writes a card (Base: gh-pages) and queues it. The log
line and the beat's "started" say which.

Tests (test_time.py): router mode writes no daily card and starts one on
TIME_VENTURE_START, off ignores the input, on is unchanged; a picked new
venture dispatches once, queues nothing and writes nothing; a v2 candidate
appears only with signups > 0, and a picked v2 writes and queues a gh-pages
card or names the open one.

Workflow change needed, and not made here (.github/workflows/ is a human's):
astra.yml must pass the venture loop's mode to Astra, or "start a new venture"
stays not actionable however TIME_VENTURE is set. Add this line to the env
block of the python3 .github/time/astra.py tick step in
.github/workflows/astra.yml, under TIME_SPINE: ${{ vars.TIME_SPINE }}:

      TIME_VENTURE_MODE: ${{ vars.TIME_VENTURE }}   # router mode makes "start a new venture" a candidate (#170)

venture.yml needs nothing: it already passes TIME_VENTURE and sets
TIME_VENTURE_START from the start input.

Closes #170

Co-Authored-By: Claude Opus 5 noreply@anthropic.com
Claude-Session: https://claude.ai/code/session_01FAqjv3TYjnKwAGToRLKUgu


Scope a needs-human stall to its dependents and say so on the spec

A card parked for a person (needs-human, error, stuck) already held no
slot (#135), but nothing said what the factory was waiting on and the scoping
was buried in an inline dependency check. Now it is explicit and said once:

  • stall_of(card) returns the beat a ready card is parked at (None while it is
    moving); moving() is that answer inverted.
  • deps_of, waits_on and dependents name the dependency graph promote_next
    already walked: a queued card is held only while a card it waits on is open,
    so a stall holds the chain behind it and nothing else, and the heartbeat log
    prints the dependency each held card waits on. A dependency Time can't read
    still counts as open, and the open ready/queued rows in hand are reused
    instead of refetched.
  • andon_note(card, beat, queued) comments once on the card's spec
    (Part of #N) naming the card, the beat it stalled at, its PR, the cards held
    behind it (transitively) and that everything else keeps moving. The note is
    keyed on the card and the beat's timestamp, so the hourly heartbeat does not
    repeat itself and a card that is retried and parks again is said again. No
    andon label is set: that would stop Astra planning, which is the opposite of
    keeping the queue moving. A card with no spec gets no note.

Tests (#247 in test_time.py): with #244 parked at needs-human, #245 and #246
(which waits on it through #245) stay queued while the independent #250 starts;
the spec gets one comment naming the blocker and what is held behind it; a
second beat adds no second comment; a second stall is said again; and once #244
closes, #245 starts.

test_time.py, test_seed.py and test_housekeeping.py pass. test_delegate.py
fails on main too, untouched by this card (World.http() takes no headers).

Co-Authored-By: Claude Opus 5 noreply@anthropic.com
Claude-Session: https://claude.ai/code/session_01FAqjv5X4dnqh4zPDe99AHj

Closes #247


Mentions are handoffs: @codex and @claude dispatch headless seat runs as their own users (#79)

Two seats beside @sonnet, @opus, @kimi..., neither of them in seats.json:

  • @codex <ask> runs a headless codex exec --sandbox workspace-write --json in a
    throwaway checkout cloned with TIME_SEAT_CODEX, under a wall-clock cap
    (TIME_SEAT_RUN_MINUTES, default 15; the process group is killed at it and nothing
    is pushed). Its last message is the reply, posted as codex. Work left in the tree
    is committed as codex, pushed to codex/issue-N-<stamp> with the seat's token, and
    the PR is opened as codex. The CLI's environment drops TIME_*, ACTIONS_*,
    LANGFUSE_*, GITHUB_TOKEN and the model keys, so the seat's hands never see Time's
    secrets, its own token included; TIME_CODEX_MODEL picks its model.
  • @claude [haiku|sonnet|opus|fable] <ask> runs a Claude seat (opus by default) with
    the repo context Time already assembles for reviews (the README, and on a PR its diff
    and checks) and posts as claude via TIME_SEAT_CLAUDE.

Without the seat's token, or if Gitea refuses the comment, Time posts for it as before.
Every posted reply is signed <sub>codex via Time · <model></sub>.

A reply whose last line starts with another seat's @mention is a handoff: Time runs that
seat on the reply, with no human between the stages. @a x -> @b y -> @c z is a chain of
stages (arrow: →, ->, =>); several seats in one stage, or on separate lines

sync.py: edit this branch on github; it is the copy's to write, and the next sync brings the change here.

From github: `claude/integrate-batch-5-1p4lwf` (git@github.com:teahubxyz/time.git), brought in by sync.py. Time's council reviews it like any PR. Commits: Router: a new venture starts only when the router picks it (TIME_VENTURE=router) The venture loop had its own clock: one card a day whatever else the board needed, which is why it sits paused at TIME_VENTURE=off. A new venture costs what a card costs, so the router should choose it against everything else. venture.py: TIME_VENTURE=router is a third mode beside on / off. No daily card on the schedule; a card starts only when asked (TIME_VENTURE_START, the dispatch input start=true). The 6-hour tick still advances and measures the open venture cards, as it does under off, and under off the start input does nothing, as before. router.py: new_venture(mode) makes "start a new venture" actionable once Astra is told the loop is in router mode, so its size and cost come from the seat like any card's; its why-not says what's missing otherwise. v2_candidates(reg) adds a "v2 of <venture>" for every live venture in ventures/ventures.json (Pages branch) with signups > 0, shown with its signups. Both have no card here (n is None), so no pick, no cloud pick and no promotion order reaches one. astra.py, live: a picked new venture dispatches venture.yml with start=true through the Actions API (the call the heartbeat already makes to run CI) and is never queued as a card. A picked v2 posts the venture's own open v2 card if the loop wrote one, else writes a card (Base: gh-pages) and queues it. The log line and the beat's "started" say which. Tests (test_time.py): router mode writes no daily card and starts one on TIME_VENTURE_START, off ignores the input, on is unchanged; a picked new venture dispatches once, queues nothing and writes nothing; a v2 candidate appears only with signups > 0, and a picked v2 writes and queues a gh-pages card or names the open one. Workflow change needed, and not made here (.github/workflows/ is a human's): astra.yml must pass the venture loop's mode to Astra, or "start a new venture" stays not actionable however TIME_VENTURE is set. Add this line to the env block of the `python3 .github/time/astra.py tick` step in .github/workflows/astra.yml, under `TIME_SPINE: ${{ vars.TIME_SPINE }}`: TIME_VENTURE_MODE: ${{ vars.TIME_VENTURE }} # router mode makes "start a new venture" a candidate (#170) venture.yml needs nothing: it already passes TIME_VENTURE and sets TIME_VENTURE_START from the start input. Closes #170 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FAqjv3TYjnKwAGToRLKUgu --- Scope a needs-human stall to its dependents and say so on the spec A card parked for a person (`needs-human`, `error`, `stuck`) already held no slot (#135), but nothing said what the factory was waiting on and the scoping was buried in an inline dependency check. Now it is explicit and said once: - `stall_of(card)` returns the beat a ready card is parked at (None while it is moving); `moving()` is that answer inverted. - `deps_of`, `waits_on` and `dependents` name the dependency graph `promote_next` already walked: a queued card is held only while a card it waits on is open, so a stall holds the chain behind it and nothing else, and the heartbeat log prints the dependency each held card waits on. A dependency Time can't read still counts as open, and the open `ready`/`queued` rows in hand are reused instead of refetched. - `andon_note(card, beat, queued)` comments once on the card's spec (`Part of #N`) naming the card, the beat it stalled at, its PR, the cards held behind it (transitively) and that everything else keeps moving. The note is keyed on the card and the beat's timestamp, so the hourly heartbeat does not repeat itself and a card that is retried and parks again is said again. No `andon` label is set: that would stop Astra planning, which is the opposite of keeping the queue moving. A card with no spec gets no note. Tests (#247 in test_time.py): with #244 parked at needs-human, #245 and #246 (which waits on it through #245) stay queued while the independent #250 starts; the spec gets one comment naming the blocker and what is held behind it; a second beat adds no second comment; a second stall is said again; and once #244 closes, #245 starts. test_time.py, test_seed.py and test_housekeeping.py pass. test_delegate.py fails on main too, untouched by this card (World.http() takes no `headers`). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FAqjv5X4dnqh4zPDe99AHj Closes #247 --- Mentions are handoffs: @codex and @claude dispatch headless seat runs as their own users (#79) Two seats beside @sonnet, @opus, @kimi..., neither of them in seats.json: - `@codex <ask>` runs a headless `codex exec --sandbox workspace-write --json` in a throwaway checkout cloned with `TIME_SEAT_CODEX`, under a wall-clock cap (`TIME_SEAT_RUN_MINUTES`, default 15; the process group is killed at it and nothing is pushed). Its last message is the reply, posted as `codex`. Work left in the tree is committed as `codex`, pushed to `codex/issue-N-<stamp>` with the seat's token, and the PR is opened as `codex`. The CLI's environment drops `TIME_*`, `ACTIONS_*`, `LANGFUSE_*`, `GITHUB_TOKEN` and the model keys, so the seat's hands never see Time's secrets, its own token included; `TIME_CODEX_MODEL` picks its model. - `@claude [haiku|sonnet|opus|fable] <ask>` runs a Claude seat (opus by default) with the repo context Time already assembles for reviews (the README, and on a PR its diff and checks) and posts as `claude` via `TIME_SEAT_CLAUDE`. Without the seat's token, or if Gitea refuses the comment, Time posts for it as before. Every posted reply is signed `<sub>codex via Time · <model></sub>`. A reply whose last line starts with another seat's @mention is a handoff: Time runs that seat on the reply, with no human between the stages. `@a x -> @b y -> @c z` is a chain of stages (arrow: `→`, `->`, `=>`); several seats in one stage, or on separate lines <sub>sync.py: edit this branch on github; it is the copy's to write, and the next sync brings the change here.</sub>
time added 11 commits 2026-10-29 20:47:53 +00:00
The venture loop had its own clock: one card a day whatever else the board
needed, which is why it sits paused at TIME_VENTURE=off. A new venture costs
what a card costs, so the router should choose it against everything else.

venture.py: TIME_VENTURE=router is a third mode beside on / off. No daily card
on the schedule; a card starts only when asked (TIME_VENTURE_START, the
dispatch input start=true). The 6-hour tick still advances and measures the
open venture cards, as it does under off, and under off the start input does
nothing, as before.

router.py: new_venture(mode) makes "start a new venture" actionable once Astra
is told the loop is in router mode, so its size and cost come from the seat
like any card's; its why-not says what's missing otherwise. v2_candidates(reg)
adds a "v2 of <venture>" for every live venture in ventures/ventures.json
(Pages branch) with signups > 0, shown with its signups. Both have no card
here (n is None), so no pick, no cloud pick and no promotion order reaches
one.

astra.py, live: a picked new venture dispatches venture.yml with start=true
through the Actions API (the call the heartbeat already makes to run CI) and
is never queued as a card. A picked v2 posts the venture's own open v2 card if
the loop wrote one, else writes a card (Base: gh-pages) and queues it. The log
line and the beat's "started" say which.

Tests (test_time.py): router mode writes no daily card and starts one on
TIME_VENTURE_START, off ignores the input, on is unchanged; a picked new
venture dispatches once, queues nothing and writes nothing; a v2 candidate
appears only with signups > 0, and a picked v2 writes and queues a gh-pages
card or names the open one.

Workflow change needed, and not made here (.github/workflows/ is a human's):
astra.yml must pass the venture loop's mode to Astra, or "start a new venture"
stays not actionable however TIME_VENTURE is set. Add this line to the env
block of the `python3 .github/time/astra.py tick` step in
.github/workflows/astra.yml, under `TIME_SPINE: ${{ vars.TIME_SPINE }}`:

          TIME_VENTURE_MODE: ${{ vars.TIME_VENTURE }}   # router mode makes "start a new venture" a candidate (#170)

venture.yml needs nothing: it already passes TIME_VENTURE and sets
TIME_VENTURE_START from the start input.

Closes #170

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FAqjv3TYjnKwAGToRLKUgu
A card parked for a person (`needs-human`, `error`, `stuck`) already held no
slot (#135), but nothing said what the factory was waiting on and the scoping
was buried in an inline dependency check. Now it is explicit and said once:

- `stall_of(card)` returns the beat a ready card is parked at (None while it is
  moving); `moving()` is that answer inverted.
- `deps_of`, `waits_on` and `dependents` name the dependency graph `promote_next`
  already walked: a queued card is held only while a card it waits on is open,
  so a stall holds the chain behind it and nothing else, and the heartbeat log
  prints the dependency each held card waits on. A dependency Time can't read
  still counts as open, and the open `ready`/`queued` rows in hand are reused
  instead of refetched.
- `andon_note(card, beat, queued)` comments once on the card's spec
  (`Part of #N`) naming the card, the beat it stalled at, its PR, the cards held
  behind it (transitively) and that everything else keeps moving. The note is
  keyed on the card and the beat's timestamp, so the hourly heartbeat does not
  repeat itself and a card that is retried and parks again is said again. No
  `andon` label is set: that would stop Astra planning, which is the opposite of
  keeping the queue moving. A card with no spec gets no note.

Tests (#247 in test_time.py): with #244 parked at needs-human, #245 and #246
(which waits on it through #245) stay queued while the independent #250 starts;
the spec gets one comment naming the blocker and what is held behind it; a
second beat adds no second comment; a second stall is said again; and once #244
closes, #245 starts.

test_time.py, test_seed.py and test_housekeeping.py pass. test_delegate.py
fails on main too, untouched by this card (World.http() takes no `headers`).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FAqjv5X4dnqh4zPDe99AHj

Closes #247
Two seats beside @sonnet, @opus, @kimi..., neither of them in seats.json:

- `@codex <ask>` runs a headless `codex exec --sandbox workspace-write --json` in a
  throwaway checkout cloned with `TIME_SEAT_CODEX`, under a wall-clock cap
  (`TIME_SEAT_RUN_MINUTES`, default 15; the process group is killed at it and nothing
  is pushed). Its last message is the reply, posted as `codex`. Work left in the tree
  is committed as `codex`, pushed to `codex/issue-N-<stamp>` with the seat's token, and
  the PR is opened as `codex`. The CLI's environment drops `TIME_*`, `ACTIONS_*`,
  `LANGFUSE_*`, `GITHUB_TOKEN` and the model keys, so the seat's hands never see Time's
  secrets, its own token included; `TIME_CODEX_MODEL` picks its model.
- `@claude [haiku|sonnet|opus|fable] <ask>` runs a Claude seat (opus by default) with
  the repo context Time already assembles for reviews (the README, and on a PR its diff
  and checks) and posts as `claude` via `TIME_SEAT_CLAUDE`.

Without the seat's token, or if Gitea refuses the comment, Time posts for it as before.
Every posted reply is signed `<sub>codex via Time · <model></sub>`.

A reply whose last line starts with another seat's @mention is a handoff: Time runs that
seat on the reply, with no human between the stages. `@a x -> @b y -> @c z` is a chain of
stages (arrow: `→`, `->`, `=>`); several seats in one stage, or on separate lines, run
side by side, as the launcher's grammar already read them. `TIME_HANDOFF_MAX` (default 6)
bounds the seat runs one comment may set off, so a ping-pong ends. No one hands off to
itself, and a mention mid-sentence is still just a mention.

Budget: `codex exec` books one ledger line (`pool: codex`, `launch: 1`, tokens from its
`--json` events, `usd` null -- no price) and is paced by `budget_check("codex")`; the
Claude seat books like every other call. Strangers' mentions are ignored as today.

Tests in test_time.py (`ALL OK 79 handoffs`): the grammar and the handoff rule;
`codex_clone`/`codex_push` against a real bare repo (the token rides the URL, never
.git/config); a trusted `@codex` comment launching the runner with the seat token,
committing and opening the PR as codex, and posting as codex; a stranger's doing
nothing; a reply ending in `@claude ...` dispatching the next seat; an arrow chain
running claude -> codex -> haiku, each stage reading the last; a ledger booking per run;
the wall-clock cap tripping cleanly; the handoff limit; a missing token, a spent budget
and a refused comment.

WORKFLOW CHANGE FOR THE OPERATOR (.github/workflows/ is a protected path, so this
commit stops at the code). In `.github/workflows/mention.yml`:

    -    timeout-minutes: 8
    +    timeout-minutes: 25   # above TIME_SEAT_RUN_MINUTES (15) plus the clone and the handoffs
         steps:
           - uses: actions/checkout@v5
             with:
               sparse-checkout: .github/time
    +      - run: npm install -g @openai/codex@0.159.2   # `@codex` runs the CLI headless
           - run: python3 .github/time/time.py mention
             env:
    @@
               TIME_DAILY_USD: ${{ vars.TIME_DAILY_USD }}
               TIME_MONTHLY_USD: ${{ vars.TIME_MONTHLY_USD }}
    +          TIME_SEAT_CODEX: ${{ secrets.TIME_SEAT_CODEX }}     # the `codex` Gitea user's token
    +          TIME_SEAT_CLAUDE: ${{ secrets.TIME_SEAT_CLAUDE }}   # the `claude` Gitea user's token
    +          CODEX_API_KEY: ${{ secrets.CODEX_API_KEY }}         # or OPENAI_API_KEY: codex's own auth
    +          TIME_SEAT_RUN_MINUTES: ${{ vars.TIME_SEAT_RUN_MINUTES }}
    +          TIME_HANDOFF_MAX: ${{ vars.TIME_HANDOFF_MAX }}
    +          TIME_CODEX_MODEL: ${{ vars.TIME_CODEX_MODEL }}

Until that lands, `@codex` answers "no TIME_SEAT_CODEX secret on this repo yet" or
"`codex` is not installed where this workflow runs", and `@claude` posts as Time.

Closes #79

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FAqjv4VTrzR61LLc4ifsjZ
main brought the steward tick (#292/#80), the planner's 4-6 coarse cards with
their `Depends on:` lines (#284/#244), the loud-no-op router (#283/#211) and the
"a card branch is never rewound to its base" guard (#289/#277). This branch
brings `@codex`/`@claude` as seats with hands and the handoff chain (#79).

Only README.md conflicted, in the feature list: the Mentions bullet is this
branch's (codex, claude, the handoff chain), the Hands-off bullet is main's
(4 to 6 cards, each with what it waits on). Both bullets are kept.

CHANGELOG.md merged under the union driver, so both sides' `### Added` lines
stand. `time.py` and `test_time.py` auto-merged in disjoint regions and were
checked for integration rather than text: `codex_push` pushes with the seat's
own token through its own git calls, so main's new `create=False` gate on
`git_commit` (and `Hands._commit`) does not touch it; `book(model, usage,
**extra)` still takes main's callers unchanged while codex books `pool`/`launch`;
main's new `pull_cord`/`park`/`BranchGone` sit clear of the seat code; and no
definition from either side was dropped.

test_time.py (both sides' markers, `ALL OK 79 handoffs` and `ALL OK 277` /
`ALL OK steward` among them), test_seed.py (3) and test_housekeeping.py (10)
all pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FAqjv1AEsy9dHGKjivMsvS
Six commits landed on main while this card was open (#283 the router's loud
no-op, #284/#244 the planner's coarse cards, #289/#277 a card branch is never
rewound, #292/#80 the steward inside Time). Only `.github/time/time.py`
conflicted; CHANGELOG.md, README.md, SKILL.md and test_time.py merged with both
sides' lines.

- `DEPENDS`: main's `re.compile(r"Depends on:?[ \t]*#(\d+)", re.I)` (#244) is the
  one definition. This card had added a narrower `Depends on #(\d+)` of its own
  further down the file, which as the later binding would have won and stopped
  the new planner's `Depends on: #N` lines from being read at all — and broken
  main's own assertion that `DEPENDS` matches both forms. Removed; `deps_of` now
  reads both, so cards filed before #244 still wait on the right ones.
- `promote_next`: this card's body is kept, since `waits_on` is the inline
  dependency check main edited, lifted out and given the `known` cache and the
  log line. Main's only change there was swapping that `re.findall` for
  `DEPENDS.findall`, which `deps_of` now does.
- `WAITING` gains `parked`. Main's #277 added a fourth way for a card to wait on
  a person: its branch is gone from under an open PR and someone must push it
  back. It keeps its `ready` label, so without this it would have held a
  TIME_PARALLEL slot until a human arrived — exactly the stall #135 removed and
  this card scopes. Now it frees its slot, holds only the cards that wait on it,
  and its spec gets the andon note, with the PR named from the `parked` beat's
  own `pr`. Tested in the #135 block of test_time.py.

test_time.py (66 blocks, test_steward.py included), test_seed.py and
test_housekeeping.py all pass. test_delegate.py fails identically on
origin/main (`World.http()` takes no `headers`): pre-existing, untouched here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FAqjv18qYU68gMq5nsc1Xq
Merge main into the router-venture branch (#170)
ci / test (pull_request) Successful in 1m7s
1816b94ff4
Four PRs landed on main since this branch forked. One file conflicted,
.github/time/astra.py, in two places, and both sides are kept:

- the module docstring: #170's venture paragraph and #211's judge-no-op
  paragraph both stay, with a sentence saying how they meet;
- the new top-level definitions: #170's `venture_registry` and
  `start_venture` beside #211's `JUDGED_SHARE` and `judged_enough`.

The body of `router_tick` merged on its own and keeps both behaviours in
the right order: #211's floor is checked first, so a reply that judged too
little of the board to be a ranking queues nothing, dispatches no venture
and writes no v2 card, and only past that floor does a venture pick reach
`start_venture`. `router.picks` and `router.last_picked` already guard the
`n` of a pick with `isinstance(n, int)`, so a venture pick (`n` is None)
records no outcome row while still marking the venture kind picked today.

#170's own tests needed the floor to pass: `tick170` judged only the
venture candidate it was about, which is 1 of 7 and under a quarter, so
every venture case had become a no-op. Its replies now also judge the
board's five cards at value 3 / size M — what the unjudged default scored
them anyway — so the ranking each case is picked from is unchanged, and
the `on`-mode case asserts the day's pick goes to the best card instead.

CHANGELOG keeps both sides' lines; #170's entry gains the clause about a
judge that ranked nothing starting no venture.

`test_time.py`, `test_seed.py` and `test_housekeeping.py` all pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FAqjv442QyYarwtgjgVQpd
# Conflicts:
#	.github/time/test_time.py
#	.github/time/time.py
#	README.md
# Conflicts:
#	.github/time/astra.py
#	.github/time/router.py
Integrate batch 5: five rebased branches landed as one
ci / test (pull_request) Successful in 1m8s
cb40eb869e
The five branches each worked alone against an older `main` and conflicted
with it and with one another. Merged one at a time, every conflict resolved
by keeping both sides' behaviour, with all three suites green after each
merge. Nothing was left out.

Where the two sides met:

- `promote_next` (#286 against #245/#285): it fills every free slot in one
  pass and returns the cards it started, and a card waiting on a person now
  reaches only the cards that wait on it — the stalled `ready` cards hold no
  slot, their specs get one andon note each, and the gate reads the open
  dependencies through `waits_on` so the log says what each held card waits
  on. `deps_met` stays for the callers and tests that read it directly.
- Astra's picks with no card (#281 against #282): the key decides. A
  `venture:` pick is dispatched or written by `start_venture`; anything else
  is one of the records' own `opt:` findings and is written as a card. Both
  show up in the beat (`started`, `opt`), in the live lines and in the dry
  run, and `router.table` says "would be written as a card" only for an
  `opt:` key, so a venture still reads as "would queue".
- `router.py`'s candidate docstring and the two additive candidate sections
  (#170's `new_venture`/`v2_candidates`, #171's optimization sources) both
  kept.
- README and CHANGELOG keep every side's lines.

Tests: both sides' blocks are kept and run — 80, 135/245/247, 170, 171, 172,
astra 1-3, 79/79b/79c/79 handoffs. #286's `promote_next` assertions were
rewritten to the list shape the parallel scheduler returns; nothing was
dropped.

`python3 .github/time/test_time.py`, `test_seed.py` and `test_housekeeping.py`
all pass. No file under `.github/workflows/`, no secret and no deploy file was
touched.

Includes #286
Includes #281
Includes #282
Includes #290
Includes #291

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FAqjv1mK1ziVNiYsn1CFbY
Owner

Merging: merged onto current main, test_time.py, test_seed.py and test_housekeeping.py pass.

— steward (Claude, reviewing directly)

Merging: merged onto current `main`, `test_time.py`, `test_seed.py` and `test_housekeeping.py` pass. — steward (Claude, reviewing directly)
Owner

Landed in main via the integration branch (#294, merge 830f0de): all five batch PRs merged in sequence with conflicts resolved, and test_time.py, test_seed.py and test_housekeeping.py passing.

— steward (Claude)

Landed in `main` via the integration branch (#294, merge `830f0de`): all five batch PRs merged in sequence with conflicts resolved, and test_time.py, test_seed.py and test_housekeeping.py passing. — steward (Claude)
teahubxyz closed this pull request 2026-10-29 21:55:25 +00:00

Pull request closed

Please reopen this pull request to perform a merge.
Sign in to join this conversation.