Issue Tracker Home
Slop · Internals

Internal Issue Tracker

Slop ships its own issue store, completely separate from GitHub. Internal issues live in SQLite, participate in the same implementation pipeline, and can be created from BMAD planning artifacts. Slop also auto-scores complexity and synthesizes diagnostic issues from failed workers.

SQLite-native UUID-keyed Same pipeline as GitHub BMAD import Auto-scoring Diagnostic synthesis

Internal Issues

An InternalIssue row lives in Slop's SQLite database. It is not a GitHub issue. Nothing about it touches the GitHub API. The only connection to a watched repo is the repoId foreign key (the owner/name slug). Internal issues appear alongside GitHub issues on the board, can be set ready, drag-reordered, and implemented by any agent harness.

UUID vs. display number: Every internal issue gets a sequential display number within its repo (1, 2, 3 ...). This number is for UX only. It is not globally unique and does not correspond to a GitHub issue number. GitHub issue #5 and internal issue #5 can coexist. Always use the UUID (id) when referencing a specific issue from code or skills.

Schema

FieldTypeNotes
idstring (cuid)Primary key -- use this everywhere
repoIdstringowner/name slug, e.g. acme/myapp
numbernumberAuto-increment display number per repo -- NOT unique across sources
titlestringRequired on create
bodystringMarkdown, defaults to empty string
labelsstring[]Serialised as JSON in SQLite
state"open" | "closed"Closed automatically when worker merges
createdAtDate
updatedAtDate

HTTP API

