feat(tireless): scaffold workspace, dashboard and staged design plan
Some checks failed
deploy / build (push) Has been cancelled
deploy / deploy (push) Has been cancelled

Autonomous issue-to-PR driver for Claude Code and OpenCode, structured per
lair/architecture generic.md.

Workspace: entities/core/data/agent library crates plus api, worker and cli
binaries. Two pieces of real logic land with tests — lane routing (cc for
judgement, oc for specification) and the limit governor.

Constraints encoded as code rather than comments:
- agents are spawned as vendor binaries; tireless never calls a provider API
- ANTHROPIC_API_KEY is never set by tireless, only passed through
- assert_not_anthropic refuses to start an OpenCode lane pointed at Anthropic
- every run passes the governor; provider rate-limit signals win over our own
  accounting

Deployment assets target bob.hanzalova.internal:23296 (registered in
port-allocations.md), fronted by hanzalova at tireless.internal.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DHhHtohxcdk1PL3tfnYJdH
This commit is contained in:
rob thijssen
2026-08-02 12:46:42 +03:00
commit 4e42f87576
72 changed files with 9829 additions and 0 deletions

12
dashboard/index.html Normal file
View File

@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>tireless</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

3079
dashboard/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

32
dashboard/package.json Normal file
View File

@@ -0,0 +1,32 @@
{
"name": "tireless-dashboard",
"private": true,
"version": "0.1.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"preview": "vite preview",
"lint": "eslint .",
"format": "prettier --write src"
},
"dependencies": {
"@tanstack/react-query": "^5.62.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-router-dom": "^7.1.0"
},
"devDependencies": {
"@eslint/js": "^9.17.0",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"@vitejs/plugin-react-swc": "^3.7.2",
"eslint": "^9.17.0",
"eslint-plugin-react-hooks": "^5.1.0",
"eslint-plugin-react-refresh": "^0.4.16",
"prettier": "^3.4.2",
"typescript": "~5.7.2",
"typescript-eslint": "^8.18.0",
"vite": "^6.0.5"
}
}

28
dashboard/src/App.tsx Normal file
View File

@@ -0,0 +1,28 @@
import { NavLink, Navigate, Route, Routes } from 'react-router-dom';
import Lanes from './routes/Lanes';
import Jobs from './routes/Jobs';
import Repos from './routes/Repos';
export default function App() {
return (
<div className="app">
<header>
<h1>tireless</h1>
<nav>
<NavLink to="/lanes">lanes</NavLink>
<NavLink to="/jobs">jobs</NavLink>
<NavLink to="/repos">repos</NavLink>
</nav>
</header>
<main>
<Routes>
<Route path="/" element={<Navigate to="/lanes" replace />} />
<Route path="/lanes" element={<Lanes />} />
<Route path="/jobs" element={<Jobs />} />
<Route path="/repos" element={<Repos />} />
</Routes>
</main>
</div>
);
}

View File

@@ -0,0 +1,37 @@
/**
* Client for tireless-api.
*
* The base URL is stamped at build time by the deploy workflow. In dev, Vite
* proxies /v1 to a local API (see vite.config.ts), so the default empty base
* resolves to a same-origin request in both cases.
*/
const BASE = import.meta.env.VITE_API_BASE_URL ?? '';
export class ApiError extends Error {
constructor(
readonly status: number,
message: string,
) {
super(message);
}
}
async function request<T>(path: string, init?: RequestInit): Promise<T> {
const response = await fetch(`${BASE}/v1${path}`, {
...init,
headers: { 'content-type': 'application/json', ...init?.headers },
});
if (!response.ok) {
throw new ApiError(response.status, await response.text());
}
return response.status === 204 ? (undefined as T) : await response.json();
}
export const api = {
get: <T>(path: string) => request<T>(path),
post: <T>(path: string, body: unknown) =>
request<T>(path, { method: 'POST', body: JSON.stringify(body) }),
patch: <T>(path: string, body: unknown) =>
request<T>(path, { method: 'PATCH', body: JSON.stringify(body) }),
delete: <T>(path: string) => request<T>(path, { method: 'DELETE' }),
};

View File

@@ -0,0 +1,6 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
/**
* Which coding agent executed a run.
*/
export type AgentKind = "claude_code" | "opencode";

View File

@@ -0,0 +1,27 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { AgentKind } from "./AgentKind";
import type { BillingMode } from "./BillingMode";
import type { PullRequestRef } from "./PullRequestRef";
import type { RunOutcome } from "./RunOutcome";
/**
* One invocation of one agent against one job.
*
* A job may have several runs: a retry after a rate limit, or a follow-up turn
* resuming the same agent session.
*/
export type AgentRun = { id: string, job_id: string, agent: AgentKind,
/**
* Model actually used, as reported by the agent.
*/
model: string | null, billing: BillingMode,
/**
* The agent's own session identifier, so a follow-up turn can resume rather
* than restart. Claude Code reports this in its stream; tireless passes it
* back via `--resume`.
*/
session_id: string | null, outcome: RunOutcome | null,
/**
* Pull request opened by this run, if any.
*/
pull_request: PullRequestRef | null, started_at: string, finished_at: string | null, };

