Issue Lifecycle: Ready to Merged
Every database write, GitHub API call, file created, skill invoked, and prompt sent - traced step by step from the moment an issue enters the ready queue to the moment its PR merges and the worktree is cleaned up.
The Pipeline
The daemon's poll cycle owns the entire flow. Every transition is a guarded CAS against the Worker row's current status - lost races don't stomp concurrent state.
Phase-by-Phase Walkthrough
The daemon does not poll GitHub labels. An issue enters the queue when the user clicks Set Ready on the board (Server Action setIssueReady in src/app/actions.ts), which writes a ReadyIssue row. Internal issues are enqueued the same way. That row is the only gate the daemon reads.
Every poll cycle (default 30 s), runTick() runs only when autoMode === "true". It reads parallelismCap and compares against active workers per repo, then claims from the ReadyIssue queue in drag-sorted order.
// src/server/daemon/claim-worker.ts const worker = await claimWorkerWithIssueConfig(deps, repoId, { number: issue.number, title: issue.title, htmlUrl: issue.htmlUrl }); // 1. Reads IssueConfig for per-issue overrides (model, mode, review config) // 2. workerRepo.upsertWorker() → Worker row, status = "claimed" // 3. Deletes IssueConfig row await deps.eventBus.publish({ type: "worker.claimed", workerId: worker.id, ... }); deps.spawnRunner(worker.id); // reserve registry slot → runWorker() async
DB write: Worker row created, status = "claimed".
The worktree path is persisted to the DB before the directory exists so the orphan reaper can never reap it mid-creation. Then the git worktree is created under a repo lock.
// Path schema: ~/.slop/worktrees/<owner@name>/<issueNumber>/ await workerRepo.setWorkerWorktreePath(workerId, worktreePathFor(repoId, issueNumber)); // Inside withRepoLock(): // git fetch origin main // git worktree add --force -B slop/issue-42 <path> origin/main const handle = await deps.createWorktree(repoId, issueNumber); // Outside lock: auto-install deps // pnpm-lock.yaml → pnpm install --frozen-lockfile // yarn.lock → yarn install --frozen-lockfile // package-lock.json → npm ci // Copy .env* files from main checkout into the worktree // find "$MAIN" -maxdepth 1 -name '.env*' -type f -exec cp -n {} "$WT/" \;
Folder created: ~/.slop/worktrees/<owner@name>/<N>/
Branch created: slop/issue-<N> off origin/<baseBranch>
File copied: .env.local into the worktree
DB write: Worker.worktreePath set, then Worker.status = "implementing"
GET /repos/{owner}/{repo}/issues/{N}. For internal issues: GET http://localhost:3100/api/internal-issues?repo=<slug>.
A Run row is created (kind = "implement"), then runSkillSession() launches the agent subprocess in the worktree directory.
// Prompt sent to agent: "/implement-issue reuse-worktree 42" // cwd: ~/.slop/worktrees/owner@name/42/ // SLOP_URL, ANTHROPIC_API_KEY, GITHUB_TOKEN injected // UNATTENDED_SYSTEM_PROMPT injected (forbids AskUserQuestion) // PreToolUse hook blocks: git stash + AskUserQuestion
What the skill does, step by step:
Step 1 - Validate the issue
# Fetch from internal store (never gh issue view - numbers collide) curl -fsS "${SLOP_URL}/api/internal-issues?repo=$SLUG" \ | jq '.[] | select(.number == 42)'
Step 2 - Resolve mode. reuse-worktree is active (passed by the daemon), so the agent reads the shipping reference from harness/skills/core/implement-issue/references/remote-mode.md and skips worktree setup.
Step 3 - Worktree setup (--reuse path).
source "$CLAUDE_SKILL_DIR/scripts/worktree-setup.sh" --reuse # Exports: IMPLEMENT_ISSUE_MAIN, IMPLEMENT_ISSUE_WT, IMPLEMENT_ISSUE_BRANCH
Step 4 - Execute. Reads CLAUDE.md, ARCHITECTURE.md, docs/surface-map.md, and all source files named in the issue in one parallel batch. Implements all acceptance criteria. Runs the combined quality gate derived from .github/workflows/:
{ pnpm exec biome check --write .
&& pnpm exec biome check .
&& pnpm typecheck
&& pnpm test:coverage
} 2>&1 | grep -E "FAIL|passed|error TS|^Error"
If the diff touched frontend files, runs /design-pass.
Step 6 - Completion (remote mode).
git add <file1> <file2> ... git commit -m "feat: <summary>" git push --force-with-lease -u origin "$IMPLEMENT_ISSUE_BRANCH" gh pr create --title "<title>" --body "## Summary\n- ...\n\n## How to test\n- ..." # No "Closes #N" - Slop manages issue close directly
Events streamed to Slop: system.init (captures sessionId), assistant messages (captures file changes), result (captures cost/tokens/summary).
DB writes: Worker.sessionId set, WorkerReport upserted, WorkerReport.implementGateSha set.
Before the verify session, a context file is written to the worktree root so the agent inherits state without needing it in the prompt.
// File: ~/.slop/worktrees/owner@name/N/.slop-verify-context.json { "issueNumber": 42, "docsOnly": false, "implementGateSha": "abc1234", // HEAD after implement "context": "agent summary...", "findings": null // null on first round, prior issues on re-dispatch }
Prompt: /verify-gate reuse-worktree
The skill reads the context, runs /cover (missing test coverage) + /review (acceptance criteria). If implementGateSha matches HEAD, the test suite is skipped (already green). Ends with exactly one final line:
SLOP_VERDICT: pass // or SLOP_VERDICT: findings // missing/unparseable verdict → findings (fail-open-safe)
Routing: pass → proceed to shipping. findings → increment Worker.verifyAttempts, re-dispatch implement with findings seeded in the context file. Budget: maxVerifyAttempts (default 5).
After the implement session (and optional verify), remoteShipping.finishImplementation() runs. It finds the PR the agent opened, arms auto-merge, and parks the worker.
// 1. Find the PR - up to 5 retries × 3 s (pulls.list lags PR creation) prNumber = await githubClient.findOpenPullRequestForIssue(issueNumber, handle.branch); // Recovery: agent exited without pushing → Slop commits + creates PR // GitHub API: POST /repos/{owner}/{repo}/pulls prNumber = await githubClient.createPullRequest({ title, body, head, base }); // 2. Enable auto-merge (squash) // GitHub GraphQL: mutation enablePullRequestAutoMerge await githubClient.enableAutoMerge(prNumber); // If PR is already clean (mergeable right now): // GitHub API: PUT /repos/{owner}/{repo}/pulls/{N}/merge await githubClient.mergePullRequest(prNumber); // 3. Park and let the lifecycle poll drive from here await transitionToWaitingCi(workerId, prNumber, deps);
DB write: Worker.status = "waiting_ci", Worker.prNumber set.
Every daemon cycle, runLifecyclePoll() reconciles all non-terminal workers with GitHub. Workers run in parallel with a concurrency cap of 5.
| GitHub State | Action |
|---|---|
| PR already merged | Finalize → merged, close issue, remove worktree |
| PR closed (not merged) | stopWorker() → cancelled |
| Issue closed out-of-band | stopWorker() → cancelled |
| Merge conflict (dirty) | transitionToResolvingConflict() - daemon spawns /resolve-conflict |
| CI check failed | transitionToFixingCi() - daemon spawns /fix-ci |
| CI green + autoReview on | transitionToWaitingReview() - daemon spawns /pr-review |
| CI green + autoReview off | transitionToWaitingMerge() |
| CI still pending | Wait for next cycle |
// GitHub API: GET /repos/{owner}/{repo}/pulls/{N} const pr = await client.getPullRequest(worker.prNumber); // GitHub API: GET /repos/{owner}/{repo}/commits/{sha}/check-runs const checks = await client.listCheckRuns(pr.headSha); // Collapses to latest attempt per check name (absorbs re-run stale results)
CI Fix, Conflict Resolution, Review, Address
These phases run on demand when the lifecycle poll detects problems. Each rebuilds the worktree from the PR head branch, dispatches a skill, and returns the worker to waiting_ci.
Triggered when a CI check run has conclusion failure, cancelled, or timed_out. Worktree is rebuilt from PR head. Failing check's output.summary (truncated to 2 000 chars) is appended to the prompt.
// Prompt sent: "/fix-ci 42 main --check 'typecheck' reuse-worktree" // Skill steps: // 1. git fetch origin main + git rebase origin/main // 2. Reproduce failing check locally // 3. Fix root cause // 4. git push --force-with-lease
After session: Worker.ciAttempts++. If ciAttempts >= maxCiAttempts → transitionToFailed().
Triggered when the PR's mergeable_state === "dirty". Worktree is rebuilt from PR head, then the agent rebases and resolves conflicts in place.
// Prompt sent: "/resolve-conflict 42 main reuse-worktree" // Skill steps: // 1. git fetch origin main // 2. git rebase origin/main // 3. Resolve conflicts (edit → git add → git rebase --continue) // 4. git push --force-with-lease // Never: git rebase -i, git stash, git rebase-merge hand-edit
After session: Worker.conflictAttempts++. If exhausted → transitionToFailed().
// Prompt sent: "/pr-review 42 reuse-worktree" // Skill API calls: gh pr view "$PR" --json number,title,body,headRefName,baseRefName,state,statusCheckRollup gh pr diff "$PR" curl -fsS "$SLOP_URL/api/internal-issues?repo=$SLUG" | jq '.[] | select(.number == $N)' // On re-review: also walks threads // GitHub API: GET /repos/{owner}/{repo}/pulls/{N}/reviews // GitHub API: GET /repos/{owner}/{repo}/pulls/{N}/comments // Posts ONE comment-event review: // GitHub API: POST /repos/{owner}/{repo}/pulls/{N}/reviews // Body includes: reviewer: **Verdict: Approve** (or Request changes)
Verdict parsed by Slop from getLatestPrReviewVerdict(prNumber). Stale verdicts (commitId ≠ PR headSha) are ignored.
| Verdict | Next state |
|---|---|
approve / approve_with_comments | waiting_merge |
request_changes | waiting_address → spawns Phase 10 |
// Prompt sent: "/address-comments 42 reuse-worktree" // Skill steps: // 1. gh api "repos/{owner}/{repo}/pulls/{N}/comments" --paginate // 2. gh api "repos/{owner}/{repo}/pulls/{N}/reviews" --paginate // 3. For each open reviewer: thread → fix / decline / question // 4. Fix commits: "fix: ... (review)" using conventional-commits format // 5. git push (once, at end) // 6. Reply in-thread: // gh api .../comments/{id}/replies -f body='addresser: fixed in <sha>...'
After session: Worker returns to waiting_ci. The loop is /pr-review → /address-comments → /pr-review until no blockers remain or the review budget (3) is exhausted.
Phase 11 - Merge and finalize
GitHub fires the auto-merge armed in Phase 5 once CI passes. Alternatively, the operator clicks Merge on the board (Server Action mergeWorker()).
// GitHub API: PUT /repos/{owner}/{repo}/pulls/{N}/merge await githubClient.mergePullRequest(prNumber); // Lifecycle poll detects pr.merged === true on the next cycle: await transitionToMerging(workerId, deps); await transitionToMerged(workerId, prNumber, deps); // Close the issue // GitHub issue: PATCH /repos/{owner}/{repo}/issues/{N} (state: "closed") // Internal issue: internalIssueRepo.closeInternalIssue(worker.internalIssueId) await closeWorkerIssue(worker, githubClient, internalIssueRepo, logger); // Remove the worktree // git worktree remove --force <path> // git worktree prune // git branch -D slop/issue-N // rm -rf <path> await deps.removeWorktree(handle.path);
If autoReportMode is on, a run_report Run fires after merge to synthesize a performance summary and file it as an issue in the watched repo.
~/.slop/worktrees/<owner@name>/<N>/ - the entire worktree is removed. DB write: Worker.status = "merged".
Skills Dispatched
Every skill lives in harness/skills/core/ and is symlinked into ~/.claude/skills/ by harness/bootstrap.sh.
Fetches the issue from the internal HTTP store, reads orientation docs, implements all acceptance criteria, runs the combined quality gate (format + lint + typecheck + tests + design-pass for UI), commits, pushes, and opens a PR. Never adds "Closes #N" - Slop manages issue close.
Reads .slop-verify-context.json, runs /cover (new test coverage) and /review (acceptance-criteria check). If implementGateSha matches HEAD, skips redundant test re-run. Ends with exactly one verdict line that Slop machine-reads. A missing/malformed verdict resolves to findings (fail-open-safe).
Fetches the base branch, rebases the issue branch onto it, and resolves every conflict in place. Never uses git stash, git rebase -i, or interactive commands. Force-pushes when the working tree is clean and checks pass locally.
Rebases onto base, reproduces the failing check locally (from CI config), fixes the root cause (never weakens tests), re-runs until passing, and force-pushes. The failing check's output.summary is appended to the prompt as diagnostic context.
Judges the PR against the issue's acceptance criteria, then collects findings (correctness, scope, tests) with an adversarial verification pass on every blocker. Posts one COMMENT-event review with inline findings. Emits a machine-readable reviewer: **Verdict: …** line Slop parses to route to merge or address-comments.
Reads every reviewer: thread. For each: fixes (commit + addresser: fixed in <sha> reply), declines with evidence (addresser: declined - <file:line>), or questions (addresser: question - …). Pushes once at end. Posts a round-summary top-level comment. Worker returns to waiting_ci.
Complete GitHub API Call Inventory
Calls made by src/server/github/client.ts (Octokit REST + GraphQL) and by the agent process (gh CLI in skill scripts).
Files and Folders Created
Isolated git worktree for the issue. Branch: slop/issue-<N> off origin/<baseBranch>. The / in owner/name is encoded as @ to keep the path two levels deep.
Carries DATABASE_URL and other local env vars the agent needs. cp -n skips files already present. If absent, seeded from .env.example.
Written by Slop before each /verify-gate dispatch. Contains issueNumber, docsOnly, implementGateSha (HEAD after implement), context (agent summary), and findings (null on first round, prior issues on re-dispatch).
Auto-installed after worktree creation. pnpm install --frozen-lockfile (or yarn/npm equivalent). Skipped on worktree resume - untracked files survive rebuild.
The agent's Edit, Write, and MultiEdit tool calls inside the worktree. All tracked as FileChange entries and persisted to WorkerReport.filesChanged.
Database Write Inventory
All status transitions use updateWorkerStatusFrom() - an atomic compare-and-swap that only writes when the Worker is in the expected state, preventing lost-race stomps.
status = "claimed", all per-issue overrides copied from IssueConfigworktreePath set (pre-creation)worktreePath updated (post-creation)status = "implementing"kind = "implement", status = "running"sessionId + sessionHarness set on first system.init messageagentPid set on subprocess spawn (orphan reaping)filesChanged, summary, costUsd, inputTokens, outputTokens, numTurnsimplementGateSha set (HEAD SHA after implement)status = "completed" | "failed"status = "verifying"kind = "verify"verifyAttempts++ (on findings re-dispatch)status = "waiting_ci", prNumber setstatus = "fixing_ci"; Run kind = "ci_fix"; ciAttempts++status = "resolving_conflict"; Run kind = "conflict"; conflictAttempts++status = "waiting_review" → "in_review"; Run kind = "pr_review"; reviewAttempts++status = "in_address"; Run kind = "pr_address"status = "merging"status = "reporting" (if autoReportMode on)status = "merged"state = "closed" (internal issues only)Status Vocabulary
Terminal statuses: merged · failed · cancelled. Display overrides: prState === "merged" → show merged regardless of internal status; issueState === "closed" → show issue_closed.
agentPid is still live is reaped first. Workers in implementing or verifying are re-dispatched with resumeSessionId set - the harness picks up the surviving session rather than re-implementing from scratch. Codex workers (no session persistence) restart fresh from the surviving worktree.