All endpoints are served by the running Slop server (default http://localhost:3100). Source: src/app/api/internal-issues/route.ts and src/app/api/internal-issues/[id]/route.ts.

GET
/api/internal-issues?repo=<owner/name> List all internal issues for a repo, ordered by creation time. Returns [] when the repo does not exist. curl 'http://localhost:3100/api/internal-issues?repo=acme/myapp'
GET
/api/internal-issues/<uuid> Fetch one issue by UUID. Returns 404 when not found. Never use the display number as the path segment. curl http://localhost:3100/api/internal-issues/cm8x2k3p40000qwer1234abcd
POST
/api/internal-issues Create an issue. repoId and title are required. Returns the created row (201). Returns 400 on missing fields, 404 on unknown repo. curl -X POST http://localhost:3100/api/internal-issues \ -H 'Content-Type: application/json' \ -d '{"repoId":"acme/myapp","title":"Add dark mode","body":"## Goal\n..."}'
PATCH
/api/internal-issues/<uuid> Update title, body, labels, or state. All fields optional. Returns the updated row. curl -X PATCH http://localhost:3100/api/internal-issues/cm8x2k3p40000qwer1234abcd \ -H 'Content-Type: application/json' \ -d '{"title":"Revised title","state":"closed"}'
Skills always use UUID. The /implement-issue skill fetches the issue body via GET /api/internal-issues?repo=<slug> and filters by display number. It never calls gh issue view, because GitHub numbers and internal numbers share the same integer space and can collide at any time.

Story Points

Slop can estimate implementation complexity before dispatching an agent. The result drives automatic model and effort selection: trivial issues get a small fast model; architectural ones get a capable high-effort one. Source: src/server/runs/suggest-points.ts

The seven-tier rubric

PtsTierWhen to assignClaude model / effort
1 Trivial Names exactly one file and one line range. No tests need updating. Pure text or config substitution -- no logic branch. Sonnet / low
2 Simple References one module or component. Pattern already exists in the codebase. At most one test file touched. No new exports or types. Sonnet / medium
3 Small feature Names files in two layers. Uses an established seam. New branch logic but no new data shape or migration. Sonnet / high
5 Medium feature Spans three or more files across at least two layers. Introduces a new type, interface, or exported helper. Tests cover an integration or optimistic update. Opus / medium
8 Large feature Mentions a DB migration, a new background job phase, state machine, or race-condition concern. Acceptance criteria include rollback or idempotency guarantees. Opus / high
13 Architectural Touches more than ten files or says "sweep the codebase" / "all call sites". Consider whether decomposition is feasible before assigning. Opus / xhigh
21 Epic Bundles several independently shippable features, or combines a schema migration with a broad refactor. Acceptance criteria read like a milestone. Always recommend decomposition into 3/5/8-point issues. Opus / ultracode
Tie-break rule: when the scoring agent is torn between two adjacent tiers, it defaults to the lower tier. Most issues are simpler than they first appear, and an honest low estimate beats a padded one.

How auto-scoring works

What runs

A 60-second, read-only, one-shot agent session. The agent receives the rubric and the issue title and body. It does not open any files, does not browse the codebase, and makes no writes. The reply must have the integer point value on line one.

What gets stored

IssueConfig.storyPoints stores the score. pointsAutoScored = true marks it as agent-derived. modelEffortFromStoryPoints = true tells the daemon to derive model and effort from the score at claim time.

Cost tracking

Each scoring session is a Run row with kind = "suggest-points". Token usage and cost appear in the Agents list alongside implementation runs.

UI triggers

Auto plan button -- bulk-scores all ready issues. Per-card score button -- scores a single issue on demand. Setting modelEffortFromStoryPoints = false opts an issue out of auto-selection.

BMAD Story Ingestion

BMAD is an AI planning methodology that produces a PRD and a set of user story markdown files. Slop can scan a watched repo's BMAD output directory and bulk-import those stories as internal issues, turning planning artifacts into actionable implementation tasks without manual copy-paste. Source: src/lib/bmad-stories.ts src/app/api/bmad-stories/route.ts

Directory layout

BMAD writes artifacts into the watched repo at a fixed relative path:

_bmad-output/
  planning-artifacts/
    prd.md                     # parent PRD (may be nested at any depth)
  implementation-artifacts/
    story-1.1.md               # story files -- one per user story
    story-1.2.md
    story-2.1.md
    ...

Slop scans _bmad-output/implementation-artifacts/ for .md files. Each file must contain a # Story N.M: <title> heading. Files without this heading are reported as parse errors.

Story file format

A minimal story file that Slop can import:

---
id: story-1.1
title: Add user authentication
status: Approved
story_points: 5
---

# Story 1.1: Add user authentication

Status: Approved

## Goal

Implement JWT-based login so users can authenticate via `/auth/login`.

## Acceptance Criteria

- `POST /auth/login` returns a signed JWT on valid credentials
- Invalid credentials return 401
- Token claims include `userId` and `exp`

The frontmatter story_points value, if present, seeds IssueConfig.storyPoints on import, skipping the scoring agent for stories that already carry an estimate from the planning phase. The body after stripping frontmatter becomes the internal issue body.

Import API

GET
/api/bmad-stories?repo=<owner/name> Scan the repo's _bmad-output/implementation-artifacts/ directory. Returns one row per .md file with storyId, name, status, imported (boolean), and any error. curl 'http://localhost:3100/api/bmad-stories?repo=acme/myapp'

Re-import detection

Before creating an issue, Slop checks whether any existing internal issue already carries the story's source line in its body:

Story: _bmad-output/implementation-artifacts/story-1.1.md

It also checks whether any issue title contains Story N.M: <name>. Both checks are needed because epic numbering restarts at 1.1 per PRD, so an id-only check would collide with stories from older planning runs on disk. If either check matches, the story is marked imported: true in the scan result and is not created again.

PRD discovery

Slop walks the full _bmad-output/ tree for any file named prd.md and picks the most recently modified one by mtime. The first # heading in that file (after stripping frontmatter, with any leading "PRD:" prefix removed) is used as a batch name for the import group.

Worker Report Synthesis

When a worker fails, or when the resource monitor detects a memory breach, Slop runs a short read-only agent session that diagnoses the root cause and files an internal issue on the watched repo. Failed runs become actionable items. Source: src/server/runs/worker-report-issue.ts

Trigger conditions

Worker failure

Any worker that reaches a terminal failure state can trigger a synthesis run. The operator can also trigger it manually from the board via the "report" button on any terminal worker card.

Memory breach

When the resource monitor detects the agent process tree exceeding the configured memory ceiling, it auto-starts a synthesis run and passes structured breach context: threshold, peak RSS, sample timeline, and per-process breakdown.

What the agent reads

The synthesis agent receives:

  • The last 400 events from the worker's run log (level, type, message, and full rawContent for errors).
  • Direct access to Slop's SQLite database via the sqlite3 CLI, including the worker's id and example queries for token/turn/cost data.
  • For memory breaches: structured data with threshold, peak RSS, per-sample timeline, and per-process RSS and CPU stats.

The session is read-only against the codebase. The agent reads source files to understand root causes. It never writes to the repo.

Filed issue format

The agent must reply with exactly one fenced slop-issue block:

```slop-issue
{"title": "permission denied on git push wasted 12 turns",
 "body": "## Token usage\n38k in / 9k out across 22 turns...\n\n## Findings\n...\n\n## Recommendations\n..."}
```

Slop parses this block, prefixes the title with ralph: (unless already present), and files the issue via createInternalIssue. An empty title causes the run to complete without filing.

TriggerLabels applied
General failure / operator-triggeredNone (plain internal issue)
Memory breach["diagnostic", "memory"]

The feedback loop

Every filed diagnostic issue is a first-class internal issue on the board. The loop is self-improving:

Worker fails or memory breach detected --> Synthesis agent reads 400 events + DB + source --> Diagnoses root cause --> InternalIssue filed (ralph: <diagnosis>) --> Operator sets issue ready --> Implementation agent fixes the root cause --> PR merged -- bug fixed

issueSource: GitHub vs. Internal

The issueSource field appears on three tables: Worker, ReadyIssue, and ReadyOrder. Its value is either "github" or "internal". The default is "github".

Why it exists

GitHub issue numbers and internal issue numbers are both integers starting at 1, incrementing independently per repo. Without issueSource, GitHub issue #5 and internal issue #5 would be indistinguishable in the database. The unique constraint on Worker is:

@@unique([repoId, issueSource, issueNumber])

This means a GitHub issue #5 and an internal issue #5 can each have an active worker at the same time, with no conflict.

Coexistence example: If you run GitHub issue #5 and internal issue #5 for the same repo simultaneously, you get two Worker rows: { issueSource: "github", issueNumber: 5 } and { issueSource: "internal", issueNumber: 5 }. The board shows them as separate cards with separate status. The daemon dispatches separate agents for each.

How it flows through the system

StepWhat happens
Set Ready (internal) addReadyIssue(repoId, number, "internal") writes a ReadyIssue row with issueSource = "internal".
Daemon claim upsertWorker receives issueSource from the ReadyIssue row and stores it on the Worker. Also stores the internalIssueId UUID.
Issue close (Phase 11) closeWorkerIssue() checks worker.issueSource. GitHub: calls PATCH /repos/{owner}/{repo}/issues/{N}. Internal: calls internalIssueRepo.closeInternalIssue(worker.internalIssueId) -- no GitHub API call.
Board view issueSource is included in the WorkerView type loaded by board-loader.ts. GitHub-sourced workers use issueSource === "github" to map against the GitHub issue list; internal workers skip this join.