Commit Graph
193 Commits
Author SHA1 Message Date
Claude cb40eb869e Integrate batch 5: five rebased branches landed as one
ci / test (pull_request) Successful in 1m8s
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
2026-10-29 20:37:57 +00:00
Claude 9f8f02e6c2 Merge remote-tracking branch 'origin/claude/card-279-astra-board-btshr3' into claude/integrate-batch-5-1p4lwf 2026-10-29 20:35:10 +00:00
Claude cb74c843a0 Merge remote-tracking branch 'origin/claude/card-171-router-optimization-m3d360' into claude/integrate-batch-5-1p4lwf
# Conflicts:
#	.github/time/astra.py
#	.github/time/router.py
2026-10-29 20:35:08 +00:00
Claude 39c0e1d34e Merge remote-tracking branch 'origin/claude/card-170-router-venture-bqq0jw' into claude/integrate-batch-5-1p4lwf 2026-10-29 20:30:59 +00:00
Claude 2325dc6a00 Merge remote-tracking branch 'origin/claude/card-247-needs-human-scope-vbi95b' into claude/integrate-batch-5-1p4lwf
# Conflicts:
#	.github/time/test_time.py
#	.github/time/time.py
#	README.md
2026-10-29 20:30:55 +00:00
Claude 1816b94ff4 Merge main into the router-venture branch (#170)
ci / test (pull_request) Successful in 1m7s
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
2026-10-29 19:54:48 +00:00
Claude 2f99c21c24 Merge origin/main into card #247: a stall's scope meets the planner's Depends on: lines
ci / test (pull_request) Successful in 1m22s
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
2026-10-29 19:53:30 +00:00
Claude bac994eb78 Merge origin/main into card #79: mentions-as-handoffs beside the landed siblings
ci / test (pull_request) Successful in 1m29s
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
2026-10-29 19:51:51 +00:00
Claude 7340596d10 Merge main into #245: the parallel scheduler reads the planner's Depends on: lines
ci / test (pull_request) Successful in 1m42s
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
2026-10-29 19:51:42 +00:00
Claude 2f8cbdd3f1 Merge main into card-171: the judge's floor and the router's own candidates
ci / test (pull_request) Successful in 1m24s
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
2026-10-29 19:51:12 +00:00
Claude ce9ac5189c Merge main into card #279: Astra's board beside the judge no-op and the planner's Depends on: lines
ci / test (pull_request) Successful in 1m18s
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
2026-10-29 19:50:34 +00:00
time 20ba8dd0b6 Steward tick inside Time: repair the block, escalate only money or a human-only step (#292)
sync / sync (push) Successful in 1m16s
ci / test (push) Successful in 1m1s
Co-authored-by: time <time@users.noreply.teahub.xyz>
2026-10-29 19:44:31 +00:00
steward 5bec04fb3a Merge #284: Planner: 4-6 coarse cards per spec, each with its Depends on: lines
sync / sync (push) Successful in 57s
ci / test (push) Successful in 1m14s
2026-10-29 12:42:44 -07:00
time f5f8c461dc Router: a judge that returns nothing is a loud no-op, never a flat ranking (#283)
sync / sync (push) Successful in 1m45s
ci / test (push) Successful in 1m5s
Co-authored-by: time <time@users.noreply.teahub.xyz>
2026-10-29 19:38:48 +00:00
time b01bb6833a A card branch is never rewound to its base while its PR is open (#289)
sync / sync (push) Successful in 1m34s
ci / test (push) Successful in 55s
Co-authored-by: time <time@users.noreply.teahub.xyz>
2026-10-29 19:36:44 +00:00
steward 6de117861e test_time: run taskmaster's node tests with the TAP reporter, so the '# fail 0' check holds whatever reporter node would pick
sync / sync (push) Successful in 49s
ci / test (push) Successful in 55s
2026-10-29 12:35:26 -07:00
Claude da08f3c892 Mentions are handoffs: @codex and @claude dispatch headless seat runs as their own users (#79)
ci / test (pull_request) Successful in 1m0s
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
2026-10-29 18:55:48 +00:00
Claude 8c47b23861 Scope a needs-human stall to its dependents and say so on the spec
ci / test (pull_request) Successful in 1m10s
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
2026-10-29 18:55:30 +00:00
Claude dbf25173d5 Router: a new venture starts only when the router picks it (TIME_VENTURE=router)
ci / test (pull_request) Successful in 1m3s
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
2026-10-29 18:55:15 +00:00
Claude 8b7d7505a0 Router: optimization candidates from the ledger, CI and parked cards
ci / test (pull_request) Successful in 1m23s
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
2026-10-29 18:55:07 +00:00
Claude 261d34f17e Astra: a board for internal work (the venture ceremony turned inward)
ci / test (pull_request) Successful in 1m11s
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
2026-10-29 18:53:55 +00:00
Claude c2124f4d7a Planner: 4-6 coarse cards per spec, each with its Depends on: lines
ci / test (pull_request) Successful in 1m11s
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
2026-10-29 18:53:45 +00:00
Claude f7ee459946 Scheduler: start dependency-ready cards in parallel up to TIME_PARALLEL (#245)
ci / test (pull_request) Successful in 1m28s
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
2026-10-29 18:53:28 +00:00
teahubxyz b5c5718861 Sync sends Time's merge back to the copy; CHANGELOG conflicts keep both sides (#204)
sync / sync (push) Successful in 21s
ci / test (push) Successful in 1m0s
Sync sends Time's merges back to the copy; CHANGELOG merges as union (rebased onto main by the steward; test_time.py passes)
2026-10-29 18:19:44 +00:00
time c38f03c11e Claude delegation: workflow_dispatch + correlation ID + durable receipt (#73)
ci / test (push) Successful in 1m7s
sync / sync (push) Successful in 1m35s
Closes #71 (rebased onto main by the steward; test_time.py passes)

Co-authored-by: Time <time@users.noreply.teahub.xyz>
2026-10-29 18:15:56 +00:00
time 3839cacfe3 Taskmaster reroll: haggle to a real Whop checkout, then coach (#209)
ci / test (push) Successful in 1m2s
sync / sync (push) Successful in 3m19s
Taskmaster reroll (rebased onto main by the steward; test_time.py and taskmaster's 83 tests pass)

Co-authored-by: time <time@users.noreply.teahub.xyz>
2026-10-29 18:15:08 +00:00
teahubxyz d09c82c202 mention: book the seats' spend to the ledger (TIME_TOKEN for the flush) (#70)
sync / sync (push) Successful in 55s
ci / test (push) Successful in 1m24s
mention.yml gets TIME_TOKEN like heartbeat, so the seats' spend flush can push the ledger (reviewed by the steward)
2026-10-29 18:12:22 +00:00
time b7ebf3b020 Venture home folder: a renamed venture keeps building and measuring at its new address (#191)
sync / sync (push) Successful in 30s
ci / test (push) Successful in 50s
Closes #160 (rebased onto main by the steward; test_time.py passes)

Co-authored-by: Time <time@users.noreply.teahub.xyz>
2026-10-29 18:10:49 +00:00
time 04c655dfec Agent-management dojo: eleven katas, runner, SOP, offline selftest (#176)
ci / test (push) Successful in 1m23s
sync / sync (push) Successful in 1m11s
Part of #85

Co-authored-by: Time <time@users.noreply.teahub.xyz>
2026-10-29 17:57:37 +00:00
time 25bae18fa8 Venture tests run ungated on Gitea shapes; signup form moves to .gitea (#200)
ci / test (push) Successful in 1m7s
sync / sync (push) Successful in 1m25s
Part of #48

Co-authored-by: Time <time@users.noreply.teahub.xyz>
2026-10-29 17:54:53 +00:00
teahubxyzandClaude Opus 5.5 988d772fbb Time checks in on the office bus (#275)
ci / test (push) Successful in 1m9s
sync / sync (push) Successful in 57s
Time checks in on the office bus (#275)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FAqjv4PBaoWnL62rDKmBJ1
2026-10-27 17:41:40 +00:00
teahubxyz 0e7f294cf4 Trace every model call to Langfuse (#274)
ci / test (push) Failing after 1m13s
sync / sync (push) Successful in 50s
2026-10-27 14:59:04 +00:00
Claude 7cc654c987 ci: scheduled workflows run on the cron lane runner
ci / test (push) Successful in 1m23s
sync / sync (push) Successful in 3m10s
astra, heartbeat, housekeeping, sync, update and venture move to runs-on: cron
(green-tea-runner-cron), so they never queue ahead of deploys on the main runner.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VVFi3wvLWKaEkpDw6eLnQC
2026-10-25 16:55:40 -07:00
time af15cf16b0 Add TIME_PARALLEL config setting (default 3) (#248)
time/worker-run cd .github/time; python3 - <<'E' p='time.py'; s=open(p).read() s=s.replace('''PLAN_PROMPT = """Split
ci / test (push) Successful in 47s
sync / sync (push) Successful in 8s
ci / test (pull_request) Successful in 46s
heartbeat / tick (pull_request_review) Successful in 12s
Co-authored-by: Time <time@users.noreply.teahub.xyz>
2026-10-22 20:07:56 +00:00
teahubxyzandClaude Opus 5.5 0ba86c64a8 gandora-mcp: bind time-inference as INFERENCE, gate on GANDORA_MCP_TOKEN, wait longer for the domain
ci / test (push) Successful in 48s
sync / sync (push) Successful in 9s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014mBuCaQSgWwqhtzsmTBQuM
2026-10-22 19:26:45 +00:00
teahubxyz 59f3959b22 gandora-mcp: dispatch-only deploy of the gandora MCP spike worker (source: gandora/site dev mcp/worker.js)
ci / test (push) Successful in 48s
sync / sync (push) Successful in 10s
2026-10-22 19:14:37 +00:00
teahubxyz f479c5f016 quc v0: the cage, the chair, the safeword, the magic words, the replay (#241)
ci / test (push) Successful in 52s
quc / deploy (push) Failing after 3m34s
sync / sync (push) Successful in 10s
time/worker-run cd .github/time && python3 - <<'E' p='time.py'; s=open(p).read() s=s.replace(''' try: ret
2026-10-22 18:26:41 +00:00
teahubxyz e81560e9ca inference: bind TTS_MODELS as a worker var, adding Inworld, Gemini, MiniMax and xAI TTS (for the gandora ASMR voice bake-off)
ci / test (push) Successful in 47s
inference / deploy (push) Failing after 1m14s
sync / sync (push) Successful in 9s
time/worker-run grep -rIil "quc\|chair" . --exclude-dir=.git --exclude-dir=node_modules | head; cat .gitignore; sed
2026-10-21 18:35:15 +00:00
teahubxyzandClaude Fable 5.1 c4d19dcfac speech: pass the style prompt and temperature through (Qwen3 / Orpheus / Chatterbox), allow ElevenLabs turbo and Chatterbox HD
ci / test (push) Successful in 43s
inference / deploy (push) Failing after 1m10s
sync / sync (push) Successful in 8s
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K6897PWcUEwHiqZMcVnNF6
2026-10-21 08:20:20 +00:00
teahubxyzandClaude Fable 5.1 1e75806f4f inference: dev.gandora.lol may call the worker
ci / test (push) Successful in 43s
inference / deploy (push) Failing after 1m10s
sync / sync (push) Successful in 7s
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K6897PWcUEwHiqZMcVnNF6
2026-10-21 06:17:10 +00:00
time bd431f3092 review: a review past the output limit says so, and is never a silent Object (#202) (#213)
ci / test (push) Successful in 43s
sync / sync (push) Successful in 8s
2026-10-21 03:08:46 +00:00
teahubxyzandClaude Fable 5.1 09e5f99b86 inference: POST /v1/speech — Venice TTS (model in TTS_MODELS) for the DJ's spoken breaks; counts as a render
ci / test (push) Successful in 44s
inference / deploy (push) Failing after 1m11s
sync / sync (push) Successful in 7s
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K6897PWcUEwHiqZMcVnNF6
2026-10-21 02:14:25 +00:00
time af52c60e4a housekeeping: list open cards whose work a closed card already did (#210)
ci / test (push) Successful in 45s
sync / sync (push) Successful in 8s
2026-10-21 02:09:45 +00:00
teahubxyzandClaude Fable 5.1 ba998b6a82 inference: a reference track sets the length — Runware refuses duration alongside inputs.audio
ci / test (push) Successful in 45s
inference / deploy (push) Failing after 1m10s
sync / sync (push) Successful in 7s
time/worker-run cd .github/time; python3 - <<'E' p='test_time.py'; s=open(p).read() s=s.replace('("I cannot do that
ci / test (pull_request) Successful in 45s
heartbeat / tick (pull_request_review) Successful in 10s
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K6897PWcUEwHiqZMcVnNF6
2026-10-21 01:42:33 +00:00
teahubxyzandClaude Fable 5.1 3ad797b37f inference: when Runware fails and Venice renders instead, say why in x-fallback
ci / test (push) Successful in 46s
inference / deploy (push) Failing after 1m10s
sync / sync (push) Successful in 7s
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K6897PWcUEwHiqZMcVnNF6
2026-10-21 01:36:27 +00:00
teahubxyzandClaude Fable 5.1 0c9ce5e966 inference: Runware wants the meter as beats per bar (2/3/4/6), not "3/4"
ci / test (push) Successful in 47s
inference / deploy (push) Failing after 1m11s
sync / sync (push) Successful in 9s
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K6897PWcUEwHiqZMcVnNF6
2026-10-21 01:34:13 +00:00
teahubxyzandClaude Fable 5.1 7dd77d29a8 inference: tracks up to 300 s
sync / sync (push) Canceled after 0s
ci / test (push) Successful in 43s
inference / deploy (push) Failing after 1m10s
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K6897PWcUEwHiqZMcVnNF6
2026-10-21 01:28:41 +00:00
teahubxyzandClaude Fable 5.1 9888e78abb inference /v1/render: duration up to AUDIO_SECONDS (300 on Runware), timeSignature, and a reference audio (base64 data URL) for remixes; render bodies may carry it (8 MB)
ci / test (push) Successful in 42s
inference / deploy (push) Failing after 1m10s
sync / sync (push) Successful in 7s
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K6897PWcUEwHiqZMcVnNF6
2026-10-21 01:28:33 +00:00
teahubxyzandClaude Fable 5.1 45d577db5b inference: quota counters cost one KV write per call and fail open — free-tier KV (1,000 writes/day) ran out and answered 500 for every call
ci / test (push) Successful in 42s
inference / deploy (push) Failing after 1m10s
sync / sync (push) Successful in 6s
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K6897PWcUEwHiqZMcVnNF6
2026-10-20 22:53:23 +00:00
teahubxyzandClaude Fable 5.1 90e5d71a64 inference: tracks up to 120 s
ci / test (push) Successful in 43s
inference / deploy (push) Failing after 1m10s
sync / sync (push) Successful in 6s
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K6897PWcUEwHiqZMcVnNF6
2026-10-20 22:35:26 +00:00