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:
26
CLAUDE.md
26
CLAUDE.md
@@ -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.
|
||||||
|
|||||||
@@ -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 3–5
|
||||||
|
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.
|
||||||
|
|||||||
44
readme.md
44
readme.md
@@ -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 1–8 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.
|
||||||
|
|||||||
Reference in New Issue
Block a user