View File

@@ -0,0 +1,11 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
/**
* Where a Claude Code run's tokens were billed.
*
* Read from the `apiKeySource` field of Claude Code's `system`/`init` stream
* event — the same signal vibe-kanban surfaces in
* `crates/executors/src/executors/claude.rs:911`. tireless records it per run
* so the dashboard can show, honestly, which runs drew on the subscription.
*/
export type BillingMode = "subscription" | "api_key" | "unknown";

View File

@@ -0,0 +1,6 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
/**
* A source forge tireless can poll and push to.
*/
export type Forge = "gitea" | "git_hub";

View File

@@ -0,0 +1,7 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { Forge } from "./Forge";
/**
* Stable coordinates for an issue on some forge.
*/
export type IssueRef = { forge: Forge, owner: string, repo: string, number: bigint, };

View File

@@ -0,0 +1,23 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { IssueRef } from "./IssueRef";
import type { JobKind } from "./JobKind";
import type { JobState } from "./JobState";
/**
* One unit of work against one issue.
*/
export type Job = { id: string, issue: IssueRef, kind: JobKind, state: JobState,
/**
* Worker identity holding the claim, if any.
*/
claimed_by: string | null,
/**
* Claims expire so a crashed worker's jobs return to the pool.
*/
claim_expires_at: string | null,
/**
* Set when this job's issue was itself produced by a Plan job. Presence of
* a parent is the primary signal to route implementation to OpenCode —
* the plan is the spec (see `doc/plan/design.md` §3).
*/
parent_job_id: string | null, attempts: number, last_error: string | null, created_at: string, updated_at: string, };

View File

@@ -0,0 +1,6 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
/**
* What tireless was asked to do with an issue.
*/
export type JobKind = "plan" | "implement";

View File

@@ -0,0 +1,10 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
/**
* Job lifecycle.
*
* A job is the unit of work-claiming. Claiming is a Postgres row transition
* under `FOR UPDATE SKIP LOCKED` (`architecture/generic.md` §3), not a forge
* label — labels cannot be claimed atomically.
*/
export type JobState = "pending" | "claimed" | "running" | "delivered" | "blocked" | "failed" | "abandoned";

View File

@@ -0,0 +1,44 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
/**
* The label vocabulary tireless reads from and writes to a forge.
*
* Labels are the *human-facing* interface: they are how an operator opts an
* issue in, and how tireless reports back. They are **not** the authority on
* state — Postgres is (see `doc/plan/design.md` §4). Labels are a best-effort
* mirror, reconciled on every poll.
*/
export type LabelProtocol = {
/**
* Opt-in marker. Without it tireless never touches an issue, regardless of
* any other label present. Default: `tireless`.
*/
opt_in: string,
/**
* Requests decomposition into an epic plus child issues. Default: `tireless/plan`.
*/
mode_plan: string,
/**
* Requests an implementation and a pull request. Default: `tireless/implement`.
*/
mode_implement: string,
/**
* Forces the Claude Code lane. Default: `tireless/agent:cc`.
*/
force_cc: string,
/**
* Forces the OpenCode lane. Default: `tireless/agent:oc`.
*/
force_oc: string,
/**
* Written by tireless while a job holds the issue. Default: `tireless/claimed`.
*/
state_claimed: string,
/**
* Written by tireless when it needs a human. Default: `tireless/blocked`.
*/
state_blocked: string,
/**
* Written by tireless when it has delivered. Default: `tireless/done`.
*/
state_done: string, };

View File

@@ -0,0 +1,23 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
/**
* How often a repo's issues are polled.
*
* The floor exists to keep tireless a well-behaved API client: a repo that is
* quiet for days does not need to be asked every thirty seconds. See
* `doc/plan/design.md` §5.
*/
export type PollSchedule = {
/**
* Seconds between polls. Clamped to `>= min_interval_seconds` by core.
*/
interval_seconds: number,
/**
* Optional quiet window (local time, `HH:MM`), during which no poll runs
* and no job is claimed. Both ends required if either is set.
*/
quiet_from: string | null, quiet_until: string | null,
/**
* When false the repo stays configured but is skipped entirely.
*/
enabled: boolean, };

View File

@@ -0,0 +1,7 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { Forge } from "./Forge";
/**
* Coordinates for a pull request tireless opened.
*/
export type PullRequestRef = { forge: Forge, owner: string, repo: string, number: bigint, head_branch: string, url: string, };

View File

@@ -0,0 +1,6 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
/**
* How a run ended.
*/
export type RunOutcome = "succeeded" | "failed" | "rate_limited" | "budget_exhausted" | "timed_out" | "cancelled";

View File

@@ -0,0 +1,20 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { Forge } from "./Forge";
import type { PollSchedule } from "./PollSchedule";
/**
* A repo tireless watches for labelled issues.
*/
export type TrackedRepo = { id: string, forge: Forge, owner: string, repo: string,
/**
* Clone URL used for the mirror cache. SSH for Gitea, HTTPS for GitHub.
*/
clone_url: string,
/**
* Branch PRs target. Usually `main`; tireless never pushes to it directly.
*/
default_branch: string, schedule: PollSchedule,
/**
* Set from the forge's `ETag` so repeat polls are conditional requests.
*/
last_etag: string | null, last_polled_at: string | null, created_at: string, };

