Router: record each pick's outcome, so a learning policy has data #203

Merged
time merged 2 commits from claude/router-pick-outcomes-99zcnh into main 2026-10-19 21:53:46 +00:00
Collaborator

From github: claude/router-pick-outcomes-99zcnh (git@github.com:teahubxyz/time.git), brought in by sync.py. Time's council reviews it like any PR.

Commits:

Router: record each pick's outcome, so a learning policy has data

Part of #72, follows #168. The router's policy is still one function
(router.score); a restless bandit needs rewards, and nothing recorded
what a pick turned into. This card is the data, not the policy.

  • router.py: the shape of an outcome and the arithmetic over the log.
    picks() reads every live pick out of Astra's beats (a card picked
    twice keeps its first pick; a dry run picked nothing), outcome()
    settles one pick into a line and returns None while its card is open,
    read_outcomes()/summary()/outcome_lines() are the read side.
  • housekeeping.py step 4: one line per settled pick appended to
    time/ledger/router-outcomes.jsonl in one commit, dry-run writes
    nothing. A pick already in the file is never written twice.
  • astra.py: one summary line per kind (picks, merged share, median
    days, median $) over the ranked table, so the owner sees what a kind
    returns before changing its weight.

Two notes on what the card asked for:

  • Spend proxy: the card offered the card's worker beats' usd or the
    runs' time window. Beats carry no usd today, so this uses the runs'
    time window — each dispatched beat until the next beat on the card
    — which attributes a run's spend to the card that ran it instead of
    charging it the whole factory's spend over the days it was open.
    Review and planning calls on the card are not counted; two cards
    whose runs overlap are each charged their own window. Documented in
    time/ledger/README.md.
  • The line carries pr as well as the shape the card listed: the card
    asks for the PR number and the line is the only place to keep it.
  • The file lives under time/ledger/, as the card says, since that is
    the one path ci.yml ignores and the sweep appends to it daily. No
    workflow change is needed: housekeeping.yml already passes
    GITHUB_TOKEN (the push) and TIME_VENTURE_MEASURE_DAYS (day 7).

A venture pick waits until its venture's day 7 has passed and then
carries the signups counted at that day; with no registry entry for the
card the line goes in with no count rather than waiting forever.

Tests: test_housekeeping.py covers a merged pick, an unmerged pick, a
pick still in flight (no line), idempotence, the venture's day-7 count
and dry-run; test_time.py section 172 covers the pure functions, the
summary's arithmetic and the log's header. All three suites green.

Closes #172

Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com
Claude-Session: https://claude.ai/code/session_0198aoK6ddbLN4d6q8r3J7vg


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

