AshbyForge

Patch notes

What changed

109 feat · 45 fix · 22 refactor · 41 housekeeping — generated from the repository's own history · 2026-09-04 21:42

September 4, 2026

fixbench

the root plan gets a plan-sized envelope

99bb1e9c

The bench submitted its root with ad-hoc defaults (30 turns, 1800s) and the first real run timed out at exactly the wall: 7 turns, no tasks, $0.31. Specs may now set max_turns/timeout in frontmatter; defaults 40/3600.

fixrepos

registerRepoOnTheFly is idempotent for a same-name-same-path re-add

f6d902af

A re-add of an archived name (bench's minute-stamped repos) failed on the worker.toml duplicate before the provisional-row upsert could clear the archive flag; an exact name+path match now proceeds, a collision on a different path still refuses.

fixrepos

re-adding an archived repository name clears the archive flag

eb05e7b0

UpsertProvisionalRepository left archived=1 standing when a name came back (bench reuses minute-stamped names), so the fresh checkout never scheduled.

fixrepos

POST /api/v1/repositories upserts the provisional store row immediately

7cd04a83

The handler wrote worker.toml and waited for the worker's ≤30s refresh to create the store row, so a task submitted right after adding a repository — forge bench's exact shape — raced into 'not registered and could not be added (already in worker.toml)'. The daemon hook already returns the protocol.Repository for precisely this; use it.

featloop

the benchmark — forge bench runs a spec as a scored recursive build

940575ca

forge bench run <name> reads bench/specs/<name>.md (frontmatter size/model/ autonomy over the objective), creates a throwaway self-origin repo, registers it, and submits the objective as a plan-mode root (bench_name → submitted_by bench:<name>); the recursion does the rest, and the CLI polls the lineage rollup until the tree settles. forge bench list <name> reads GET /api/v1/bench/{name} — every run's score, cost, wall time, and weakness: the…

featloop

calibration surfaces — size buckets in stats, the per-ask rollup on lineage

2aa9d469

stats gains a Sizes table (S/M/L/unsized: runs, verified rate, cost per run and total, turns, p50, mean overall score — whether the sizing is honest) plus a ?size= filter; the stats page renders it. GET /work/{id}/lineage now carries the tree's rollup — works, attempts, cost, tokens, outcome census, wall seconds once settled, and the newest supervise assessment's scores and weakness: the ask's single answer-bearing view.

featloop

continuation Works — the return path of recursive decomposition

39081368

Every plan batch now ends in a supervise-mode continuation Work, blocked on:terminal on every member: it reviews what actually landed (the new forge_work_outcomes tool — per-work state, summary, cost, turns for the caller's batch, cross-tree reads refused) and either declares the goal met with 1-5 scores or emits corrective tasks, which fan out as the next round with a fresh continuation — bounded by [plan] supervise_rounds (default 3; the final round's…

featloop

size and scores become data — the learning loop's dimensions

4f40ded5

work.size (S|M|L, optional, frozen at creation): submitted on POST /api/v1/tasks or assigned per-task by the planning agent (planTask.Size finally lands instead of being decoded and dropped). attempt_facts gains root_work_id, size, workflow_name, and the five score_* columns; recordFacts copies the work dimensions and lifts 1-5 ratings out of the result envelope (top-level scores from run-mode judges, assessment.scores from the coming supervise mode).…

featscratch

promotion becomes a curation Work; the library becomes a repo; the flow pipeline joins the base

00342cba

Scratch promotion no longer copies a synthesized file inline. Crossing the threshold queues a high-priority Work (priority 80, cause=promotion, submitted_by=forge:scratch, autonomy auto, integrate-on-green) running the new promote-scratch base directive against the library repository itself: the curator dedupes (an existing script that already covers the functionality gets sharper metadata instead of a twin), extends near-miss scripts, and applies a…

featscratch

the organic script layer — agent scripts earn their way into the library

3fb8d74f

forge_scratch: an agent that needs a quick script saves-and-runs it through the daemon instead of a worktree throwaway. The cache is an LRU-bounded SQLite table ([scratch] max, default 200, evicted by last use); every run counts, with distinct calling attempts remembered; scratch scripts join forge_library / the search API as kind "scratch" so the cheap path is search-and-reuse-by-name rather than regenerate — which is also what makes the counters honest.…

featui

Routines get their own page; the library tree collapses

ef0a2b10

Routines move off the Directives page entirely: /routines is a table of triggers — name, linked target chip, objective, schedule, class, repos, Run/Edit — with the trigger dialog (target datalist now server-rendered with directives, scripts, and workflows). The Directives page is purely the library: collapsible personas/directives/scripts/fragments sections (remembered per folder in localStorage, auto-expanded while filtering) and the filter box. Nav…

featdirectives

scripts in any language — .js is the sandboxed default

906e3687

The JS-only rule was an artifact of goja being the embedded engine, not a design principle. scripts/<name>.<ext> now accepts any language: .js keeps running in-process in the goja sandbox (no filesystem/network — what the UI scaffolds by default), and any other extension runs as a daemon subprocess — interpreter from the shebang or the extension map (.py/.sh/.rb/.pl; neither = load error), JSON ScriptInput on stdin, one JSON value on stdout, PATH-only…

fixcli

directives update merges upstream/HEAD, grafting the first sync

64896db8

FETCH_HEAD is marked not-for-merge when the local branch name differs from upstream's (git-init'd libraries sit on master, the base repo on main), so `git merge FETCH_HEAD` silently reported "already up to date". The command now resolves and merges upstream/HEAD explicitly, and the first sync of a bootstrapped library (no shared ancestor) retries with --allow-unrelated-histories — verified against production, where the graft landed the base metadata with…

featbase

starter scripts, descriptions, and tool flags (bridge P6a)

ea561630

Bumps forge-library to 179dd30: scripts/ gains summarize-steps (workflow glue) and pick-largest (a tool: true pure-compute example); every base directive carries description: for search, the read-only ones tool: true; the plan-build-review seed workflow becomes callable and described. Existing installs pick all of it up through Ensure's additive sync on the next boot.

feattools

the tool/skill bridge — forge_library, script/directive/workflow run (bridge P5)

4026fa19

Four static bridge tools with call-time dynamism (the registry freezes at NewServer and each attempt's MCP list is fetched once, so per-item tools would fight the architecture):

featlibrary

search & discovery — one ranking for UI, CLI, and agents (bridge P4)

77cb11da

Library.Search is in-memory term scoring over the hot-swapped tree (tens of files; the kb's FTS5 is for thousands of growing rows): all terms must match, name (3) > description (2) > body (1), ties by name, empty query browses. GET /api/v1/library/search merges workflow rows under the same scoring — one ranking behind the Directives page's new tree filter, `forge directives search`, and the forge_library agent tool (next phase).

featworkflows

named library scripts + the script: trigger target (bridge P3)

c59fcef4

A script node may now reference the library by name — {"script": "<name>", "params": {...}} — instead of pasting source (exactly one of the two). Resolution happens against the LIVE library at execution, the same freshness rule directive nodes have at materialization: a script deleted mid-run fails its node cleanly and the skip cascade handles the rest. Node params ride into input.params (flow.ScriptInput gains the channel); the header timeout applies…

featworkflows

description + tool metadata on the row (bridge P2)

21c7c3d1

Workflows are the one definition that lives in SQLite, so their search/tool metadata lives on the row: a one-line description and a tool flag (agents may fire tool-flagged workflows via forge_workflow_run, coming in P5). Both ride the generation snapshot, ship via seeds JSON, edit in the graph editor's settings panel, and show on the list.

featdirectives

scripts join the library — /**forge header metadata (bridge P1)

a17e8215

scripts/<name>.js is a fourth library kind: goja JavaScript defining main(input), goja-compiled at load (a syntax error refuses the load, last-good retained), with an optional /**forge comment header parsed with frontmatter strictness — description, a multi-line-capable JSON input schema, timeout_ms (clamped), and tool: true (which requires description + schema: a callable an agent cannot understand is a mistake, not a tool). The header is a legal JS…

refactor

remove the legacy dual-mode machinery — target-only, directive-only

155ed6ef

Pre-stability there is no install base to carry compatibility for, so the transitional layer goes:

featbase

the base library becomes a forkable upstream (directives P9)

0352a805

The starter tree moves to a standalone repo (github.com/Ronin11/forge-library, private) mounted as a git submodule at internal/core/directives/starter — it versions and updates independently of forge while go:embed still ships it in the binary (Justfile gains the submodule-init guard). The base grows eight role directives (plan-project, design-approach, implement-task, review-change, qa-feature, walk-as-user, write-docs, audit-security) built on the…

featui,api

the Prompts surface becomes Directives (directives P8b)

e1f5c27c

Routes: /api/v1/prompts* → /api/v1/directives*, /api/v1/prompt-test(s) → /api/v1/directive-test(s) (/api/v1/personas* stays). The page moves to /directives with a permanent redirect from /routines (query preserved, so old deep links keep working), nav says Directives, and the page assets rename (directives.html, directives.js; window.ForgeDirectives with the old hook name aliased). Routines remain a tree section on the page — thin trigger shells linking…

refactor

internal/core/prompts becomes internal/core/directives (directives P8a)

a1099dc4

Mechanical: git mv + package rename + importer qualifiers. Exported names (Library, Fragment, Composition) unchanged.

fixmigrate

dry run predicts graph conversions from the would-split set

0268adbb

Dry mode leaves rows unsplit, so the graph step's target predicate saw only the present and reported every node as unconvertible. The would-split set now stands in, making the dry report match what the wet run does — verified against a production snapshot: 1 rename, 29 splits, 4 graph conversions, 0 skips, byte-identical bodies, no-op second run.

featmigrate

the directives restructure migration (directives P7)

c8dc4571

migratedirectives.Run executes the one-time restructure with state-derived idempotence (no flags — every step's predicate is a state check, so the daemon runs it on every boot and "once" is emergent): (1) ~/.forge/prompts renames to ~/.forge/directives when the configured path is the new default (atomic; .git and history ride along; custom paths untouched; both-exist prefers directives and warns); (2) every content routine splits into a byte-deterministic…

featweb

the self-improvement loop follows content into git (directives P6)

7d6eebd3

A routine proposal against a directive-target routine writes its prompt/ model/effort updates to directives/<name>.md through the same write → validate-whole-library → revert-on-failure → commit path a UI edit takes (prompts.RewriteDirective preserves frontmatter shape and key order); operational updates still bump the row. Content proposals carry a directive: applied_ref, so the A/B auto-revert sweep naturally excludes them — content A/B rides git…

featui,cli

authoring surfaces speak targets (directives P5)

dd95ae34

The routine dialog gains Target (directive/workflow datalist) + Objective; setting a target disables and clears the content fields — the directive owns them — and collect never sends stale copies. The library tree gets a directives/ section; a directive detail shows its composed body, manifest, byte-exact preview, and a real-model run panel recording under directive:<name>. A trigger routine's detail names its target (linked), shows the default objective,…

featworkflows

directive nodes — graphs compose library content (directives P4)

9c75b6a7

A directive node runs a Work built from a directives-library file: content (mode/model/persona/task) from the library at materialization time, binding (repositories/objective/persona override) plus an operational envelope (timeout, max_turns, budget class, model) from the node config. The engine treats routine and directive nodes as the same queue-backed kind; frozen run graphs with routine nodes validate, execute, and render forever — they are never…

featweb

materializeRoutine — one compose seam; target routines run (directives P3)

050ebefb

materializeRoutine is now the single place executable content comes together: directive resolution → per-call overrides → persona composition → {{objective}} injection, with precedence per-call > directive frontmatter > persona default (model) > routine row objective. createWorkTx, renderPreview, and the experiments' variant previews all route through it, replacing three mirrored compose paths — a preview is byte-faithful to a run by construction, and the…

featprompts

directives/ — executable task content in the library (directives P2)

eded2038

A directive is a library file under directives/: frontmatter names its mode (required) and optionally a persona, model, and effort; the body is the task text, with {{> fragment}} includes expanded and {{objective}}/{{repo}}/ {{run.*}} left for the downstream substitutions. Persona references check at resolve time, not load time, so one bad reference cannot brick the library. WithVariant re-validates directives the way it does personas, which the…

featstore

routines gain target + objective — the trigger-shell shape (directives P1)

9b940769

A routine is now exactly one of two shapes: a trigger (target = directive:<name> | workflow:<name>, content fields empty) or a legacy content routine (today's rules verbatim). Content fields stay on the struct because frozen work snapshots and routine_generations history decode through it; only stored rows go content-less, in a later phase. Objective is the trigger's default {{objective}}.

featworkflows

expected cost per run on the workflows list

11c2c31d

GET /api/v1/workflows/{name}/estimate prices each routine node from the same 30-day attempt-facts history the router uses: p50 notional USD for the routine on its model, model-wide facts as fallback, and an honest "none" when nothing is known — no made-up numbers. The list page shows the sum per workflow ("≈ $0.42", "(partial)" when only some nodes have history, "†" when branching or loops mean the real run can differ), with the per-node breakdown in the…

featprompts

expected-cost line on the optimize panel; fable model alias

57aa7a3c

The panel now prices an experiment before Start: list prices ($/MTok, newly exposed on /api/v1/personas) times a rough token model — the real composed prompt's chars/4 as input per run, ~700 output tokens per run, plus the generation call (which writes every variant in full, the dominant optimizer cost) and the judging call. Live-updates as models and variant count change; the tooltip states the assumptions.

featprompts

LLM-assisted optimization experiments for personas and routines

45b48169

An experiment hands a prompt to an optimizer model with a stated goal, gets back up to 12 candidate rewrites, runs each (plus the untouched baseline) on the target model with the tester's inputs, and has the optimizer judge the outputs blind — labels shuffled, baseline unmarked. Results come back ranked with scores and rationales; a winning variant is applied through the same validated save path as a hand edit, never automatically. Targeting a smaller…

featui

deep links to prompts — selection and composer mode in the URL

a8f40124

/routines?sel=prompt:<name>|routine:<name>[&mode=<mode>] selects on load and preselects the composer's mode section. Tree items are real anchors (middle- click and copy-link work), clicks pushState so back/forward walk your selection history, and the composer's mode picker keeps the URL in step via replaceState. Cross-links land where personas are referenced: composition manifests (composer and routine preview) link each fragment name, a routine's persona…

featprompts

remember test runs per prompt — the iterate loop's memory

9584bd46

Every prompt-test run is recorded against its subject (persona:<name> or routine:<name>): the inputs (mode, task, objective, repo), the chosen model, the composed prompt, the composition manifest, the output, and timing — trimmed to the newest 20 per subject, a scratchpad not an archive. GET /api/v1/prompt-tests?subject=… serves them newest first.

featui

run prompt tests against a real model, with configurable sizing

b792be90

POST /api/v1/prompt-test builds the preview — a saved routine or a synthetic persona+mode+task — and runs the composed prompt through the concierge's model seam (headless claude --print): one completion at a chosen model alias, no worktree, no tools, no task, no budget machinery. The persona and routine testers on the Prompts page gain a model picker (aliases from the daemon, the persona/routine default preselected) and a Run test button showing the…

featui

edit and test prompts in place on the Prompts page

f5a2c8a7

Editing: the detail pane's Edit swaps the source for a textarea and PUT /api/v1/prompts/{name...} saves it — the write is validated by reloading the whole tree (an edit that breaks composition is reverted and refused with the loader's reason, so the library on disk is never left broken), committed best-effort to the library repo, and hot-reloaded through a new PromptsReload seam so the page composes the new version immediately instead of waiting for the…

featui

the Routines page becomes Prompts — library tree, composition previews, prompt testing

59be7370

The page now mirrors the folder structure it manages: a server-rendered tree of personas/, fragments/ (nested dirs grouped), and routines/, with a detail pane driven by prompts.js. A fragment shows its source and disk path (git owns authoring); a persona adds a Composed pane — pick a mode, see the exact expansion and its manifest (fragments + library commit). A routine shows its binding, opens the existing edit dialog (via a ForgeRoutines hook), runs, and…

feattools

forge_kb_new gains repo targeting — learnings versioned with their repo

0ff18d79

The read path already merged every registered repository's .forge/notes into the kb index; now the write path can put them there: forge_kb_new with repo=<name> resolves the registered checkout and writes the note under its .forge/notes (created on demand), indexed immediately and journaled with the repo. An unregistered repo is a 400. TODO gains a P2 for exploring repo-specific prompt fragments — noted messy because composition is per-Work while {{repo}}…

featplugins

teams and email channel plugins

f300a445

Two first-party events+intake plugins over the wire contract: teams posts question/failure/budget/proposal cards through an incoming webhook and optionally polls the channel via Microsoft Graph for /answer, /help, and concierge messages; email does the same over SMTP/IMAP. Docs in PLUGINS.md, build targets in the Justfile. (Authored in-tree by concurrent sessions; committed here with the suite green.)

fixstore

ResetMergeAttempts — a human requeue grants a fresh rebase budget

76ae2137

The call site landed with the workflow-revamp commits; this adds the store half (definition + journal + test) so a target whose rebase attempts were exhausted no longer bounces from requeue straight back to conflict.

featprompts

starter library — nine standard roles from shared fragments

daae394d

Shipped via go:embed and written on fresh bootstrap, then owned by the user's git history: architect (opus), implementer, senior-reviewer, qa-engineer, debugger, docs-writer, maintainer, triager (haiku), and security-reviewer (opus), each a short opinionated identity with mode sections for its natural modes (plan, implement/greenfield, review/audit, verify, explore, docs, maintain/curate, intake). Six shared fragments carry the common ground —…

+ housekeeping: 6 chore, 2 docs, 1 test

September 3, 2026

featpersonas

git-backed prompt library composed into routines at run creation

94a0c547

The judgment half of a prompt moves out of routine rows into a git-versioned Markdown tree (~/.forge/prompts): personas/<name>.md are identities routines name via a new persona field, fragments/<name>.md are building blocks composed with {{> name}} and {{> name key="value"}} parameters. Personas may carry a default model (the routine's own wins) and '## mode:' sections composed only into matching runs. The language is includes and parameters only — no…

+ housekeeping: 1 docs

September 2, 2026

featui

drag-and-drop workflow graph editor, run view, and AI drafting

508f35ea

A hand-rolled SVG editor (static/graph.js, no framework, no build step, all colors from theme tokens): palette drag-in of routine/script/switch/join nodes, node dragging with snap and a positions-only PATCH, port-to-port edge drawing (green success / red failure / purple case) with automatic loop declaration when a connection closes a cycle, per-type config panels, client lint mirroring server validation, snapshot undo/redo, Sugiyama-lite auto-layout for…

featworkflow

wire cron scheduling and retry-from-node

6a03a21b

The schedule columns finally do something: a 30s daemon loop backfills next_due_at for enabled schedules, fires due routines (one Work, trigger schedule) and workflows (one engine run), skips-if-still-running with a journaled schedule.skipped, and advances next_due_at in the admission transaction. Missed occurrences fire once, then jump forward. Cron strings are validated at save (robfig/cron parser only — the daemon loop is the runner), closing the…

featworkflow

run engine — first-class runs, stepwise materialization, JS steps, switches, loops

d436b2ce

Runs become rows (graph frozen at creation) driven by a reconciliation engine: flow.Evaluate is the pure half (token passing over node instances — readiness, skip cascade, join all/any, capped loop re-entry, switch case routing, run-status aggregation) and the web driver applies its diff in one CAS-guarded transaction, so advancing is idempotent and restarts just re-evaluate. Script and switch nodes run embedded JavaScript in a goja sandbox (no host…

featworkflow

graph data model — typed nodes, conditional edges, lossless steps migration

53035600

Workflows become a canonical graph: routine/script/switch/join nodes joined by success/failure/always/case edges, with declared loop back-edges carrying iteration caps. Legacy steps bodies are accepted forever and converted losslessly (GraphFromSteps); rows are backfilled at open, and pre-backfill rows convert on read. The static run path instantiates routine-only graphs topologically; graphs needing runtime evaluation are refused until the run engine…

+ housekeeping: 1 misc

September 1, 2026

featcli

task add --max-turns and --timeout

f35f223a

The API has always accepted per-task budget overrides (workRequest max_turns/timeout_seconds) but the CLI never exposed them, so every CLI ad-hoc submission silently got the 30-turn default — tonight's implement batch died on the budget cliff during its own bootstrap. The overnight scripts avoided this only by posting to the API directly.

fixconfig

cache-write prices at the 1-hour TTL rate

c4f564b2

The claude-code executor caches at the 1h TTL (2× input per MTok written); the embedded table priced writes at the 5m rate (1.25×), which is the systematic notional-vs-reported drift doctor has been warning about on cache-heavy attempts.

featui

live app chip and app link on the Repos list (P2)

bbe1d280

The list chip was task health only — ten apps could be running while every row said idle, and App ↗ used the static app_url that goes stale when a restart leases a new port. The UI gains an appStatus seam (wired from the run supervisor like SetPluginHealth); each non-archived row shows an app state pill mapped onto the existing palette, and the App link prefers the supervisor's live URL over app_url.

fixworker

retained worktrees detach HEAD so their branches stay usable

f0b113c6

A linked worktree pins its checked-out branch; retained worktrees held forge/ad-hoc-* refs long after their attempts finished, blocking checkout of those branches in the registered repos (seen across the greenfield batch). Retention now detaches the worktree's HEAD — same commit, dirty files untouched, ref freed. Resumable (waiting_human) worktrees go through Keep and still hold their branch for the resume.

featworker

a max-turns cliff with commits lands as a reviewable partial

c2dc6841

The executor dying on its own --max-turns used to bury finished work as failed (the TUI attempt died at 101/100 with three commits retained; e2e at 71/70). The worker now reports a cliff exit as budget_exceeded when turns reached the effective budget (grants included), and store.Complete routes a budget_exceeded failure that left commits through verifying → unverified (budget_cliff, retained) so the branch surfaces for review or retry; with nothing…

fixworker

errcheck findings in the merged overnight branches

7af163f2

The overnight fixes ran a lighter gate: greenfield's flatten now notes an unremovable shell directory on the attempt's lifecycle stream instead of dropping the error, and the skip-bad-repo test's deferred Close reports. Import paths in the new runner_test follow the core move.

refactortui

form the tui tree from the CLI's client half (Stage 6)

17f07477

cli_client.go and the nineteen operator command files (plus plugin_omarchy, which imports no Forge package and is used only by cmd_plugin) become package tui; cmd/forge keeps main's dispatch, the daemon/worker/MCP hosts, fake-claude, and the run supervisor. The shared plumbing is tui's exported surface: Context (fields plus Flags/Parse/Logger/Fail/PrintJSON/ ResolveLogging/Client), the client's Connect/Do, TailFile (shared with forge daemon logs), and one…

refactorui

split internal/web/ui into its own package (Stage 5b)

5a9430a1

ui.go, ui_kb.go, ui_actions.go and the four ui tests move in beside the templates and static/ they embed (the embed paths lose their ui/ prefix). Direction: ui imports web — a legal web→web edge — rather than web importing ui, so the shared read-path surface is exported in place instead of moved: RepositoryStates, BuildRepoDetail, RepoDetail, ComputeLineage, LineageData (ByID), ParseSince, RPCKnown; AttentionDeadline, shared by the sweep and the Human…

refactorweb

rename internal/controlplane to internal/web (Stage 5a)

0e924fa1

git mv + package rename; cmd/forge, the boundary rules, and smoke-m11.sh follow. The Engine stays embedded in Server — the plan's named eng field only pays off when Engine leaves the package, which this rename does not do.

refactortools

form the tools tree (Stage 4)

7f1bea59

internal/mcpserve → internal/tools/mcpserve, and controlplane's plugintools.go (+test) → internal/tools/pluginbridge — it imports exactly one Forge package, internal/tools, and moves with only its package clause and cmd_daemon's three call sites touched. internal/tools itself was already the tools-module root per the package map (§4); the supervision seam only added to it.

refactordaemon

cut internal/core/daemon out of controlplane (Stage 3)

6c55911b

daemon.go + bootstrap.go become package daemon: the lock, daemon.json, the well-known FORGE_HOME file names, the restart env keys, and bootstrap. controlplane's health/restart surfaces and cmd/forge qualify. drain.go's non-HTTP half (markStateDraining, waitInflightIdle) stays behind for now: both are methods on Server's drain state, which §5 keeps until the type split deepens.

refactorengine

cut internal/core/engine out of controlplane (Stage 3)

4dbc8544

budget, scheduler, router, queue, prune (+ their test pairs and scheduler_m9_test) become package engine; controlplane and cmd/forge qualify the moved identifiers. Exports the cut demanded: BudgetResets/BudgetReset, InQuietHours, SeedFromID, TargetStates. TestJournalBudgetResets moves the other way — it drives journalBudgetResets through NewServer, so it lands in handlers_usage_test.go with small copies of the budget test helpers.

refactorconfig

cut internal/core/config out of controlplane (Stage 3)

31168721

config.go + config_models.go (and their tests) become package config; controlplane and cmd/forge qualify the moved identifiers. Exports the plan predicted: AutoDecideOn, EnabledOn, Decider (methods on moved config types that controlplane calls). Weights.Score becomes router.go's weightScore free function — the type lives in core/config, the routing arithmetic stays with the router. route_claim_test duplicates the tiny loadConfigFrom helper (test helpers…

refactorcontrolplane

split Engine out of Server, same package (Stage 2)

81d5c9d2

Engine owns the decision/orchestration state — store, policy, config, the injected func seams, the assistant/attention/supervision/auto-eval state. Server keeps the HTTP surface: transport, auth token, mux, drain state, the inflight counter, exec-restart, and the SSE stream plumbing.

fixtests

relative fixture/case paths follow the core move (Stage 1)

17237831

Four tests resolved testdata/, evals/ and .scratch/ via ../.. from the old package depth; internal/core adds a level. Surfaced by the full race gate after the moves landed.

refactorcore

move internal/modes to internal/core/modes (Stage 1)

f547746f

go:generate directive updated and the registry regenerated in the same commit; the generated file's imports are the only regeneration diff.

refactorcore

move internal/integrator to internal/core/integrator (Stage 1)

8b7988d6

smoke-m9.sh's literal paths follow.

refactorcore

move internal/eval to internal/core/eval (Stage 1)

9b9e08b4

test-integration recipe path follows.

refactorcore

move internal/doctor to internal/core/doctor (Stage 1)

59f7ccee
refactorcore

move internal/stats to internal/core/stats (Stage 1)

96a90981
refactorcore

move internal/plugin to internal/core/plugin (Stage 1)

858f2b22
refactorcore

move internal/worker to internal/core/worker (Stage 1)

4bc9de8a

Boundary rule path follows.

refactorcore

move internal/store to internal/core/store (Stage 1)

b2efc38a

The worker boundary rule's deny pattern follows the store to its new path.

refactorcore

move internal/kb to internal/core/kb (Stage 1)

4cace44e
refactorcore

move internal/logging to internal/core/logging (Stage 1)

85addd8f
refactorcore

move internal/protocol to internal/core/protocol (Stage 1)

80368b43
refactorcore

move internal/model to internal/core/model (Stage 1)

43d0ef7e

git mv plus import rewrites; boundary rule paths updated (MODULARIZATION.md §8 Stage 1).

featboundary

fail-closed import linter with the module rule table (Stage 0)

e1d9408a

just boundary's rules were shell loops over `go list <path> 2>/dev/null`: a moved path made the loop body vanish and the recipe still printed ok — the exact trap the modularization's git mv stages would walk into (MODULARIZATION.md §7). Rules now go through one check() helper that fails when a rule's package set is empty, and the recipe expresses §3's tree table on today's layout: controlplane is web (ceiling, presence-asserted), every other internal tree…

fixmodes

verify prompt — restore side-effect dirt before returning

4d8c8bfe

Verifying a Node subject runs npm install, which rewrites package-lock.json; L0's write-scope-none gate then failed the verify target with l0:changes_mismatch, discarding passing verdicts (3 of 5 overnight re-verifies). The preamble now tells the verifier to git checkout / git clean anything its checks dirtied.

featverify

forge task reverify — re-check unverified work without re-running it

2ad50248

A failed verify attempt used to dead-end its subject: the verdict only applies while the subject is verifying, approve requires verifying too, and the only edge out of unverified was a full re-run via task retry. When the verifier (not the subject) is what failed, that re-run wastes the completed work.

fixverify

raise the verify budget to 60 turns / 2400s

a27f8548

Real subjects (a full just check, Playwright runs) exhausted 30 turns and died on the executor's --max-turns cliff at exactly 31 — below where supervision's soft-budget nudge (~80% of soft_turns, default 80) can ever fire — landing their subjects unverified(verify_attempt_failed).

fixservice

bake the installing shell's PATH into the systemd user units

432f9cb7

Units started at boot get systemd's stock PATH (/usr/local/bin:/usr/bin), which misses version managers like mise — after a reboot the worker could not find the claude-code executor until the units were restarted from a login shell. forge service install now writes Environment=PATH= from the installing shell.

fixgreenfield

account for .git when checking for flatten

fb9f232d

The flatten check needs to ignore hidden directories like .git when determining if the nested slug directory is the only content.

fixgreenfield

flatten double-nested projects in greenfieldMove

2540fc2f

When agents build in a subdirectory named after the project instead of at the worktree root, greenfieldMove would move the entire worktree, resulting in <projects_root>/<slug>/<slug>/ with a stray forge.toml.

+ housekeeping: 2 docs

August 31, 2026

fixworker

skip invalid repos instead of refusing to start

6d283d17

The worker now skips repositories that fail validation (e.g., missing origin remote) and logs a warning, rather than refusing to start entirely. This allows the worker to start with the valid repositories as long as at least one is valid. If all repositories fail validation and there is no greenfield projects_root, the worker still fails with a clear error message.

fixworker

an empty marker sweeps nothing

74cb46ed

The one place that rule belongs is Sweep, not the callers.

fixworker

a declared check no longer waits for, or leaks, its background processes

11433678

CombinedOutput hands the check's stdout to everything the check starts, so a build that backgrounds a preview server made Forge wait for the server, not for the check — until the check timeout killed the group ten minutes later. The output now goes to a pipe the worker owns, so the check's exit is the check's exit, and the process group is killed as soon as it exits, so a server the build left behind dies with it. The tail is kept by a bounded writer…

fixworker

kill everything an attempt spawned when it ends

d9088768

The launch's process group was the only thing an attempt killed, and only when it was cancelled, timed out, or lost its lease — a natural exit left the group alone, and nothing at all could reach a descendant that had left it. A headless chromium from an L2 browser check and a preview server a build backgrounded therefore outlived their attempt, and the worker's unit, which journald reports as "Unit process N (chromium) remains running after unit stopped".

featworker

find an attempt's escaped processes by environment marker

5e948e25

A process group only holds the descendants that stay in it. A headless chromium calls setsid, and a preview server a build backgrounded is handed to init when its shell exits — both outlive the attempt that started them, and neither can be reached through the launch's pgid.

featsupervision

supervisor-initiated watchdog (Phase B)

0e7f51d2

A sibling sweep to RunAttention (~45s) reads each running attempt's live signals and adjudicates any that trip a trigger: silence (no ingested event for silence_minutes), spin (one tool+args signature dominating the window with zero artifact growth), or a cliff (near the hard ceiling). It reuses the Phase A adjudicator and recordDecision, and delivers the one-time proactive nudge once an attempt crosses ~80% of soft_turns.

fixverify

exempt greenfield from L0.2 changes exactness

feb59777

A new_project build writes hundreds of files and reports a coarse changes[] summary, so exact set equality with git's changed paths measured the summary rather than the work: every greenfield attempt false-failed as unverified=l0:changes_mismatch despite passing its declared checks. Under WritesNewProject both diffs are still recorded in the verdict (alongside l0_changes_exact:false) but no longer fail; greenfield is an L2 mode, verified through its…

featsupervision

child-initiated budget negotiation (Phase A)

36328b5b

Turn the max_turns cliff into governed negotiation. A working agent nearing a soft limit calls forge_request_budget(dimension, amount, reason) instead of dying; one adjudicator decides against its live progress and the FULL extension ledger on a cheapest-first policy ladder — deterministic grants and the unambiguous kills (over the hard ceiling; diminishing-returns-by-ledger; clear spin), with the ambiguous middle escalating to the opus decider (the…

featprogress

surface a running attempt's live turn/token tally

86d64a7b

When an attempt is RUNNING, `forge task show` printed `turns 0 tokens 0/0` because turns/tokens are only populated at completion — a healthy-but-early agent looked identical to a wedged one. The daemon already ingests the signals (events, heartbeats, forge_note_progress); this surfaces them.

featui

Human Queue countdown + decided-by-Forge marker

37664c6c

The Human Queue shows each non-critical question's criticality and a countdown to auto-decision ("Forge decides in ~1h40m", soonest deadline first) and marks critical items "needs you" with no countdown. The task page and queue mark an auto-answered question "decided by Forge (opus)" with its rationale, visually distinct from a human answer. Reuses the card/chip CSS and adds an `until` template helper; the SLA is wired into the UI via SetAttention.

featcontrolplane

time-aware attention sweep auto-decides non-critical questions

d0bf3c9e

A new [attention] config block (auto_decide default true, wait_active_minutes 240, wait_quiet_minutes 20, model opus) and a sibling sweep loop (RunAttention, every 60s in the daemon errgroup). Each tick reads the open questions fresh and, for a non-critical one past its deadline (asked_at + a wait that shortens during [budget.quiet_hours]; low burns down at the quiet wait even in active hours), calls the decider via the injected modelCall with the…

feattools

criticality param on forge_ask

dc14a733

Agents declare a question's criticality (critical|normal|low, default normal); critical always blocks for a human, everything else may be auto-decided after the time-of-day wait. Validated against the model enum before CreateQuestion.

featstore

question criticality + audited auto-answer path

dfa34e3a

A Question now carries a criticality (critical|normal|low, model.Criticality, agent-declared, default normal) and a rationale, added by a new migration that backfills existing rows to normal. CreateQuestion validates the enum and threads it from protocol.QuestionRequest. AutoAnswerQuestion records the attention sweep's decision through the shared answer core: answer, answered_by "auto:<model>", rationale, and a distinct question.auto_answered journal…

feat

concierge — an LLM front door for inbound messages

36f42a31

One endpoint (POST /api/v1/assistant/message) interprets a message with a cheap model into one action — create a task, report status, or reply — and executes it directly (no confirm gate; the budget policy is the backstop, per "err on the side of action"). Lean: a single model call per message, a small action vocabulary, an in-memory per-sender session for context. The daemon injects the model-call primitive (headless claude); the controlplane owns the…

fixplugin/signal

bound + persist the question→reply map; reply is never a task

49b38aa9

The in-memory map grew without limit (entries only cleared on a matching reply) and was lost on restart, so a reply to a pre-restart question silently filed a task. Now: the map is bounded (oldest evicted past a cap), persisted to pending.json so replies survive a restart, and a reply whose quote matches nothing gets a clear "that question isn't open" instead of becoming a stray task — a reply always means "answer", never new work.

featplugin/signal

answer a question by replying to its notification

ba0ca609

Typing /answer <id> was a pain. Now signal-cli's send timestamp is captured when a question notification goes out and mapped to that question; when a reply quotes the message (signal-cli surfaces quote.id), the reply text answers the question directly — no id to type. /answer <id> still works as a fallback, and a plain (non-reply) message still files a task.

featui

mobile nav — hamburger menu with a vertical popout

866d53a3

The top bar wrapped into an ugly multi-row block on narrow screens. Below 900px (where the eleven links stop fitting) the nav now collapses to a hamburger that toggles a vertical link popout — closing on selection, Escape, or an outside click — while desktop keeps the inline bar. Adds an i-menu glyph. Also notes Tailscale in NOTES.md as the future external-access path.

fixplugin/signal

run signal-cli in a stable working directory

7c7558cc

The GraalVM-native signal-cli aborts with "could not determine current working directory" when it inherits a CWD that has been unlinked — which happens when a plugin reinstall swaps the plugin dir out from under the running process. Pin cmd.Dir to the user home so send/receive are immune.

fix

reap plugins across exec-restart; keep plugin config on reinstall

a0cc4955

Two related plugin-lifecycle bugs that combined to wedge a restart with "address already in use":

fixplugin/signal

serialize signal-cli calls; cap receive window

f4be6141

signal-cli holds an exclusive lock on the account store, so a send that lands during a blocking receive waits (or, worst case, contends). Guard every invocation with a mutex and cap the inbound receive wait at 3s so a queued outbound notification is never blocked long behind a poll.

featplugin

signal — bidirectional Signal bridge

95c54216

A new plugin that notifies you over Signal (new questions, target failures, proposals) and turns your Signal messages into work: a bare message files a task, /answer <id> <text> resolves a waiting question, /status summarizes the queue. Combines the notify plugin's journal-consumption (outbound) with the github-issues plugin's intake (inbound), shelling out to signal-cli for I/O — no library, no daemon; the account is registered/linked out of band.

feat

role-pipeline flows — routines, workflows, setup script

65b87699

scripts/setup-role-flows.py provisions the "route work through roles" experiment on the daemon: eight role routines (r-triage front door, r-challenge "but why though?" gate, r-pm, r-architect, r-programmer, r-reviewer, r-qa, r-eval) and four flows of increasing depth/order — flow-quick, flow-standard, flow-deep, flow-design-first. Every step stacks on the previous so each role reads the prior role's committed deliverable under flow/; every flow gates on…

featworkflow

thread a run objective into steps via {{objective}}

8ab8aff6

Workflow and routine runs now accept an `objective` that is substituted for a {{objective}} placeholder in the routine prompt at work creation (per-work, the way {{repo}} is per-repo at claim). An empty objective leaves a self-directed fallback, so a flow works whether or not a specific objective is supplied — the foundation for routing a specific request through a role pipeline.

fixrepo

archive tolerates an already-deleted checkout

f4f90746

safeRemoveCheckout errored when the checkout directory was already gone, so a repository whose checkout had been removed by hand could never be archived (archive → stat checkout: no such file → 400). Treat a missing checkout as a no-op — there is nothing to delete — so archival stays the daemon-authored way to retire such a repository.

feat

role-based routine templates

898405f5

A built-in library of routine starting points for common working roles — Product Manager (plan), Architect (explore), Programmer (implement), Reviewer (review), QA Tester (audit), End User (explore), Tech Writer (docs), Security Auditor (audit). Each pairs the right execution mode with a role-framed prompt scaffold (a bracketed objective placeholder to replace) and sensible model/class defaults.

featbudget

forecast_pacing defaults off

12bdd5e0

Make the relaxed admission the global default: forecast_pacing now defaults false, so normal/backlog work is gated by current utilization alone and a transient burn-rate spike no longer walls off queuing (the hard stop still protects the ceiling). Set forecast_pacing = true to restore burn-rate pacing.

featbudget

forecast_pacing knob to relax admission on burn-rate spikes

4572abd7

The forecast_over_target admission rule defers normal/backlog work when the recent 1h burn rate projected to the window reset would overshoot the target. Near the end of a session a transient spike extrapolates across the whole window and walls off queuing new work even while current utilization has headroom.

featui

provenance strip and the /work tree view

9048b203

Task detail gains a breadcrumb (root → parent), a backward cause/deps row and a forward children/blocked row, and a View-work link — all from one lineage lookup, and nothing at all for a lone Work. New /work/{id} view canonicalizes any member id to its root and renders the whole tree (collapsible subtrees) with a rollup header: cost, tokens, attempts, files, and derived state counts. Task and queue rows link non-root tasks to their Work view.

featcontrolplane

work lineage API

74bbb4e2

GET /api/v1/work|tasks/{id}/lineage resolves any member id to its root and returns the whole provenance tree — each node's derived Work state and caused_by depth, plus the dependency edges internal to the tree. The shared computeLineage helper also backs the UI's task-detail and /work views.

featcontrolplane

stamp provenance at the three spawn sites

d870522f

Plan tasks are caused_by the plan Work (plan_task); verify follow-ups thread the subject Work through createFollowUp and set caused_by with cause verify/follow_up, closing the audit gap where the subject link lived only in attempt-granular snapshot JSON. A new optional caused_by on a work request lets callers chain intent explicitly (validated: the Work must exist).

featstore

read and enforce work provenance

61d8cca8

Add CausedByWorkID/RootWorkID/Cause to the Work struct, INSERT, workColumns, and scanner. CreateWork derives root_work_id from the caused_by parent (which must exist) so callers only set the immediate cause, and journals the links. New WorkTree and WorkDependenciesWithin queries back the lineage view.

featstore

work provenance schema, model enum, and design note

36d30ef1

Add the caused_by_work_id / root_work_id / cause columns to work with a backfill for existing plan batches and verify follow-ups, the model.Cause enum validated alongside Trigger, and a DESIGN.md §3 "Provenance" section the schema comments reference. Backend readers land in following commits.

featui

configurable max_turns and timeout on the New-task dialog

b6ee8781

Ad-hoc tasks defaulted to haiku / 30 turns / 1800s with no way to raise them from the UI, so a real multi-file change could exhaust its turn budget and fail with error_max_turns (as one did). workRequest now accepts optional max_turns and timeout_seconds overrides for ad-hoc work, and the New-task dialog exposes both as number fields (blank = the ad-hoc default).

feat

app lifecycle — Start/Stop/Rebuild a repo's app, no agent

fea1edcc

A repo can declare a [run] section in .forge/config.toml (build/start/stop, hot_reload, port_env, health_path, ready_log, [run.env]); the daemon supervises the app so the basic dev loop needs no LLM:

featui

proposal detail page with decision history

64fda901

Each proposal id in the list now links to /proposals/{id}: the full record — kind, target, source, eval score, applied ref — with the complete rationale and verification plan, the before/after and A/B-outcome JSON pretty-printed, and a Decision history table built from the proposal's journal entries (created → decided → applied/reverted, eval-scored, force-approved). Approve / Reject work in place on the detail page too. Adds a prettyJSON template helper.

feat

Repos page — add by path/URL, archive (keep metadata), restore

4ee6667e

A dedicated /repos page to manage the repositories Forge works on, plus the lifecycle behind it:

featui

Line Forge icon system — sprite, nav, state pills, favicon

214e2843

Stroke-based SVG icons on a 24px grid (1.75px stroke, round caps/joins, currentColor) matching the UI's hairline aesthetic. No build step, no icon font, no external assets — one inline <symbol> sprite emitted once by the layout, used via <svg class="i"><use href="#i-…"/></svg>.

featui

responsive layout + cross-size overflow tests

158b3fbc

The UI forced a horizontal page scroll on phones and small tablets. Make it fit:

featplugin

configurable plugin roots for out-of-tree plugins

9fc71722

Make out-of-tree plugins first-class so users keep customizations in their own directories with no change to a Forge checkout. The plugin boundary is already a wire contract (separate binaries over the daemon's Unix socket), so no repo split is needed — only discovery from more than one root.

featui

dashboard usage/searchbar header + a Settings hub

fe848b06

Dashboard: the two usage windows are stacked vertically and share the top row with the search bar, sitting above the timeline instead of below it — the whole budget picture is visible before the activity view.

featui

Tasks page — New-task button, open-by-default, infinite scroll

fd4fc0fc

Three Tasks-page improvements:

featui

generic RPC dispatcher replaces bespoke action endpoints

ea24f807

The Human-queue "Send test toast" and any small UI-triggered daemon action went through a per-action HTTP route (POST /api/v1/notify/test) plus a queueTriggers registry. Adding an action meant a new route + handler + client wiring every time. Replace that with one endpoint, POST /api/v1/rpc/{method}, dispatching to a named-handler registry (rpcMethods) — the registry is the allowlist, every call is journaled rpc.invoked, and a UI element carries only the…

featui

search filters as deletable chips, persisted across pages

e4333ffc

The GitHub-style search DSL now renders committed filters as chips (each with a delete ×); Enter/blur/accepting a value commits the typed token into a chip and clears the input, Backspace on an empty input pops the last chip back to edit. The whole query mirrors into ?q= (shareable) AND localStorage, so active filters carry as you move between client-mode pages (a URL ?q= wins over the saved set). Also capitalized 'Human Queue' in the nav and page title.

feat

parallel session work — auto-eval override, workflows, search bar, notify actions

9f23481e

Merges the concurrent feature work developed in parallel sessions on the shared checkout (all gate-green together): - auto-eval + approve override: the daemon scores ungraded routine/mode_prompt proposals itself (background sweep, fake executor), and approve takes a force flag (UI 'Approve anyway?') that skips the eval gate, journaled. - workflows: routines strung together with blocked_by edges — one Work per step in a single batch (store.Workflow,…

featrepos

repository controls — state, pause/resume, detail page, app URL

00f719e8

Give each registered repository a derived state, per-repo controls, and a detail page so work can be stopped and started per repository.

fixproposals

block unapplyable proposals at creation; clean the UI refusal

622fcb90

A routine/mode_prompt/process/tool/code proposal must carry a concrete 'after' at creation — an empty one makes it unapplyable, so approving it later dead-ends on 'after is required' (an early smoke-era retro filed advisory-only routine proposals before its prompt required 'after'). Doc proposals are exempt. The eval-gate refusal now names the exact command (forge eval --record-proposal <id>) and the UI strips the trailing sentinel so it reads 'Refused:…

featui

answer a waiting task's question from its detail page

1864b890

The task detail page showed an open question but only the CLI command to answer it. Add the same inline answer form the human queue uses (shared data-answer-form handler in app.js), so a waiting_human task can be answered and resumed directly from /tasks/<id>. Browser test asserts the form renders on the waiting task's page.

featui

live interactive timeline on the dashboard

07ba39d2

A trailing-window wall-clock timeline at the top of the dashboard, driven by GET /api/v1/timeline?window=<15m|1h|6h>. Two controls: the window and a view toggle (by task / by attempt / by repo) — one lanes-of-bars engine, the toggle regroups attempts into lanes. Finished bars split into phase segments (agent/verify/git_inspect/… reusing the task-timeline vocabulary, colored per phase); running bars extend to now with a reduced-motion-gated live stripe,…

feat

add root README.md

f276f46d

Add README explaining what Forge is, how to build and run it, and linking to design docs. Addresses GitHub issue #1.

featplugin,forge

github-issues model option; forge.toml integration branch

6b20a58b

github-issues tasks can set model per repo (an implement task on real code wants sonnet, not the ad-hoc haiku default). forge.toml declares integration_branch=main + task_branches=forge/* so an ingested fix with integrate=true can be gated-pushed by the merge queue.

featplugin

github-issues — turn GitHub issues into Forge tasks

d085ac4d

A first-party intake+annotate plugin: polls gh for open labeled issues on configured repos, creates one Forge task per new issue (linked back via external_refs), and comments the outcome on the issue when the task finishes (branch + task URL, or the failure reason). Dedupes across restarts via a state file; uses the host's authenticated gh (no token handling). Per-repo config maps a github repo to a registered Forge repo with mode/class/…

featui

click-to-copy forge commands anywhere in the web UI

36d25723

A single app.js pass upgrades every inline <code> whose text is a forge command (not inside a fenced block) into a click-to-copy button: role/tabindex for keyboard use, a copy glyph affordance, and a 'copied ✓' flash on success (loopback is a secure context, so navigator.clipboard works; a select-text fallback covers the rest). Covers the human queue, empty states, hints, and kb inline code at once, and any future command. Browser test asserts a command…

fixplugins

page the journal to the true tail on start

96aa2422

startCursor asked for limit 1000 but the journal endpoint caps at 500, and the loop treated 'fewer than asked' as the end — so it stopped at the cap and treated ~500 as the tail. On (re)start the plugin then replayed every event after it: for notify that meant a burst of stale desktop notifications for old questions/failures/proposals. Page until a genuinely empty result instead. Same fix in status-file (benign there — its snapshot rewrite is idempotent).

featnotify

desktop notifications route to the UI on click

a5cdff2a

Each notification carries an omarchy-exec-argv hint — a JSON ["xdg-open", <url>] the Omarchy shell runs on click as safe positional args (no shell) — routing a question or failure to its task page, a proposal to /proposals, a throttle to the dashboard. The hint is notification data, so it survives a shell restart and needs no live sender (unlike a libnotify --action, which is why the plugin was fire-and-forget before). The URL stays in the body as a…

featui

render kb notes with goldmark + bluemonday

68b2ab6e

Replace the hand-rolled minimal markdown renderer (which mangled ordered lists, nested lists, tables, and real links) with goldmark (GFM: tables, task lists, strikethrough, autolinks) sanitized through bluemonday. Raw HTML is escaped (WithUnsafe off) and the output is sanitized, so a note built from untrusted repository content (constitution 9) cannot inject script or a javascript: URL; relative /kb/<id> links survive. [[wiki links]] are rewritten to…

featui

Knowledge page — browse and read kb notes in the web UI

6bb120dd

/kb lists every note (full-text search via ?q=, one-click tag filter) and /kb/{id} renders a note's markdown body with its links and backlinks. A small, safe renderer (escape-first, fixed tag set) covers the note format: headings, fenced + inline code, bold, [[wiki links]] to other notes, bullet lists. So briefs, audit findings, retros, and system-test grades are readable in the browser instead of catting files. Store gains ListKbNotes.

featm10

runners, models, cost-vector routing, escalation

397d47e5

[runners.<name>]/[models.<alias>]/[routing] config with embedded Anthropic defaults (bootstrap never writes them, so upgrades refresh prices; sonnet=mid, haiku=small, opus=frontier). The worker probes each runner and advertises runner:<name> ready|down|unauthenticated; runner capacity is a second slot dimension enforced at claim time. Every attempt records a notional usd cost vector (tokens x price), runner_seconds, and honest-NULL window deltas. The…

+ housekeeping: 2 docs, 6 test

August 30, 2026

featm9

write-set leases, plan/integrate modes, merge queue, gated push, stacking

a0a9d92a

Path globs are per-repo leases (path_lease when a running work's write set intersects; conservative glob intersection; undeclared = whole repo; lockfiles an implicit exclusive lease); non-writing modes (verify, plan) are lease-exempt so an L2 verify never deadlocks against its own subject. plan mode emits a task DAG the daemon creates as one batch; integrate mode resolves a conflicted rebase only. Merge queue: integrate=true work goes succeeded ->…

featm11

steer, repo briefs, forge.toml bootstrap, ask budget, intake dedupe

89afe4d2

forge task tell injects stream-json user turns into a live attempt via --input-format stream-json (steer capability; stdin stays open); pending steers are journal-backed (attempt.steer / steer_delivered markers) so they survive daemon restarts and ride the heartbeat response. Repo briefs: the newest 'brief: <repo>' kb note is injected via --append-system-prompt and hashed into the prompt version; tokens_to_first_edit computed from usage metrics up to the…

featm12

backup/restore, rollback, evals, health

9ecd6003

forge backup (VACUUM INTO + kb/modes/config/plugin state, tar.gz, nightly with [backup] keep retention, journaled) and forge restore into a fresh empty FORGE_HOME only; migrations snapshot the db first; every healthy daemon start captures last-known-good binary+db and forge daemon rollback restores the db and names the good binary without overwriting anything. forge eval runs golden cases under evals/ through per-case temp homes with real…

featdoctor

sandbox capability row fails with a fix hint when missing

92a69502
featm8

bubblewrap sandbox, netproxy allowlist, offline harness proof

1d82fa99

Executors run under bwrap (per-executor sandbox flag, default on for claude-code): worktree and artifacts rw at real paths, tmpfs HOME, ro system, private /tmp, --unshare-pid --die-with-parent, explicit allowlist env; ~/.ssh and ~/.config/gh proven invisible under real bwrap tests. A per-attempt HTTP(S) proxy enforces [sandbox] allow_hosts (CONNECT + plain forward, 403 + event on deny); no --unshare-net on this box (no pasta/slirp), so direct-dial bypass…

featm11

notify plugin, curate mode, forge task retry

9aafb455

notify (first-party, events): desktop notifications for questions, target failures (30s debounce with suppression counts), budget throttling (edge-detected from hard_stop deferrals), and new proposals, with per-kind toggles. curate mode: consolidates superseded retro notes into one summary note with supersedes links, kb tools only. forge task retry: terminal failed/unverified/cancelled targets transition back to pending as a fresh attempt (finished…

fixstore

a finished attempt is never rebound by a claim

47614747

M11 retry: the resume lookup returned the latest attempt regardless of finished_at, so a retried target's next claim rebound the finished attempt instead of creating a new one. Filter to unfinished attempts — the waiting_human resume still rebinds because the answer clears finished_at.

fixplugin

install registers third-party plugins in place

885cb5ee

A directory dropped under <home>/plugins had no store row, so enable said not found (M7 smoke 4). install now falls back to registering it without a copy, building first when the manifest says so.

fixplugin

build first-party plugins in the source checkout

c4d09ce0

The copied dir has no go.mod (M7 smoke 1: 'go.mod file not found'); the build runs in the repo module and the binary ships with the copy.

featm7

plugins — supervisor, tokens and scopes, journal SSE, MCP bridge, status-file, Omarchy indicator

0af200f6

Core: manifest discovery, process supervision with backoff and captured stderr, per-start token minting (tokens never rest on disk), one scope table gating plugin bearers on both transports (403 + plugin.denied journal row), journal SSE with persisted ack cursors, plugin MCP servers bridged into /api/v1/tools namespaced <plugin>_<tool>, external_refs writes, plugin CLI, doctor and System-page health. First-party plugins: status-file (journal SSE consumer…

fixcli

--help on a parent command exits 0

e9b2c87f

M6 smoke 10: task/routine/kb/daemon/worker/proposal printed usage but exited 2 on --help; a help request is not bad input.

fixprompt

label the routine prompt in the assembled prompt

78984f4c

Repeated haiku attempts (M6 smoke 6/7, both routine and ad-hoc) answered 'no task was provided' with the task present: every mode preamble ends with a '## Result' format section, and the unlabeled task text after it reads as trailing noise. A 'YOUR TASK (the routine prompt):' header anchors it.

fixm6

on-the-fly registration records a base branch

0d62acda

A checkout without refs/remotes/origin/HEAD gave resolve_base nothing to fall back to (M6 smoke 7: prepare_failed on the fresh box). Registration now detects origin/HEAD, else the checkout's current branch, and writes base_branch into worker.toml and the provisional row.

featm6

repositories on the fly (DESIGN 1.3)

a5e3dc8d

M6 smoke 7: a fresh FORGE_HOME bootstraps an empty [repositories], so task add on a fresh box had nothing to run against. --repo X now resolves a registered name, an absolute path (CLI makes it absolute; the daemon's cwd is not the user's), or <projects_root>/X: the daemon validates the checkout, appends it to worker.toml, and records a provisional repositories row with no worker; the worker re-reads worker.toml on its registration tick (<=30s), validates…

fixcli

task add waits for worker registration on a fresh home

3b86de7e

M6 smoke 7: on a just-bootstrapped FORGE_HOME the auto-started daemon's worker registers repositories a beat after the CLI's create, so task add failed 'repository is not registered'. Retry that specific error for up to 20s (DESIGN §1: task add must work on a fresh box with no other setup).

fixdaemon

keep the lock fd close-on-exec except across the exec restart

ccc29b9a

M6 smoke 6: the worker inherited the daemon lock fd (its CLOEXEC was cleared at startup for the future exec), so a daemon kill -9 left the flock held by the worker and auto-start refused with 'lock is held but daemon.json names dead pid' until the worker exited. The lock is now CLOEXEC for its whole life; execRestart clears it, with the listener dups, just before the exec.

featm6

drain-exec restart, SSE task logs, init, doctor, service units

b53bfba0

daemon restart drains in place: POST drain {exec, timeout} validates the new binary, waits for in-flight requests, and execs it with the lock and both listener descriptors inherited — same pid, no socket gap; the restarted image journals daemon.restarted. GET /work/{id}/stream is SSE (journal rows + attempt events, resumable by journal id); task logs [-f] and task add --wait consume it; daemon logs -f tails the file. forge init (--yes, --service,…

fixretro

require concrete after values in proposals

76c41adb

M5 smoke 24 filed a routine proposal with no after payload; approve correctly refused to apply it, but the preamble never told the agent what an appliable proposal needs. Applied to the live preamble too (prompt assembly reads it per claim).

fixm5

proposal targets accept bare names

d4783d78

Retro agents file targets both ways (inventory, routine:inventory); the kind already picks the prefix, so it is optional (M5 smoke 24 filed bare names that approve would have 409'd on).

featm5

reflection — proposals end to end

77616070

forge_propose tool; proposals API (atomic approve: decide + apply + mark applied in one transaction, a failed apply rolls the approval back); forge proposal CLI; attention/dashboard/UI proposals page; funnel + retro cost in stats. Apply engine per kind: routine/process new generations (source proposal:<id>), mode_prompt live preamble with backup, tool script dirs gated on their own tests, code as a forge/proposal-<id8> branch built in a temp worktree…

featm5

proposal store foundation

14b77228

Proposal struct over the M0 proposals table with the status machine (proposed → approved|rejected, approved → applied → reverted) in model; create/decide/apply/revert with journal rows in the same transaction; constitution-target guard at the data layer (constitution 8); funnel counts in one scan; UpdateRoutineFrom records proposal:<id> generation sources; applyProposal stub seam on the server for the M5 apply engine.

fixverify

render subject claims into the verify prompt; enforce the envelope for every autonomy level

26f0c14b

M4 smoke 22: the verify agent got an empty prompt (VerifyWork set no Prompt), answered in fenced prose because --json-schema was only passed when autonomy allows questions, and the subject landed unverified=verify_attempt_failed. Three fixes: VerifyWork(env, c) renders the subject's summary and claims into the Work prompt at creation time (no shared context, so claims travel in the prompt — VERIFICATION.md); ModeInfo carries the mode's ResultSchema and…

fixbudget

admit dependency-triggered verify follow-ups as interactive

0726bf8e

An implement Target reached 'verifying' and its L2 verify Work then sat deferred on forecast_over_target:seven_day — the subject's own verification starved behind the soft budget rules, stranding it. Order now judges trigger=dependency Work as interactive at admission: the marginal spend was committed when the subject ran. Hard stops and the daily cap still defer it (constitution 6). Docs: DESIGN §10.2 rule 2, VERIFICATION.md L2.

featm4

eleven modes, verification L0-L3, greenfield, per-repo .forge/, UI browser tests

1662a629

Modes: one package per mode with embedded preambles (constitution 9 verbatim), schemas extending the one envelope, checkpoints, write scopes, follow-ups; generated registry (go generate, modes/gen). Prompt assembly: live preamble file > embedded, repo .forge/modes overlays, autonomy block, context block; the hashable template travels separately from the rendered prompt. Verification: L0 scope enforcement (none/kb_only/docs_only with ** globs), L1…

fixm3

enforce the needs_input envelope by schema; watch a held worker lock; short task ids

73140102

--json-schema now carries the envelope for ask/checkpoint attempts (a prose question parsed as nothing and the pause was lost — found by smoke 18). ensureWorker watches a lock held by a predecessor and spawns when it frees (a wedged orphan worker left the system with no worker at all). task show/cancel/answer accept unique id prefixes.

featm3

budget policy, forge usage, queue interactions, human queue, stats, retro pack

80ae5ac4

Budget: pure ComputeUsage/Decide (targets, hard stops, quiet hours, burn-down line, daily USD cap), BudgetPolicy behind SchedulerPolicy with a 10 s cache, reset-boundary journal, GET /api/v1/usage, forge usage, dashboard gauges. Queue: move_before with the dependency guard scoped to the moved item, queue move/block CLI, drag-and-drop page. Human queue page answers questions in place. Stats: internal/stats shared by tools and the daemon (verified vs…

featm2

forge mcp, tool registry, knowledge base, prune

b43f2911

forge mcp: hand-written line-delimited JSON-RPC MCP server; repository tools (forge_repo_status, forge_check, forge_diff_summary) run in-process in the worktree; every call is an mcp-source span pair with input/output sha256. Daemon: tool registry (14 daemon tools incl. kb and forge_ask/note_progress), GET/POST /api/v1/tools with worker-or-attempt-token auth, mcp token rebind on claim replay, kb index (FTS5, incremental by mtime+size), kb routes, reindex…

fixworker

reconcile falls back to a cleanup patch when complete needs a lease token

a3bea15e

Manifests never store lease tokens, so a worker_restart completion for a still-leased target is refused 400; the git and cleanup fields must land via PATCH anyway (found by smoke 7).

fixworker

a 204 empty claim is not a decode error

c81d82b6

docs: record the local bare mirror as the Forge repo's origin

fixcli

accept flags after positionals; pass FORGE_FAKE_* to the fake executor

b1dd82d9
featdaemon

HTTP server on socket and loopback, sweeper, facts on complete, UI, thin CLI

3b346c5e

Server: handshake, register/claim/heartbeat/events/complete/cleanup, routines, tasks, queue, questions, attention, journal, log-level, drain; token auth on TCP only. Sweeper extends leases on start and fails expired ones. UI: dashboard, tasks, task detail with phase timeline, routines, system. CLI: auto-starting client (lock hand-off, systemd, handshake mismatch), daemon start|stop|restart| status|logs|log-level, worker start, task…

featworker

attempt runner, event batching, verification, poll loop, reconcile

f95fceb0

Ten phases as spans with monotonic durations; heartbeat from claim to complete; needs_input becomes a Question and a resume continues seq and elapsed; L0/L1 verification from Forge's own git inspection and declared checks; reconcile kills orphans by pid identity, cleans or retains, and reports through complete or a cleanup patch. Tested end to end against the real fake-claude binary and temporary git repositories.

fix

errcheck-clean worker, controlplane, and fake-claude tests

0c6e37af
featworker

git layer, manifests, supervisor, parser, executor, config, cleanup decision, client

67c46858

feat(controlplane): facts computation, queue order, admission picker, config, daemon lock/state/listeners, bootstrap feat(cli): forge fake-claude replays stream-json fixtures; six fixtures under testdata/fixtures so every test runs without claude or network

featstore

SQLite store with the full schema, journal, claim/lease/sweep, events, facts

2be79d3d

One ULID-named migration carries every table through M12. Writes go through a single serialised connection in explicit transactions; every Work/Target/ Attempt/Question state change journals in the same transaction. Claim is idempotent by claim_request_id, complete by finished_at + lease hash; the sweeper fails expired leases and a starting daemon extends them. Event batches use one prepared statement; the benchmark ceiling lives in bench/threshold.txt.

featmodel

ids, names, target state machine, work state, reasons, dependencies

390dd93a

feat(protocol): wire types for register, claim (with policy and MCP token), heartbeat, events, complete, cleanup, handshake

featlogging

cross-cutting logging standard and journal specification

bb440762

One slog handler per process: trace level, per-component levels by dotted prefix, correlation attrs stamped from context, stderr under the flags (--log-level, --log-format, -v, -vv; FORGE_LOG_LEVEL/FORGE_LOG_FORMAT; [log]), an always-on JSON-at-debug file sink with size rotation, SIGUSR1 debug toggle, child stderr forwarding. Every subcommand registers the flags. STYLE §8/§9 and DESIGN §3 Journal / §15 Logging document the standard and the journal table.

+ housekeeping: 5 chore, 1 constitution, 8 docs, 1 model, 2 test, 3 worker