Files
tireless/asset/config/config.toml.tmpl
rob thijssen 581e6ae738 feat(discover): add the discovery lane and the autonomy boundary
The docs described a reactive executor: every entry point was a human label, and
the only issue tireless ever created was a plan child. Nothing surveyed a repo or
proposed work, which is the half that makes this continuous rather than
on-demand.

Add JobKind::Discover, routed always to Claude Code (proposing work is the
highest-judgement, lowest-volume task), the tireless/discover and
tireless/proposed labels, and prompt/discover.cc.md as a fourth member of the
versioned prompt set. The contract version does not move: the plan structure is
unchanged, and bumping for less than a shape change trains people to bump
reflexively.

With discovery comes the question of where the loop closes, which was previously
unspecified — routing inferred that plan children are auto-admitted, but nothing
said so. State it as a rule and enforce it:

  Admission is inherited, never invented.

may_opt_in() lets tireless label a plan child, because a human admitted its
parent, and refuses to label a discovered issue, because nothing has been
admitted. It is a function rather than a config flag on purpose: the failure it
prevents is unbounded, not merely wrong, so relaxing it should require review.

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

154 lines
6.2 KiB
Cheetah

# tireless configuration.
#
# Rendered from this template by the deploy workflow, substituting {{PLACEHOLDER}}
# values from Gitea repo secrets. The rendered file is never committed.
#
# Secrets that belong in the *environment* rather than here (tokens, and the
# optional ANTHROPIC_API_KEY) live in /etc/tireless/tireless.env — see
# asset/systemd/tireless-runner.service.
[api]
# Registered in architecture/port-allocations.md.
#
# NOT loopback: the nginx that fronts this runs on the hanzalova proxy, not on
# bob (doc/plan/design.md §6.2), so the API has to be reachable across the mesh.
# The boundary is therefore firewalld plus the mesh itself — asset/firewalld/
# opens 23296, and nothing outside the mesh can route to it.
#
# If ingress is ever moved onto bob alongside the API, change this back to
# 127.0.0.1 and drop the firewalld service; the two decisions belong together
# and disagreeing about them is how you get a service that is either
# unreachable or wider open than intended.
bind = "0.0.0.0:23296"
[database]
# mTLS, passwordless (architecture/generic.md §5). The host cert identifies the
# client; pg_ident.conf maps its CN to the role. Certs rotate every 24h.
host = "magrathea.kosherinata.internal"
port = 5432
database = "tireless"
user = "tireless_rw"
client_cert = "/etc/pki/tls/misc/{{DEPLOY_HOST_FQDN}}.pem"
client_key = "/etc/pki/tls/private/{{DEPLOY_HOST_FQDN}}.pem"
root_cert = "/etc/pki/ca-trust/source/anchors/root-internal.pem"
[forge.gitea]
base_url = "https://git.lair.cafe"
# Token for the dedicated `tireless` bot account, from the environment.
token_env = "GITEA_TOKEN"
[forge.github]
enabled = false
token_env = "GITHUB_TOKEN"
[poll]
# Floor on per-repo poll interval, in seconds. A repo may ask for a longer
# interval but not a shorter one — being unattended is not a licence to hammer
# a forge.
min_interval_seconds = 120
default_interval_seconds = 300
# Jitter added to each repo's schedule so N repos do not all fire together.
jitter_seconds = 30
[labels]
# `tireless` admits an issue; `tireless/*` says what to do with it. Any label
# omitted here keeps its default, so overriding one does not mean restating all.
opt_in = "tireless"
mode_discover = "tireless/discover"
mode_plan = "tireless/plan"
mode_implement = "tireless/implement"
force_cc = "tireless/agent:cc"
force_oc = "tireless/agent:oc"
# Written by tireless, never by an operator.
state_proposed = "tireless/proposed"
state_claimed = "tireless/claimed"
state_blocked = "tireless/blocked"
state_done = "tireless/done"
[discover]
# The discovery lane surveys a repo and proposes issues. It is anchored to a
# long-lived tracking issue carrying `tireless` + `tireless/discover`, so unlike
# the other lanes it recurs against the same issue — the cooldown is what keeps
# it from re-running on every poll.
#
# Proposals are created WITHOUT the opt-in label and wait for a human. That is
# the autonomy boundary (doc/plan/design.md §2.5), and it is enforced in code by
# `tireless_entities::may_opt_in`, not by this file.
cooldown_hours = 168
# A survey wanting to file more than this has misunderstood the job. The excess
# is dropped and reported rather than opened.
max_proposals_per_run = 8
[prompt]
# System prompts are compiled into the binary from prompt/*.md and are versioned
# as a set (see prompt/readme.md). Point these at files on disk to override —
# useful for iterating on plan quality without a redeploy. An override must
# declare the same `contract-version:` as the build, or the service refuses to
# start rather than run a mismatched set.
# discover_cc = "/etc/tireless/prompt/discover.cc.md"
# plan_cc = "/etc/tireless/prompt/plan.cc.md"
# implement_oc = "/etc/tireless/prompt/implement.oc.md"
# implement_cc = "/etc/tireless/prompt/implement.cc.md"
[work]
# Per-repo bare mirrors and per-job clones live here.
root = "/var/lib/tireless"
# Keep a failed job's clone for this long so it can be inspected. Successful
# jobs are cleaned immediately.
failed_retention_hours = 72
[lane.cc]
# Claude Code. A subscription is one person's allowance: one session at a time.
max_concurrent = 1
max_runs_per_window = 12
window_hours = 5
# Wall-clock ceiling for a single run.
timeout_seconds = 3600
model = "opus"
# Consecutive failures before the lane stops asking for work.
failure_threshold = 3
[lane.oc]
# OpenCode against helexa cortex. The constraint here is the GPU fleet, not a
# bill, so the ceilings are far higher.
#
# This lane must never be pointed at Anthropic: OpenCode is a third-party
# harness, and driving an Anthropic subscription through one is the pattern
# Anthropic blocks. tireless refuses to start if `provider` or `base_url` looks
# Anthropic-shaped. See doc/plan/design.md §3.
provider = "lair-helexa"
base_url = "http://hanzalova.internal:31313/v1"
# Pin a model name, never a capability alias (`helexa/large`, `helexa/balanced`,
# `helexa/small`). An alias that starts resolving elsewhere would change how
# tireless implements plans between one job and the next, with no deploy and no
# signal -- the same reason the agent packages are pinned.
#
# Current fleet options for this lane:
# Qwen/Qwen3.6-27B warm, pinned on beast (= helexa/large today),
# system prompt live-verified (helexa#179)
# Qwen/Qwen3-Coder-Next coder-specialised, better suited to executing a
# written spec -- but cold, feasible only on
# beast, so adopting it displaces the pinned 27B.
# Operator decision; see doc/plan/design.md §2.4.
# Qwen/Qwen3-Next-80B-A3B-Thinking as above, and see the surface note below.
model = "Qwen/Qwen3.6-27B"
# Prefer the chat/completions surface. On /v1/responses, `/no_think` is ignored
# and a small output budget can be spent entirely on the reasoning block,
# returning "" with status "incomplete" (helexa#223, open). Treat that as its own
# outcome -- it is a token-budget artifact, not a failed run, and must not
# consume a retry.
surface = "chat_completions"
max_concurrent = 2
max_runs_per_window = 240
window_hours = 5
timeout_seconds = 2700
failure_threshold = 3
[quiet]
# Optional global quiet window in local time. No poll runs and no job is claimed
# between these times. Leave unset to run around the clock.
# from = "23:00"
# until = "07:00"