From github: `claude/router-pick-outcomes-99zcnh` (git@github.com:teahubxyz/time.git), brought in by sync.py. Time's council reviews it like any PR. Commits: Router: record each pick's outcome, so a learning policy has data Part of #72, follows #168. The router's policy is still one function (`router.score`); a restless bandit needs rewards, and nothing recorded what a pick turned into. This card is the data, not the policy. - `router.py`: the shape of an outcome and the arithmetic over the log. `picks()` reads every live pick out of Astra's beats (a card picked twice keeps its first pick; a dry run picked nothing), `outcome()` settles one pick into a line and returns None while its card is open, `read_outcomes()`/`summary()`/`outcome_lines()` are the read side. - `housekeeping.py` step 4: one line per settled pick appended to `time/ledger/router-outcomes.jsonl` in one commit, dry-run writes nothing. A pick already in the file is never written twice. - `astra.py`: one summary line per kind (picks, merged share, median days, median $) over the ranked table, so the owner sees what a kind returns before changing its weight. Two notes on what the card asked for: - Spend proxy: the card offered the card's worker beats' `usd` or the runs' time window. Beats carry no `usd` today, so this uses the runs' time window — each `dispatched` beat until the next beat on the card — which attributes a run's spend to the card that ran it instead of charging it the whole factory's spend over the days it was open. Review and planning calls on the card are not counted; two cards whose runs overlap are each charged their own window. Documented in `time/ledger/README.md`. - The line carries `pr` as well as the shape the card listed: the card asks for the PR number and the line is the only place to keep it. - The file lives under `time/ledger/`, as the card says, since that is the one path `ci.yml` ignores and the sweep appends to it daily. No workflow change is needed: `housekeeping.yml` already passes `GITHUB_TOKEN` (the push) and `TIME_VENTURE_MEASURE_DAYS` (day 7). A venture pick waits until its venture's day 7 has passed and then carries the signups counted at that day; with no registry entry for the card the line goes in with no count rather than waiting forever. Tests: `test_housekeeping.py` covers a merged pick, an unmerged pick, a pick still in flight (no line), idempotence, the venture's day-7 count and dry-run; `test_time.py` section 172 covers the pure functions, the summary's arithmetic and the log's header. All three suites green. Closes #172 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0198aoK6ddbLN4d6q8r3J7vg --- <sub>sync.py: edit this branch on github; it is the copy's to write, and the next sync brings the change here.</sub>
time added 1 commit 2026-10-19 19:15:40 +00:00
Router: record each pick's outcome, so a learning policy has data
heartbeat / tick (pull_request_review) Successful in 15s
ci / test (pull_request) Successful in 45s
e69fbd32eb
Part of #72, follows #168. The router's policy is still one function
(`router.score`); a restless bandit needs rewards, and nothing recorded
what a pick turned into. This card is the data, not the policy.

- `router.py`: the shape of an outcome and the arithmetic over the log.
  `picks()` reads every live pick out of Astra's beats (a card picked
  twice keeps its first pick; a dry run picked nothing), `outcome()`
  settles one pick into a line and returns None while its card is open,
  `read_outcomes()`/`summary()`/`outcome_lines()` are the read side.
- `housekeeping.py` step 4: one line per settled pick appended to
  `time/ledger/router-outcomes.jsonl` in one commit, dry-run writes
  nothing. A pick already in the file is never written twice.
- `astra.py`: one summary line per kind (picks, merged share, median
  days, median $) over the ranked table, so the owner sees what a kind
  returns before changing its weight.

Two notes on what the card asked for:

- Spend proxy: the card offered the card's worker beats' `usd` or the
  runs' time window. Beats carry no `usd` today, so this uses the runs'
  time window — each `dispatched` beat until the next beat on the card
  — which attributes a run's spend to the card that ran it instead of
  charging it the whole factory's spend over the days it was open.
  Review and planning calls on the card are not counted; two cards
  whose runs overlap are each charged their own window. Documented in
  `time/ledger/README.md`.
- The line carries `pr` as well as the shape the card listed: the card
  asks for the PR number and the line is the only place to keep it.
- The file lives under `time/ledger/`, as the card says, since that is
  the one path `ci.yml` ignores and the sweep appends to it daily. No
  workflow change is needed: `housekeeping.yml` already passes
  `GITHUB_TOKEN` (the push) and `TIME_VENTURE_MEASURE_DAYS` (day 7).

A venture pick waits until its venture's day 7 has passed and then
carries the signups counted at that day; with no registry entry for the
card the line goes in with no count rather than waiting forever.

Tests: `test_housekeeping.py` covers a merged pick, an unmerged pick, a
pick still in flight (no line), idempotence, the venture's day-7 count
and dry-run; `test_time.py` section 172 covers the pure functions, the
summary's arithmetic and the log's header. All three suites green.

Closes #172

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0198aoK6ddbLN4d6q8r3J7vg
haiku approved these changes 2026-10-19 20:07:59 +00:00
Dismissed
haiku left a comment
Collaborator

Approve, with two non-blocking follow-ups.

