Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 41 additions & 9 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,20 +1,52 @@
# ClawOSS Environment copy to .env and fill in values
# DO NOT COMMIT .env it is gitignored
# ClawOSS Environment - copy to .env and fill in values
# DO NOT COMMIT .env - it is gitignored

# === Required ===

# GitHub Personal Access Token needs public_repo scope at minimum
# GitHub Personal Access Token - needs public_repo scope at minimum
GITHUB_TOKEN=ghp_your-token-here

# Kimi Code direct API key (required — OpenRouter is NOT supported due to content filter)
KIMI_API_KEY=sk-kimi-your-key-here
# === LLM Runtime (Option A: single provider, recommended) ===
# Cost fields are USD per 1M tokens
CLAWOSS_PROVIDER_ID=openrouter
CLAWOSS_PROVIDER_BASE_URL=https://openrouter.ai/api/v1
CLAWOSS_PROVIDER_API_FORMAT=openai-completions
CLAWOSS_PROVIDER_AUTH_HEADER=true
CLAWOSS_PROVIDER_API_KEY_ENV=OPENROUTER_API_KEY
OPENROUTER_API_KEY=sk-or-your-key-here

CLAWOSS_MODEL_ID=moonshotai/kimi-k2.5
CLAWOSS_MODEL_NAME=Kimi K2.5
CLAWOSS_MODEL_REASONING=true
CLAWOSS_MODEL_CONTEXT_WINDOW=262144
CLAWOSS_MODEL_MAX_TOKENS=65536
CLAWOSS_MODEL_INPUT_COST=0.45
CLAWOSS_MODEL_OUTPUT_COST=2.20
CLAWOSS_MODEL_CACHE_READ_COST=0
CLAWOSS_MODEL_CACHE_WRITE_COST=0

# Optional, comma-separated
CLAWOSS_FALLBACK_MODELS=

# === LLM Runtime (Option B: advanced, pass raw OpenClaw providers JSON) ===
# If set, this takes priority over the single-provider fields above.
# CLAWOSS_MODEL_PROVIDERS_JSON={"openai":{"baseUrl":"https://api.openai.com/v1","apiKey":"${OPENAI_API_KEY}","api":"openai-completions","authHeader":true,"models":[{"id":"gpt-4.1","name":"GPT-4.1","reasoning":true,"input":["text"],"cost":{"input":2,"output":8,"cacheRead":0,"cacheWrite":0},"contextWindow":1048576,"maxTokens":32768}]}}
# CLAWOSS_PRIMARY_MODEL=openai/gpt-4.1

# === Runtime Controls ===
CLAWOSS_HEARTBEAT_INTERVAL_MINUTES=5
CLAWOSS_TOKEN_BUDGET_TOTAL=2000000
CLAWOSS_COST_BUDGET_USD_TOTAL=50
CLAWOSS_MVP_CYCLES=3
CLAWOSS_MVP_MAX_CANDIDATES=20
CLAWOSS_MVP_DISCOVERY_REPOS=cli/cli,vitest-dev/vitest,astral-sh/ruff

# === Optional ===

# GitHub identity for PRs (defaults shown)
GITHUB_USERNAME=BillionClaw
GITHUB_EMAIL=billionclaw+clawoss@users.noreply.github.com
# GitHub identity for PRs
GITHUB_USERNAME=your-github-username
GITHUB_EMAIL=your-github-email-or-noreply

# Dashboard (for dashboard-reporter skill)
# Dashboard
DASHBOARD_URL=https://clawoss-dashboard.vercel.app
CLAW_API_KEY=your-shared-secret-here
24 changes: 24 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,27 @@ config/auth-profiles.json
workspace/EOF
workspace/subagent-work/
dashboard/local.db
dashboard/demo-local.db
dashboard/demo-local.db.bak-*
.idea/
tmp/
208184

