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.
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.
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
| Field | Type | Notes |
|---|---|---|
id | string (cuid) | Primary key -- use this everywhere |
repoId | string | owner/name slug, e.g. acme/myapp |
number | number | Auto-increment display number per repo -- NOT unique across sources |
title | string | Required on create |
body | string | Markdown, defaults to empty string |
labels | string[] | Serialised as JSON in SQLite |
state | "open" | "closed" | Closed automatically when worker merges |
createdAt | Date | |
updatedAt | Date |
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.
[] when the repo does not exist.
curl 'http://localhost:3100/api/internal-issues?repo=acme/myapp'
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..."}'
/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
| Pts | Tier | When to assign | Claude 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 |
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
_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 fullrawContentfor errors). - Direct access to Slop's SQLite database via the
sqlite3CLI, 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.
| Trigger | Labels applied |
|---|---|
| General failure / operator-triggered | None (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:
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.
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
| Step | What 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. |