name: heartbeat # Time's clock: hourly, right away when a card gets `ready` or a spec gets `spec`, and when a PR is reviewed # (dry-run until TIME_MODE=live; merges only with TIME_AUTOMERGE=on). on: schedule: - cron: "7 * * * *" workflow_dispatch: issues: types: [labeled] pull_request_review: types: [submitted] issue_comment: types: [created] # (GitHub-style `permissions:` are ignored by Gitea; the Actions token has write access to this repo) concurrency: group: heartbeat cancel-in-progress: false jobs: tick: # issues: only `ready`/`spec` labels. comments: only on PRs, only a reviewer verdict (Approve/Object), never Time's own. if: >- (github.event_name != 'issues' || contains(fromJSON('["ready","spec"]'), github.event.label.name)) && (github.event_name != 'issue_comment' || (github.event.issue.pull_request && github.event.comment.user.login != 'gitea-actions' && (startsWith(github.event.comment.body, '**Approve') || startsWith(github.event.comment.body, 'Approve') || startsWith(github.event.comment.body, '**Object') || startsWith(github.event.comment.body, 'Object')))) runs-on: ubuntu-latest timeout-minutes: 55 # workers can overlap inside one job now (#75); under the hourly cadence, and the concurrency group queues the next run steps: - uses: actions/checkout@v5 with: sparse-checkout: .github/time persist-credentials: false # the worker's shell must not find the token in .git/config - run: python3 .github/time/time.py heartbeat env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} CURSOR_API_KEY: ${{ secrets.CURSOR_API_KEY }} VENICE_API_KEY: ${{ secrets.VENICE_API_KEY }} ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} # user-level secret on teahubxyz (docs/decisions.md on the spine) TIME_TOKEN: ${{ secrets.TIME_TOKEN }} # repo secret: the `time` bot opens and merges PRs (Gitea denies that to the Actions identity) TIME_SEAT_CLAUDE: ${{ secrets.TIME_SEAT_CLAUDE }} # umbrella seat, retired once the per-seat tokens below are in use (#76) TIME_SEAT_HAIKU: ${{ secrets.TIME_SEAT_HAIKU }} # one Gitea user per seat (model line): a seat's verdict is a real review by its own user (#76) TIME_SEAT_SONNET: ${{ secrets.TIME_SEAT_SONNET }} TIME_SEAT_OPUS: ${{ secrets.TIME_SEAT_OPUS }} TIME_SEAT_FABLE: ${{ secrets.TIME_SEAT_FABLE }} TIME_TRUSTED: ${{ vars.TIME_TRUSTED }} # org owners etc.; collaborators are trusted without being listed TIME_RUN_IMAGE: node:22-bookworm # the worker's shell runs in a throwaway container, not next to these secrets TIME_MODE: ${{ vars.TIME_MODE }} TIME_DAILY_CAP: ${{ vars.TIME_DAILY_CAP }} TIME_DAILY_USD: ${{ vars.TIME_DAILY_USD }} # spend governor: dollars per day (default 5) … TIME_MONTHLY_USD: ${{ vars.TIME_MONTHLY_USD }} # … and per month (default 100); ledger in time/ledger/ TIME_CAP_SLACK: ${{ vars.TIME_CAP_SLACK }} # a run launched within budget may finish up to this x the daily cap (default 1.5; #117) TIME_ANTHROPIC_API: ${{ vars.TIME_ANTHROPIC_API }} # messages (default, prompt caching) | compat = the OpenAI-compatible endpoint (#119) TIME_CURSOR_MODEL: ${{ vars.TIME_CURSOR_MODEL }} TIME_REVIEW: ${{ vars.TIME_REVIEW }} # one seat, or a comma-separated council (#77) TIME_REVIEW_TIEBREAK: ${{ vars.TIME_REVIEW_TIEBREAK }} # the seat called only on a split, e.g. venice:kimi (#77) TIME_REVIEW_QUORUM: ${{ vars.TIME_REVIEW_QUORUM }} # approves minus objects needed to land (default 1; #77) TIME_STUCK_HOURS: ${{ vars.TIME_STUCK_HOURS }} TIME_AUTOMERGE: ${{ vars.TIME_AUTOMERGE }} TIME_REQUIRE_CHECKS: ${{ vars.TIME_REQUIRE_CHECKS }} TIME_PROTECTED: ${{ vars.TIME_PROTECTED }} TIME_PARALLEL: ${{ vars.TIME_PARALLEL }} TIME_PLANNER: ${{ vars.TIME_PLANNER }} TIME_WORKER: ${{ vars.TIME_WORKER }} TIME_WORKER_STEPS: ${{ vars.TIME_WORKER_STEPS }} TIME_WORKER_MAX_TOKENS: ${{ vars.TIME_WORKER_MAX_TOKENS }}