Repository navigation
Releases: handarbeit/fabrik
Release list
v0.0.89
Fabrik v0.0.89
Summary
The engine stops looping on things it cannot finish. A community report (#2388)
showed a stage running again and again after it had already finished, while GitHub
writes were failing. Fabrik now refuses to start a worker when it cannot take the issue's
lock. It holds all new work while GitHub is unreachable, and it records a finished stage
and publishes that result later instead of running the stage again. It also caps how many
times one stage of one item is dispatched before pausing for a human.
State that holds up under webhooks, restarts and concurrent writes. One ordering
rule now decides between webhook, listing, deep-fetch and live reads: an older read never
overwrites a newer fact. Gates now decide from fresh live reads. Merge-train caps and the
usage-limit hold survive a restart. All webhook transports share one intake that drops
duplicate deliveries. Fabrik now applies more webhook events directly: dependencies,
review threads, label, milestone and sub-issue changes, and comment edits.
New overseer action: fabrik_set_blocked_by adds or removes blocked-by edges from
an MCP session. It checks for cycles before writing.
Features
fabrik_set_blocked_byoverseer action: add or remove an item's blocked-by edges, guarded against self-blocks, PR blockers and cycles (#2385)max_stage_dispatches(default 25,0disables): pause an item after its stage has been dispatched that many times in one episode. An unchanged re-dispatch of an oversized closed session starts fresh instead of resuming it (#2395)- A transport-neutral webhook intake: one persisted delivery dedupe and per-item serial apply queue for every transport, with an engine-level echo registry (#2351)
- Webhook events for facts the engine uses:
sub_issues,pull_request_review_thread,label,milestone, and commentedited/deletedare applied directly (#2352) issue_dependencieswebhooks wake and re-check the affected items, and a closing blocker refreshes its dependents (#1995)
Fixes
- A stage worker is never spawned when the
fabrik:editinglock label can't be written. While GitHub is unreachable, dispatch, reinvokes and conflict resolution hold (#2393) - A stage that finished is recorded and its output is published by retry, never re-run (#2394)
- The board cache never serves an empty project ID, and every board write defers when the ID is unknown (#2396). A cache started on an empty board now bootstraps on a later poll (#2411)
- A forced Validate re-run is completed only by a Validate stage run, never by a comment-review pass or a catch-up landing (#2378)
- A usage-limit refusal of a reinvoke or comment review now waits until the hold lifts, instead of looping through refused workers (#2407)
projects_v2_itemwebhooks are decoded fromnode_id, so board column changes are applied from the delivery itself (#2383)- Hookdeck reconnects log their cause (#2384)
- The self-write freshness baseline is anchored to GitHub's own timestamps, never the daemon clock (#1945)
- Pruefer removes the empty
memory/directory the Claude CLI leaves in a review's profile folder (#2406)
Improvements
- One observed-at ordering rule for every write into the item Store (#2348)
- Gate decisions read live and apply the read to the Store. Dispatch re-reads the candidate first (#2350)
- Merge-train ejection counts, trial-rate window, auto-repair attempts and the usage-limit hold survive a restart (#2349)
- Documentation updated for v0.0.88 (#2381)
Internal
- Pinned the marketplace plugin entries to v0.0.89
- Set fabrik plugin to 0.0.89 (paired with the binary)
- Live e2e gate: coverage complete for engine SHA c7b8cfb (166 test/leg pairs passed, 2 known skip(s), 36 structural skip(s)) under E2E_MATRIX=full (204 required pairs of 208 in the full 2×2) across 3 gate invocations.
- Sim twins, captured webhook fixtures, a per-fact ordering test matrix, a live webhook-reconcile test and its registry entry (#2409), plus review follow-ups across the statefix chain
Upgrading
- New dispatch bound (#2395): an item whose stage is dispatched 25 times in one episode
is paused withfabrik:paused+fabrik:awaiting-input. Raise it with
--max-stage-dispatches/FABRIK_MAX_STAGE_DISPATCHES/max_stage_dispatches, or set
0to disable. A negative or non-integer value is refused at startup. - Webhook events (#2352):
gh webhook forward(--webhooks) picks up the new events on
restart. A hook you created yourself keeps its own event list: to get these events, add
sub_issues,pull_request_review_thread,labelandmilestoneto the organisation
hook, repository hook, GitHub App or Hookdeck source. Until you do, Fabrik learns those
facts from its normal fetches as before.
# Auto-upgrade from a running Fabrik instance
# Fabrik checks for new releases each poll cycle and upgrades automatically with --auto-upgrade
# Or download directly (auto-detects OS and architecture)
gh release download --repo handarbeit/fabrik \
--pattern "fabrik_*_$(uname -s | tr A-Z a-z)_$(uname -m | sed 's/x86_64/amd64/' | sed 's/aarch64/arm64/').tar.gz" \
-O - | tar xzv0.0.88
Fabrik v0.0.88
Summary
Pruefer reviews through plugins, and keeps reading until it has read the whole PR.
The default review is now an embedded plugin skill (pruefer-review:review, #2306).
Operators can load their own review plugins and pick one by plugin:skill, falling back to
the default in a way the posted review shows (#2307). A repo can supply plugins through its
standard Claude Code plugin declarations, but only behind three opt-ins that default off
(#2366). Reviews resume the same session until every staged diff entry has been read, up to
a bound (#2375). Review tools are coded within an operator ceiling, and outside authors get
a read-only review (#2308, hardened by #2370).
Engine state fixes from the 2026-10-15 state audit (#2341–#2347). fabrik:paused now
wins at every advance and landing site. Every engine merge is pinned to the head the gates
judged. The Validate completion SHA survives a restart. Three cache-write paths no longer
overwrite newer state with older data.
Upgrading
- Pruefer: repo-supplied review plugins are off by default (#2366). To allow them,
set the operator'srepo_plugins. The repo must also name each plugin in its base-ref
.pruefer/config.yamlplugins:key, and the PR must target the default branch.
A plugin from another repository needs arepo_plugin_marketplacesallowlist entry and
a pinned commit. Trust model: a plugin's hooks and MCP servers run as the daemon's OS
user. - Pruefer:
review_tools(#2308, #2370) is validated at startup. A rule that names a
test runner, interpreter, launcher, or an exec-, write- or network-capable tool is
refused and Pruefer will not start. Remove the rule, or narrow it to a read-only command. - Pruefer: review sessions no longer live in the Claude profile (#2363). Transcripts
are written to.pruefer/transcripts/and kept 7 days. - Pruefer:
max_coverage_passes(default 2,0disables) bounds the new coverage
loop (#2375). Each extra pass is another resumed invocation against your Claude quota.
# Auto-upgrade from a running Fabrik instance
# Fabrik checks for new releases each poll cycle and upgrades automatically with --auto-upgrade
# Or download directly (auto-detects OS and architecture)
gh release download --repo handarbeit/fabrik \
--pattern "fabrik_*_$(uname -s | tr A-Z a-z)_$(uname -m | sed 's/x86_64/amd64/' | sed 's/aarch64/arm64/').tar.gz" \
-O - | tar xzEngine
-
The pause guard (#2341, ADR 2341). Every automatic advance or landing decision on
the stage path and in the merge train refuses afabrik:pauseditem, and refuses one
whose labels cannot be read. A paused merge-train member keeps the whole landing Queued
and counts nothing against the train. -
Merges are pinned to the judged head (#2342, ADR 2342). These are all pinned to the
head the CI and review gates cleared:- the merge-queue enqueue
- the auto-merge enable
- the direct-merge fallback
- the merge train's landing
A head that moved in between defers the landing silently. Only the head Validate
verified can land. -
Validate completion SHA rebuilt after a restart (#2343, ADR 2343). It is recovered
from the engine's own Validate comment, so a push after a restart still invalidates
Validate. If it cannot be recovered, Validate re-runs and the item is never landed
unverified. -
Comment-review issue body writes reach the cache (#2346). A poll-only daemon no
longer feeds the next stage the pre-edit spec. -
Deep-fetch apply writes only deep-owned facts (#2344, ADR 2344). It no longer writes
the poll-start snapshot over newer state, and the cache-hit copy keeps the merge-queue
fields. -
projects_v2_itemwebhooks filtered by project and Status field (#2345, ADR 2345).
Events for other boards, or for edits to fields other than Status, are ignored. Without
the filter they were triggering fetches and could be matched as echoes of the engine's
own writes. Not exercised live in this release: the gate beds have no webhooks
configured, so this path is covered only by unit tests and recorded payload fixtures.
Verification with a real delivery follows the release. -
Layer 1 status refresh is applied as a GitHub read (#2347, ADR 2347), not as an
engine write. It no longer creates engine-write protection, or engine attribution, for
a value it only read.
Pruefer
- Embedded review skill (#2306, ADR 2306). The plugin is written to a fresh temp
directory for each review and is the only--plugin-dirpassed. TheSkillgrant is
scoped to the run's own reference. A review whose skill errors, or is never invoked,
posts nothing. - Operator plugins and
review_skill(#2307, ADR 2307). The base branch is pinned to
one SHA per review, and config and guidance are read at it. The fallback to the default
skill is posted as aReview skill fallbacknotice. - Repo plugins from standard declarations (#2366, ADR 2366). These are
.claude-plugin/marketplace.jsonand.claude/settings.json
extraKnownMarketplaces, read at the base SHA:- The tree's size and paths are checked from its listing before any file is read.
- Plugins are cached by tree SHA, within a byte cap.
- Skipped plugins are named in the fallback notice.
- Coded review tools; outside authors read-only (#2308, ADR 2308). Trust is decided
from the author's association with the repo, with Dependabot excepted. Hardening (#2370):- tool names are compared case-insensitively;
- a wider denylist of side-effect-capable tools;
- every granted rule is checked again before each run.
- Coverage resume loop (#2375, ADR 2375). Pruefer only resumes when the first pass
produced a complete review and left staged entries unread. The posted review's coverage
block is the union of every accepted pass's coverage. - Staged-entry batch limit (#2362, ADR 2362). The reviewer reads diff entries one at a
time, or in batches of at most 20 KiB, below the size at which the Claude CLI cuts a
result to a 2 KB preview. Entries seen only as a preview are counted as unread. - Head SHA fallback (#2331). When
refs/pull/<N>/headis missing, Pruefer fetches the
head commit by its SHA. - Review sessions moved (#2363, ADR 2363) to
.pruefer/transcripts/, kept 7 days.
Internal
-
Pinned the marketplace plugin entries to v0.0.88
-
Set fabrik plugin to 0.0.88 (paired with the binary)
-
Live e2e gate: coverage complete for engine SHA 8a55369 (166 test/leg pairs passed, 0 known skip(s), 34 structural skip(s)) under E2E_MATRIX=full (200 required pairs of 204 in the full 2×2) across 2 gate invocations.
-
internal/skillref(#2305, ADR 2305). The skill catalogue, resolver and load-failure
detector moved out ofengine/into a leaf package shared with Pruefer. -
Engine state audit design note (PR #2353). Corrected the PM plugin's install example
(#2336). -
v0.0.87 documentation pass (#2338).
-
Release gate: full 2×2 matrix (
E2E_MATRIX=full), because this release changes the
landing path (#2341, #2342). The gate ran on enginece941763, and coverage was carried
to8a553693. The commits in between change onlypruefer/, Pruefer docs, ADRs, specs,
CLAUDE.mdand twotests/e2efixes (PR #2377). No engine, embedded or build input
changed. -
Watch items reviewed at the cut. On #2359, the dev Pruefer completed 98 reviews with the
embedded skill, with 0never invokedfailures and 0 fallbacks. On #2111, there were no
multi-member trials on the dev bed and no qualifying hit.
v0.0.87
Fabrik v0.0.87
Summary
The fabrik plugin's MCP tools work with no configuration (#2322). fabrik mcp now
finds its subscriber name on its own, so the overseer actions (fabrik_promote,
fabrik_set_autonomy, fabrik_advance and the rest) no longer refuse in a fresh install.
The plugin also stops overriding a per-session name, so several Claude Code sessions can
each attach to the daemon under their own name.
Upgrading
- The plugin's "Subscriber name" setting is gone (#2322). If you set it, export
FABRIK_SUBSCRIBERin the session's environment instead (shell profile, direnv or inline)
to keep receiving pushed channel events. A value stored for the old setting is ignored. - The default name does not receive pushes. With nothing set, the name is your OS
username: it signs the overseer actions' audit comments and isfabrik_subscribe's
default, but it never starts the push stream. Nothing Claude Code sends when the server
starts says whether the session can receive pushes, and a stream under a name every
session shares would let aclaude -pscript take over your interactive session's
stream. SetFABRIK_SUBSCRIBER(or pass--subscriber) to receive pushed events. - If you register
fabrik mcpyourself with--subscriberinstead of using the plugin,
disable the plugin's copy. The channel source then readsserver:fabrikrather than
plugin:fabrik@fabrik.
# Auto-upgrade from a running Fabrik instance
# Fabrik checks for new releases each poll cycle and upgrades automatically with --auto-upgrade
# Or download directly (auto-detects OS and architecture)
gh release download --repo handarbeit/fabrik \
--pattern "fabrik_*_$(uname -s | tr A-Z a-z)_$(uname -m | sed 's/x86_64/amd64/' | sed 's/aarch64/arm64/').tar.gz" \
-O - | tar xzMCP and the fabrik plugin
- Subscriber name resolution (#2322).
fabrik mcptakes the first of:--subscriber,
FABRIK_SUBSCRIBER,CLAUDE_TOPIC(exported by thetopicsession launcher), then the
OS username ($USER, else the system user database). A blank value or an unexpanded
${...}placeholder counts as unset at every step. The plugin's.mcp.jsonno longer
setsFABRIK_SUBSCRIBER, and itsuserConfigno longer hassubscriber. - A session whose default-named stream is taken over by another session is told how to give
each session its own name.fabrik_subscribeand a pushedfabrik_situationsay plainly
when the name is the default and events are held rather than delivered. - Every action tool's description now states where its subscriber name comes from. See
ADR 2322, which amends ADRs 2141, 1966-b and 1966-c.
Documentation
- The v0.0.86 documentation pass (#2333, PR #2335): the User Guide covers the
merge-train stale-trial rebuild, landing settle, cancelled-check rule, Validate after a
merged PR, comment-run blocking, the exact dev-remote rule and Pruefer transcript
retention; the README and marketing page gain v0.0.86 highlights.
Internal
-
Pinned the marketplace plugin entries to v0.0.87
-
Set fabrik plugin to 0.0.87 (paired with the binary)
-
Live e2e gate: coverage complete for engine SHA 4008446 (72 test/leg pairs passed, 0 known skip(s), 3 structural skip(s)) under E2E_MATRIX=sparse (75 required pairs of 200 in the full 2×2) across 2 gate invocations.
-
Release gate: coverage carried from v0.0.86's gate (engine
2c61010, 72 of 72
runnable pairs). Every change since then is incmd/mcp.go,internal/mcpstdio, the PM
plugin (plugin/fabrik, not embedded), docs and release metadata; nothing inengine/,
the embedded workflow plugin or the gate changed, and neither the engine nor the gate
runner depends oncmdorinternal/mcpstdio.
v0.0.86
Fabrik v0.0.86
Summary
A bug-fix release. Every issue in it came from a report: community reports, our own
production incidents, or the v0.0.85 gate. There are no new features.
The merge train no longer turns GitHub timing into member failures. A trial that goes
dirty because the base moved during its CI is now rebuilt, not charged to a member (#2298).
A landing that reads blocked for a moment right after mark-ready is waited out on the same
PR, and a green trial is kept instead of rebuilt (#2297). A pending-eject signal no longer
leaks from one train run into a later batch (#1557).
A cancelled or unrecognised CI result is no longer a pass (#2296). A cancelled, stale or
empty check-run conclusion counted as green, so an item could advance or land without a
passing run. It is now treated as not passed. Each gate re-runs the cancelled jobs once.
Fabrik stops overwriting work it can't see (#2295). Before working on an existing issue
branch, Fabrik syncs it from origin. Every push now uses a lease on a remote head it has
verified. A branch that has diverged from its remote pauses the item instead of
force-pushing over the other side.
Each Claude invocation runs on the account you configured (#2293, #2294). Fabrik and
Pruefer resolve CLAUDE_CONFIG_DIR from one ordered set of sources: flag, environment,
.env, config.yaml, then the inherited value. They log which account each process uses
and warn when the sources disagree.
Fewer silent waits. A merge state GitHub never computes now pauses after 30 minutes,
and a CI-timeout pause names the gate that actually held the item (#2302). A Validate run
that finishes after its PR merged takes the terminal path (#2299). A denied edit that no
allowed_tools rule can grant pauses on the first occurrence and names the file (#2301). A
comment-review run can now ask for input (#2280).
Session handling. A time-sliced stage resumes its Claude session again (#2292, a
v0.0.85 regression). A new stage option, resume_session, can start a fresh session when the
saved one has grown large (#2300).
Upgrading
No configuration changes are needed. Behaviour changes to know about:
- Cancelled checks block (#2296). A PR whose only non-green check run was cancelled used
to pass the CI gate. Fabrik now re-runs the cancelled jobs once. If a check is still
cancelled after that, the item waits or goes the CI-fix route like any red run.
skippedandneutralstill pass. - A diverged issue branch pauses (#2295). If someone pushes to
fabrik/issue-Nand the
local branch has commits the remote lacks, Fabrik now pauses the item with a comment
instead of pushing. Resolve the branch and comment to resume. - New pause: unknown merge state (#2302). A PR whose mergeable state stays
unknownfor
30 minutes, with no worker running, pauses with a comment naming the PR, head and
check-run state. Change the bound with--merge-state-unknown-timeout/
FABRIK_MERGE_STATE_UNKNOWN_TIMEOUT(minutes). - New pause: ungrantable denied edit (#2301). A run that a denied file edit inside the
worktree stopped (for example under.claude/), when the CLI's--allowedToolsalready
allows that tool on the whole worktree, pauses at once and names the file. It no longer
burns three tools-denied retries first. CLAUDE_CONFIG_DIRprecedence (#2293, #2294). A bareCLAUDE_CONFIG_DIRinherited
from the shell now ranks below.envandconfig.yaml. To override for one run, use
--claude-config-dirorFABRIK_CLAUDE_CONFIG_DIR(Pruefer:-claude-config-diror
PRUEFER_CLAUDE_CONFIG_DIR). Startup logs the winner and warns when sources disagree.- Dev self-upgrade needs an exact GitHub remote (#1209). A source checkout rebuilds
itself only if itsoriginis exactlygithub.com/<owner>/<repo>over https or ssh. A
checkout whose remote uses an~/.ssh/confighost alias, or a token in the URL, now
reports "not a source checkout" and does not self-upgrade. - Restart resumes interrupted stages (#2300). A stage that a restart interrupted, or
one that ended incomplete, resumes its saved session after the restart.resume_session
defaults toresume, so nothing else changes unless you set it.
# Auto-upgrade from a running Fabrik instance
# Fabrik checks for new releases each poll cycle and upgrades automatically with --auto-upgrade
# Or download directly (auto-detects OS and architecture)
gh release download --repo handarbeit/fabrik \
--pattern "fabrik_*_$(uname -s | tr A-Z a-z)_$(uname -m | sed 's/x86_64/amd64/' | sed 's/aarch64/arm64/').tar.gz" \
-O - | tar xzEngine
- Stale merge-train trials are rebuilt (#2298, reported in #2213). A trial PR that reads
dirtyis checked with one live read of how far its branch is behind the base. If the base
moved (or the read fails), the trial is stale: Fabrik re-pins the base and rebuilds the same
member set, spending one cycle of the run's persisted rebase budget. No member is charged,
bisected, ejected or paused. When the budget is spent the batch dissolves, naming the old
and new base SHAs. Adirtytrial on an unmoved base is still red, with a diagnostic that
says so. A restart counts and rebuilds once. See ADR 2298. - A merge-train landing waits out a transient
blocked(#2297, reported in #2237). After
mark-ready, Fabrik merges only when CI is green and GitHub reports a mergeable state. It
waits out pending checks under the CI backstop, and an all-green butblockedstate for up
to 5 minutes, on one deadline that is never reset. It retries a merge refusal on the same
PR inside that wait. If the attempt still ends without a merge and the base has not moved,
the green trial PR and its branch are kept and resumed rather than rebuilt. A waiting landing
posts nothing; onemerge failurecomment is posted per PR, and only for a real failure.
See ADR 2297. - Pending-eject signals belong to one train run (#1557). A review or comment eject raised
for a member while a train run was in flight could survive that run's early returns and be
applied to a later, unrelated batch. Each signal now carries the run it was raised for, is
acted on only by that run and is cleared when the run releases its claim. The scan raises
it again if the finding is still unresolved. See ADR 1557. - Cancelled, stale, empty or unknown check-run conclusions are not passing (#2296,
reported in #2215). The stage CI gate treats them as a failure, re-runs the cancelled jobs
on the existing flake budget and says cancelled in the CI-fix comment. The merge train's
fast path declines; admission defers. A trial re-runs once, then is abandoned as an
infrastructure problem, so no member is charged. A singleton catch-up builds a trial
instead of ejecting. See ADR 2296. - Issue-branch sync and verified-lease push (#2295, reported in #2058). When Fabrik
reuses a worktree, it first compares the localfabrik/issue-Nwith origin. It
fast-forwards when behind, resets when the local branch holds nothing new and keeps the
local branch when the remote holds nothing new. It changes the branch only on a clean, idle
tree and never pushes during the sync. A diverged branch pauses the item with one comment.
Every engine push now uses--force-with-leaseon a remote head Fabrik has verified. An
unreadable remote refuses the push. The #2089 zero-ahead guard stays in front. See ADR 2295. - A Validate run that ends after its PR merged takes the terminal path (#2299, reported
in #1810). Validate completion reads the linked PR live. If it has merged, the item goes to
Done instead of armingfabrik:awaiting-cion a merged PR. A PR that was closed without
merging pauses the item. A merge is deferred while a worker for the item is running, and no
push is attempted for a merged PR. See ADR 2299. - Unknown merge state is bounded, and the CI-timeout pause names the real blocker
(#2302, reported in #1809). Details are under "Upgrading". A clock starts at the first
unknownreading for a head. It resets when GitHub computes a state or the head changes,
and it never fires on the first evaluation after a start. The CI backstop pause now reads
which gate held the item: the CI gate keeps today's text, and any other gate is named with
its reason and the check-run state. See ADR 2302. - First-occurrence pause on an ungrantable denied edit (#2301, reported in #1981).
Details are under "Upgrading". Fabrik decides from the--allowedToolsit actually passed,
never from prose. The pause comment names the path and gives three options. It is not a
failure and does not touchmax_retries. See ADR 2301. - The tools-denied exemption requires a causal denial (#1556). A run that hit any
permission denial used to be exempt frommax_retries. It is now exempt only when the
denial plausibly stopped it: no commit, a denial tied to a write-capable tool call on the
main thread, and no later successful edit. Other runs count as ordinary failures. - A comment-review or reinvoke run can block on input (#2280).
FABRIK_BLOCKED_ON_INPUT
from a comment pass, CI-fix, review or rebase reinvoke now pauses the item as on the stage
path. The comment is marked processed and the cycle breakers are not charged. - Every resume path clears the comment breaker (#1575).
fabrik:revalidate(and the
merge-train auto-repair that reuses it) and a Validate SHA invalidation cleared the cycle
counters but not the #1089 comment circuit breaker. All four resume paths now reset both. - A time-sliced stage resumes its session (#2292, a v0.0.85 regression from #2232). A
turn-cap ormax_wall_timestop skips the retry backoff by design. The same change also
made the continuation start a fresh Claude session, losing the conversation. The backoff
exemption is now a separate flag, so the continuat...
v0.0.85
Fabrik v0.0.85
Summary
A stage now runs before any comment that was waiting when the item entered it (#2220).
A comment posted while an item sat in an earlier column (Backlog, say) used to be handed
to the stage's comment skill on entry, and that pass could complete the stage on its own.
On #2218 the specify skill never ran and no Specify comment was posted; the same could
skip the Research comment, the Plan comment or Implement's draft PR. The stage now runs
first, and the comment pass follows once the stage has produced its output.
SIGHUP is a real graceful restart (#2259). The first SIGHUP (or ctrl+r in the TUI)
used to cancel every in-flight worker; on 2026-10-13 that threw away a comment pass 14
turns in, which then ran again from the start. The first SIGHUP now stops starting new work
and waits for running workers to finish before re-exec'ing. A second SIGHUP restarts now.
Smaller engine fixes. Merge-train Queued order follows GitHub's own move time (#2246). A
stale fabrik:awaiting-input is cleared once nothing is paused (#1970). A usage-limit exit
on a resumed session is classified as a usage limit, not a resume failure (#1949). An
already-landed origin issue branch is no longer adopted into a new worktree (#1917). A
+dirty dev build rebuilds when its checkout moves, for Fabrik and Pruefer (#2234).
The TUI shows what Fabrik is waiting on (#2238). The In Progress panel lists items held
on CI, reviews, a retry, a rebase, the merge train or a human reply, and the header no longer
says "nothing to do" while those waits are open.
CI is cheaper and faster. A PR that changes no Go build input skips the Go suites while
still reporting success (#2254), and Linux -race runs the sim at parallelism 16 (#2228).
This release's live gate is the first certified run on two test beds (#1990).
Upgrading
No configuration changes are needed. Two behaviour changes to know about:
- A SIGHUP restart can take as long as the longest running worker (#2259). The first
SIGHUP, orctrl+r, stops polling and starts no new work of any kind, then waits, with no
deadline of its own, until in-flight workers finish; each is still bounded by its stage's
max_wall_timeand the 15-minute inactivity timeout. To restart immediately, send SIGHUP
again (or pressctrl+ragain): the remaining workers are cancelled, the usual
--drain-deadlinedrain runs, and Fabrik re-execs. A second SIGHUP no longer force-quits
with exit status 1. SIGHUP never pauses an item. SIGINT and SIGTERM are unchanged, and
during a SIGHUP drain they stop the daemon instead of restarting it. Scripts that send
SIGHUP and expect a restart within seconds should send it twice. - A stage runs before a pending comment (#2220). If you comment on an item before it
reaches a stage, expect the stage's own run first and then one comment pass that applies
your comment, instead of the comment pass alone. That is one more invocation than before.
The advance waits for the comment pass, including a stage that reports
FABRIK_NO_WORK_NEEDED: with a comment pending, it is marked complete but does not move
to Done until the comment pass has run.
# Auto-upgrade from a running Fabrik instance
# Fabrik checks for new releases each poll cycle and upgrades automatically with --auto-upgrade
# Or download directly (auto-detects OS and architecture)
gh release download --repo handarbeit/fabrik \
--pattern "fabrik_*_$(uname -s | tr A-Z a-z)_$(uname -m | sed 's/x86_64/amd64/' | sed 's/aarch64/arm64/').tar.gz" \
-O - | tar xzEngine
- A stage runs before any comment pending on entry (#2220). Before, a pending comment
sent the item to the comment skill ahead of the stage, andFABRIK_STAGE_COMPLETEfrom
that pass completed the stage. Now a pending comment goes to the comment path only once the
stage has run on this entry, judged from durable state:stage:<S>:completeorfailed,
fabrik:reworking:<S>, the stage's CI/review/done wait marker, an engine pause, the stage's
own🏭 **Fabrik — stage: <S>**comment, or a resume from a pause. Anything undeterminable
counts as "not run". The stage run no longer marks the pending comments as seen (no 🚀),
so the comment pass still happens, and the existing feedback gate holds the advance until
it has.FABRIK_NO_WORK_NEEDEDnow goes through the same gate. An incomplete stage run
with a pending comment is retried as a stage run under its normal backoff. See ADR 2220. - SIGHUP drains in-flight workers before restarting (#2259). Details are under
"Upgrading". While draining, the in-flight set (#issue stage kind elapsed) is logged on
the first SIGHUP and every minute, followed once bysend SIGHUP again to restart now. The
TUI title showsdraining for restart (N left), andfabrik_healthgains adraining
object. The poll is reported as not overdue while draining, so nopipeline-stalledevent
fires. A merge-train run waiting on a trial's CI does not hold the restart; its persisted
run resumes after the exec. A reinvoke that was only queued for a slot is dropped and
re-dispatched by the new process. See ADR 2259. - Merge-train Queued order uses GitHub's move time (#2246). Members whose moves into
Queued the engine saw in the same pass were ordered by board-listing order. In the v0.0.84
gate, members moved as A, B, C, P formed the batch[A, P, B, C]. The board and probe
queries now read the Status value's ownupdatedAt(one extra scalar), and the batch is
sorted by that time, then by issue number. The time survives a restart, so a restart no
longer reshuffles a batch. GitHub's times have one-second resolution, so moves in the same
second still fall back to issue number. The engine's own moves use daemon time. The local
API's merge-train position uses the same order. See ADR 2246. - A stale
fabrik:awaiting-inputis cleared (#1970). Two routes left it behind after a
comment-review run that did the work and never asked for input. Many pause messages tell
you to remove onlyfabrik:paused, which leftfabrik:awaiting-inputon the item, and
only aFABRIK_STAGE_COMPLETErun used to clear it. Fabrik now removes it after every
comment pass and reinvoke, and when processing an item, whenever a live read shows
fabrik:pausedis absent. A real pause is never lifted this way. Also, if removing
fabrik:pausedon a resume failed, the comment was still processed, leaving the item
paused with nothing to resume it. A failed removal now leaves the comment unprocessed, and
the next poll retries. - A usage-limit exit on a resumed session is classified as a usage limit (#1949). On
--resume, Claude reports the session's cumulative turns and cost, and Fabrik read them as
this invocation's work. A limit that rejected the invocation before it did anything then
counted as a resume failure: in the v0.0.83 gate this abandoned a healthy session after two
exits and delayed the account-wide usage-limit hold by two invocations. A resumed
invocation whose own output has no user turn is now judged on zero usage, so the exit is a
usage limit and the session and resume-failure counter are left alone. A limit hit after
real work in the same invocation is still not detected, as before. See ADR 1949. - An already-landed origin issue branch is not adopted (#1917). When the local
fabrik/issue-Nwas missing, Fabrik recreated it fromorigin/fabrik/issue-N. After a
fabrik:revalidate, a reopen or a landing-verification failure, that branch could be
merged history, giving phantom diffs and conflicts. Fabrik now forks fresh from base, with
one log line saying why, when the origin tip is already on base, or equals the head of a
merged PR for the item (its linked PR or itsfabrik:credited-pr:<N>PR, which covers
squash and rebase merges). A branch with unmerged commits is still adopted, and so is one
whose ancestry check fails. Adopting a branch whose open PR targets a different base now
logswarn: base mismatch, naming both. See ADR 1917. +dirtydev builds rebuild, and every upgrade check logs its outcome (#2234). A dev
build stampeddev(<sha>)+dirtywas treated as unparseable, so once its checkout was
pulled it compared as up to date and never rebuilt, with nothing logged. It is now
compared like a clean build. A dirty build at the same commit as HEAD is still not
rebuilt, since a rebuild would pick up nothing committed. Each check logs one outcome line,
and the checkout's dirty paths are logged once per process (up to 10). The code is shared,
so Pruefer dev builds get the same fix. See ADR 2234.
TUI
- The In Progress panel shows items Fabrik is waiting on (#2238). Open items held on a
known gate get a row beside the running and🔒blocked rows: CI, reviews, a retry or
time slice, a rebase, a merge-train position, or a human reply.⏳marks an external wait
and✋an item that needs you;✋rows sort first, and at most 5 rows show, then+K more waiting. Row text uses only facts the engine already holds, and unknown facts are left
out. While such waits are open, the header readswaiting on N item(s)[, M need you]
where it used to read[poll] nothing to do. Stalled items and red CI are not listed. The rows make
no GitHub calls. See ADR 2238.
Documentation
- The v0.0.84 documentation pass (#2256, PR #2258) merged after the v0.0.84 tag, so it
ships here. The User Guide adds.fabrik-context/ci-status.md, an ordered per-project
upgrade procedure, Pruefer review notes, theentwurf@fabrikremoval, Specify's
carry-forward of human-added sections, more warnings-panel entries and the v0.0.83
Plan-comment recovery note, and corrects the worker-variable count to six. The README
gains v0.0.84 highlights and afabrik mcprow, and the docs site and FAQ drop the stale
"no binary install" c...
v0.0.84
Fabrik v0.0.84
Summary
A Claude Code session can now oversee a running Fabrik daemon directly. This is the
release's headline. The daemon serves a local API on a private per-project socket, and the
new fabrik mcp stdio server exposes it to Claude Code as tools. The tools answer "what
needs me?" from state the daemon already holds, at no GitHub API cost. A small set of
audited actions can promote, advance, park, change autonomy, revalidate, or clear a Claude
usage-limit hold. Events (a settled Validate, a pending decision, a pause, an arrival, a
column move, a merge, a stalled poll loop) are pushed into the session as they happen. The
fabrik PM plugin is rebuilt around this and now ships pinned to each release, so
installing it is the easy way to turn the feature on.
Stage skills are addressed by their real names, and customising them no longer strands
a project. Claude Code registers plugin skills under their plugin's namespace. Fabrik
prompted bare names, which Claude models quietly bridged and other models did not (#1373).
Every skill key now accepts plugin:skill. The base stage plugin is renamed
fabrik-stages, and the fabrik name passes to the PM plugin. fabrik upgrade moves
edits to .fabrik/plugin/ into a local-workflows plugin and rewrites stage YAML to
qualified names. A skill reference that does not resolve, or a skill that fails to load in
the worker, is now always reported. Before, the stage could complete without its
instructions. This needs an explicit fabrik upgrade; see "Upgrading".
The merge train was reworked around the early-October incidents. Those incidents had
finished work paused overnight, throwaway trial PRs for every behind singleton, flakes
ejecting unrelated members, and a CI run that never started holding a slot for two hours.
A behind singleton now catches up on its own branch and lands without a trial. A red
singleton is repaired through Validate once before anyone is paged. Batches avoid members
that touch the same files. A run that never starts and a single flake are treated as
infrastructure. Trial state is persisted, so a restart resumes a trial or bisection. The
train has a reserved worker slot and holds a slot only while Claude is resolving a
conflict. Several of these change default behaviour; read the merge-train section before
upgrading a train-enabled project.
The board shows what Fabrik is doing, and operator faults have one place to land.
Three optional display-only project fields are written: Fabrik (a one-line status),
Last activity and Last run. fabrik init --create-board and fabrik repair-board --apply create them. The TUI warnings panel now collects operator-level faults that used
to scroll past as log lines, from unrecognised config keys to missing display fields.
The engine's view of an item is harder to corrupt. A stale board listing or probe could
revert the engine's own label, Status and close writes (#2159, #2178). An engine self-write
could hide a human comment, a review thread or a CI result from the cache (#2180). Every
reinvoke left a phantom lock behind (#2179). A catch-up landing stripped Validate
completion from landed items (#2158). The feedback gate could hold on a review the
dispatcher could not see, until someone restarted the daemon (#2160). All six are
addressed.
Running a daemon is safer. Two engines can no longer serve one board, and an engine
that loses its lock stops itself. A stale local branch can no longer be force-pushed over
an open PR. fabrik.log is rotated rather than truncated on restart. Workers no longer
inherit the daemon's own credentials. Orphaned test processes are reaped by ownership.
Under GitHub App auth, the installation must now grant actions: write, or the daemon
refuses to start.
Upgrading
Before you install the binary (GitHub App auth only): grant actions: write (#2105).
Startup and fabrik init --github-app now refuse an installation that lacks it (read is
refused too). That includes the next self-upgrade re-exec, so an --auto-upgrade daemon
whose App lacks the grant will not come back up. Two manual steps are needed:
- An App owner sets the permission at
https://github.com/organizations/<owner>/settings/apps/<slug>/permissions. - An org admin accepts the request at
https://github.com/organizations/<owner>/settings/installations/<id>.
Then restart Fabrik. The refusal message prints both URLs with your values filled in. PAT
mode is unaffected. Workers use the permission to read CI job logs and re-run failed jobs,
and the merge train and the wait_for_ci gate use it to re-run flakes and retrigger CI that
never started. Earlier in this cycle the permission was optional, with an advisory notice
(#2071); the notice did not lead anyone to grant it, so it is now required.
Then, for each project:
- Stop the daemon.
fabrik upgradedoes not check for one, and changing the plugin
under a live daemon broke a worker's skill load in testing. Note which items the stop
pauses. - Install v0.0.84 and run
fabrik upgradein the Fabrik directory. It decides from
what is on disk, not from version numbers, so skipping releases is fine, and a second run
changes nothing. It never commits.- Uncustomised
.fabrik/plugin/: the plugin is refreshed tofabrik-stages, unedited
skills/fabrik-*directories from old releases are pruned, and stage skill keys are
rewritten (fabrik-reviewandfabrik:fabrik-reviewboth become
fabrik-stages:review). - Customised
.fabrik/plugin/: the whole tree moves to
.fabrik/plugins/local-workflows/. Its manifest name becomeslocal-workflows, and a
clean base plugin is restored. Stages whose skill you edited point at
local-workflows:<old name>, and the rest atfabrik-stages:<skill>. - Your own
plugin_dirsplugins: a bare name that one of them provides is qualified
to that plugin, not switched to Fabrik's skill. - Only the four skill keys in
.fabrik/stages/*.yamlare edited. Comments and quoting
are preserved.
- Uncustomised
- Review the diff and commit it.
- Run
fabrik repair-board, thenfabrik repair-board --apply, to add the three
display fields. Otherwise each start shows a "Display fields missing" warnings-panel
entry. Setproject_fields.status_line/last_activity/last_runtooffin
.fabrik/config.yamlif you don't want them. - Restart the daemon and check the warnings panel. Then unpause the items the stop
paused by removingfabrik:pausedandfabrik:awaiting-input.
Replacing only the binary does not break anything. Old skill references keep resolving,
with a skill_reference_deprecated panel notice. A customised install also shows "Workflow
migration pending — run 'fabrik upgrade'". The daemon never migrates on its own; the
dev-build auto-upgrade runs fabrik upgrade --no-migrate. --no-migrate keeps the old
behaviour. To undo a migration, restore .fabrik/plugin, .fabrik/plugins/local-workflows
and .fabrik/stages from git.
From now on, customise only .fabrik/plugins/local-workflows/. An edit to
.fabrik/plugin/ blocks the next fabrik upgrade ("workflow migration refused"). To keep
the edit, move it into local-workflows, point the stage at local-workflows:<skill> and
run fabrik upgrade --force. To drop it, just run --force.
Other changes to check:
- Worker environment (#2027). Workers now receive only six engine-set
FABRIK_*
variables:FABRIK_ISSUE,FABRIK_REPO,FABRIK_WORKTREE,FABRIK_ROOT,FABRIK_PR
andFABRIK_WORKER_OWNER. A custom skill or script that reads any other must name it in
FABRIK_WORKER_ENV_PASSTHROUGH. - Merge-train defaults. These are on by default when
merge_trainis on:- The singleton catch-up pushes merge commits onto PR branches (
singleton_catch_up: off
to disable). - Red singletons get one auto-repair before pausing (
max_train_auto_repair_attempts: 0
restores the pause). - Members touching the same path are never batched together. List lockfiles, changelogs
and similar inmerge_train_overlap_ignore. - The host can run
max_concurrent + 1Claude processes, because of the reserved train
slot.
- The singleton catch-up pushes merge commits onto PR branches (
- Spec persistence is opt-in for existing stage files (#2034). An existing
specify.yamllackspersist_spec: true. The drift warning names it, andfabrik refresh-stages --applyadds it. - Log files. Add
.fabrik/fabrik.log.*to.gitignore(#2094). - PM plugin users: run
/plugin marketplace update fabrik,/plugin install fabrik@fabrikand/reload-plugins. If you registeredfabrik mcpby hand, remove that
registration (claude mcp remove fabrik) or you will run two servers.entwurf@fabrikis
no longer served from this marketplace; Entwurf has its own. - Pruefer operators: if
claudeauthenticates through an environment variable (for
exampleCLAUDE_CODE_OAUTH_TOKEN), name it inPRUEFER_REVIEW_ENV_PASSTHROUGH(#2192). - Plan comments damaged under v0.0.83 (PR #2216). v0.0.83's Implement skill replaced
the whole Plan comment with the literal text@/tmp/…each time it ticked a task. Look
for Plan comments whose body is a bare@/…path, and restore each from the comment's
GitHub edit history; the previous revision holds the original Plan.
# Auto-upgrade from a running Fabrik instance
# Fabrik checks for new releases each poll cycle and upgrades automatically with --auto-upgrade
# (App auth: grant actions: write first — see above)
# Or download directly (auto-detects OS and architecture)
gh release download --repo handarbeit/fabrik \
--pattern "fabrik_*_$(uname -s | tr A-Z a-z)_$(uname -m | sed 's/x86_64/amd64/' | sed 's/aarch64/arm64/').tar.gz" \
-O - | tar xzMCP overseer
To turn it on, install the PM plugin (/plugin marketplace add handarbeit/fabrik, then
`/plugin install ...
v0.0.83
Fabrik v0.0.83
Summary
Fabrik can now run as a GitHub App. This is the release's headline and its largest
change: the engine authenticates as an App installation rather than a personal access
token, which buys it a stable <slug>[bot] identity and — the part that matters
operationally — its own rate-limit bucket. Every PAT a user holds draws on one shared
5,000-point GraphQL budget; an installation gets its own, larger allowance that scales
with the account. By the end of the cycle App mode is self-contained: no user:, no PAT,
commits authored as the bot, HTTPS git pushing as the installation, and event-driven
ingestion via Hookdeck where gh webhook forward cannot follow. PAT mode is not
deprecated and never will be: a user-owned project board cannot use App auth at all,
because GitHub strips organization permissions from user-account installations. The two
modes are co-equal.
Getting onto a board is no longer a manual setup chore. fabrik init --github-app
creates the App under your organization, waits for you to install it, and verifies its
grants; fabrik init --create-board builds a fully-configured Projects v2 board straight
from your stage configs; and the new fabrik repair-board diagnoses and — only when you
ask it to — repairs a board whose columns have drifted from your stages.
Fabrik no longer advances or lands past feedback it has not processed. A review that
arrived while a stage was running, or while it waited on CI, was invisible to the review
gate — and on 2026-09-29 a real defect in a bot review was fixed only because a human
copied it into a comment. Every advance and landing decision now consults one live
predicate covering comments, unresolved review threads and unaddressed review bodies, and
holds while anything is outstanding. A Queued merge-train member that receives a human
comment is ejected back to have it addressed, and a comment that arrives after the work
has merged gets an explicit "not applied" reply instead of being processed onto a dead
branch.
The merge train got correct and cheaper. It could land the same member twice, admit
members whose own CI was already red and then bisect for hours to rediscover it, clear on a
partial check set while a queued job had not yet started, and pick a different random batch
every poll once the queue outgrew max_batch_size. All four are fixed. Conflict
resolutions are now replayed across trials with git rerere, and a trial's still-valid
prefix is reused instead of rebuilt from base.
The autonomous loop got meaningfully harder to wedge. A wake signal walked straight
through GraphQL rate-limit backoff, turning an ordinary throttle into a self-sustaining
outage that starved every project sharing the account's token. Claude session limits
arriving as an api_error 429 were treated as transient, so nothing backed off; the
suspension now recognises them and runs until the reset time the CLI reports. And when
GitHub silently stopped creating closing-keyword links on 2026-09-30, Fabrik paused items
for "broken linkage" and left landed issues open — it now confirms linkage from the PR body
and closes landed issues itself.
Features
- The engine can authenticate as a GitHub App installation (#1713). When App
credentials are configured (github_app_id,github_app_private_key_path,
github_app_installation_id— all or none), the engine builds its GitHub client from a
githubauth.Reconcilerand keeps a token-refresh loop alive for its lifetime. Two
concrete wins: a separate, larger rate-limit bucket instead of a share of the one
5,000-point GraphQL budget every one of your PATs competes for, and a stable bot
identity distinct from the operator's account. Startup reads the installation's
granted permissions — not the App's requested ones, which diverge silently — and
refuses to start with a message naming each missing grant. PAT mode is permanent and
co-equal, not legacy: a user-owned board can never use App auth, so the PAT path
remains the only option there, by GitHub's design rather than ours. - App mode needs no operator identity (#1893). Under App auth there is no
user:and
no PAT:user is requiredis skipped (a leftoveruser:is tolerated, ignored and
logged), and the engine's token is zeroed. Commits are authored as<slug>[bot]with
the bot's noreply address — an operator-chosen git identity on an existing clone is
never overwritten. Lock labels becomefabrik:locked:<slug>-<hash>, unique per host and
directory, so two local instances of one App never share a lock; a leftover
fabrik:locked:<user>from a PAT-era run is swept once at startup. "Awaiting input"
mentions go to the issue's human assignees, else its human author, else nobody — never
a bot. PAT mode is unchanged by construction. - HTTPS git authenticates as the App installation (#1846). Previously HTTPS git was
refused at startup under App auth and the only workaround pushed with the operator's SSH
key. The engine now injects a git credential helper into its own environment that
overrides any host helper and serves the installation token from a file rewritten on
every token rotation — so engine and worker pushes go out as the App, and a worker
running past the token's ~1h lifetime still pushes. Requirescontents: write, checked
at startup only in HTTPS mode; SSH setups (git_ssh: true) are unaffected. - Real-time events on multi-repo boards, in both auth modes (#1142).
gh webhook forwardhonours only one--repo, so on a multi-repo board without org mode Fabrik
silently received webhooks for one repo and polled the rest — while reporting the stream
healthy. In PAT mode the limitation remains, but it is no longer silent: the effective
subscription is logged on every (re)start, a periodic check warns on any managed repo
without a forwarding hook, and the TUI shows partial coverage. For App auth, where
gh webhook forwardrefuses installation tokens entirely, the newevent_source: hookdeckingests the App's single webhook via Hookdeck — the transport Pruefer already
used, now shared — covering every repo the installation is granted. fabrik init --github-app(#1715) — an end-to-end App setup flow with grant
verification and an explicit org-only guard, so the user-owned-board dead end is caught
at setup time rather than at first poll. When no installation exists yet for--owner,
an interactive run now waits (up to 10 minutes) for you to install the App and then
carries on, instead of exiting with "install, then re-run" (#1889); scripted runs still
fail fast. The username prompt is gone under App auth (#1893).fabrik init --create-board(#1714) — creates a fully-configured Projects v2 board
from the stage configsinitjust extracted: Status options in stage order (including
Done), described, and written into.fabrik/config.yaml. Previously the board had to be
built by hand, with column names matching stage names exactly or startup would refuse.
--repois now optional (#1889): without it the board is created unlinked and the
config left multi-repo, since the engine discovers repos from board items. Under App
auth, a--repothe installation was never granted is caught before anything is
created, with an error naming the repos it does cover.fabrik repair-board(#1714) — diagnoses a board whose Status columns no longer
cover your configured stages, and adds the missing ones. Dry-run by default; it only
mutates under--apply, and it never deletes a column. Both entry points refuse
explicitly on a user-owned board. Under App auth it builds its client from the
github_app_*settings when no--tokenis given (#1893).--no-browser/FABRIK_NO_BROWSER/no_browser(#1763) — suppresses browser
opening during App reconciliation. Defaulted to suppressed, on the reasoning that a
long-running daemon is a bad place for an unconditional browser-open.--retry-backoff/FABRIK_RETRY_BACKOFF/retry_backoff(#1831) — stage retry
backoff is now its own setting (default 60s, minimum 1s) instead of10 × poll. Raising
pollto protect a shared rate limit silently multiplied retry latency: at a 180s poll,
a stage that ran out of turns while making progress waited 30 minutes before each retry.
GitHub re-check cadences stay tied topoll, since those genuinely are about API cost.fabrik:toolchain-stale(#1786) — workers inheritPATHfrom the shell that
launched the daemon, so a long-lived daemon silently pins every worker to that shell's
toolchain; in the reported case workers kept running Node 20 for weeks after the repo
moved to Node 24. Before each invocation Fabrik now compares.nvmrc,engines.node,
go.modand.tool-versionsagainst the version actually onPATH, and posts one
warning naming both when they disagree. Warn-only — it never skips an invocation — and it
stays silent on anything it cannot compare.- Plugin staleness is reported even when the plugin is customized (#1787). A
customized.fabrik/plugin/used to hide the fact that it was also behind the embedded
skills. Startup,fabrik upgradeand the TUI badge ([u] custom workflow (N stale)) now
report both, and point atfabrik upgrade --reconcile. Upgrade refusal behaviour is
unchanged. fabrik:reworking(#1802) — while a stage is re-entered to process a steering
comment, its stalestage:<Stage>:completeis now cleared and restored afterwards, with
fabrik:reworkingmarking the window, so the board no longer shows a stage as complete
while it is being reworked. Gating is unaffected; a crash mid-rework is repaired at
startup.- Pruefer gains operator configuration. Review cadence and per-repo
repo_cadencegate how often the bot reviews a given PR (#1610);review_guidance/
review_guidance_modelet an...
v0.0.82
Fabrik v0.0.82
Summary
A reliability release for Fabrik's own release process. Cutting v0.0.81 took eleven
attempts, and the code being released was never once at fault — every failure was the test
harness: a run that hung silently for seventeen hours, a test bed whose accumulated state
outgrew a fixed timeout, a parallelism cap that missed the path CI actually uses, and a
duplicate pre-gate that made every release win the same flaky coin toss twice.
Those are fixed here, along with the discipline that found them. Three separate guards this
cycle turned out to be deletable without failing a single test — their logic was covered, the
wiring that invokes them was not — so neutralization (break the guard, prove the test goes red)
is now part of what the Review stage does by default.
Almost all of this is internal tooling. Two things users will see: Review now applies that
neutralization check to guards it reviews, and the merge train's new single-member fast path
finally says so on the PR it lands — it had been merging silently.
Fixes
- The merge train's single-member fast path landed silently (#1692). v0.0.81 added it (#1644)
as a fourth landing path and wired it into neither of the train's two observability channels:
no🏭 Fabrik merge-train — Landed …comment on the member's PR, and no repo-scoped
landing completeline. So it merged PRs correctly and invisibly — the PR carried no record
of what merged it or what validated it, and from the log alone the train looked like it had
started and never finished, with nothing on its TUI row. Both are now emitted. The comment's
shape differs by design: the fast path cites the member's own PR, because under that path
that PR is the merge vehicle — there is no separate integration PR to name.
Improvements
- The Review stage now verifies guards by neutralization — when a change adds a guard,
Review is asked to identify which mutation the new test catches and confirm it actually fails.
Three guards this cycle were deletable with no test failing, in each case because the test
asserted the outcome rather than the reason, so any rejection satisfied it (#1687).
Internal
-
⚠️ Live e2e integration suite SKIPPED for this release: live e2e gate run in full against this exact ref (819c48e) immediately before this cut, but across three invocations rather than one pass: every leg green, 0 test failures (off 47 tests; on 46 tests, package pass; isolated runaway-guard leg PASS rc=0). A clean single-shot run is blocked on this host by two release-gate defects found during this cut and filed as #1693 and #1694 — see this release notes file's Internal section for the full per-leg detail and caveats. -
The e2e runner can no longer hang silently (#1676). Its GitHub API budget probes had no
timeout at all —|| echo ""guards a failing call, not a hanging one — so a stalled
connection blocked the release script indefinitely. Probes are now bounded, a post-suite
watchdog fires if the script stops progressing aftergo testexits, and a stall detector
reports when output goes quiet. A gate that fails loudly costs an hour; one that hangs
silently cost a night. -
Four release-gate defects fixed together (#1677): the release path now resets the test bed
rather than letting state accumulate until startup outgrew the mode-switch's lock window;
-parallelis capped on the repo-wide-racerun in both the release script and CI, via a
shared helper, closing the path that still crashed under ThreadSanitizer's fork handling; the
measured parallelism band is recorded in an ADR; and the pre-gate SHA guard that was supposed
to stop the suite running twice per release now actually engages. -
The gate checks its own preconditions (#1684). It detects another process competing for
the same GitHub token before spending live budget, and verifies the review bot is reachable
before starting a run whose scenarios depend on it — previously both were discovered only
after an hour of work. The operational contract (what must be up, what must be down, and the
measured budget cost of a leg) is now documented rather than folklore. -
Documentation refresh for v0.0.81 (#1680).
-
✅ Live e2e integration suite: every leg green against this exact ref (
819c48e2), 0 test
failures, but across three invocations rather than one pass — a clean single-shot run remains
blocked on this host by two release-gate defects found during this cut, both filed. Coverage:
sim + wire-contract pre-gate (295s); off leg 47 tests, 0 fail; on leg 46 tests, 0 fail,
packagepass(4620s) — all six merge-train scenarios green (HappyPathLanding,
TwoBasesConcurrent, BisectionEjectsPoisoner, RestartSafety, RedSingletonReroutesOffQueued,
YoloAutoMergeLabel); isolated runaway-guard leg PASS (362s, rc=0). The off leg's
RUN INVALIDfired on GraphQL backoff after every test had already passed — backoff can only
manifest as a failure or timeout, and there were none. Newly filed, not fixed here: #1693
(E2E_BED_REFunusable for any ref butmain, since the bed is a single-branch clone — this
caused one verification run to silently build the bed from the unfixed engine) and #1694 (a
post-suite output-drain stall aborts the run before its remaining legs, which silently skipped
the runaway-guard leg entirely). Both are the same family as #1676/#1677: the release gate's own
failure modes being quieter than the failures it exists to catch.
Upgrading
# Auto-upgrade from a running Fabrik instance
# Fabrik checks for new releases each poll cycle and upgrades automatically with --auto-upgrade
# Or download directly (auto-detects OS and architecture)
gh release download --repo handarbeit/fabrik \
--pattern "fabrik_*_$(uname -s | tr A-Z a-z)_$(uname -m | sed 's/x86_64/amd64/' | sed 's/aarch64/arm64/').tar.gz" \
-O - | tar xzv0.0.81
Fabrik v0.0.81
Summary
The merge train grew up. Three changes reshape how it works. It now runs one train per
base branch rather than one per repo, so a maintenance release on maint/1.2 batches and
lands onto maint/1.2 — previously a base:<branch> member would have been forked from the
default branch and had its integration PR opened against it, landing release-line work in the
wrong place, silently. It now skips the trial entirely for a single member that is already
a fast-forward of the pinned base with its own CI green and complete, which removes a whole
redundant CI cycle from the common case. And it is finally visible in the TUI, as an
ordinary job row alongside the stages, instead of writing its progress into a status line the
next event overwrote.
Also here: Pruefer gains repo-resident configuration, installation-derived repo discovery, and
config reload without a restart.
Features
-
Merge train: one train per resolved base branch (#1648). Queued members are partitioned
bybase:<branch>and an independent train runs per(repo, base)— its own pinned base
SHA, trial branch, integration PR and guards. Members without abase:label are bucketed
under a sentinel that never touches git at grouping time, so the default path is unchanged
and cannot fail closed on a transient git hiccup. -
Merge train: single-member fast path (#1644). When a batch is down to one member whose
head already contains the pinned base (a fast-forward, so the trial's tree would be
identical), whose PR is mergeable, and whose own CI is green and complete, Fabrik lands
that PR directly instead of building and re-validating a trial. Gated strictly on positive
evidence — any ambiguity falls through to the unchanged trial path. -
The merge train appears in the TUI (#1661). It emits a job row like a stage does, keyed
by repo, showing the batch's members and updating as it moves through assembly, CI and
landing. Repo-level log lines are now routed to that row instead of a single-line header
that the next event overwrote. -
A comment names the PR when
base:<branch>suppresses GitHub's linkage (#1649). GitHub
creates no issue↔PR link at all for a PR targeting a non-default base — not merely no
auto-close — so the issue's Development panel shows nothing. Fabrik now posts a one-time
comment naming the PR. Informational only: it gates nothing and carries no retry. -
Pruefer: repo-resident configuration (#1642), resolved at the PR's base ref, so a
repository can carry its own review settings. -
Pruefer: installation-derived repo discovery (#1641). The watched repo set is derived
from GitHub App installations rather than hand-listed, withmax_derived_reposand
repo_rederivation_intervalto bound it. -
Pruefer: config reload without a restart (#1640), via SIGHUP.
-
Pruefer: per-user GitHub App auth reconciler (#1253) — manifest flow, no shared key.
Fixes
- The merge train ignored
base:<branch>(#1646, reported by the community; fixed by
#1647 and #1648). It resolved its base once from the repository default and never consulted
each member's label, so abase:-labelled member would fork its trial from the wrong base
and open its integration PR against the wrong branch. The failure was silent and landed in
the wrong place rather than erroring. #1647 shipped the immediate safety measure — exclude
such members from batching, log it, and comment on the issue so the operator sees it —
and #1648 replaced that exclusion with proper per-base batching.
Internal
-
Auto-bumped entwurf plugin to 0.1.1 (source changed since v0.0.80)
-
⚠️ Live e2e integration suite SKIPPED for this release: live e2e gate run against this exact ref (6e6c0d1) across separate invocations, not one pass — a clean single-shot run is blocked on this host by the release-gate defects filed as #1676/#1677/#1678. Coverage achieved: off leg 69 pass / 0 fail (ok, 4656s; RUN INVALID fired AFTER the leg passed, on GraphQL backoff, not on any test); on leg 71 pass with one verified-false failure (TestYoloAutoMergeLabel — the issue landed and #1616's landing-verification logged 'credited PR #5392 confirmed merged — landing verified'; only #1275's un-retried cosmetic comment was absent); isolated runaway-guard leg PASSED and certified (406s, ok, no backoff, budget 5000->5000). All six merge-train scenarios passed live against the new per-base code: HappyPathLanding, RestartSafety, RedSingletonReroutesOffQueued, TwoBasesConcurrent (#1648's headline two-base concurrency scenario), BisectionEjectsPoisoner, RunawayGuardPausesBatch. Sim suite, wire-contract tests and go test -race ./... green; every issue in this release was neutralization-verified before merge. -
Test-infrastructure repairs (#1624): the sim suite's parallelism no longer inherits
GOMAXPROCS(on a many-core machine that made the suite less reliable, not more), git
invocations are bounded, aborted runs reap their owngo testchildren instead of leaving
CPU-burning orphans, and committed runtime figures were replaced by the runner reporting its
own elapsed time — they had gone stale twice, once self-contradictorily. -
Coverage for the merge train's fail-closed identity guard, which could previously be deleted
with no test failing (#1622). -
Documentation refresh for v0.0.80 (#1626).
Upgrading
# Auto-upgrade from a running Fabrik instance
# Fabrik checks for new releases each poll cycle and upgrades automatically with --auto-upgrade
# Or download directly (auto-detects OS and architecture)
gh release download --repo handarbeit/fabrik \
--pattern "fabrik_*_$(uname -s | tr A-Z a-z)_$(uname -m | sed 's/x86_64/amd64/' | sed 's/aarch64/arm64/').tar.gz" \
-O - | tar xzv0.0.80
Fabrik v0.0.80
Summary
A correctness release, and an urgent one. Under merge-train mode, Fabrik could close an
issue as COMPLETED — board moved to Done, PR closed, a comment citing the batch that
landed it — when the work had never merged at all. Every signal read green, which is what
made it dangerous: nothing was red, so nothing revisited the item, and the branch survived
only because nothing had deleted it yet. If you run with merge_train: on, upgrade.
Two changes address it. The first removes the cause: the merge train now identifies its own
integration PR structurally, by the trial branch it created, instead of by a marker that
every merge-train PR carries in any state — so a different batch's already-merged PR can no
longer be mistaken for this one's. The second is cause-agnostic: after any Done transition
credited to a merge, Fabrik now verifies the credited PR actually merged, and reopens the
issue with an explanation if it didn't. The first fixes the bug we found; the second catches
the next one, whatever its cause.
Also in this release: fabrik-project-onboarding, an optional marketplace plugin that lets
a product manager or business owner author GitHub Spec Kit specifications with no terminal,
git, or engineering background.
Features
fabrik-project-onboardingplugin — three skills (onboard,write-spec,clarify)
for authoring Spec Kit specifications in Claude Cowork or the desktop app, with no
engineering background required. Output is a standardspecs/NNN-feature/folder that
stays byte-compatible with upstream Spec Kit, so it can be handed to Fabrik or to any
development team. Optional and not installed with Fabrik; adapted from
github/spec-kit (MIT), with full attribution in
NOTICE.md.
Fixes
-
Merge train could close an issue as
COMPLETEDwith the work unmerged (#1615).
findIntegrationPRmatched on a marker carried by every merge-train PR in every state, so
it returned "the most recently updated merge-train PR in this repo" rather than this
trial's. When the match was a different batch's already-merged PR, the merge was skipped
as already done and the member's issue and PR were closed anyway, citing a batch containing
none of their changes. The integration PR is now identified by the trial's own branch; a
closed-unmerged trial fails loudly instead of landing; a batch that dropped a member can no
longer claim it; and the "landed via batch #N" comment is only emitted once PR #N is
observed merged. The same structural-identity rule now also governs the reconstruct sweep,
which could close an unrelated PR whose description merely quoted the marker. -
Nothing verified that work advanced to Done had actually landed (#1616). A false
COMPLETEDwas undetectable: no dispatch path revisits a Done item, so there was no signal
to act on. Fabrik now runs a post-Done settle scan that confirms the credited PR really
merged, honoursbase:<branch>, and — on a confirmed failure — reopens the issue, moves it
off Done, labels it, and comments naming the branch, the base, and the PR that was
credited. It is deliberately conservative: anything inconclusive retries rather than
reversing a landing. -
Two dead configuration keys removed, and two documents corrected that described the
opposite of the actual behaviour (PR #1606).
Internal
-
⚠️ Live e2e integration suite: run in full against this exact ref (ccc85418), but as separate invocations rather than one pass inside the release script —--skip-integrationwas used only to avoid running it a second time against an unchanged tree. Coverage was complete: all three legs green (off @-parallel=4: 27 pass / 3 by-design skips; on @-parallel=2: HappyPathLanding, RestartSafety, RedSingleton; plus the isolated runaway-guard leg). The two failures seen were re-run in isolation and verified non-vacuous: TestBaseBranchPipeline (a GitHub 5xx on the test's own merge call, ref E7FD:C4C9C:162AFE3:498D9F6:6A820DCC) and TestMergeTrainBisectionEjectsPoisoner (setup race; on re-run: bisection engaged, poison ejected, poison correctly not landed). A clean single-shot run is blocked on this host by the ThreadSanitizer fork/exec flake tracked in #1624. -
The simulated bed is now a mandatory, free pre-gate ahead of the live e2e suite, and covers
four further sequence-shaped engine behaviours; release cuts run the live integration suite
by default rather than on request (#1454, #1592, #1603).
Upgrading
# Auto-upgrade from a running Fabrik instance
# Fabrik checks for new releases each poll cycle and upgrades automatically with --auto-upgrade
# Or download directly (auto-detects OS and architecture)
gh release download --repo handarbeit/fabrik \
--pattern "fabrik_*_$(uname -s | tr A-Z a-z)_$(uname -m | sed 's/x86_64/amd64/' | sed 's/aarch64/arm64/').tar.gz" \
-O - | tar xz