Checked against the card: each settled pick gets one line with merged, PR, days, usd and signups; in-flight picks produce nothing; the sweep is idempotent; dry-run writes nothing; Astra gets a per-kind summary; the file sits under time/ledger/. The deviations (extra pr field, runs'-window spend proxy) are disclosed in the PR and documented in time/ledger/README.md. CI is green.

  1. housekeeping.py:~272 calls tm.git_commit(..., append=...). The test stub Forge.push accepts append=, but the stub was written to match the call, so tests can't confirm the real time.py signature. Please confirm git_commit accepts append before merging, or the first live sweep with a settled pick will fail.
  2. test_time.py (section 172): astra.tm.gh = gh80 and astra.tm.issues = ... are never restored to their originals; the block ends by reassigning gh80. Later sections that touch tm.gh will run against the fake. Save the originals and restore them at the end of the block.

Minor, no action needed: window_usd and day_after look right, including the December month rollover in months().

review by haiku · claude-haiku-5-5 · via Time

**Approve**, with two non-blocking follow-ups. Checked against the card: each settled pick gets one line with merged, PR, days, usd and signups; in-flight picks produce nothing; the sweep is idempotent; dry-run writes nothing; Astra gets a per-kind summary; the file sits under `time/ledger/`. The deviations (extra `pr` field, runs'-window spend proxy) are disclosed in the PR and documented in `time/ledger/README.md`. CI is green. 1. `housekeeping.py:~272` calls `tm.git_commit(..., append=...)`. The test stub `Forge.push` accepts `append=`, but the stub was written to match the call, so tests can't confirm the real `time.py` signature. Please confirm `git_commit` accepts `append` before merging, or the first live sweep with a settled pick will fail. 2. `test_time.py` (section 172): `astra.tm.gh = gh80` and `astra.tm.issues = ...` are never restored to their originals; the block ends by reassigning `gh80`. Later sections that touch `tm.gh` will run against the fake. Save the originals and restore them at the end of the block. Minor, no action needed: `window_usd` and `day_after` look right, including the December month rollover in `months()`. <sub>review by haiku · claude-haiku-5-5 · via Time</sub>

⏱️ Time: PR #203 is approved but conflicts with main, and I don't guess at conflicts: I left claude/router-pick-outcomes-99zcnh as it was. Merge origin/main into it and resolve:

CHANGELOG.md

<<<<<<< HEAD
- The router records what each pick turned into (#172), so a learning policy (a restless bandit) has rewards to read. Housekeeping's daily sweep settles every card the router picked in live mode into one line of `time/ledger/router-outcomes.jsonl` — `{"n", "kind", "picked", "settled", "merged", "pr", "days", "usd", "signups"}` — once the card closes: merged or not and its PR from the card's own beats, the days from pick to close, the dollars the ledger booked while the card's worker ran (each `dispatched` beat until the next beat on the card: the ledger has no card field, so this is the runs'-window proxy, not the card's exact bill), and, for a venture pick, the venture's signups at day 7 (that pick waits until day 7 has passed). A pick whose card is still open is in flight and produces nothing; one already in the file is never written twice. Over the ranked table Astra's log now shows one line per kind — picks, merged share, median days, median $ — so the owner can see what a kind returns before changing its weight. The file sits under `time/ledger/`, the one path CI ignores, since the sweep appends to it daily. No policy change: `router.score` still makes every pick.

Comment /time retry to send the worker back to the branch with these hunks.

⏱️ Time: PR #203 is approved but conflicts with `main`, and I don't guess at conflicts: I left `claude/router-pick-outcomes-99zcnh` as it was. Merge `origin/main` into it and resolve: `CHANGELOG.md` ``` <<<<<<< HEAD - The router records what each pick turned into (#172), so a learning policy (a restless bandit) has rewards to read. Housekeeping's daily sweep settles every card the router picked in live mode into one line of `time/ledger/router-outcomes.jsonl` — `{"n", "kind", "picked", "settled", "merged", "pr", "days", "usd", "signups"}` — once the card closes: merged or not and its PR from the card's own beats, the days from pick to close, the dollars the ledger booked while the card's worker ran (each `dispatched` beat until the next beat on the card: the ledger has no card field, so this is the runs'-window proxy, not the card's exact bill), and, for a venture pick, the venture's signups at day 7 (that pick waits until day 7 has passed). A pick whose card is still open is in flight and produces nothing; one already in the file is never written twice. Over the ranked table Astra's log now shows one line per kind — picks, merged share, median days, median $ — so the owner can see what a kind returns before changing its weight. The file sits under `time/ledger/`, the one path CI ignores, since the sweep appends to it daily. No policy change: `router.score` still makes every pick. ``` Comment `/time retry` to send the worker back to the branch with these hunks.
time added the needs-human label 2026-10-19 20:08:17 +00:00
time added 1 commit 2026-10-19 21:45:39 +00:00
card #172: merge main (CHANGELOG lines from both sides kept)
heartbeat / tick (pull_request_review) Successful in 12s
ci / test (pull_request) Successful in 45s
eb8009fb88
time removed the needs-human label 2026-10-19 21:45:39 +00:00
haiku approved these changes 2026-10-19 21:53:33 +00:00
haiku left a comment
Collaborator

Approve

The change matches card #172: one settled line per live pick, the spend proxy is stated and documented, the venture day-7 count is in, and the summary header is wired into Astra's table. CI is green. I did not run the suites locally, so this rests on the CI status and the diff.

Non-blocking notes:

  • test_time.py, section 172 (diff around @@ -1331): the block reassigns astra.tm.issues, astra.tm.think, and astra.tm.gh after the section-80 restore and never puts them back. Later sections that use these functions now see the fakes. CI passes, so nothing breaks today, but a save/restore like the one in section 80 would prevent a future failure.
  • housekeeping.py step_outcomes / signups_by: signup_people(until) re-reads every signup issue for each venture pick. Fine at current volume; cache it per run if the pick count grows.
  • housekeeping.py step_outcomes (dedup): have is read once per sweep, so two overlapping sweeps could write the same pick twice. The daily schedule makes this unlikely.
  • housekeeping.py window_usd: the window compares ledger at strings against stamp() output, so it depends on the ledger writing YYYY-MM-DDTHH:MM:SSZ. The PR says it does; worth a quick check against a real ledger line.
  • router.py outcome (merged detection): merged keys off a merged beat. A card closed by hand after a PR landed without a merged beat would log merged: false. Acceptable for now, and the README describes the field as the card's own beats.

review by haiku · claude-haiku-5-5 · via Time

**Approve** The change matches card #172: one settled line per live pick, the spend proxy is stated and documented, the venture day-7 count is in, and the summary header is wired into Astra's table. CI is green. I did not run the suites locally, so this rests on the CI status and the diff. Non-blocking notes: - **test_time.py, section 172 (diff around `@@ -1331`):** the block reassigns `astra.tm.issues`, `astra.tm.think`, and `astra.tm.gh` after the section-80 restore and never puts them back. Later sections that use these functions now see the fakes. CI passes, so nothing breaks today, but a save/restore like the one in section 80 would prevent a future failure. - **housekeeping.py `step_outcomes` / `signups_by`:** `signup_people(until)` re-reads every signup issue for each venture pick. Fine at current volume; cache it per run if the pick count grows. - **housekeeping.py `step_outcomes` (dedup):** `have` is read once per sweep, so two overlapping sweeps could write the same pick twice. The daily schedule makes this unlikely. - **housekeeping.py `window_usd`:** the window compares ledger `at` strings against `stamp()` output, so it depends on the ledger writing `YYYY-MM-DDTHH:MM:SSZ`. The PR says it does; worth a quick check against a real ledger line. - **router.py `outcome` (merged detection):** `merged` keys off a `merged` beat. A card closed by hand after a PR landed without a `merged` beat would log `merged: false`. Acceptable for now, and the README describes the field as the card's own beats. <sub>review by haiku · claude-haiku-5-5 · via Time</sub>
time merged commit 4814d45280 into main 2026-10-19 21:53:46 +00:00
time deleted branch claude/router-pick-outcomes-99zcnh 2026-10-19 21:53:47 +00:00
Sign in to join this conversation.