Skip to content

Releases: handarbeit/fabrik

v0.0.89

Choose a tag to compare

@arbeithand arbeithand released this 22 Oct 00:24

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_by overseer action: add or remove an item's blocked-by edges, guarded against self-blocks, PR blockers and cycles (#2385)
  • max_stage_dispatches (default 25, 0 disables): 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 comment edited/deleted are applied directly (#2352)
  • issue_dependencies webhooks 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:editing lock 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_item webhooks are decoded from node_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 with fabrik:paused + fabrik:awaiting-input. Raise it with
    --max-stage-dispatches / FABRIK_MAX_STAGE_DISPATCHES / max_stage_dispatches, or set
    0 to 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, label and milestone to 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 xz

v0.0.88

Choose a tag to compare

@arbeithand arbeithand released this 18 Oct 15:39

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's repo_plugins. The repo must also name each plugin in its base-ref
    .pruefer/config.yaml plugins: key, and the PR must target the default branch.
    A plugin from another repository needs a repo_plugin_marketplaces allowlist 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, 0 disables) 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 xz

Engine

  • The pause guard (#2341, ADR 2341). Every automatic advance or landing decision on
    the stage path and in the merge train refuses a fabrik:paused item, 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_item webhooks 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-dir passed. The Skill grant 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 a Review skill fallback notice.
  • Repo plugins from standard declarations (#2366, ADR 2366). These are
    .claude-plugin/marketplace.json and .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>/head is 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 of engine/ 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 engine ce941763, and coverage was carried
    to 8a553693. The commits in between change only pruefer/, Pruefer docs, ADRs, specs,
    CLAUDE.md and two tests/e2e fixes (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 0 never invoked failures and 0 fallbacks. On #2111, there were no
    multi-member trials on the dev bed and no qualifying hit.

v0.0.87

Choose a tag to compare

@arbeithand arbeithand released this 15 Oct 12:59

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_SUBSCRIBER in 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 is fabrik_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 a claude -p script take over your interactive session's
    stream. Set FABRIK_SUBSCRIBER (or pass --subscriber) to receive pushed events.
  • If you register fabrik mcp yourself with --subscriber instead of using the plugin,
    disable the plugin's copy. The channel source then reads server:fabrik rather 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 xz

MCP and the fabrik plugin

  • Subscriber name resolution (#2322). fabrik mcp takes the first of: --subscriber,
    FABRIK_SUBSCRIBER, CLAUDE_TOPIC (exported by the topic session 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.json no longer
    sets FABRIK_SUBSCRIBER, and its userConfig no longer has subscriber.
  • A session whose default-named stream is taken over by another session is told how to give
    each session its own name. fabrik_subscribe and a pushed fabrik_situation say 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 in cmd/mcp.go, internal/mcpstdio, the PM
    plugin (plugin/fabrik, not embedded), docs and release metadata; nothing in engine/,
    the embedded workflow plugin or the gate changed, and neither the engine nor the gate
    runner depends on cmd or internal/mcpstdio.

v0.0.86

Choose a tag to compare

@arbeithand arbeithand released this 15 Oct 11:13

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.
    skipped and neutral still pass.
  • A diverged issue branch pauses (#2295). If someone pushes to fabrik/issue-N and 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 unknown for
    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 --allowedTools already
    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_DIR precedence (#2293, #2294). A bare CLAUDE_CONFIG_DIR inherited
    from the shell now ranks below .env and config.yaml. To override for one run, use
    --claude-config-dir or FABRIK_CLAUDE_CONFIG_DIR (Pruefer: -claude-config-dir or
    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 its origin is exactly github.com/<owner>/<repo> over https or ssh. A
    checkout whose remote uses an ~/.ssh/config host 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 to resume, 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 xz

Engine

  • Stale merge-train trials are rebuilt (#2298, reported in #2213). A trial PR that reads
    dirty is 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. A dirty trial 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 but blocked state 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; one merge failure comment 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 local fabrik/issue-N with 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-lease on 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 arming fabrik:awaiting-ci on 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
    unknown reading 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 --allowedTools it actually passed,
    never from prose. The pause comment names the path and gives three options. It is not a
    failure and does not touch max_retries. See ADR 2301.
  • The tools-denied exemption requires a causal denial (#1556). A run that hit any
    permission denial used to be exempt from max_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 or max_wall_time stop 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...
Read more

v0.0.85

Choose a tag to compare

@arbeithand arbeithand released this 15 Oct 00:06

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, or ctrl+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_time and the 15-minute inactivity timeout. To restart immediately, send SIGHUP
    again (or press ctrl+r again): the remaining workers are cancelled, the usual
    --drain-deadline drain 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 xz

Engine

  • 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, and FABRIK_STAGE_COMPLETE from
    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>:complete or failed,
    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_NEEDED now 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 by send SIGHUP again to restart now. The
    TUI title shows draining for restart (N left), and fabrik_health gains a draining
    object. The poll is reported as not overdue while draining, so no pipeline-stalled event
    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 own updatedAt (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-input is 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 only fabrik:paused, which left fabrik:awaiting-input on the item, and
    only a FABRIK_STAGE_COMPLETE run 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:paused is absent. A real pause is never lifted this way. Also, if removing
    fabrik:paused on 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-N was missing, Fabrik recreated it from origin/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 its fabrik: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
    logs warn: base mismatch, naming both. See ADR 1917.
  • +dirty dev builds rebuild, and every upgrade check logs its outcome (#2234). A dev
    build stamped dev(<sha>)+dirty was 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 reads waiting 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, the entwurf@fabrik removal, 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 a fabrik mcp row, and the docs site and FAQ drop the stale
    "no binary install" c...
Read more

v0.0.84

Choose a tag to compare

@arbeithand arbeithand released this 14 Oct 11:52

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:

  1. An App owner sets the permission at
    https://github.com/organizations/<owner>/settings/apps/<slug>/permissions.
  2. 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:

  1. Stop the daemon. fabrik upgrade does 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.
  2. Install v0.0.84 and run fabrik upgrade in 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 to fabrik-stages, unedited
      skills/fabrik-* directories from old releases are pruned, and stage skill keys are
      rewritten (fabrik-review and fabrik:fabrik-review both become
      fabrik-stages:review).
    • Customised .fabrik/plugin/: the whole tree moves to
      .fabrik/plugins/local-workflows/. Its manifest name becomes local-workflows, and a
      clean base plugin is restored. Stages whose skill you edited point at
      local-workflows:<old name>, and the rest at fabrik-stages:<skill>.
    • Your own plugin_dirs plugins: 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/*.yaml are edited. Comments and quoting
      are preserved.
  3. Review the diff and commit it.
  4. Run fabrik repair-board, then fabrik repair-board --apply, to add the three
    display fields. Otherwise each start shows a "Display fields missing" warnings-panel
    entry. Set project_fields.status_line / last_activity / last_run to off in
    .fabrik/config.yaml if you don't want them.
  5. Restart the daemon and check the warnings panel. Then unpause the items the stop
    paused by removing fabrik:paused and fabrik: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
    and FABRIK_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_train is 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 in merge_train_overlap_ignore.
    • The host can run max_concurrent + 1 Claude processes, because of the reserved train
      slot.
  • Spec persistence is opt-in for existing stage files (#2034). An existing
    specify.yaml lacks persist_spec: true. The drift warning names it, and fabrik refresh-stages --apply adds it.
  • Log files. Add .fabrik/fabrik.log.* to .gitignore (#2094).
  • PM plugin users: run /plugin marketplace update fabrik, /plugin install fabrik@fabrik and /reload-plugins. If you registered fabrik mcp by hand, remove that
    registration (claude mcp remove fabrik) or you will run two servers. entwurf@fabrik is
    no longer served from this marketplace; Entwurf has its own.
  • Pruefer operators: if claude authenticates through an environment variable (for
    example CLAUDE_CODE_OAUTH_TOKEN), name it in PRUEFER_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 xz

MCP overseer

To turn it on, install the PM plugin (/plugin marketplace add handarbeit/fabrik, then
`/plugin install ...

Read more

v0.0.83

Choose a tag to compare

@arbeithand arbeithand released this 01 Oct 07:37

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.Reconciler and 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 required is skipped (a leftover user: 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 become fabrik: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. Requires contents: 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 forward honours 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 forward refuses installation tokens entirely, the new event_source: hookdeck ingests 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 configs init just 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.
    --repo is 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 --repo the 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 --token is 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 of 10 × poll. Raising
    poll to 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 to poll, since those genuinely are about API cost.
  • fabrik:toolchain-stale (#1786) — workers inherit PATH from 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.mod and .tool-versions against the version actually on PATH, 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 upgrade and the TUI badge ([u] custom workflow (N stale)) now
    report both, and point at fabrik upgrade --reconcile. Upgrade refusal behaviour is
    unchanged.
  • fabrik:reworking (#1802) — while a stage is re-entered to process a steering
    comment, its stale stage:<Stage>:complete is now cleared and restored afterwards, with
    fabrik:reworking marking 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_cadence gate how often the bot reviews a given PR (#1610); review_guidance /
    review_guidance_mode let an...
Read more

v0.0.82

Choose a tag to compare

@arbeithand arbeithand released this 06 Sep 15:19

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 complete line. 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 after go test exits, 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;
    -parallel is capped on the repo-wide -race run 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,
    package pass (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 INVALID fired 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_REF unusable for any ref but main, 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 xz

v0.0.81

Choose a tag to compare

@arbeithand arbeithand released this 29 Aug 20:01

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
    by base:<branch> and an independent train runs per (repo, base) — its own pinned base
    SHA, trial branch, integration PR and guards. Members without a base: 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, with max_derived_repos and
    repo_rederivation_interval to 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 a base:-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 own go test children 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 xz

v0.0.80

Choose a tag to compare

@arbeithand arbeithand released this 17 Aug 00:01

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-onboarding plugin — 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 standard specs/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 COMPLETED with the work unmerged (#1615).
    findIntegrationPR matched 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
    COMPLETED was 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, honours base:<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-integration was 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