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
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
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
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
Four sibling cards landed on main under this branch (#244 the planner, #283 the
router's no-op judge, #289 the branch rewind, #292 the steward inside Time). Two
files conflicted, and both sides' behaviour is kept.
- `promote_next` stays this branch's one-pass fill: it labels every free slot's
worth of dependency-ready `queued` cards `ready` (the router's live ranking
first, else oldest), returns the list it started, and prints
`promote: ready=N moving=M free=F`. Main's single-card version, which returned
one number and read the dependency line inline, is gone.
- The gate takes main's side on the dependency line: `deps_met` now matches with
the shared `DEPENDS` pattern instead of its own `Depends on #(\d+)`, so the
`Depends on: #N` lines #244's planner writes hold a card back as well as a
hand-written `Depends on #N`. Without this the two cards cancelled out — the
planner wrote a line the scheduler could not read, and every card of a planned
spec looked free to start at once (main's own heartbeat test at test_time.py:333
catches it). The memoisation across the pass is unchanged.
- astra.py's header keeps this branch's description of the heartbeat (parallel
promotion, up to TIME_PARALLEL) with main's `Depends on: #N` spelling.
- test_time.py (section 245): both spellings gate, and one pass reads a shared
dependency once rather than once per card.
- CHANGELOG.md keeps both sides' lines. The #245 entry's closing sentence, that a
spec Time plans itself still chains card on card until the planner stops writing
`Depends on #N` for every step, was written while #244 was open and is now
false: #244 has landed, so the entry says instead that a planned spec opens with
every card that waits on nothing. README's heartbeat line takes the same
spelling.
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_01FAqjv1FxvvoAsdTCXtto8G
Several siblings landed while #171 was out for review (#211 the judge floor,
#244 the planner's `Depends on:` lines, #80 the steward inside Time, #289,
#292). Both sides keep their behaviour.
`.github/time/astra.py`, two conflicts, both resolved by keeping each side:
- the helpers. #171 added the three sources the router reads (`ledger_week`,
`action_runs`, `parked_cards`); #211 added `JUDGED_SHARE` and
`judged_enough`. Nothing overlaps, so both are in, #211's constant left where
main put it, next to the tick that uses it.
- `router_tick`. #211 counts how much of the board came back judged, logs the
reply's size and stop reason, and makes a reply under the quarter floor a loud
no-op. #171 had only replaced the inline `date()` in the `rank` call with the
`today_d` it computes once for `opt_candidates`. Kept #211's guard, with
`today_d` as the `rank` argument: same value, one clock reading for the tick.
`.github/time/router.py` and `CHANGELOG.md` merged themselves; checked rather
than trusted. The router carries both #244's `DEPENDS` regex (the `Depends on:`
form, used by `candidates`) and #171's `ledger_candidates` / `run_candidates` /
`parked_candidates` / `opt_candidates` / `named`. The changelog keeps #211's
entry under Changed and #171's under Added.
`.github/time/test_time.py`, one conflict where the two sections claim the same
slot after 198. They are not independent: section 171 repoints `astra.tm`'s
issues, comments, labels and new cards at its own world and its own lists, and
restores only `gh`, so 211 run after it would assert against an empty `said80`
and a board that is no longer the six candidates it counts. So 211 goes first,
over the clean 80 world, and 171 after it, which is where each side already ran:
the state section 82 inherits is unchanged.
One assertion inside 171 needed the fix, not the markers. Its last block kills
all three sources and expects the tick to still rank the day ("unreadable is no
candidates from it, never a failed tick"), but its seat reply names only the
three `opt:` keys -- which are gone with the sources, so the reply now judges
nothing and #211's floor ends the tick before the ranking, with no `opt` in the
beat. The reply judges the card still on the board as well, so the block tests
what it means to: the sources are dead, the ranking still goes up with
`"opt": []`. Both rules hold, and neither is weakened to let the other pass.
`python3 .github/time/test_time.py` (65 sections, 211 and 171 among them),
`test_seed.py` (3) and `test_housekeeping.py` (10) all pass. No workflow, secret
or deploy file is touched.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FAqjv1iMAmWAp7UhYpouvo
Several siblings landed on main since this branch cut: the loud no-op for a
judge that returns nothing (#211), the planner's 4-6 coarse cards with their
own `Depends on:` lines (#244), a card branch that is never rewound to its
base (#277), the steward inside Time (#80) and the router's no-op judge (#283).
Both sides keep their behaviour:
- `.github/time/astra.py` merged cleanly. The docstring carries main's #211
paragraph and this branch's #279 board paragraph; `judged_enough()` and the
unjudged gate in `router_tick()` stand beside `slate()`, `rank()` and
`deliberate()`. `tick()` consults `router_config()` only when
`TIME_ASTRA_PITCH` is unset, so the board still decides the day whatever
router.json says and the router keeps main's gate on the path it owns.
- `.github/time/test_time.py` was the one conflict: both sides append a block
after `ALL OK 198`. Kept both, the #279 board checks first and main's #211
checks after. They do not share state: the board checks load their own
`astra`/`time` modules (`astra279`) and pop every `TIME_ASTRA_*` they set,
while #211 stubs the suite's own `astra.tm.think`.
- `CHANGELOG.md` keeps both sides' lines: #279 under Added, main's #211, #244,
#277 and #80 entries untouched.
`python3 .github/time/test_time.py` (which also runs test_steward.py and the
dojo selftest), `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_01FAqjv2gKAvzWq6GqZdSpLW
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
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_01FAqjv5X4dnqh4zPDe99AHjCloses#247
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
Rebuilt on current main from the closed PR #199 (refs/attic/pr-199), whose code
no longer rebased; its tests are kept and its review round is carried over.
The router now proposes optimization work the factory's own records show, read by
arithmetic over the last seven days, instead of waiting for a human to notice it.
Three sources in `router.py`, each finding a synthetic candidate keyed
`opt:<source>:<id>`, kind optimization, with a title and body already drafted and
no card (`n` is None):
- the ledger (`ledger_candidates`): a model or an entry point that booked over
half the week's spend, and a model whose spend per call is double its own
median, so a few heavy calls carry the bill. Grouped by `model` and by `entry`,
falling back to the `pool` the call booked to, which is what this repo's ledger
carries today.
- the Actions runs (`run_candidates`): a workflow with two or more failed or
cancelled runs, with the count and the last run's id, from `/actions/runs` in
either shape (the list, or `{"workflow_runs": [...]}`).
- the parked cards (`parked_candidates`, fed by `astra.parked_cards`): an `error`
or `stuck` beat older than a day, no open PR, #78's automatic retries spent and
no trusted `/time retry` the next heartbeat will act on.
Astra's seat judges these like any other candidate. Live, a picked one is written
as a card (unqueued, a candidate from tomorrow) and never worked on in place; dry,
the ranked table shows it and says what would become of it. The cloud lane and the
promotion order never reach one: both already skip a candidate with no number.
`pick` does reach one and weighs it against the day's money and slots like the
rest, which the section comment now says out loud.
No duplicates: `named` reads `Router: opt:<source>:<id>` back out of a card's body,
and `router_tick` keys the open cards and the ones closed lately, so a finding is
not proposed again the day its card closes while the seven-day window still holds
the week it came from (`repeat_of` can't catch it: the title carries a dollar
amount, which moves).
One change on top of #199, from running `ledger_candidates` over this repo's own
ledger: a share needs two groups that each spent over the floor, not just two
groups. The week is 60 anthropic calls beside three free Venice ones, so the old
guard let through "Why did anthropic cost $2.11 of the week's $2.11?" — a
dimension where everything went through one spender by definition. Pinned by test.
Tests: `python3 .github/time/test_time.py` (new section 171), `test_seed.py` and
`test_housekeeping.py` are green. `test_delegate.py` fails on main too and is not
in CI. No workflow, secret or deploy file is touched.
Closes#171
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FAqjv4QpKWnK35CMX9Paeb
The venture loop deliberates (seats pitch, the board ranks, a quorum decides);
the self-improvement loop didn't: Astra was one seat choosing the factory's own
work alone. This gives Astra the same board, reusing the venture board's
machinery rather than inventing a second one. A rebuild of the stale PR #69
(`refs/attic/pr-69`) on current main.
How it runs, only when TIME_ASTRA_PITCH is set:
- Slates. Every seat in TIME_ASTRA_PITCH reads the same board, ledger and spine
context Astra reads today and proposes up to MAX_READY + MAX_NEW cards:
existing issues to queue (the single planner's own guards: a `DONE:` line and
none of the SKIP labels) or new cards. The slates merge into one field; the
same issue, or the same new title, from two seats is one card with both
proposers named.
- Ranking. Every seat in TIME_ASTRA_BOARD (default: the pitching seats) ranks up
to three cards; Borda 3/2/1, the venture board's tally, and `venture.top`
applies its quorum of two.
- Picks. The top picks are queued or created in ranked order: per run at most
TIME_ASTRA_MAX_READY picks and TIME_ASTRA_MAX_NEW of them new, the same
per-run caps the single planner has. A card one seat alone wants is never
opened, so the board can't spam issues.
- Log. The Astra log comment carries each seat's note, a table of score and
backers per card, each seat's reason, and what was queued. Humans object
there as before.
Mechanics. `astra.py` loads `venture.py` beside `time.py` and points it at
Astra's own `time` module (`vt.tm = tm`), so the board's seats spend through
this run's governor and ledger, not a second copy.
One judgment call the card couldn't make (the router landed after PR #69 was
written): the board is the explicit variable, so with TIME_ASTRA_PITCH set it
decides the day whatever time/router.json says (today `live`), and the router's
own ranking is not run. Unset, Astra is byte-for-byte the router or the single
planner it is now.
Cost. One pitch round plus one ranking round, in parallel: with three Claude
seats that is six small calls a day on top of today's one, well under the $5
day. Venice seats work too but are paused by decision.
Tests. Three new sections in test_time.py, offline against a fake forge and
scripted seats. `astra 1` is the TIME_ASTRA_PITCH-unset path (no router.json, so
the router is off), asserting the exact labels, the exact new issue and the log
text. `astra 2` is the board: duplicates folded, guarded issues dropped before
the vote, only quorum picks queued, and the one-backer card (`new-3`) asserted
never opened. `astra 3`: no quorum queues nothing; MAX_NEW=0 still queues the
existing pick; a board of one seat never reaches quorum. test_time.py,
test_seed.py and test_housekeeping.py are green here. (test_delegate.py fails on
main as well, in its own fake's `http()` signature: untouched by this change.)
The workflow change this needs is a proposal for a human: `.github/workflows/`
is off limits to the worker, so `astra.yml` is unchanged here. The exact change,
under `jobs.plan.steps[1].env` (after `TIME_ASTRA_SEAT`):
TIME_ASTRA_PITCH: ${{ vars.TIME_ASTRA_PITCH }} # the board: seats that each propose a slate (unset: the router or one planner)
TIME_ASTRA_BOARD: ${{ vars.TIME_ASTRA_BOARD }} # the board: seats that rank the field (default: the pitching seats)
and in the header comment, line 3, `ready` is already wrong (the code labels
`queued`):
-# labels a few cards `ready` for the heartbeat. It writes its reasoning to the "Astra log" issue; reply there to steer it.
+# labels a few cards `queued` for the heartbeat to promote. It writes its reasoning to the "Astra log" issue; reply there to steer it.
+# With TIME_ASTRA_PITCH (and TIME_ASTRA_BOARD) set, several seats propose slates and a board ranks them, the venture
+# ceremony turned inward: only cards two seats want get queued.
Turning it on is then a variable, not a deploy:
TIME_ASTRA_PITCH="sonnet, haiku, opus" (seat names, as in seats.json). Leave
TIME_ASTRA_BOARD unset to have the same seats rank.
Closes#279
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FAqjv23aZXMXLjfU8HFJCV
The planner asked its seat for "small cards, one pull request each" and
chained whatever came back: card 2 waited on card 1, card 3 on card 2,
however little the work actually needed. A spec became a long single file
of thin cards, and with TIME_PARALLEL at 3 (#243) two of the three slots
sat empty.
- PLAN_PROMPT asks for 4 to 6 cards, each shippable on its own (a module
and its tests, a group of endpoints, a schema and its migration), and
for `depends_on`: the positions in the list a card can't start without.
- plan_deps() maps those positions to the cards already filed, dropping a
reference to itself or to a later step. A seat that says nothing falls
back to the step before it, so a human's `- title — DONE: x` lines keep
the order they were written in.
- Every card carries what it waits on: one `Depends on: #N` line per card,
or `Depends on: none`. The planned comment names the cards nothing
blocks.
- time.py's promote_next and router.py's candidates() read both the new
line and the older `Depends on #N`, so cards filed before this still
wait on the right ones (#184).
Tests: the planner's own checks now assert the line on every card; a new
seat-plan check (two cards free, three waiting) ends with both free cards
promoted and dispatched in one heartbeat, and a router check reads a card
that carries one line of each form.
No workflow change needed.
Closes#244
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FAqjv1tfHenkbKtJZkZ52r
One heartbeat started one card. `promote_next` labelled the single best `queued`
card `ready` and the run dispatched it, so cards that waited on nothing still
went one behind the other, however many slots TIME_PARALLEL allowed.
- `promote_next` fills every free slot in one pass and returns the cards it
started: the best `queued` cards whose every `Depends on #N` card is closed
(the router's live ranking first, else oldest first), up to TIME_PARALLEL in
flight at once. The same heartbeat dispatches them, so independent cards build
at the same time.
- `deps_met` is the gate, with the dependency lookups memoised across the pass
(the cards of one spec tend to name the same dependency). A card whose
dependency is still open stays `queued` however free the slots are.
- The cap counts the cards already in flight, and only the ones that are moving
(#135): two against a cap of three leaves one slot, a full cap starts nothing,
and TIME_PARALLEL=1 is the old one-at-a-time chain. The log line is now
`promote: ready=N moving=M free=F`.
- Tests (test_time.py, sections 80 and 245): the gate (a card behind an open
dependency is never started, and starts once that card is closed), the cap
arithmetic at 1/2/4/5 slots against three cards in flight, and whole
heartbeats that leave two and then three cards building at once.
- README, CHANGELOG and astra.py's header say what the heartbeat now does.
Not in this card: a spec Time plans itself still chains card on card, because
`plan_spec` writes `Depends on #<previous>` for every step. #244 is the card that
stops that; until it lands, parallel starts show up on cards whose dependencies
are written by hand (or absent). No workflow change is needed for this card.
test_time.py, test_seed.py and test_housekeeping.py are green. test_delegate.py
fails on main before this change too (its fake `World.http` takes no `headers`
kwarg, which the Messages API call passes); it is untouched here.
Closes#245
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FAqjv5NCpnmkuxczozbcDU