# Local agent/runtime artifacts
/memory/
/workspace/.work/
/workspace/.sync-state/
/workspace/*.bak
/workspace/*.swp
/workspace/tmp_*.py
/workspace/tmp_*.json
/workspace/[0-9]*
/workspace/=*
/workspace/{*}
/workspace/railwayapp-cli/
/workspace/subagent-implementation.md
/reports/mvp-run-*.json
/reports/mvp-run-*.md

# Generated local service units may contain machine paths or secrets
/config/systemd/dashboard-sync.service
31 changes: 26 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,34 @@ An <a href="https://github.com/openclaw/openclaw">OpenClaw</a> agent that autono
### Quick Start

```bash
git clone https://github.com/billion-token-one-task/ClawOSS.git
git clone https://github.com/onthebed/ClawOSS.git
cd ClawOSS
cp .env.example .env # edit with your API keys
bash scripts/setup.sh
bash scripts/restart.sh
```

### Continuous Run MVP Dry-Run

Use this path when validating the continuous-run MVP without creating a live PR:

```bash
cp .env.example .env # fill model, GitHub, dashboard, and budget values
npm run mvp:dry-run
```

The runner completes 3 heartbeat cycles by default, checks dashboard pause and budget guardrails before work, discovers candidate GitHub issues, applies CLA / duplicate / already-fixed / blocklist / avoidRepos filters, generates PR title/body/creation command, and writes a report under `reports/mvp-run-*.md`.
`CLAWOSS_MVP_DISCOVERY_REPOS` can be used to pin discovery to a controlled repo set during validation.

Useful options:

```bash
node scripts/mvp-runner.mjs --cycles 3 --max-candidates 20
node scripts/mvp-runner.mjs --cycles 3 --issue owner/repo#123
```

If `/api/agent/health-check` returns `pauseAgent: true` or `budget.paused: true`, the runner stops before spawning, commenting, pushing, or preparing PR creation.

---

## First Run Stats
Expand Down Expand Up @@ -125,7 +146,7 @@ bash scripts/restart.sh
░ ▼ ▼ ▼ ░
░ ┌─────────────────┐ ┌──────────────────┐ ┌──────────────────────┐ ░
░ │ GitHub │ │ Vercel Dashboard │ │ Telemetry Hooks │ ░
░ │ (BillionClaw) │ │ /api/ingest/* │ │ dashboard-reporter │ ░
░ │ (configured user)│ │ /api/ingest/* │ │ dashboard-reporter │ ░
░ │ │ │ │ │ audit-logger │ ░
░ │ PRs · Commits │ │ heartbeat │ │ pii-sanitizer │ ░
░ │ Reviews │ │ metrics │ │ dashboard-sync.sh │ ░
Expand Down Expand Up @@ -316,7 +337,7 @@ The `plugins/pii-sanitizer/index.js` (101 lines) performs bidirectional `@` swap
░ ░
░ pr-ledger-sync.sh (185 lines) — runs every 60s via launchd ░
░ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ░
░ Source 1: gh search prs --author BillionClaw --limit 200
░ Source 1: gh search prs --author "$GITHUB_USERNAME" --limit 200 ░
░ Source 2: grep subagent-result-*.md for PR URLs ░
░ Python merger: pr_map keyed by URL, GH is authoritative for status ░
░ Result files fill in issue numbers, existing ledger preserves mappings ░
Expand Down Expand Up @@ -377,7 +398,7 @@ The `plugins/pii-sanitizer/index.js` (101 lines) performs bidirectional `@` swap
░ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ░
░ ░
░ Step 1 ░░ source .env ░
░ Step 2 ░░░ git config user.name BillionClaw
░ Step 2 ░░░ git config user.name "$GITHUB_USERNAME"
░ Step 3 ░░░░ gh auth login --with-token ░
░ Step 4 ▒▒▒▒ ln -sf workspace → ~/.openclaw/workspace ░
░ Step 5 ▒▒▒▒▒ sed __WORKSPACE_PATH__ → deploy config ░
Expand Down Expand Up @@ -441,7 +462,7 @@ clawOSS/
├── AGENTS.md ················ 163 lines — prime directive + rules
├── HEARTBEAT.md ············· 244 lines — 8-step autonomous loop
├── SOUL.md ·················· persona, tone, boundaries
├── IDENTITY.md ·············· @BillionClaw
├── IDENTITY.md ·············· configured GitHub identity
├── USER.md ·················· operator profile
├── hooks/
│ ├── dashboard-reporter/ ·· 628 lines — telemetry to Vercel
Expand Down
39 changes: 14 additions & 25 deletions config/openclaw.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
{
"agents": {
"defaults": {
"bootstrapMaxChars": 12000,
"bootstrapTotalMaxChars": 40000,
"contextInjection": "continuation-skip",
"timeoutSeconds": 900,
"llm": {
"idleTimeoutSeconds": 300
},
"compaction": {
"mode": "safeguard",
"reserveTokens": 130000,
Expand All @@ -24,11 +31,11 @@
]
},
"model": {
"primary": "minimax/MiniMax-M2.7",
"fallbacks": ["kimi-coding/k2p5"]
"primary": "__PRIMARY_MODEL__",
"fallbacks": []
},
"subagents": {
"model": "minimax/MiniMax-M2.7",
"model": "__PRIMARY_MODEL__",
"maxConcurrent": 14,
"archiveAfterMinutes": 1440,
"maxChildrenPerAgent": 15,
Expand All @@ -42,7 +49,7 @@
"default": true,
"name": "ClawOSS",
"workspace": "__WORKSPACE_PATH__",
"model": "minimax/MiniMax-M2.7",
"model": "__PRIMARY_MODEL__",
"tools": {
"profile": "coding"
},
Expand All @@ -51,36 +58,18 @@
},
"heartbeat": {
"every": "5m",
"model": "minimax/MiniMax-M2.7",
"model": "__PRIMARY_MODEL__",
"session": "main",
"target": "none",
"prompt": "You are autonomous. Read HEARTBEAT.md and execute EVERY step 0-7. Do NOT just reply HEARTBEAT_OK.\n\nGOAL: MERGED PRs. Not submitted PRs \u2014 MERGED. A PR that never gets reviewed is zero output.\n\nPRIORITY ORDER (follow this EVERY cycle):\n1. NEW PRs FIRST: Fill all 10 impl slots with new implementations. Discover issues, triage, spawn.\n2. FOLLOW-UPS ONLY AFTER all 10 slots are full or no new work exists.\n3. The PR Monitor (always-on) handles simple follow-ups automatically. Main agent focuses on NEW work.\n\nTRUST-BUILDING: Stop spray-and-pray. Focus on 10-15 repos where we build reputation. Return to repos that merged our PRs. A repo that merged your PR is 10x more likely to merge the next one.\n\nFINDING REPOS: Discover target repos using CRITERIA, not a hardcoded list. Stars >= 200, active development, merge velocity > 0, review rate > 50%, open PRs < 50. Search: 'topic:llm/agent/rag/ai' + 'label:bug/help-wanted'. Check trusted repos FIRST.\n\nPR TYPES: Bug fixes, docs fixes, typo fixes, test additions. NOT features or refactors.\nMIX: 60% easy wins (docs, typos, tests) + 40% bug fixes. A merged typo fix > an unreviewed bug fix.\n\nCLA REPOS: CLAs require manual signing. Do not attempt to sign CLAs.\n\nQUALITY: Understand codebase deeply. Trace root causes. Read .github/workflows/. Target 25-100 LOC (max 200). PR descriptions: write like a human developer, not an AI. No 'This PR addresses...', 'Upon investigation...', 'I identified...'. Jump straight to what's broken and what you did.\n\nSUPERSESSION CHECK: Before starting ANY issue, check: is it assigned? Does it have linked PRs from other contributors? If yes, SKIP \u2014 working on superseded issues wastes cycles and annoys maintainers.\n\nDEDUP: Check for spawned_pending and lock files before spawning. Multiple PRs per repo is fine. ALWAYS use 'BillionClaw' explicitly \u2014 NEVER use @me. New PRs get priority over follow-ups. No daily caps \u2014 ship as many quality PRs as possible.\n\nFOLLOW UP on open PRs \u2014 bump stale ones, respond to reviews, merge approved ones.\n\nSCORING: Use P(merge) 0-100 weighted formula for prioritization. P(merge) >= 30 to attempt, >= 60 for priority spawning. Sort work queue by P(merge) descending. 10 impl/followup slots + 4 always-on (scout, PR monitor scan, PR monitor deep, PR analyst) = 14 total. NEVER reply HEARTBEAT_OK — there is ALWAYS work to do. If queue is empty, run discovery. If discovery finds nothing, check follow-ups. If no follow-ups, expand to new niches. The agent must ALWAYS be working on something.\n\nWEB SEARCH: You have web_search and web_fetch tools (Perplexity). Use them aggressively — search before implementing, search when stuck, search to validate approaches. 5-10 searches per cycle minimum.\n\nIf work exists, DO IT. NEVER idle. NEVER reply HEARTBEAT_OK. There is ALWAYS something to do.",
"prompt": "Resume the ClawOSS heartbeat loop. Read workspace/HEARTBEAT.md from disk and execute it. Goal: merged PRs. Prioritize new implementation work before follow-ups, obey dashboard pause and budget controls immediately, use the configured GitHub username explicitly instead of @me, and never idle while valid work exists. If bootstrap files look truncated, re-read the needed files from disk before acting.",
"lightContext": false
}
}
]
},
"models": {
"mode": "merge",
"providers": {
"minimax": {
"baseUrl": "https://api.minimaxi.com/v1",
"apiKey": "${MINIMAX_API_KEY}",
"api": "openai-completions",
"authHeader": true,
"models": [
{
"id": "MiniMax-M2.7",
"name": "MiniMax M2.7",
"reasoning": true,
"input": ["text"],
"cost": { "input": 0.5, "output": 1.5, "cacheRead": 0.125, "cacheWrite": 0.5 },
"contextWindow": 204800,
"maxTokens": 131072
}
]
}
}
"providers": {}
},
"gateway": {
"port": 18789,
Expand Down
45 changes: 42 additions & 3 deletions dashboard/app/api/agent/health-check/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@ export const dynamic = "force-dynamic";

import { NextResponse } from "next/server";
import { db, ensureDb } from "@/lib/db";
import { pullRequests, prReviews, agentLogs } from "@/lib/schema";
import { eq, sql, gte } from "drizzle-orm";
import { pullRequests, prReviews, agentLogs, heartbeats, metricsTokens, settings } from "@/lib/schema";
import { eq, sql, gte, desc } from "drizzle-orm";
import { preferAccurateMetrics } from "@/lib/metrics-source";
import { computeBudgetStatus, extractRuntimeSnapshot } from "@/lib/runtime";
import type { DashboardSettings } from "@/lib/types";

/**
* Hard blocklist — repos where submitting PRs risks bans or reputation damage.
Expand Down Expand Up @@ -47,6 +50,32 @@ export async function GET() {
await ensureDb();
const now = new Date();
const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate());
const latestHeartbeat = await db
.select()
.from(heartbeats)
.orderBy(desc(heartbeats.timestamp))
.limit(1);
const runtime = extractRuntimeSnapshot(latestHeartbeat[0]?.metadata);
const metricRows = preferAccurateMetrics(
await db.select().from(metricsTokens).orderBy(desc(metricsTokens.timestamp))
);
const settingsRow = await db.query.settings.findFirst({
where: eq(settings.key, "dashboard_settings"),
});
const dashboardSettings = (settingsRow?.value || {}) as Partial<DashboardSettings>;
const budget = computeBudgetStatus(
runtime,
metricRows.reduce(
(acc, metric) => {
acc.inputTokens += metric.inputTokens || 0;
acc.outputTokens += metric.outputTokens || 0;
acc.costUsd += metric.costUsd || 0;
return acc;
},
{ inputTokens: 0, outputTokens: 0, costUsd: 0 }
),
Boolean(dashboardSettings.agentPaused)
);

// Basic stats
const [totalResult, mergedResult, openResult] = await Promise.all([
Expand Down Expand Up @@ -166,6 +195,12 @@ export async function GET() {
// Quick directives
const directives: string[] = [];

if (budget.paused) {
directives.unshift(
`PAUSE NOW: ${budget.pauseReason}. Do not spawn, comment, or submit until budget is increased or reset.`
);
}

if (approvedPRs.length > 0) {
directives.unshift("MERGE NOW: " + approvedPRs.length + " approved PR(s) ready to merge: " + approvedPRs.map((pr) => pr.repo + "#" + pr.number).join(", ") + ". Run `gh pr merge --squash` if CI passes, or comment asking maintainer to trigger CI.");
}
Expand Down Expand Up @@ -203,7 +238,11 @@ export async function GET() {
}

return NextResponse.json({
healthy: directives.length === 0,
healthy: directives.length === 0 && !budget.paused,
pauseAgent: budget.paused,
pauseReason: budget.pauseReason,
manuallyPaused: Boolean(dashboardSettings.agentPaused),
budget,
stats: {
total,
merged,
Expand Down
22 changes: 22 additions & 0 deletions dashboard/app/api/connection-status/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { NextResponse } from "next/server";
import { db, ensureDb } from "@/lib/db";
import { heartbeats, metricsTokens, agentLogs } from "@/lib/schema";
import { desc, gte, sql, eq, and } from "drizzle-orm";
import { preferAccurateMetrics } from "@/lib/metrics-source";
import { computeBudgetStatus, extractRuntimeSnapshot } from "@/lib/runtime";

export async function GET() {
try {
Expand Down Expand Up @@ -59,6 +61,24 @@ export async function GET() {
.orderBy(desc(metricsTokens.timestamp))
.limit(1);

const allMetrics = preferAccurateMetrics(
await db.select().from(metricsTokens)
);
const totals = allMetrics.reduce(
(acc, metric) => {
acc.inputTokens += metric.inputTokens || 0;
acc.outputTokens += metric.outputTokens || 0;
acc.costUsd += metric.costUsd || 0;
return acc;
},
{ inputTokens: 0, outputTokens: 0, costUsd: 0 }
);
const runtime = extractRuntimeSnapshot(hb?.metadata);
const budget = computeBudgetStatus(runtime, totals);
if (budget.paused) {
connectionMessage = budget.pauseReason || "Agent paused by budget guardrail";
}

// Data pipeline status
const hasHeartbeats = (recentHeartbeats[0]?.count || 0) > 0;
const hasMetrics = !!lastMetric[0];
Expand All @@ -80,6 +100,8 @@ export async function GET() {
errorsLastHour: recentErrors[0]?.count || 0,
lastMetricAt: lastMetric[0]?.timestamp || null,
},
runtime,
budget,
hasAnyData: hasHeartbeats || hasMetrics,
});
} catch (error) {
Expand Down
15 changes: 15 additions & 0 deletions dashboard/app/api/github/sync/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,22 @@ export const dynamic = "force-dynamic";
import { NextResponse } from "next/server";
import { syncPRsFromGitHub } from "@/lib/github";

const MANUAL_GITHUB_SYNC =
process.env.CLAWOSS_DASHBOARD_MANUAL_GITHUB_SYNC === "true";

export async function GET() {
if (!MANUAL_GITHUB_SYNC) {
return NextResponse.json(
{
ok: false,
disabled: true,
reason:
"GitHub PR backfill is disabled for this deployment. Set CLAWOSS_DASHBOARD_MANUAL_GITHUB_SYNC=true to re-enable.",
},
{ status: 403 }
);
}

try {
const result = await syncPRsFromGitHub();
return NextResponse.json(result);
Expand Down
Loading