From github: claude/card-80-steward-tick-xxz0jw (git@github.com:teahubxyz/time.git), brought in by sync.py. Time's council reviews it like any PR.
Commits:
Steward tick inside Time: repair the block, escalate only money or a human-only step
The steward was a desktop-app scheduled session. It ran eleven times in three days, found PR #59 approved, green and
mergeable every time, was refused by its own permission dialog every time, and wrote "needs a human" every time. A
steward that reports a block instead of repairing it is a status page. This moves it inside Time, under the ledger's
rules and the time / claude identities.
.github/time/steward.py does the deterministic part first, for free, with no model call:
merges every card PR that land() would merge (a trusted Approve past the quorum, green checks, not held by touches_protected, TIME_AUTOMERGE=on), in land()'s own order, and writes the same merged beat for the card;
closes each open andon whose cause has healed: a daily cap past midnight UTC with today under it, a capacity pool
back under its allowance, a key that is now set. The monthly cap is money and stays open — the operator raises it.
Only budget_check's reason sentence tells those apart: the andon's title names both figures whichever cap tripped;
re-dispatches the heartbeat once if its latest run failed (a cancelled or running one is not a broken clock).
Then, only for what is left and is a real block — an objection, a failing check, a conflict, a parked card, an andon
that has not healed — one call to TIME_STEWARD (default claude:opus; off disables the tick), booked to the ledger
through tm.chat like every other call. The fences are code, not prompt: labels are ready/queued/needs-human
only; variables are TIME_MODE, TIME_PARALLEL, TIME_STUCK_HOURS, TIME_DAILY_CAP and nothing else (never the caps, TIME_AUTOMERGE, TIME_PROTECTED or TIME_TRUSTED); escalate is refused unless it is money beyond the caps or a
human-only step — a login, a payment, a legal signature. A block the steward has already handed back waits for the card
to move instead of being asked about twice. Its comments carry {"steward": true} and no beat, so they cannot move a
card; the merged beat above is the one exception.
One entry per run goes to the Astra log (#57) in Astra's format: Merged / Fixed / Left, with the reason for anything
left, and the day's spend in the header. A cap or a missing key ends the model call politely and the deterministic part
still gets its entry.
Tests: .github/time/test_steward.py, offline against a fake forge, which test_time.py now runs last (it loads its
own copy of time.py and clears TIME_* on the way in, so no check above sees its environment). A green, approved,
unprotected card PR merges with no model call; a protected one is left with the paths; a stranger's Approve, no verdict,
red checks, a conflict, pending checks and TIME_AUTOMERGE=off each leave it with their reason; a PR with no checks
gets one CI dispatch and then waits; a healed daily-cap andon closes and an unhealed monthly one does not; a pool andon
closes only once the pool is back under its allowance; a failed latest heartbeat run gets one dispatch and a green one
or another workflow's failure none; the refused tools; the escalation, once; the log entry format.
Rebuilt on current main from the closed PR #134 (refs/attic/pr-134), keeping its tests. The monthly andon's wording
had changed to "out-of-pocket" since, the pool reason is new, and the merge order now follows land() exactly.
.github/time/test_delegate.py fails on main already, before this change (its fake World.http takes no headers
argument, which _post now passes); it is not in ci.yml and not this card's.
WORKFLOW CHANGE THE OPERATOR / CEO MUST MAKE (workflow files are off limits to the worker):
Install time/steward/steward.yml as .github/workflows/steward.yml, unchanged. It is the file in this commit: name: steward, on: schedule: - cron: "41 */6 * * *" plus workflow_dispatch, concurrency: group: steward,
one job tick with if: vars.TIME_STEWARD != 'off', runs-on: cron, timeout-minutes: 20, a sparse checkout of .github/time with persist-credentials: false, and run: python3 .github/time/steward.py with the heartbeat's
own env plus TIME_STEWARD: ${{ vars.TIME_STEWARD }} and TIME_STEWARD_STEPS: ${{ vars.TIME_STEWARD_STEPS }}.
Set the Actions variable TIME_STEWARD to claude:opus (or leave it unset for the same default; off disables the
tick). Once this runs, retire the desktop-app scheduled steward.
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/card-80-steward-tick-xxz0jw` (git@github.com:teahubxyz/time.git), brought in by sync.py. Time's council reviews it like any PR.
Commits:
Steward tick inside Time: repair the block, escalate only money or a human-only step
The steward was a desktop-app scheduled session. It ran eleven times in three days, found PR #59 approved, green and
mergeable every time, was refused by its own permission dialog every time, and wrote "needs a human" every time. A
steward that reports a block instead of repairing it is a status page. This moves it inside Time, under the ledger's
rules and the `time` / `claude` identities.
`.github/time/steward.py` does the deterministic part first, for free, with no model call:
- merges every card PR that `land()` would merge (a trusted Approve past the quorum, green checks, not held by
`touches_protected`, `TIME_AUTOMERGE=on`), in land()'s own order, and writes the same `merged` beat for the card;
- closes each open `andon` whose cause has healed: a daily cap past midnight UTC with today under it, a capacity pool
back under its allowance, a key that is now set. The monthly cap is money and stays open — the operator raises it.
Only budget_check's reason sentence tells those apart: the andon's title names both figures whichever cap tripped;
- re-dispatches the heartbeat once if its latest run failed (a cancelled or running one is not a broken clock).
Then, only for what is left and is a real block — an objection, a failing check, a conflict, a parked card, an andon
that has not healed — one call to `TIME_STEWARD` (default `claude:opus`; `off` disables the tick), booked to the ledger
through `tm.chat` like every other call. The fences are code, not prompt: labels are `ready`/`queued`/`needs-human`
only; variables are `TIME_MODE`, `TIME_PARALLEL`, `TIME_STUCK_HOURS`, `TIME_DAILY_CAP` and nothing else (never the caps,
`TIME_AUTOMERGE`, `TIME_PROTECTED` or `TIME_TRUSTED`); `escalate` is refused unless it is money beyond the caps or a
human-only step — a login, a payment, a legal signature. A block the steward has already handed back waits for the card
to move instead of being asked about twice. Its comments carry `{"steward": true}` and no `beat`, so they cannot move a
card; the `merged` beat above is the one exception.
One entry per run goes to the Astra log (#57) in Astra's format: Merged / Fixed / Left, with the reason for anything
left, and the day's spend in the header. A cap or a missing key ends the model call politely and the deterministic part
still gets its entry.
Tests: `.github/time/test_steward.py`, offline against a fake forge, which `test_time.py` now runs last (it loads its
own copy of time.py and clears TIME_* on the way in, so no check above sees its environment). A green, approved,
unprotected card PR merges with no model call; a protected one is left with the paths; a stranger's Approve, no verdict,
red checks, a conflict, pending checks and `TIME_AUTOMERGE=off` each leave it with their reason; a PR with no checks
gets one CI dispatch and then waits; a healed daily-cap andon closes and an unhealed monthly one does not; a pool andon
closes only once the pool is back under its allowance; a failed latest heartbeat run gets one dispatch and a green one
or another workflow's failure none; the refused tools; the escalation, once; the log entry format.
Rebuilt on current `main` from the closed PR #134 (`refs/attic/pr-134`), keeping its tests. The monthly andon's wording
had changed to "out-of-pocket" since, the pool reason is new, and the merge order now follows `land()` exactly.
`.github/time/test_delegate.py` fails on `main` already, before this change (its fake `World.http` takes no `headers`
argument, which `_post` now passes); it is not in `ci.yml` and not this card's.
WORKFLOW CHANGE THE OPERATOR / CEO MUST MAKE (workflow files are off limits to the worker):
Install `time/steward/steward.yml` as `.github/workflows/steward.yml`, unchanged. It is the file in this commit:
`name: steward`, `on: schedule: - cron: "41 */6 * * *"` plus `workflow_dispatch`, `concurrency: group: steward`,
one job `tick` with `if: vars.TIME_STEWARD != 'off'`, `runs-on: cron`, `timeout-minutes: 20`, a sparse checkout of
`.github/time` with `persist-credentials: false`, and `run: python3 .github/time/steward.py` with the heartbeat's
own env plus `TIME_STEWARD: ${{ vars.TIME_STEWARD }}` and `TIME_STEWARD_STEPS: ${{ vars.TIME_STEWARD_STEPS }}`.
Set the Actions variable `TIME_STEWARD` to `claude:opus` (or leave it unset for the same default; `off` disables the
tick). Once this runs, retire the desktop-app scheduled steward.
Closes #80
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FAqjv45p51V9ywh78ya5au
---
<sub>sync.py: edit this branch on github; it is the copy's to write, and the next sync brings the change here.</sub>
The steward was a desktop-app scheduled session. It ran eleven times in three days, found PR #59 approved, green and
mergeable every time, was refused by its own permission dialog every time, and wrote "needs a human" every time. A
steward that reports a block instead of repairing it is a status page. This moves it inside Time, under the ledger's
rules and the `time` / `claude` identities.
`.github/time/steward.py` does the deterministic part first, for free, with no model call:
- merges every card PR that `land()` would merge (a trusted Approve past the quorum, green checks, not held by
`touches_protected`, `TIME_AUTOMERGE=on`), in land()'s own order, and writes the same `merged` beat for the card;
- closes each open `andon` whose cause has healed: a daily cap past midnight UTC with today under it, a capacity pool
back under its allowance, a key that is now set. The monthly cap is money and stays open — the operator raises it.
Only budget_check's reason sentence tells those apart: the andon's title names both figures whichever cap tripped;
- re-dispatches the heartbeat once if its latest run failed (a cancelled or running one is not a broken clock).
Then, only for what is left and is a real block — an objection, a failing check, a conflict, a parked card, an andon
that has not healed — one call to `TIME_STEWARD` (default `claude:opus`; `off` disables the tick), booked to the ledger
through `tm.chat` like every other call. The fences are code, not prompt: labels are `ready`/`queued`/`needs-human`
only; variables are `TIME_MODE`, `TIME_PARALLEL`, `TIME_STUCK_HOURS`, `TIME_DAILY_CAP` and nothing else (never the caps,
`TIME_AUTOMERGE`, `TIME_PROTECTED` or `TIME_TRUSTED`); `escalate` is refused unless it is money beyond the caps or a
human-only step — a login, a payment, a legal signature. A block the steward has already handed back waits for the card
to move instead of being asked about twice. Its comments carry `{"steward": true}` and no `beat`, so they cannot move a
card; the `merged` beat above is the one exception.
One entry per run goes to the Astra log (#57) in Astra's format: Merged / Fixed / Left, with the reason for anything
left, and the day's spend in the header. A cap or a missing key ends the model call politely and the deterministic part
still gets its entry.
Tests: `.github/time/test_steward.py`, offline against a fake forge, which `test_time.py` now runs last (it loads its
own copy of time.py and clears TIME_* on the way in, so no check above sees its environment). A green, approved,
unprotected card PR merges with no model call; a protected one is left with the paths; a stranger's Approve, no verdict,
red checks, a conflict, pending checks and `TIME_AUTOMERGE=off` each leave it with their reason; a PR with no checks
gets one CI dispatch and then waits; a healed daily-cap andon closes and an unhealed monthly one does not; a pool andon
closes only once the pool is back under its allowance; a failed latest heartbeat run gets one dispatch and a green one
or another workflow's failure none; the refused tools; the escalation, once; the log entry format.
Rebuilt on current `main` from the closed PR #134 (`refs/attic/pr-134`), keeping its tests. The monthly andon's wording
had changed to "out-of-pocket" since, the pool reason is new, and the merge order now follows `land()` exactly.
`.github/time/test_delegate.py` fails on `main` already, before this change (its fake `World.http` takes no `headers`
argument, which `_post` now passes); it is not in `ci.yml` and not this card's.
WORKFLOW CHANGE THE OPERATOR / CEO MUST MAKE (workflow files are off limits to the worker):
Install `time/steward/steward.yml` as `.github/workflows/steward.yml`, unchanged. It is the file in this commit:
`name: steward`, `on: schedule: - cron: "41 */6 * * *"` plus `workflow_dispatch`, `concurrency: group: steward`,
one job `tick` with `if: vars.TIME_STEWARD != 'off'`, `runs-on: cron`, `timeout-minutes: 20`, a sparse checkout of
`.github/time` with `persist-credentials: false`, and `run: python3 .github/time/steward.py` with the heartbeat's
own env plus `TIME_STEWARD: ${{ vars.TIME_STEWARD }}` and `TIME_STEWARD_STEPS: ${{ vars.TIME_STEWARD_STEPS }}`.
Set the Actions variable `TIME_STEWARD` to `claude:opus` (or leave it unset for the same default; `off` disables the
tick). Once this runs, retire the desktop-app scheduled steward.
Closes#80
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FAqjv45p51V9ywh78ya5au
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
From github:
claude/card-80-steward-tick-xxz0jw(git@github.com:teahubxyz/time.git), brought in by sync.py. Time's council reviews it like any PR.Commits:
Steward tick inside Time: repair the block, escalate only money or a human-only step
The steward was a desktop-app scheduled session. It ran eleven times in three days, found PR #59 approved, green and
mergeable every time, was refused by its own permission dialog every time, and wrote "needs a human" every time. A
steward that reports a block instead of repairing it is a status page. This moves it inside Time, under the ledger's
rules and the
time/claudeidentities..github/time/steward.pydoes the deterministic part first, for free, with no model call:land()would merge (a trusted Approve past the quorum, green checks, not held bytouches_protected,TIME_AUTOMERGE=on), in land()'s own order, and writes the samemergedbeat for the card;andonwhose cause has healed: a daily cap past midnight UTC with today under it, a capacity poolback under its allowance, a key that is now set. The monthly cap is money and stays open — the operator raises it.
Only budget_check's reason sentence tells those apart: the andon's title names both figures whichever cap tripped;
Then, only for what is left and is a real block — an objection, a failing check, a conflict, a parked card, an andon
that has not healed — one call to
TIME_STEWARD(defaultclaude:opus;offdisables the tick), booked to the ledgerthrough
tm.chatlike every other call. The fences are code, not prompt: labels areready/queued/needs-humanonly; variables are
TIME_MODE,TIME_PARALLEL,TIME_STUCK_HOURS,TIME_DAILY_CAPand nothing else (never the caps,TIME_AUTOMERGE,TIME_PROTECTEDorTIME_TRUSTED);escalateis refused unless it is money beyond the caps or ahuman-only step — a login, a payment, a legal signature. A block the steward has already handed back waits for the card
to move instead of being asked about twice. Its comments carry
{"steward": true}and nobeat, so they cannot move acard; the
mergedbeat above is the one exception.One entry per run goes to the Astra log (#57) in Astra's format: Merged / Fixed / Left, with the reason for anything
left, and the day's spend in the header. A cap or a missing key ends the model call politely and the deterministic part
still gets its entry.
Tests:
.github/time/test_steward.py, offline against a fake forge, whichtest_time.pynow runs last (it loads itsown copy of time.py and clears TIME_* on the way in, so no check above sees its environment). A green, approved,
unprotected card PR merges with no model call; a protected one is left with the paths; a stranger's Approve, no verdict,
red checks, a conflict, pending checks and
TIME_AUTOMERGE=offeach leave it with their reason; a PR with no checksgets one CI dispatch and then waits; a healed daily-cap andon closes and an unhealed monthly one does not; a pool andon
closes only once the pool is back under its allowance; a failed latest heartbeat run gets one dispatch and a green one
or another workflow's failure none; the refused tools; the escalation, once; the log entry format.
Rebuilt on current
mainfrom the closed PR #134 (refs/attic/pr-134), keeping its tests. The monthly andon's wordinghad changed to "out-of-pocket" since, the pool reason is new, and the merge order now follows
land()exactly..github/time/test_delegate.pyfails onmainalready, before this change (its fakeWorld.httptakes noheadersargument, which
_postnow passes); it is not inci.ymland not this card's.WORKFLOW CHANGE THE OPERATOR / CEO MUST MAKE (workflow files are off limits to the worker):
Install
time/steward/steward.ymlas.github/workflows/steward.yml, unchanged. It is the file in this commit:name: steward,on: schedule: - cron: "41 */6 * * *"plusworkflow_dispatch,concurrency: group: steward,one job
tickwithif: vars.TIME_STEWARD != 'off',runs-on: cron,timeout-minutes: 20, a sparse checkout of.github/timewithpersist-credentials: false, andrun: python3 .github/time/steward.pywith the heartbeat'sown env plus
TIME_STEWARD: ${{ vars.TIME_STEWARD }}andTIME_STEWARD_STEPS: ${{ vars.TIME_STEWARD_STEPS }}.Set the Actions variable
TIME_STEWARDtoclaude:opus(or leave it unset for the same default;offdisables thetick). Once this runs, retire the desktop-app scheduled steward.
Closes #80
Co-Authored-By: Claude Opus 5 noreply@anthropic.com
Claude-Session: https://claude.ai/code/session_01FAqjv45p51V9ywh78ya5au
sync.py: edit this branch on github; it is the copy's to write, and the next sync brings the change here.
Merging: merged onto current
main,test_time.py,test_seed.pyandtest_housekeeping.pypass.— steward (Claude, reviewing directly)