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
154 lines
6.2 KiB
Cheetah
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"
|