docs: state the purpose, the autonomy boundary and the dogfooding plan

design.md described what tireless does to an issue but never what it is for, and
in one place said the opposite of the intent: §1 assigned "identifying what
should be worked on" to the operator, which is the thing discovery automates.
A reader — human or agent — would have concluded the system is human-triggered
only and never built the lane.

Rewrite §1 around continuous multi-repo development. Add §2.5, the autonomy
boundary, as a table of which transitions are automatic and which are human,
with the alternatives considered and why they lose. Add §2.6 on anchoring
discovery to a tracking issue rather than making Job.issue a sum type, and what
that buys. Add stage 6 for the lane, renumbering scheduling to 7 and hardening
to 8, with the reasoning for placing a cheap stage late.

Add §10 on tireless working on tireless: that a merged PR restarts the runner
that opened it and why that is survivable but not free, which areas must be
routed to the stronger lane because they are constraint-bearing, and the four
questions dogfooding is expected to answer.

Two new invariants. Admission is inherited (§2.5), and every constraint must be
reachable from a binary's startup path — the rule the previous commit's orphaned
guards violated.

AGENTS.md symlinks CLAUDE.md: both agents look for their own filename, the
implementation prompts tell them to, and a symlink is the only version of this
that cannot drift.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013TxK1CWPkFXqdcXMJ4hVe6
This commit is contained in:
rob thijssen
2026-08-07 15:36:54 +03:00
parent 581e6ae738
commit ddb2574bbe
4 changed files with 361 additions and 42 deletions

1
AGENTS.md Symbolic link
View File

@@ -0,0 +1 @@
CLAUDE.md

View File

