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
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
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 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
time/worker-run git fetch origin gh-pages 2>&1|tail -1; git show FETCH_HEAD:ventures/dollhouse/app.js | sed -n 140,3
seed: land a venture built elsewhere on yolopreneur.lol (#64)
Actions -> seed -> Run workflow with a slug, the URL(s) of a .tgz whose top folder is the slug, and its sha256
(plus a name and a one-liner for a new venture). seed.py checks the bundle, copies it over ventures/<slug>/ on
gh-pages in one commit, adds or refreshes the venture's line in ventures/ventures.json, and the workflow asks
Teahub pages to pull. Nothing in the bundle runs. test_seed.py runs in CI. Merged at the owner's request.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K9wtGiGEeq1H5DA3T88PRe
One batch card scouts once, the seats pitch a field, and the board ranks it; the top k
(each wanted by two seats) become venture cards in the batch's milestone at 'picked'.
Children run the ordinary loop side by side, wait at 'ready' until every sibling is
settled or the hold runs out, then launch together. No child left behind: one batch
retry for a held child, then it is counted dead in the cohort retro, which closes the
card and the milestone. Fix rounds now yield the single worker run per tick to cards
still due their first build, so a failing page can't starve its siblings.
Claude-Session: https://claude.ai/code/session_01VDkN7ukK1kXkqov8FaZxva
Co-authored-by: Claude <noreply@anthropic.com>
Each venture's Worker can now sign people up and in with passkeys (WebAuthn):
no passwords, no email, no OAuth app, nothing to configure. The relying party
is the venture's own host and the session a __Host- cookie (HttpOnly, Secure,
SameSite=Lax, 30 days), stored hashed in the venture's D1 database. ES256 and
RS256 keys; challenges are single-use and expire in five minutes; origin,
relying party, user presence and the signature counter are checked; auth
endpoints refuse cross-origin calls. Items saved while signed in are recorded
as that person's, and /api/me/items lists them.
time.js gains Time.accounts, user, signUp, signIn, signOut and mine; builds
are told to use them for what the paid tier promises and never to put sign-in
in front of the free product. test_auth.mjs runs the whole ceremony against a
real SQLite and a software authenticator, including replay, phishing origin,
another site's passkey, a cloned counter, a tampered signature and a duplicate
registration; CI pins node 22 so it runs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wyb3QQDESFkosKRXzrXqwr
With CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID, each venture gets its own
Worker (time-<slug>) with its own D1 database and Workers AI. The Worker serves
the venture's GitHub Pages folder under <slug>.<TIME_VENTURE_DOMAIN> (or
workers.dev) and adds a small API: share links stored in D1, and a
rate-limited small model. The page reaches it through time.js, which Time
writes next to index.html and the build may not edit; every call degrades to
null, so products still work on Pages alone and in the headless checks.
The backend is deployed when a venture is picked, so the plan, the signal
issue and the page all carry the venture's own domain. A Cloudflare failure
never holds a venture: it launches on Pages and the chores retry. The chores
also backfill older ventures and redeploy every venture when worker.js or its
settings change (a revision hash), a couple per tick; update.yml carries the
new files. The doctor checks the token. Offline tests cover the provisioner,
the loop with and without Cloudflare, the backfill and redeploys, and the
worker itself (node, against a fake D1, AI and Pages).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wyb3QQDESFkosKRXzrXqwr
The schedule starts one venture card a day, and Run workflow only
advanced the cards already open. Run workflow now has a "Start a new
venture now" checkbox (gh workflow run venture.yml -f start=true) that
opens a fresh card even when today's has run; a second card on the same
day is titled "Venture <day> (2)". The schedule is unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UpT7eWzeLhRwuYmsSQnbRb
Taste test on the relaunched MDecks: the notplanned launch-page template
and its rotation of print styles turned the working tool into a section
under a big hero, and every page looked like the same magazine. Now:
- the brief puts the tool in the first screen under a slim header (name,
a few words, the one signup button); how it works, why, pricing and
limits sit quietly below;
- the look follows the product (a slide tool should feel like a slide
tool), dark or light; the design rotation is gone;
- headless Chrome fails a page whose product starts below the top third
of the first screen, at phone and laptop size;
- an earlier version can be kept on purpose (prior_note), so MDecks can
be rebuilt from the first design people liked.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UpT7eWzeLhRwuYmsSQnbRb
Ported from the notplanned venture template:
- launch round: every board seat writes a launch plan (positioning, first
users, pricing, channels, 7-day success and kill lines); one seat
synthesizes them into LAUNCH.md with where each idea came from
- launch page brief: one primary call to action in the hero, the working
product preloaded with a sample, why-this-exists with real star counts,
pricing (free plus at most two honest early-access tiers), honest limits,
a design direction that rotates and never repeats the last six
- signups through a GitHub issue form prefilled with venture and plan;
each signup is thanked on its own issue
- checks: deterministic gates, then headless Chrome measuring at exactly
390x844 and 1280x800 (horizontal scroll, button below the fold, JS
errors); two fix rounds, then andon with needs-human and /time retry
- a contact-sheet screenshot of phone and desktop on the card
- 7-day decision on distinct people: v2 card, kill, or one more week then
park; a next version counts only signups after its own launch
- scheduling: a card that is measuring never blocks the others; one new
card a day; a tick runs light stages back to back and one worker run
- the studio index at /ventures/ is generated from ventures.json
Security: the worker's shell no longer sees tokens or keys, its checkout
keeps no token in .git/config, and workflows don't persist credentials.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UpT7eWzeLhRwuYmsSQnbRb
venture.py runs one stage per tick on a `venture` card: scout (GitHub search,
repos with the most new stars this week), pitch (each seat proposes one static
page product), board (Borda over seat rankings), build (the Venice worker on
gh-pages), ship (GitHub Pages). TIME_VENTURE=on starts it; off by default.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UpT7eWzeLhRwuYmsSQnbRb
A seat gets hands through the repo's own Actions token: list/read/write files on
branch time/card-N via the contents API, run shell on a checkout, open the PR.
Same contract as the Cursor worker (PR body says Closes #N), so review, landing
and next-card are unchanged. Workflow files are refused to the seat. Needs only
VENICE_API_KEY; no Cursor key.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UpT7eWzeLhRwuYmsSQnbRb
- spec issues (label spec) become queued cards, parsed deterministically, seat fallback
- TIME_AUTOMERGE=on: squash-merge when the latest trusted verdict is Approve and all checks pass
- Object, red or missing checks, or protected paths go to needs-human
- merged card frees the slot; the next unblocked queued card gets ready and dispatches in the same run
- update.yml pulls the engine from teahubxyz/time into project repos via PR
- ci.yml runs Time's own tests
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UpT7eWzeLhRwuYmsSQnbRb