rob thijssen 6545d1980b
All checks were successful
deploy / deploy (push) Successful in 5m35s
feat(data): add the initial schema and migrations
Closes #2. Three tables mirroring tireless-entities, plus the sqlx
compile-time-checking decision the rest of stage 1 inherits.

RUNTIME QUERIES, NOT `query!`. No .sqlx metadata, no DATABASE_URL to build.
lairball, the other house project on this cluster, does the same. The deciding
argument is specific to tireless: compile-time checking makes a database a build
dependency, and this crate is meant to be modified by a 27B model working
unattended. A build that fails without a database it cannot provision is one
that model cannot fix, and its documented failure mode is to improvise. The cost
is named in store.rs — a malformed query is caught by a test, not by cargo.

Enums are text with a check constraint, not Postgres enum types: adding a
JobKind variant should be a migration, not an ALTER TYPE holding a lock. Unit
tests assert every serde variant appears in the schema, so adding a variant
without a migration fails the build rather than the first job of that kind.

That surfaced a spelling that would have been permanent: `rename_all =
"snake_case"` turns Forge::GitHub into `git_hub`, across the database, the JSON
API and the generated TypeScript. Renamed to `github` now, while nothing is
persisted and GitHub support is still disabled.

The live-issue index is PARTIAL, and both directions of getting it wrong are
silent. A plain unique constraint on (forge, owner, repo, number) would forbid
re-running a terminal job, and would forbid the discovery lane outright, since
discovery recurs against one tracking issue on a cooldown. Partial on the
non-terminal states gives at most one live job per issue and unlimited history.

The claim index orders by created_at alone and leaves kind as a filter, because
the claim takes LIMIT 1 and can stop at the first match. Leading with kind sorts
every pending row on every claim: measured at 720 buffers versus 4, and the gap
grows with the backlog rather than staying fixed. INCLUDE (kind) was measured
too and dropped — FOR UPDATE visits the heap regardless.

Verified against Postgres 18, the same major as the house cluster: migrations
apply to an empty database and are a no-op on the second run; all eight
constraints reject what they should and admit what they should; and two
concurrent claimers of one pending job produce exactly one winner, with the
loser skipping rather than blocking.

Those live tests are #[ignore]d, not skipped on a missing variable, so a green
`cargo test` never implies the schema was exercised. CLAUDE.md says how to run
them and that an applied migration must never be edited.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013TxK1CWPkFXqdcXMJ4hVe6
2026-08-07 18:47:16 +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%