@@ -8,6 +8,11 @@ House conventions live in [`~/git/architecture`](https://git.lair.cafe/lair/arch
(`generic.md` is the baseline). This project's deliberate deviations are listed (`generic.md` is the baseline). This project's deliberate deviations are listed
at the bottom of `readme.md`. at the bottom of `readme.md`.
**tireless works on tireless.** This repo is its own first subject, so changes
here are both the product and the test of it. If you are an agent working from a
tireless-authored issue, design.md §10 says what that implies — in particular,
that some areas of this repo are constraint-bearing and are routed deliberately.
## Invariants — do not "clean these up" ## Invariants — do not "clean these up"
These are terms-of-service and safety constraints expressed as code. Each has These are terms-of-service and safety constraints expressed as code. Each has
@@ -44,11 +49,11 @@ tests. If one seems redundant, read design.md §3 before touching it.
coding agent. `SYSTEM_PROMPT_FLAG` is `--append-system-prompt` for this coding agent. `SYSTEM_PROMPT_FLAG` is `--append-system-prompt` for this
reason. reason.
8. **The three prompts in `prompt/` are one contract — edit them together.** 8. **The four prompts in `prompt/` are one set — edit them together.**
`plan.cc.md` emits the structure `implement.oc.md` consumes and `plan.cc.md` emits the structure `implement.oc.md` consumes and
`tireless_core::plan::validate` enforces. Changing one alone breaks the `tireless_core::plan::validate` enforces. Changing one alone breaks the
handoff silently, as a bad pull request rather than an error. Bump handoff silently, as a bad pull request rather than an error. Bump
`contract-version:` in all three plus `SYSTEM_PROMPT_CONTRACT_VERSION` `contract-version:` in all four plus `SYSTEM_PROMPT_CONTRACT_VERSION`
together; `PromptSet::load` refuses a mismatched set. together; `PromptSet::load` refuses a mismatched set.
9. **A plan is validated, not trusted.** Never enqueue implementation work from 9. **A plan is validated, not trusted.** Never enqueue implementation work from
@@ -57,6 +62,18 @@ tests. If one seems redundant, read design.md §3 before touching it.
a small model needs them; do not relax them because a plan looks fine to a a small model needs them; do not relax them because a plan looks fine to a
human reader. human reader.
10. **Admission is inherited, never invented.** tireless may apply the opt-in
label only to an issue descended from one a human opted in.
`may_opt_in(JobKind::Discover)` is `false` and must stay false: it is the
only thing standing between "proposes work" and "generates its own work
indefinitely". See design.md §2.5. This is not a policy to relax once the
system is trusted — the failure it prevents is unbounded, not merely wrong.
11. **Every constraint above is reachable from a binary's startup path.** A
guard that exists as a tested function nobody calls is not a guard. If you
add one, wire it into `Config::validate` or `tireless_agent::preflight::run`
so it cannot be bypassed by a caller that forgot.
## Quality gate ## Quality gate
Before considering a change complete: Before considering a change complete:
@@ -65,9 +82,12 @@ Before considering a change complete:
cargo fmt --all cargo fmt --all
cargo clippy --all-targets --all-features -- -D warnings cargo clippy --all-targets --all-features -- -D warnings
cargo test --workspace cargo test --workspace
cd dashboard && npm run build cd dashboard && npm run lint && npm run build
``` ```
This exact block is what a plan targeting this repo should use as its runnable
`Acceptance` (design.md §10.3).
## Commits ## Commits
Conventional Commits (`type(scope): subject`), imperative, under ~70 chars. Conventional Commits (`type(scope): subject`), imperative, under ~70 chars.

View File

@@ -1,6 +1,7 @@
# tireless — design and staged implementation plan # tireless — design and staged implementation plan
**Status:** planning. Nothing in stages 1+ is built yet. **Status:** stage 0 built (workspace, domain model, policy, config, prompts,
deployment). Stages 1+ are specified but not implemented.
**Conventions:** [`~/git/architecture`](https://git.lair.cafe/lair/architecture) — **Conventions:** [`~/git/architecture`](https://git.lair.cafe/lair/architecture) —
`generic.md` is the baseline; deviations are flagged inline below and in `readme.md`. `generic.md` is the baseline; deviations are flagged inline below and in `readme.md`.
@@ -8,14 +9,44 @@
## 1. What tireless is ## 1. What tireless is
A daemon that watches labelled issues on Gitea (and, for legacy repos, GitHub), **A system for keeping several repositories moving without an operator driving
claims them, and either **plans** them (decomposing an issue into an epic and each change by hand.**
child issues) or **implements** them (producing a branch and a pull request).
A human reviews the output. Nothing merges itself.
The point is to move the operator's attention up a level: from writing It watches Gitea (and, for legacy repos, GitHub), and runs three kinds of work
implementation plans and implementations, to identifying what should be worked against the repos it is given:
on and reviewing what came back.
- **Discover** — survey a repository and *propose* issues worth opening.
- **Plan** — decompose an admitted issue into an epic and child issues, each one
specified well enough to be implemented by a model that cannot ask questions.
- **Implement** — produce a branch and a pull request.
Those three compose into a loop: proposals become issues, issues become specs,
specs become pull requests, and pull requests become the next thing a human
looks at. A human reviews the output. Nothing merges itself.
### The point
Each stage moves the operator's attention up a level.
Without tireless, the operator writes the plan and the implementation. With
planning and implementation automated, they identify what to work on and review
what came back. With discovery automated too, they are handed a ranked list of
*candidate* work and decide what deserves to exist at all.
That last step is the one that makes this worth building across several repos
rather than one. Reviewing a pull request is bounded work. Noticing that a repo
has drifted from its own design document, or that a stated guarantee has no test
behind it, is unbounded work that scales with the number of repositories and
gets skipped first when there are six of them. It is also exactly the kind of
reading a model does tirelessly and a person does once a quarter.
### Where the human stays
Automating discovery is the point at which a system like this can start
generating its own work indefinitely, so the boundary is drawn explicitly and
enforced in code rather than left to good intentions — see §2.5. In short:
**a human decides what enters the system, and a human decides what merges.**
Everything between those two points runs unattended.
### What tireless is not ### What tireless is not
@@ -23,6 +54,8 @@ on and reviewing what came back.
*driving agents*; its task/project/board model is deliberately absent here. *driving agents*; its task/project/board model is deliberately absent here.
The forge's issues are the work list. The forge's issues are the work list.
- **Not a merge robot.** It opens PRs. Review and merge stay human. - **Not a merge robot.** It opens PRs. Review and merge stay human.
- **Not an autonomous backlog.** It proposes; it does not admit its own
proposals. A discovered issue sits inert until a person opts it in (§2.5).
- **Not a model provider client.** It never speaks to Anthropic or any inference - **Not a model provider client.** It never speaks to Anthropic or any inference
API directly. This is load-bearing — see §3. API directly. This is load-bearing — see §3.
- **Not a multi-tenant service.** One operator, one subscription, one fleet. - **Not a multi-tenant service.** One operator, one subscription, one fleet.
@@ -34,17 +67,48 @@ on and reviewing what came back.
### 2.1 The loop ### 2.1 The loop
At the level of one job:
``` ```
poll ──▶ enqueue ──▶ claim ──▶ prepare clone ──▶ run agent ──▶ deliver ──▶ report poll ──▶ enqueue ──▶ claim ──▶ prepare clone ──▶ run agent ──▶ deliver ──▶ report
▲ │ ▲ │
└──────────────────────────── reconcile ◀──────────────────────────────────┘ └──────────────────────────── reconcile ◀──────────────────────────────────┘
``` ```
At the level of a repository, the three job kinds chain — with the human gates
marked, because they are the whole reason this is a supervised system:
```
┌──────────── survey, on a cooldown ────────────┐
│ │
▼ │
┌─────────┐ proposes ┌────────────┐ │
│ Discover│─────────────▶ │ proposed │ │
└─────────┘ (unadmitted)│ issues │ │
└─────┬──────┘ │
★ human opts in │
▼ │
┌───────────┐ │
│ Plan │ │
└─────┬─────┘ │
validated, children │
inherit admission │
▼ │
┌───────────┐ │
│ Implement │ │
└─────┬─────┘ │
▼ │
pull request │
★ human reviews & merges ─────┘
```
Two `★` gates, and only two. Everything between them is unattended.
Two systemd units, one binary: Two systemd units, one binary:
| Unit | Role | Spends tokens? | | Unit | Role | Spends tokens? |
| --- | --- | --- | | --- | --- | --- |
| `tireless-poller` | discovers opted-in issues, enqueues, mirrors labels | no | | `tireless-poller` | finds opted-in issues, enqueues, mirrors labels | no |
| `tireless-runner` | claims jobs, prepares clones, drives agents, opens PRs | **yes** | | `tireless-runner` | claims jobs, prepares clones, drives agents, opens PRs | **yes** |
Splitting them means the discovery loop can run continuously while the Splitting them means the discovery loop can run continuously while the
@@ -59,15 +123,21 @@ tireless reports back the same way.
| Label | Written by | Meaning | | Label | Written by | Meaning |
| --- | --- | --- | | --- | --- | --- |
| `tireless` | human | **Opt-in.** Without it tireless ignores the issue entirely, whatever else is present. | | `tireless` | human¹ | **Opt-in.** Without it tireless ignores the issue entirely, whatever else is present. |
| `tireless/discover` | human | Survey this repository and propose work. Applied to a tracking issue (§2.6). |
| `tireless/plan` | human | Decompose into an epic and child issues. | | `tireless/plan` | human | Decompose into an epic and child issues. |
| `tireless/implement` | human | Implement and open a PR. | | `tireless/implement` | human | Implement and open a PR. |
| `tireless/agent:cc` | human | Force the Claude Code lane. | | `tireless/agent:cc` | human | Force the Claude Code lane. |
| `tireless/agent:oc` | human | Force the OpenCode lane. | | `tireless/agent:oc` | human | Force the OpenCode lane. |
| `tireless/proposed` | tireless | Opened by a discovery run. **Awaiting a human decision** — deliberately not opted in. |
| `tireless/claimed` | tireless | A job holds this issue. | | `tireless/claimed` | tireless | A job holds this issue. |
| `tireless/blocked` | tireless | Needs a human; tireless has stopped. | | `tireless/blocked` | tireless | Needs a human; tireless has stopped. |
| `tireless/done` | tireless | Delivered — children created, or PR opened. | | `tireless/done` | tireless | Delivered — children created, or PR opened. |
¹ And by tireless in exactly one case: a child issue decomposed from a parent a
human already opted in. See §2.5 — that exception is the whole autonomy design,
and it is enforced by `tireless_entities::may_opt_in` rather than by convention.
The opt-in label is separate from the mode labels on purpose. Removing one label The opt-in label is separate from the mode labels on purpose. Removing one label
(`tireless`) disables the issue without destroying the operator's expressed (`tireless`) disables the issue without destroying the operator's expressed
intent about *how* it should be handled, and a single unlabelled repo full of intent about *how* it should be handled, and a single unlabelled repo full of
@@ -84,6 +154,7 @@ Implemented in `tireless-core::routing`, with tests.
| Job | Lane | Why | | Job | Lane | Why |
| --- | --- | --- | | --- | --- | --- |
| `Discover` | Claude Code (Opus) | Deciding what is worth building is the highest-judgement task in the system, and the lowest volume. Its output sets what everything downstream spends its budget on. |
| `Plan` | Claude Code (Opus) | Decomposition is the high-judgement, low-volume task. Getting a plan wrong is expensive downstream; getting it right is worth the strong model. | | `Plan` | Claude Code (Opus) | Decomposition is the high-judgement, low-volume task. Getting a plan wrong is expensive downstream; getting it right is worth the strong model. |
| `Implement`, descended from a tireless plan | OpenCode (helexa) | A tireless plan *is* a spec. Executing a written spec is what a local model on the GPU fleet does well, at no subscription cost. | | `Implement`, descended from a tireless plan | OpenCode (helexa) | A tireless plan *is* a spec. Executing a written spec is what a local model on the GPU fleet does well, at no subscription cost. |
| `Implement`, human-written issue | Claude Code | No plan behind it means the issue needs interpretation before it needs code. | | `Implement`, human-written issue | Claude Code | No plan behind it means the issue needs interpretation before it needs code. |
@@ -91,9 +162,14 @@ Implemented in `tireless-core::routing`, with tests.
The general rule: **Claude Code gets judgement, OpenCode gets specification.** The general rule: **Claude Code gets judgement, OpenCode gets specification.**
This also produces a pleasing economic shape. The subscription is spent on the This also produces a pleasing economic shape. The two lanes that *create* work —
scarce thing (planning, and interpreting under-specified work), while the bulk discovery and planning — are the expensive ones, and they are also the two
of mechanical implementation runs on hardware already sitting in the office. bounded to a handful of runs per window. The lane that *consumes* work, which is
the bulk of it by volume, is free. The subscription is spent on the scarce thing,
while mechanical implementation runs on hardware already sitting in the office.
Spending is highest exactly where a mistake is cheapest to notice, which is the
same ordering the staged plan uses (§7).
### 2.4 The plan contract ### 2.4 The plan contract
@@ -185,6 +261,84 @@ package versions: an alias that silently starts resolving to a different model
would change implementation behaviour between one job and the next with no would change implementation behaviour between one job and the next with no
deploy and no signal. deploy and no signal.
### 2.5 The autonomy boundary
A system that proposes its own work and then acts on it has no natural stopping
point. This section says exactly where the human is, and why there.
| Transition | Automatic? | Why |
| --- | --- | --- |
| survey → proposed issues | **yes** | Output is text. A bad proposal costs an issue nobody opens. |
| proposed issue → planned | **no — human** | The only place anyone decides the work is worth doing at all. |
| plan → child issues | **yes** | Decomposing admitted work is not a new decision about scope. |
| child issue → implemented | **yes** | The child descends from something a human admitted. |
| pull request → merged | **no — human** | Review is the point. Nothing merges itself. |
The rule that produces this, stated once:
> **Admission is inherited, never invented.**
> tireless may opt an issue in only when it descends from an issue a human opted
> in. Discovery output has no admitted ancestor, so it is created unlabelled and
> waits.
This is `tireless_entities::may_opt_in`, with tests. It is deliberately a
function and not a config flag: an operator who wants more autonomy should have
to change code and pass review, because the failure mode is not a bad pull
request but an unbounded one.
**Why this specific gate and not another.** The alternatives were considered:
- *Gate every transition* — a human labels each plan child too. This makes
`parent_job_id`-based routing (§2.3) nearly dead code, and turns a plan of
eight children into eight relabelling chores, which is the work the system
exists to remove.
- *Gate only at merge* — discover → plan → implement runs unattended. One
enthusiastic survey then consumes an entire window budget on work nobody
agreed to, and the operator discovers this from their review queue.
Gating at ideation puts the single human decision at the only point where the
question is *"should this exist?"* rather than *"is this done correctly?"* — and
leaves the second question to code review, where it already lived.
**What this costs.** Proposals accumulate if nobody triages them. That is the
intended failure mode: a backlog of unadmitted suggestions is inert and free,
whereas a backlog of in-flight jobs is neither. Discovery is rate-limited
(§2.6) partly so the inert pile grows slowly enough to stay readable.
### 2.6 Anchoring the discovery lane
Discovery differs structurally from the other two kinds: planning and
implementation act on *an issue*, but a survey acts on *a repository*. It
recurs, and it has no natural subject.
tireless anchors it to a **long-lived tracking issue** in each repo, carrying
`tireless` + `tireless/discover`. That issue is the survey's subject, and each
run comments its findings there before opening anything.
The alternative — making discovery a repo-level scheduled activity, with
`Job.issue` becoming a sum type over issue-or-repo — was rejected. It is
structurally purer, and it costs: every layer that handles a job (claiming,
leasing, label mirroring, the API, the dashboard) would need to handle a job with
no issue behind it, in order to serve one job kind. The tracking issue buys the
same recurrence for free and brings two things the repo-level design would have
had to invent:
- **A place to report.** A survey that proposes nothing still has something to
say, and comments on the tracking issue are a readable history of what has
been considered and dismissed.
- **The usual controls.** Removing a label pauses discovery; the claim, lease and
reconciliation machinery all apply unchanged.
Because it recurs against a terminal job, a **cooldown** (`discover.cooldown_hours`,
default weekly) governs re-enqueue. Without it the tracking issue would
re-enqueue on the next poll after every run, and the most expensive lane in the
system would run continuously against a repo that had not changed.
A per-run proposal cap (`discover.max_proposals_per_run`, default 8) bounds the
output. A survey wanting to file forty issues has misunderstood the job, and the
operator should learn that from a truncated list and a note, not from their
notification inbox.
--- ---
## 3. Constraints ## 3. Constraints
@@ -374,13 +528,24 @@ job), so §1's "extract when the second consumer appears" test is met.
| Concern | Value | | Concern | Value |
| --- | --- | | --- | --- |
| Host | `bob.hanzalova.internal` | | Host | `bob.hanzalova.internal` — binaries, units, config, job trees |
| API port | **23296** — derived per `port-allocations.md` §3, registry updated | | API port | **23296** — derived per `port-allocations.md` §3, registry updated |
| Ingress | `tireless.internal` on the hanzalova proxy, mesh-only, per-service internal cert | | API bind | `0.0.0.0:23296`, reachable across the mesh only |
| Dashboard | static, `/var/www/tireless`, served by nginx | | Ingress | `tireless.internal` on the **hanzalova proxy**, mesh-only, per-service internal cert |
| Dashboard | static, `/var/www/tireless` **on the proxy**, served by nginx |
| Database | `magrathea.kosherinata.internal:5432`, mTLS | | Database | `magrathea.kosherinata.internal:5432`, mTLS |
| Deploy | Gitea Actions, build-and-rsync, musl static | | Deploy | Gitea Actions, build-and-rsync, musl static |
**Ingress is not on bob.** nginx runs on the office proxy: it serves the built
dashboard from its own web root and reverse-proxies `/v1` to bob across the
mesh. Three artefacts encode that single decision and must agree —
`asset/nginx/tireless.hanzalova.conf`, the `0.0.0.0` bind in
`asset/config/config.toml.tmpl`, and `asset/firewalld/tireless-api.xml` opening
the port. If ingress ever moves onto bob, all three change together: loopback
bind, no firewalld service, vhost relocated. They were briefly inconsistent
during stage 0 — a loopback bind with a remote proxy — which is a configuration
that deploys green and then serves nothing.
bob was chosen because it already hosts vibe-kanban and helexa-bench, and sits on bob was chosen because it already hosts vibe-kanban and helexa-bench, and sits on
the same site as cortex (`hanzalova.internal:31313`) — so the highest-volume the same site as cortex (`hanzalova.internal:31313`) — so the highest-volume
path, OpenCode implementation runs, stays local rather than crossing the path, OpenCode implementation runs, stays local rather than crossing the
@@ -430,14 +595,24 @@ Each stage is independently deployable and independently verifiable, per
without spending tokens is proven first**, and the first token-spending without spending tokens is proven first**, and the first token-spending
capability produces text (issues), not code. capability produces text (issues), not code.
### Stage 0 — Foundations *(scaffolded)* ### Stage 0 — Foundations *(built)*
Workspace, entities, routing and budget logic with tests, binaries that start and Workspace and domain model; routing, budget, plan validation, policy and config
stop cleanly, dashboard shell, deployment assets, CI. with tests; the four system prompts; binaries that load and validate their
configuration, run preflight and stop cleanly; dashboard shell; deployment
assets; CI.
*Done when:* `tireless-api` answers `/v1/ready` on bob:23296, the dashboard loads Every constraint in §3 is enforced by code reachable from a binary's startup
at `tireless.internal`, all three units are active, and the deploy workflow is path — `Config::validate` calls the Anthropic guard, `PromptSet::resolve` checks
green end to end. the prompt set, and the runner refuses to start without a credential store.
There is no invariant here that is merely written down.
*Done when:* `tireless-api` answers `/v1/ready` from the proxy, the dashboard
loads at `tireless.internal`, all three units are active, `tireless preflight`
reports the expected billing mode, and the deploy workflow is green end to end.
*Not done yet:* nothing in this stage has been deployed to bob. The workflow has
never run.
### Stage 1 — Forge ingestion (read-only) ### Stage 1 — Forge ingestion (read-only)
@@ -525,15 +700,40 @@ end-to-end by OpenCode on the GPU fleet, with zero subscription usage; the run
demonstrably respected its `Out of scope` section; and a captured upstream demonstrably respected its `Out of scope` section; and a captured upstream
request shows `implement.oc.md` in the winning position. request shows `implement.oc.md` in the winning position.
### Stage 6 — Scheduling and dashboard control ### Stage 6 — Discovery lane
The tracking-issue anchor (§2.6), `prompt/discover.cc.md` applied via
`--append-system-prompt`, proposal issues created with `tireless/proposed` and
**without** the opt-in label, the cooldown and the per-run proposal cap.
*Done when:* a survey of a real repository proposes issues an operator agrees
are worth reading; none of them are opted in; a second poll within the cooldown
does not re-enqueue; and an artificially small `max_proposals_per_run`
demonstrably truncates and says so.
*Why last among the capability stages, despite being cheap.* Its output is text,
so by the §7 ordering it looks like it belongs beside planning. But a proposal
has nowhere to go until the loop below it closes — a discovery lane running
against a system that cannot yet plan or implement just produces issues the
operator must triage by hand, which they could have written themselves. The
constraint here is not risk, it is that the value only exists once stages 35
work.
It is also the stage that makes tireless *continuous* rather than *on-demand*,
which is why §1 describes it as the point and §7 schedules it last. Those are not
in tension: it is the capstone, not the foundation.
### Stage 7 — Scheduling and dashboard control
Schedule editing, repo add/remove, lane pause/resume, budget and limit-signal Schedule editing, repo add/remove, lane pause/resume, budget and limit-signal
display, run history with per-run billing mode. display, run history with per-run billing mode, and the proposal triage view —
the list of `tireless/proposed` issues awaiting a human, which is the operator's
main working surface once discovery runs.
*Done when:* the operator can add a repo, change its cadence, and pause the *Done when:* the operator can add a repo, change its cadence, admit or dismiss a
Claude Code lane without touching a shell. proposal, and pause the Claude Code lane without touching a shell.
### Stage 7 — Hardening ### Stage 8 — Hardening
Dead-letter semantics for repeatedly failing jobs, Prometheus metrics, alerting Dead-letter semantics for repeatedly failing jobs, Prometheus metrics, alerting
on tripped breakers, retention and cleanup of job directories, and the optional on tripped breakers, retention and cleanup of job directories, and the optional
@@ -580,7 +780,7 @@ falling back to an API key.
**Unattended agents with commit rights are a real exposure.** Bounded by: a bot **Unattended agents with commit rights are a real exposure.** Bounded by: a bot
account that cannot push to any default branch; hardened units; per-job clones; account that cannot push to any default branch; hardened units; per-job clones;
and human review before merge. Stage 7's container backend tightens this further, and human review before merge. Stage 8's container backend tightens this further,
and the executor interface is shaped so it can drop in without touching and the executor interface is shaped so it can drop in without touching
agent-driving code. agent-driving code.
@@ -620,3 +820,79 @@ rather than forcing an eviction to find out.
**Not yet decided:** whether a failed implementation should automatically open a **Not yet decided:** whether a failed implementation should automatically open a
`tireless/blocked` issue describing what it could not do, or simply comment on `tireless/blocked` issue describing what it could not do, or simply comment on
the original. Deferred to stage 4, when there is real failure data to look at. the original. Deferred to stage 4, when there is real failure data to look at.
---
## 10. Dogfooding: tireless on tireless
`lair/tireless` is the first tracked repo, and its own backlog is maintained as
issues in the format this system consumes. That is deliberate: the plan contract
(§2.4) asserts that an Opus-authored spec is executable by a 27B model, and the
cheapest place to find out whether that is true is a repository whose conventions
are already written down and whose reviewer wrote the contract.
It also means the failure modes below are not hypothetical, and are worth
knowing before the first self-directed run.
### 10.1 A merged pull request restarts the thing that opened it
`deploy.yaml` runs on merge to `main` and restarts `tireless-runner`. If the
runner is mid-job — quite likely, since merging a tireless PR is exactly when
other tireless work is in flight — that job's agent is killed after
`TimeoutStopSec=120`, well inside a run ceiling of an hour.
This is survivable by design: the claim lease expires and the job returns to the
pool (§4.2), so nothing is lost except the tokens already spent. It is *not*
free, and it gets worse as concurrency rises.
Left as-is for now, because the alternatives all have a cost that is currently
larger than the problem: draining properly means a deploy that can block for an
hour, and skipping the restart means running a stale binary silently. Revisit in
stage 8, when metrics say how often it actually happens.
### 10.2 Some changes must not go to the cheap lane
The 27B implementation lane follows a specification faithfully — including
following it off a cliff. Certain files in this repo are constraint-bearing in a
way a literal implementer cannot be expected to infer:
| Area | Why |
| --- | --- |
| `CLAUDE.md` invariants | Each looks like redundant defensive code in isolation. That is precisely what makes them look like cleanup. |
| `prompt/*.md` | Behavioural specs, versioned as a set. A weakened instruction becomes a bad PR hours later, unattended. |
| `doc/plan/design.md` | The reasoning that makes the code make sense. |
| `crates/tireless-core/src/policy.rs` | Terms of service as code. |
Label issues touching these `tireless/agent:cc`. The override exists for exactly
this (§2.3), and the general rule holds: **work whose risk is "a capable model
tidies away something load-bearing" belongs on the lane that can read the
reasoning and weigh it.**
### 10.3 The quality gate is the acceptance command
Every child issue planned against this repo should carry the gate from
`CLAUDE.md` as its runnable acceptance:
```sh
cargo fmt --all
cargo clippy --all-targets --all-features -- -D warnings
cargo test --workspace
cd dashboard && npm run lint && npm run build
```
This is the stopping condition §2.4 requires, and it is unusually good at being
one: it is fast, it is total, and it fails loudly. A plan for this repo that
omits it has failed to do the easy part.
### 10.4 What dogfooding is expected to reveal
Named in advance, so the answers are evidence rather than rationalisation:
- **Does a plan written for a 27B model actually work?** §9's open question. The
first real answer arrives in stage 5.
- **Is the discovery prompt able to say "nothing this week"?** The failure mode
is manufactured findings — a survey that proposes work to look productive
costs the operator the exact attention the system exists to protect.
- **Does the two-gate boundary hold in practice, or does the operator start
rubber-stamping?** If admission becomes reflexive, the gate is decorative and
the honest response is to make discovery propose less, not to move the gate.

View File

@@ -1,12 +1,20 @@
# tireless # tireless
Watches labelled issues on Gitea (and GitHub, for legacy repos), claims them, and Keeps several repositories moving without an operator driving each change by
either decomposes them into an epic with child issues or implements them and hand. It watches Gitea (and GitHub, for legacy repos) and does three things:
opens a pull request. A human reviews everything; nothing merges itself.
- **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: Two coding agents do the work, each spawned as the vendor's own binary:
- **Claude Code** — planning, and implementation of issues that need - **Claude Code** — discovery, planning, and implementation of issues that need
interpretation. Uses the operator's Claude subscription by default, or interpretation. Uses the operator's Claude subscription by default, or
pay-as-you-go if an API key is supplied. pay-as-you-go if an API key is supplied.
- **OpenCode** — implementation of issues that a tireless plan already specified, - **OpenCode** — implementation of issues that a tireless plan already specified,
@@ -20,8 +28,18 @@ Full design, constraints and the staged implementation plan:
## Status ## Status
Stage 0 (foundations) is scaffolded. Ingestion, claiming and the agent lanes are **Stage 0 (foundations) is built and passes its gate; nothing is deployed yet.**
not built yet — see §7 of the design document for what lands when.
Working: the domain model, routing, budgets, plan validation, the policy guards,
configuration loading and validation, the four system prompts, and preflight.
`tireless preflight` runs today.
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 ## Build
@@ -30,7 +48,7 @@ cargo test --workspace
cargo clippy --all-targets --all-features -- -D warnings cargo clippy --all-targets --all-features -- -D warnings
cargo fmt --all cargo fmt --all
cd dashboard && npm ci && npm run build cd dashboard && npm ci && npm run lint && npm run build
``` ```
## Run locally ## Run locally
@@ -56,15 +74,16 @@ including the interactive Claude Code login and the Gitea bot account — is
| | | | | |
| --- | --- | | --- | --- |
| Host | `bob.hanzalova.internal` | | Host | `bob.hanzalova.internal` (binaries, units, job trees) |
| API port | `23296` (registered in `architecture/port-allocations.md`) | | API port | `23296` (registered in `architecture/port-allocations.md`), bound `0.0.0.0`, mesh-only |
| Dashboard | `https://tireless.internal` (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 | | Database | `magrathea.kosherinata.internal:5432`, mTLS |
## Conventions ## Conventions
Follows [`lair/architecture`](https://git.lair.cafe/lair/architecture); Follows [`lair/architecture`](https://git.lair.cafe/lair/architecture);
`generic.md` is the baseline. Two deliberate deviations: `generic.md` is the baseline. Three deliberate deviations:
- **`tireless-agent` crate** beyond the standard entities/core/data split. - **`tireless-agent` crate** beyond the standard entities/core/data split.
Process orchestration is not data access, and it is shared by the runner and Process orchestration is not data access, and it is shared by the runner and
@@ -72,3 +91,6 @@ Follows [`lair/architecture`](https://git.lair.cafe/lair/architecture);
- **`MemoryDenyWriteExecute=false` on `tireless-runner`.** Both agents are Node - **`MemoryDenyWriteExecute=false` on `tireless-runner`.** Both agents are Node
programs and V8's JIT needs write-then-execute pages. The API and poller keep programs and V8's JIT needs write-then-execute pages. The API and poller keep
the setting. (§8) 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.