Fabrik Stage Lifecycle
This document describes what the Fabrik engine does before, during, and after each stage invocation, including comment processing. It is intended as a reference for writing and refining stage skills.
Pipeline Overview
Backlog → Specify → Research → Plan → Implement → Review → Validate → Done
| Stage | Order | Read-Only | UpdateIssueBody | PostToPR | CreateDraftPR | MarkPRReady | MaxTurns |
|---|---|---|---|---|---|---|---|
| Specify | 0 | Yes | Yes | No | No | No | 20 |
| Research | 1 | Yes | No | No | No | No | 50 |
| Plan | 2 | Yes | No | No | No | No | 50 |
| Implement | 3 | No | No | Yes | Yes | Yes | 50 |
| Review | 4 | No | No | Yes | No | Yes | 30 |
| Validate | 5 | No | No | Yes | No | No | 50 |
| Done | 99 | N/A | N/A | No | No | No | N/A |
Phase 1: Item Qualification (Poll Loop)
Two-Phase Filtering
-
Shallow pre-filter (
itemMayNeedWork): Uses board data only (no comments). Checks stage exists,updatedAtchanged, not paused (unless awaiting-input), not locked by another user. Does NOT filter on completion labels — completed items may have new comments. -
Deep fetch (
FetchItemDetails): Only for items that pass the shallow filter. Fetches comments and linked PRupdatedAtfrom GitHub GraphQL (~2 points each). -
Full check (
itemNeedsWork): With comments loaded. New comments trigger processing even on completed stages. PRs only support comment processing. Awaiting-input items only pass if new comments exist (the resume trigger).
Rate Limit Cost
Shallow query: ~16 points/poll. Deep fetch: ~2 points per active item. Typical poll: ~20-30 points, well within the 5,000/hour GraphQL limit.
Phase 2: Pre-Stage Setup
Lock & Label Acquisition
fabrik:locked:<user>— prevents other instances from picking up the issuestage:<name>:in_progress— signals active work on the board- Both held through cooldown retries (not released until completion or permanent failure)
Worktree Setup
Each issue gets .fabrik/worktrees/issue-<N> on branch fabrik/issue-<N>:
- First run: Created from
origin/main, rebased onto latest - Retry (
attempted=true): Returned as-is — no rebase, preserves Claude’s context - Rebase conflicts: Silently aborted (
git rebase --abort) — Claude works from current base
Read-Only Stage Stashing
For read_only: true stages (Specify, Research, Plan): dirty state is auto-stashed before Claude runs and restored after. Claude sees a clean worktree.
Context Files
Before each Claude invocation, the engine writes context documents to .fabrik-context/ in the worktree. These files are excluded from git by two mechanisms: a .gitignore file written inside .fabrik-context/ that excludes all files in the directory, and a pre-rebase step that runs git rm -rf --cached .fabrik-context/ to remove any accidentally tracked context files before rebasing.
| File | Content |
|---|---|
.fabrik-context/issue.md |
The issue body (spec) — always written |
.fabrik-context/stage-Specify.md |
Specify stage comment output |
.fabrik-context/stage-Research.md |
Research stage comment output |
.fabrik-context/stage-Plan.md |
Plan stage comment output |
.fabrik-context/stage-Implement.md |
Implement stage comment output |
.fabrik-context/stage-Review.md |
Review stage comment output |
.fabrik-context/pr-description.md |
Linked PR description (for post_to_pr stages) |
.fabrik-context/codebase-changes.md |
Files changed on origin/<baseBranch> since the prior stage ran (omitted on first stage or when no changes) |
Stage invocation: Writes only stages prior to the current stage. Implement sees Specify, Research, Plan but not its own output.
Comment processing: Writes prior stages and the current stage. Claude needs to see the current stage output to build upon it.
Session Resume
Session file: ~/.fabrik/sessions/issue-<N>/<stageName>.session. On retry, loaded via --resume to restore conversation context.
Model Override
Labels matching model:<name> on the issue override the stage’s configured model.
Phase 3: Claude Invocation
Prompt Construction
When skill: is set (recommended), the prompt is a minimal directive:
You are operating as the Fabrik <StageName> agent for issue #<N>.
Follow the instructions in the <skill-name> skill exactly.
---
# Issue #N: <title>
URL: <url>
## Spec / Issue Body
<full issue body>
## Labels
<comma-separated>
## Prior Discussion
<all comments>
## New Comments
<unprocessed comments only>
---
Context files are available in .fabrik-context/ in your working directory:
- .fabrik-context/issue.md — the issue body (spec)
- .fabrik-context/stage-{Name}.md — output from prior stages
- .fabrik-context/pr-description.md — the linked PR description (if applicable)
When you have completed all work for this stage, end your response with:
FABRIK_STAGE_COMPLETE
If you have unresolved questions that must be answered before the stage can proceed:
FABRIK_BLOCKED_ON_INPUT
These two markers are mutually exclusive.
Comment Review Prompt
When comment_skill: is set, comment processing uses a similar directive:
You are operating as the Fabrik <StageName> comment reviewer for issue #<N>.
Follow the instructions in the <comment-skill> skill exactly.
---
# Issue #N: <title>
URL: <url>
## New Comments to Process
<each comment with author, timestamp, body>
---
Context files are available in .fabrik-context/
...
Claude Arguments
--plugin-dir <absolute-path-to-.fabrik/plugin>
--output-format json
--verbose
--resume <sessionID> (if retry)
--model <override> (if label or stage config)
--max-turns <N> (if configured)
--allowedTools <tool> ... (if restricted)
Output Logging
Raw Claude output saved to ~/.fabrik/logs/issue-<N>/<stage>-output-<timestamp>.json after every invocation. Viewable through the TUI’s l key (piped through fabrik _stream-filter for human-readable display).
Phase 4: Post-Stage Handling
Output Parsing
Three JSON formats supported (tried in order):
- Single result object:
{"result": "...", "session_id": "..."} - JSON array:
[{"type":"system",...}, ..., {"type":"result","result":"..."}] - NDJSON (stream-json): One JSON object per line
Empty result with valid session ID is accepted (max turns hit — Claude was mid-tool-use).
If parsing fails: error message posted instead of raw output. Full output in log files.
Issue Body Update
Before posting output, checks for FABRIK_ISSUE_UPDATE_BEGIN/END markers:
- Only applied if
update_issue_body: true(Specify only) - Other stages: warning logged, markers stripped, issue body untouched
- Enforced by the engine — skills can’t override this
Marker Stripping
All Fabrik markers are stripped from output before posting:
FABRIK_STAGE_COMPLETEFABRIK_BLOCKED_ON_INPUTFABRIK_SUMMARY_BEGIN/FABRIK_SUMMARY_ENDFABRIK_ISSUE_UPDATE_BEGIN/FABRIK_ISSUE_UPDATE_END
Output Posting
If post_to_pr: true (Implement, Review, Validate):
- Detailed output posted on the linked PR
- Brief summary (from
FABRIK_SUMMARYmarkers) posted on the issue - Falls back to issue if no PR found
Otherwise (Specify, Research, Plan):
- Full output posted directly on the issue as a stage comment
Comments Marked as Seen
After a stage runs, any pre-existing user comments get a rocket reaction via markCommentsSeenByStage. They were included in the prompt as context and should not trigger the awaiting-input unblock logic on subsequent polls.
Completion Path
When FABRIK_STAGE_COMPLETE is detected (regardless of Claude’s exit code — as of v0.0.26, a non-zero exit is treated as a warning, not a failure, when the marker is present):
- Lock released (
fabrik:locked:<user>andstage:<name>:in_progressremoved) - Retry tracking cleared
- Draft PR created (if
create_draft_pr: true) - PR marked ready (if
mark_pr_ready_on_complete: true) stage:<name>:completelabel added- Auto-advance to next stage (if
auto_advance: trueor globalyolo)
Blocked-on-Input Path
When FABRIK_BLOCKED_ON_INPUT is detected (and Claude ran without error):
fabrik:paused+fabrik:awaiting-inputlabels added- Lock released
- Retry count NOT incremented, no
stage:<name>:failedlabel - Issue waits until user comments (auto-detected, see Comment Processing)
Incomplete Path (No Marker)
- WIP commit (unless read-only):
git add -A && git commit -m "WIP: ..." - Branch pushed
- Cooldown timer:
pollSeconds * 10seconds - Lock held through cooldown
- Retry count incremented; after
max_retries:fabrik:paused+stage:<name>:failed, lock released
Branch Pushing
Always pushed after Claude runs (success or failure): git push --force-with-lease -u origin fabrik/issue-<N>
Phase 5: Comment Processing
Comment processing is triggered when new comments from the configured user are found. It runs independently of stage processing — even completed stages can process new comments.
Comment Detection
A comment is “new” if it:
- Is authored by the configured user
- Is not in the in-memory processed set
- Doesn’t start with
🏭 **Fabrik(skip Fabrik’s own output) - Doesn’t have a ROCKET reaction (durable “processed” marker)
Comment Processing Flow
- Eyes reaction added to all new comments
fabrik:editinglabel added- Worktree prepared (fresh rebase, not a retry)
- Context files written (prior stages + current stage)
- Claude invoked with
comment_skill(or default comment prompt)- Always resumes existing session
- Output processed:
FABRIK_ISSUE_UPDATEmarkers: applied ifupdate_issue_body: true, stripped regardless- All Fabrik markers stripped
- Stage comment rewritten (or created) via
findStageComment+UpdateComment - Exception:
post_to_prstages post a new “(comment review)” comment on the issue
fabrik:editinglabel removed- Rocket reaction added to processed comments
- Completion check: If
FABRIK_STAGE_COMPLETEwas in the output,handleStageCompletefires — the stage completes directly from comment processing without needing an extra stage invocation
Awaiting-Input Auto-Resume
When a user comments on an issue with fabrik:paused + fabrik:awaiting-input:
itemMayNeedWorklets it through (special exception for awaiting-input)itemNeedsWorkchecksfindNewComments— returns true only if new comments existprocessItemcallsunblockAwaitingInput→ removes both labels, clears cooldown- Routes to
processCommentswith the new comments - Comment processing can signal
FABRIK_STAGE_COMPLETEto complete the stage immediately
Stage Comment Rewriting
For non-post_to_pr stages, comment processing rewrites the existing stage comment:
findStageCommentscans for the most recent comment matching🏭 **Fabrik — stage: {Name}**- If found:
UpdateCommentreplaces its body - If not found:
AddCommentcreates a new stage comment
For post_to_pr stages, comment processing posts a new “(comment review)” comment on the issue (not the PR).
Key Differences: Stage Run vs Comment Processing
| Aspect | Stage Run | Comment Processing |
|---|---|---|
| Session | Fresh or resume on retry | Always resume |
| Worktree update | Skip on retry | Always rebase |
| Completion | Checked, honored | Checked, honored |
| Blocked-on-input | Checked, honored | Not checked |
| Issue body update | If update_issue_body: true |
If update_issue_body: true |
| Output destination | Stage comment or PR | Rewrite stage comment or new issue comment |
| Lock | fabrik:locked:<user> |
fabrik:editing |
| Reaction flow | Comments marked seen (rocket) | Eyes → editing → rocket |
Phase 6: Cleanup Stage (Done)
The Done stage (cleanup_worktree: true) is terminal:
- No Claude invocation, no lock, no labels
- Skipped entirely if no worktree exists for the issue — no worktree means there’s nothing to clean up
- Removes worktree directory when it exists
- Adds
stage:Done:completelabel - Respects
fabrik:paused(skips if paused)
Markers Reference
| Marker | Direction | Purpose | Where Checked |
|---|---|---|---|
FABRIK_STAGE_COMPLETE |
Claude -> Engine | Stage finished successfully; honored even on non-zero Claude exit (v0.0.26+) — engine logs a warning | Stage runs AND comment processing |
FABRIK_BLOCKED_ON_INPUT |
Claude -> Engine | Stage needs user input | Stage runs only |
FABRIK_ISSUE_UPDATE_BEGIN/END |
Claude -> Engine | Updated issue body | Stage runs AND comment processing (gated by update_issue_body) |
FABRIK_SUMMARY_BEGIN/END |
Claude -> Engine | Brief summary for issue | Stage runs with post_to_pr: true |
Labels Reference
| Label | Set by | Purpose |
|---|---|---|
fabrik:locked:<user> |
Engine | Lock during stage processing |
fabrik:editing |
Engine | Lock during comment processing |
fabrik:paused |
Engine or User | Pause processing |
fabrik:awaiting-input |
Engine | Paused waiting for user comment (auto-resumes) |
stage:<name>:in_progress |
Engine | Stage actively running |
stage:<name>:complete |
Engine | Stage completed successfully |
stage:<name>:failed |
Engine | Stage hit max retries |
model:<name> |
User | Override Claude model |
Stage YAML Options
name: Research # Required: matches board column name
order: 2 # Required: processing priority (lower = earlier)
skill: fabrik-research # Plugin skill name (recommended)
comment_skill: fabrik-research-comment # Plugin skill for comment processing
prompt: | # Inline prompt (legacy, used when skill not set)
...
comment_prompt: | # Inline comment prompt (legacy)
...
model: sonnet # Optional: Claude model
max_turns: 50 # Optional: turn limit per invocation
comment_max_turns: 15 # Optional: max turns for comment review (default: min(max_turns, 15))
allowed_tools: # Optional: restrict Claude's tools
- Read
- Grep
read_only: false # Stash/restore worktree (for analysis stages)
update_issue_body: false # Allow FABRIK_ISSUE_UPDATE to modify issue body (Specify only)
post_to_pr: false # Route output to linked PR
create_draft_pr: false # Create draft PR on completion
mark_pr_ready_on_complete: false # Mark PR ready on completion
auto_advance: null # Override global yolo (true/false/null)
cleanup_worktree: false # Terminal stage — remove worktree
completion:
type: claude # Only supported type
Either skill or prompt is required (unless cleanup_worktree is true). When skill is set, the engine sends a directive prompt and the skill is loaded via --plugin-dir.