rob thijssen 98f193d16b
All checks were successful
deploy / deploy (push) Successful in 6m1s
feat(data): implement JobStore and the Gitea read client
Closes #3 and #4.

Both specs had gaps that only appeared on contact, which is worth recording
because it is evidence for the plan-quality question in #10:

- `enqueue(&[DiscoveredIssue])` cannot know a JobKind. It is derivable from
  labels, so the store now carries the LabelProtocol and core gained
  `job_kind_for`. The precedence when an operator applies several mode labels
  had to be decided: plan beats implement, because planning produces the
  implementation children and so loses nothing, while the reverse silently
  discards the decomposition that was also asked for.
- `claim_next(worker, allowed_lanes)` had no lane to filter on. Migration 0002
  adds one, recorded at enqueue by `routing::lane_for` — the same function
  `route` now delegates to, so the two cannot drift. Deriving it at claim time
  instead would mean a forge request per claim, since labels are not stored.
- `list_opted_in_issues` returned a bare Vec with nowhere to put the ETag that
  #4's own step 3 requires, so a caller could not make the next poll
  conditional. It returns an `IssuePage` now, which also carries `not_modified`
  — a 304 is not an empty repo, and a poller that conflated them would treat
  every quiet poll as every issue having disappeared.

The claim is one statement: a CTE takes the row lock with SKIP LOCKED and the
update writes the claim, so select and update share a transaction without
managing one by hand. Returning a job to pending clears the claim, because
`pending_holds_no_claim` refuses the half-done version — the database is what
makes lease expiry safe rather than the code remembering to.

Enum values round-trip through serde rather than a hand-written match, so the
schema's check constraints and the Rust types are provably one vocabulary.

Also replaced a test from #2 that asserted exactly one migration exists. It
failed on the first legitimate migration, which teaches people to edit the
assertion rather than think. It now asserts what it was reaching for: versions
unique and ascending.

Verified against Postgres 18 — 15 database tests covering enqueue idempotency
across repeated polls, lane filtering including the agent:oc override, claim
metadata, lease expiry and reclaim, terminal jobs refusing transition, renewal
requiring you still hold the claim, and re-routing a queued job when its labels
change. Plus 12 mock-forge tests: If-None-Match sent and 304 distinguished from
empty, ETag surfaced, 429 retried but bounded, 503 retried then succeeding, 404
not retried, pull requests filtered out, and every unimplemented write failing
without touching the forge.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013TxK1CWPkFXqdcXMJ4hVe6
2026-08-07 20:20:09 +03:00

tireless

Keeps several repositories moving without an operator driving each change by hand. It watches Gitea (and GitHub, for legacy repos) and does three things:

  • discovers — surveys a repo and proposes issues worth opening;
  • plans — decomposes an issue into an epic and child issues, each specified well enough for a model that cannot ask questions;
  • implements — produces a branch and a pull request.

Those chain into a loop with exactly two human gates: a person decides what enters the system, and a person decides what merges. Discovery proposes but never admits its own proposals; nothing merges itself.

Two coding agents do the work, each spawned as the vendor's own binary:

  • Claude Code — discovery, planning, and implementation of issues that need interpretation. Uses the operator's Claude subscription by default, or pay-as-you-go if an API key is supplied.
  • OpenCode — implementation of issues that a tireless plan already specified, against the self-hosted helexa fleet. Never Anthropic; enforced at startup.

The rule of thumb: Claude Code gets judgement, OpenCode gets specification.

Full design, constraints and the staged implementation plan: doc/plan/design.md.

Status

Stage 0 (foundations) is built, deployed and verified.

Working: the domain model, routing, budgets, plan validation, the policy guards, configuration loading and validation, the four system prompts, and preflight. All three units run on bob, the dashboard is served at https://tireless.internal, and the deploy workflow is green end to end.

Nothing is polled or claimed yet — that is stage 1. The runner is up but has no job store to claim from.

Not built: Postgres persistence, the forge clients, the poll loop, and every agent executor. Stages 18 in §7 of the design document say what lands when.

tireless is its own first tracked repo — see design.md §10 for what that implies, including which parts of this repo are deliberately routed to the stronger lane.

Build

cargo test --workspace
cargo clippy --all-targets --all-features -- -D warnings
cargo fmt --all

cd dashboard && npm ci && npm run lint && npm run build

Run locally

cargo run -p tireless-api -- --config ./config.toml
cargo run -p tireless-worker -- --config ./config.toml poll
cargo run -p tireless-worker -- --config ./config.toml run

cd dashboard && npm run dev     # proxies /v1 to 127.0.0.1:23296

tireless preflight verifies configuration and credentials without starting a service: it reports which billing mode a Claude Code run would use and asserts the OpenCode lane is not pointed at Anthropic.

Deploy

CI-driven via Gitea Actions on merge to main (architecture/deployment-gitea-actions.md). One-time host provisioning — including the interactive Claude Code login and the Gitea bot account — is script/infra-setup.sh.

Host bob.hanzalova.internal (binaries, units, job trees)
API port 23296 (registered in architecture/port-allocations.md), bound 0.0.0.0, mesh-only
Ingress nginx on the hanzalova proxy — not on bob
Dashboard https://tireless.internal (mesh only), served from the proxy
Database magrathea.kosherinata.internal:5432, mTLS

Conventions

Follows lair/architecture; generic.md is the baseline. Three deliberate deviations:

  • tireless-agent crate beyond the standard entities/core/data split. Process orchestration is not data access, and it is shared by the runner and the CLI. (§1)
  • MemoryDenyWriteExecute=false on tireless-runner. Both agents are Node programs and V8's JIT needs write-then-execute pages. The API and poller keep the setting. (§8)
  • AGENTS.md is a symlink to CLAUDE.md. Both agents look for their own filename and the instructions are identical; a symlink is the only version of this that cannot drift.
Description
Autonomous issue-to-PR development driver for Claude Code and OpenCode
Readme 618 KiB
Languages
Rust 83.5%
Shell 8.6%
TypeScript 7%
JavaScript 0.5%
CSS 0.3%
Other 0.1%