Two of the four points hold and are fixed; two don't, and nothing changed for
them. The review was read from the diff alone, and the two it gets wrong are
both about code the diff doesn't carry.
**The cloud lane (holds as a test, not as a bug).** `pick_cloud` already skips a
candidate with no card: `router.py:333` reads `if r["n"] is not None and
r["actionable"] and ...`, and so does `order()` at `router.py:357`. Both guards
came with the cloud lane in #197, for `venture:new`, which has `n: None` for the
same reason an `opt:` candidate does; this card added a second kind of cardless
candidate and inherited them. So `tm.label(None, "cloud")` cannot happen and the
table cannot say "to the cloud today" for an `opt:` row. No code changed. The
test the review asks for is worth having and is now there: an `opt:` candidate
judged value 5 ranks first (3.75 against 0.765) under `per_day: 2, sizes:
["S", "M"]`, and the cloud lane takes only the real card behind it. Dropping
the `n is not None` from `pick_cloud` fails it.
**The parked window (holds as wording only).** The arithmetic is already what
the review proposes. `_recent(at, today, 1)` is `at >= today - 1 day`, and a
parked card is a candidate when that is false, i.e. when `at < today - 1 day` —
the review's own suggested fix. In whole days that is "parked the day before
yesterday or earlier", which is what "older than a day" means at day
granularity, so the threshold matches the card. What was wrong was the
docstring's "nor is one parked only today": it also holds back one parked
yesterday. `parked_candidates` now says so, and the boundary is pinned by test
(0 and 1 days yield nothing, 2 yields the candidate). No code changed.
**A closed card no longer lets its source come back (fixed).** This was real.
`named` was given only the open cards, so the day an optimization card closed
the same finding was proposed again — the ledger's window is seven days and
outlives the card written about it, and `repeat_of` cannot catch it because the
title carries the dollar amount, which moves. `router_tick` now keys the cards
closed lately too (`named(open_issues + closed)`; `closed_lately()` moved above
the candidate gathering, no extra call). Suppression lasts as long as
`closed_lately`'s fortnight, by which time the seven-day window no longer holds
the week the card was about, so a finding that is still true comes back as a
fresh question rather than never.
**`parked_cards` ignored a human retry (fixed).** The fourth point arrived
truncated at "astra.py, `parked_c", so I audited the function instead of
guessing at it, and it had a gap of its own: it checked #78's automatic retries
but not `/time retry`. The heartbeat clears a parked card's `last` for either
one (`time.py:1613` for the typed retry, `time.py:1622` for the automatic), so a
card someone trusted had already asked to retry was still proposed as "retry or
split #N" — a card for work the next heartbeat was about to do. It now skips
those, reusing `tm.retry_requested` so the rule is the heartbeat's, and the
tests cover the three cases that distinguish it: a trusted retry after the beat
holds the card back, a stranger's does not, and neither does one older than the
beat it would answer.
Audited and left alone: `open_pr` stays on `linked_prs`, not `adoptable`. It
looks like the broader check, but a card whose last beat is `error` is never
adopted (`time.py:1637` adopts only from `None`/`needs-done`/`would-dispatch`),
so a `PR: #N` line on a parked card does not put it in the review path, and
using `adoptable` would have hidden a genuinely stuck card. `linked_prs` is also
exactly what the heartbeat uses for this same decision at `time.py:1622`.
`python3 .github/time/test_time.py`, `test_seed.py` and `test_housekeeping.py`
are green. Each fix fails its new check when reverted.
Closes#171
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RqK2e5z4sMp2BKghDt1D8w
The router only ranked work someone had written a card for, so optimization
showed up only when a human noticed it. It now reads three of the factory's own
records over the last seven days and proposes the rest itself, deterministically.
router.py, one synthetic candidate per finding (`opt:<source>:<id>`, kind
optimization, no card yet, a drafted title and body that carries its own key and
a `DONE:` line):
- `ledger_candidates`: the week's spend grouped by model and by entry point (a
line's `entry` where it has one, else `pool`, the route it booked to). A group
that took over half the bill is a candidate, where there is a second group in
that dimension to compare it with; so is a model whose spend per call is double
its own median, which is a few heavy calls behind a cheap average. Pennies
(under $0.25 a week) are left alone and one group yields at most one candidate.
- `run_candidates`: Actions runs as `/actions/runs` gives them (the shape
`tea_actions` reads: the list, or `workflow_runs`). A workflow with two or more
failed or cancelled runs in the window is a candidate, with the failure count
and the last run's id.
- `parked_candidates`: a card parked by an `error` or `stuck` beat older than a
day, with no open PR and #78's automatic retries spent ("retry or split #N").
One Time will still retry on its own is left to it.
- `named` / `opt_candidates`: a source an open card already names (`Router:
opt:<source>:<id>` in the body) is not proposed again.
astra.py gathers the three sources (`ledger_week`, `action_runs`, `parked_cards`;
each is context, so unreadable means no candidates from it, never a failed run)
and the seat judges them like any other candidate. Live, a picked one is written
as a card, unqueued and a candidate from tomorrow, and is never worked on in
place; dry, the ranked table shows it as "would be written as a card". The log
line and the beat's `opt` say which source became which card.
Tests (test_time.py section 171): a ledger with one dominant route yields one
candidate, and so does the entry-point dimension, the median rule, pennies, a
single group and a stale window; two failed runs of one workflow yield one
candidate with its count and last run id; a parked card yields one unless it has
an open PR, retries left or was parked today; a source an open card names yields
none; Astra live writes the card unqueued while dry writes nothing.
Closes#171
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EzRXxLDJF4YDoZgzSsbW24
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
The first default model had been retired on Workers AI (5028 "deprecated"),
found by calling the live backend. Default to @cf/google/gemma-4-26b-a4b-it,
and accept both the older { response } and the OpenAI-style choices answer.
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
grok's Docscope round generated a real sample .docx and the finished page
with scripts inside run, then tried to cp them into place: each run starts
from a fresh checkout, so none of it reached the branch and the round
burned its steps. The run tool now takes keep: [paths] and commits exactly
those files afterwards (inside the checkout, not workflows, up to 2 MB,
at most ten), and the rules say plainly that everything else a run does is
thrown away.
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
Docscope's fix round ran 'python -m http.server' to test its page; the
600s timeout raised and took the whole round down. The run tool now stops
a command after TIME_RUN_TIMEOUT (default 180s) and tells the model to use
one-shot commands.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UpT7eWzeLhRwuYmsSQnbRb
Docscope's next fix round dodged the phrase check: 'This would be the real
implementation using DecompressionStream. For now, we'll create a realistic
representation...' and still returned canned parts. Phrase matching can be
talked around, so the inference tier now reads the code: a seat
(TIME_VENTURE_JUDGE, default grok) answers whether the output depends on
the user's input and lists features the page claims for the free product
that the code doesn't implement (paid tiers and stated limits don't count).
It runs only after the deterministic and browser gates pass, and its
absence (outage, no verdict) never blocks a launch.
Checked against the live pages: it passes MDecks and catches Docscope
('parseOfficeFile always returns createRealisticParsedParts(file), which
hardcodes identical XML regardless of input').
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UpT7eWzeLhRwuYmsSQnbRb
The cost cascade, applied to fixes: the cheap worker gets the first round;
when its page still fails the checks, the next round goes to a stronger
model instead of the same one again.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UpT7eWzeLhRwuYmsSQnbRb
Docscope's fix round added 19KB of script that renders on load and passed
the scripts-on/off check, but the code says 'Simulate processing (in a real
implementation, this would parse the file)': a dropped file gets a canned
leak report. For a privacy tool that is worse than no tool.
- the checks flag the phrases generated code uses for canned output;
- the brief and fix rounds say the sample must be real input through the
same code path as a user's input, and point at browser built-ins
(DecompressionStream for zip, DOMParser for XML).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UpT7eWzeLhRwuYmsSQnbRb
Docscope launched as a facade: a well-shaped page with no script at all.
The checks verified structure, never that the product works. Now:
- deterministic: the page and its local scripts must carry real code;
- headless Chrome loads a scripts-off twin next to the live page and
compares #try: if the product renders nothing of its own on first load
(a preloaded sample run through the real code), the page fails;
- fix rounds get the product description, so 'no working product' is
fixable in a round;
- at launch the chair rewrites the launch drafts against the README and
the page as built, with no invented people, stories or results (a draft
had made up a leaked-client-file anecdote); LAUNCH.md gets the same.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UpT7eWzeLhRwuYmsSQnbRb
- A venture stage that fails because Venice stays overloaded now posts one
'waiting for capacity' note and retries every tick without spending one
of its three strikes (MDecks and Docscope were one 429 from being closed).
- The worker hands over to TIME_WORKER_FALLBACK (default grok build) when its
model stays overloaded after the backoff retries.
- Fix: venture builds passed the worker spec with its venice: prefix, so the
seat lookup missed and every venture was built by kimi's code model
instead of the configured worker.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UpT7eWzeLhRwuYmsSQnbRb
One overloaded reply in the middle of a worker run used to throw away the
whole build (the MDecks relaunch hit exactly that). Seats and the worker
now retry up to three times (15s, 60s, 135s) before giving up; a 4xx like
a bad key still fails at once.
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
Pitches carry a name, who it's for, a next version and a CTA. The board may
rank nothing and needs two seats behind a winner. Every venture gets a
`signal` issue (👍 = I want the next version) and a Want-more section on the
page pointing at it. After ship: a launch stage drafts posts for the chair
(never sent), and a measure stage 24h later queues an iterate card or parks
the venture. Seats that reply in prose get re-asked once.
Co-Authored-By: Claude Fable 5.1 <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
GitHub skips pull_request workflows for commits made with the Actions token,
so a Venice worker's own test run is recorded as a commit status and counted
as the PR's check. A verdict posted in the same beat as the pr beat now counts.
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