51
dashboard/src/index.css Normal file
View File

@@ -0,0 +1,51 @@
:root {
color-scheme: light dark;
font-family: ui-sans-serif, system-ui, sans-serif;
line-height: 1.5;
}
body {
margin: 0;
}
.app {
max-width: 72rem;
margin: 0 auto;
padding: 1.5rem;
}
.app > header {
display: flex;
align-items: baseline;
gap: 1.5rem;
border-bottom: 1px solid color-mix(in srgb, currentColor 20%, transparent);
padding-bottom: 0.75rem;
margin-bottom: 1.5rem;
}
.app > header h1 {
font-size: 1.25rem;
margin: 0;
letter-spacing: -0.01em;
}
nav {
display: flex;
gap: 1rem;
}
nav a {
color: inherit;
text-decoration: none;
opacity: 0.6;
}
nav a.active {
opacity: 1;
font-weight: 600;
}
.placeholder {
opacity: 0.6;
font-style: italic;
}

28
dashboard/src/main.tsx Normal file
View File

@@ -0,0 +1,28 @@
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { BrowserRouter } from 'react-router-dom';
import App from './App';
import './index.css';
// Server state lives in React Query; the dashboard is a rendering and
// interaction layer only (architecture/generic.md §4).
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 10_000,
refetchOnWindowFocus: true,
},
},
});
createRoot(document.getElementById('root')!).render(
<StrictMode>
<QueryClientProvider client={queryClient}>
<BrowserRouter>
<App />
</BrowserRouter>
</QueryClientProvider>
</StrictMode>,
);

View File

@@ -0,0 +1,12 @@
/**
* Job list and detail: what tireless has claimed, what it is running, what it
* delivered, and what is blocked waiting on a human.
*/
export default function Jobs() {
return (
<section>
<h2>jobs</h2>
<p className="placeholder">Job listing arrives with stage 2.</p>
</section>
);
}

View File

@@ -0,0 +1,18 @@
/**
* Lane status: the page that answers "is tireless about to spend my
* subscription, and how much is left?".
*
* Shows, per lane: in-flight runs, window budget consumed, the most recent
* provider limit signal, and whether the circuit breaker has tripped. Stage 6
* adds the pause/resume controls; until then this is read-only.
*/
export default function Lanes() {
return (
<section>
<h2>lanes</h2>
<p className="placeholder">
Lane telemetry arrives with stage 3 (Claude Code) and stage 5 (OpenCode).
</p>
</section>
);
}

View File

@@ -0,0 +1,15 @@
/**
* Polled repo management: add and remove Gitea/GitHub repos, and edit each
* one's poll schedule and quiet window.
*
* This is the primary reason the dashboard exists — the poller's schedule is
* meant to be changed without a redeploy.
*/
export default function Repos() {
return (
<section>
<h2>repos</h2>
<p className="placeholder">Repo CRUD arrives with stage 1.</p>
</section>
);
}

10
dashboard/src/vite-env.d.ts vendored Normal file
View File

@@ -0,0 +1,10 @@
/// <reference types="vite/client" />
interface ImportMetaEnv {
/** Stamped at build time by the deploy workflow. */
readonly VITE_API_BASE_URL?: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}

23
dashboard/tsconfig.json Normal file
View File

@@ -0,0 +1,23 @@
{
"compilerOptions": {
"target": "ES2022",
"useDefineForClassFields": true,
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["src"]
}

View File

@@ -0,0 +1 @@
{"root":["./src/App.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/api/client.ts","./src/api/generated/AgentKind.ts","./src/api/generated/AgentRun.ts","./src/api/generated/BillingMode.ts","./src/api/generated/Forge.ts","./src/api/generated/IssueRef.ts","./src/api/generated/Job.ts","./src/api/generated/JobKind.ts","./src/api/generated/JobState.ts","./src/api/generated/LabelProtocol.ts","./src/api/generated/PollSchedule.ts","./src/api/generated/PullRequestRef.ts","./src/api/generated/RunOutcome.ts","./src/api/generated/TrackedRepo.ts","./src/routes/Jobs.tsx","./src/routes/Lanes.tsx","./src/routes/Repos.tsx"],"version":"5.7.3"}

23
dashboard/vite.config.ts Normal file
View File

@@ -0,0 +1,23 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react-swc';
// Build output is static and served by nginx from /var/www/tireless — no Node
// in production (architecture/generic.md §4). The API base URL is stamped at
// build time from VITE_API_BASE_URL by the deploy workflow.
export default defineConfig({
plugins: [react()],
build: {
outDir: 'dist',
sourcemap: true,
},
server: {
port: 5173,
proxy: {
// Dev-only: point at a local tireless-api on its registered port.
'/v1': {
target: 'http://127.0.0.1:23296',
changeOrigin: true,
},
},
},
});