Patch notes
What changed
September 4, 2026
the root plan gets a plan-sized envelope
99bb1e9cThe 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.
registerRepoOnTheFly is idempotent for a same-name-same-path re-add
f6d902afA 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.
re-adding an archived repository name clears the archive flag
eb05e7b0UpsertProvisionalRepository left archived=1 standing when a name came back (bench reuses minute-stamped names), so the fresh checkout never scheduled.
POST /api/v1/repositories upserts the provisional store row immediately
7cd04a83The 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.
the benchmark — forge bench runs a spec as a scored recursive build
940575caforge 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…
calibration surfaces — size buckets in stats, the per-ask rollup on lineage
2aa9d469stats 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.
continuation Works — the return path of recursive decomposition
39081368Every 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…
size and scores become data — the learning loop's dimensions
4f40ded5work.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).…
promotion becomes a curation Work; the library becomes a repo; the flow pipeline joins the base
00342cbaScratch 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…
the organic script layer — agent scripts earn their way into the library
3fb8d74fforge_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.…
Routines get their own page; the library tree collapses
ef0a2b10Routines 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…
scripts in any language — .js is the sandboxed default
906e3687The 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…
directives update merges upstream/HEAD, grafting the first sync
64896db8FETCH_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…
starter scripts, descriptions, and tool flags (bridge P6a)
ea561630Bumps 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.
the tool/skill bridge — forge_library, script/directive/workflow run (bridge P5)
4026fa19Four 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):
search & discovery — one ranking for UI, CLI, and agents (bridge P4)
77cb11daLibrary.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).
named library scripts + the script: trigger target (bridge P3)
c59fcef4A 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…
description + tool metadata on the row (bridge P2)
21c7c3d1Workflows 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.
scripts join the library — /**forge header metadata (bridge P1)
a17e8215scripts/<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…
remove the legacy dual-mode machinery — target-only, directive-only
155ed6efPre-stability there is no install base to carry compatibility for, so the transitional layer goes:
the base library becomes a forkable upstream (directives P9)
0352a805The 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…
the Prompts surface becomes Directives (directives P8b)
e1f5c27cRoutes: /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…
internal/core/prompts becomes internal/core/directives (directives P8a)
a1099dc4Mechanical: git mv + package rename + importer qualifiers. Exported names (Library, Fragment, Composition) unchanged.
dry run predicts graph conversions from the would-split set
0268adbbDry 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.
the directives restructure migration (directives P7)
c8dc4571migratedirectives.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…
the self-improvement loop follows content into git (directives P6)
7d6eebd3A 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…
authoring surfaces speak targets (directives P5)
dd95ae34The 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,…
directive nodes — graphs compose library content (directives P4)
9c75b6a7A 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…
materializeRoutine — one compose seam; target routines run (directives P3)
050ebefbmaterializeRoutine 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…
directives/ — executable task content in the library (directives P2)
eded2038A 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…
routines gain target + objective — the trigger-shell shape (directives P1)
9b940769A 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}}.
expected cost per run on the workflows list
11c2c31dGET /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…
expected-cost line on the optimize panel; fable model alias
57aa7a3cThe 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.
LLM-assisted optimization experiments for personas and routines
45b48169An 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…
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…
remember test runs per prompt — the iterate loop's memory
9584bd46Every 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.
run prompt tests against a real model, with configurable sizing
b792be90POST /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…
edit and test prompts in place on the Prompts page
f5a2c8a7Editing: 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…
the Routines page becomes Prompts — library tree, composition previews, prompt testing
59be7370The 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…
forge_kb_new gains repo targeting — learnings versioned with their repo
0ff18d79The 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}}…
teams and email channel plugins
f300a445Two 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.)
ResetMergeAttempts — a human requeue grants a fresh rebase budget
76ae2137The 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.
starter library — nine standard roles from shared fragments
daae394dShipped 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
git-backed prompt library composed into routines at run creation
94a0c547The 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
drag-and-drop workflow graph editor, run view, and AI drafting
508f35eaA 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…
wire cron scheduling and retry-from-node
6a03a21bThe 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…
run engine — first-class runs, stepwise materialization, JS steps, switches, loops
d436b2ceRuns 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…
graph data model — typed nodes, conditional edges, lossless steps migration
53035600Workflows 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
task add --max-turns and --timeout
f35f223aThe 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.
cache-write prices at the 1-hour TTL rate
c4f564b2The 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.
live app chip and app link on the Repos list (P2)
bbe1d280The 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.
retained worktrees detach HEAD so their branches stay usable
f0b113c6A 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.
a max-turns cliff with commits lands as a reviewable partial
c2dc6841The 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…
errcheck findings in the merged overnight branches
7af163f2The 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.
form the tui tree from the CLI's client half (Stage 6)
17f07477cli_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…
split internal/web/ui into its own package (Stage 5b)
5a9430a1ui.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…
rename internal/controlplane to internal/web (Stage 5a)
0e924fa1git 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.
form the tools tree (Stage 4)
7f1bea59internal/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.
cut internal/core/daemon out of controlplane (Stage 3)
6c55911bdaemon.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.
cut internal/core/engine out of controlplane (Stage 3)
4dbc8544budget, 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.
cut internal/core/config out of controlplane (Stage 3)
31168721config.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…
split Engine out of Server, same package (Stage 2)
81d5c9d2Engine 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.
relative fixture/case paths follow the core move (Stage 1)
17237831Four 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.
move internal/modes to internal/core/modes (Stage 1)
f547746fgo:generate directive updated and the registry regenerated in the same commit; the generated file's imports are the only regeneration diff.
move internal/integrator to internal/core/integrator (Stage 1)
8b7988d6smoke-m9.sh's literal paths follow.
move internal/eval to internal/core/eval (Stage 1)
9b9e08b4test-integration recipe path follows.
move internal/doctor to internal/core/doctor (Stage 1)
59f7cceemove internal/stats to internal/core/stats (Stage 1)
96a90981move internal/plugin to internal/core/plugin (Stage 1)
858f2b22move internal/worker to internal/core/worker (Stage 1)
4bc9de8aBoundary rule path follows.
move internal/store to internal/core/store (Stage 1)
b2efc38aThe worker boundary rule's deny pattern follows the store to its new path.
move internal/kb to internal/core/kb (Stage 1)
4cace44emove internal/logging to internal/core/logging (Stage 1)
85addd8fmove internal/protocol to internal/core/protocol (Stage 1)
80368b43move internal/model to internal/core/model (Stage 1)
43d0ef7egit mv plus import rewrites; boundary rule paths updated (MODULARIZATION.md §8 Stage 1).
fail-closed import linter with the module rule table (Stage 0)
e1d9408ajust 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…
verify prompt — restore side-effect dirt before returning
4d8c8bfeVerifying 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.
forge task reverify — re-check unverified work without re-running it
2ad50248A 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.
raise the verify budget to 60 turns / 2400s
a27f8548Real 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).
bake the installing shell's PATH into the systemd user units
432f9cb7Units 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.
account for .git when checking for flatten
fb9f232dThe flatten check needs to ignore hidden directories like .git when determining if the nested slug directory is the only content.
flatten double-nested projects in greenfieldMove
2540fc2fWhen 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
skip invalid repos instead of refusing to start
6d283d17The 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.
an empty marker sweeps nothing
74cb46edThe one place that rule belongs is Sweep, not the callers.
a declared check no longer waits for, or leaks, its background processes
11433678CombinedOutput 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…
kill everything an attempt spawned when it ends
d9088768The 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".
find an attempt's escaped processes by environment marker
5e948e25A 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.
supervisor-initiated watchdog (Phase B)
0e7f51d2A 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.
exempt greenfield from L0.2 changes exactness
feb59777A 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…
child-initiated budget negotiation (Phase A)
36328b5bTurn 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…
surface a running attempt's live turn/token tally
86d64a7bWhen 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.
Human Queue countdown + decided-by-Forge marker
37664c6cThe 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.
time-aware attention sweep auto-decides non-critical questions
d0bf3c9eA 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…
criticality param on forge_ask
dc14a733Agents 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.
question criticality + audited auto-answer path
dfa34e3aA 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…
concierge — an LLM front door for inbound messages
36f42a31One 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…
bound + persist the question→reply map; reply is never a task
49b38aa9The 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.
answer a question by replying to its notification
ba0ca609Typing /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.
mobile nav — hamburger menu with a vertical popout
866d53a3The 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.
run signal-cli in a stable working directory
7c7558ccThe 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.
reap plugins across exec-restart; keep plugin config on reinstall
a0cc4955Two related plugin-lifecycle bugs that combined to wedge a restart with "address already in use":
serialize signal-cli calls; cap receive window
f4be6141signal-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.
signal — bidirectional Signal bridge
95c54216A 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.
role-pipeline flows — routines, workflows, setup script
65b87699scripts/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…
thread a run objective into steps via {{objective}}
8ab8aff6Workflow 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.
archive tolerates an already-deleted checkout
f4f90746safeRemoveCheckout 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.
role-based routine templates
898405f5A 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.
forecast_pacing defaults off
12bdd5e0Make 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.
forecast_pacing knob to relax admission on burn-rate spikes
4572abd7The 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.
provenance strip and the /work tree view
9048b203Task 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.
work lineage API
74bbb4e2GET /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.
stamp provenance at the three spawn sites
d870522fPlan 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).
read and enforce work provenance
61d8cca8Add 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.
work provenance schema, model enum, and design note
36d30ef1Add 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.
configurable max_turns and timeout on the New-task dialog
b6ee8781Ad-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).
app lifecycle — Start/Stop/Rebuild a repo's app, no agent
fea1edccA 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:
proposal detail page with decision history
64fda901Each 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.
Repos page — add by path/URL, archive (keep metadata), restore
4ee6667eA dedicated /repos page to manage the repositories Forge works on, plus the lifecycle behind it:
Line Forge icon system — sprite, nav, state pills, favicon
214e2843Stroke-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>.
responsive layout + cross-size overflow tests
158b3fbcThe UI forced a horizontal page scroll on phones and small tablets. Make it fit:
configurable plugin roots for out-of-tree plugins
9fc71722Make 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.
dashboard usage/searchbar header + a Settings hub
fe848b06Dashboard: 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.
Tasks page — New-task button, open-by-default, infinite scroll
fd4fc0fcThree Tasks-page improvements:
generic RPC dispatcher replaces bespoke action endpoints
ea24f807The 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…
search filters as deletable chips, persisted across pages
e4333ffcThe 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.
parallel session work — auto-eval override, workflows, search bar, notify actions
9f23481eMerges 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,…
repository controls — state, pause/resume, detail page, app URL
00f719e8Give each registered repository a derived state, per-repo controls, and a detail page so work can be stopped and started per repository.
block unapplyable proposals at creation; clean the UI refusal
622fcb90A 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:…
answer a waiting task's question from its detail page
1864b890The 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.
live interactive timeline on the dashboard
07ba39d2A 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,…
add root README.md
f276f46dAdd README explaining what Forge is, how to build and run it, and linking to design docs. Addresses GitHub issue #1.
github-issues model option; forge.toml integration branch
6b20a58bgithub-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.
github-issues — turn GitHub issues into Forge tasks
d085ac4dA 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/…
click-to-copy forge commands anywhere in the web UI
36d25723A 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…
page the journal to the true tail on start
96aa2422startCursor 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).
desktop notifications route to the UI on click
a5cdff2aEach 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…
render kb notes with goldmark + bluemonday
68b2ab6eReplace 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…
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.
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
write-set leases, plan/integrate modes, merge queue, gated push, stacking
a0a9d92aPath 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 ->…
steer, repo briefs, forge.toml bootstrap, ask budget, intake dedupe
89afe4d2forge 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…
backup/restore, rollback, evals, health
9ecd6003forge 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…
sandbox capability row fails with a fix hint when missing
92a69502bubblewrap sandbox, netproxy allowlist, offline harness proof
1d82fa99Executors 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…
notify plugin, curate mode, forge task retry
9aafb455notify (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…
a finished attempt is never rebound by a claim
47614747M11 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.
install registers third-party plugins in place
885cb5eeA 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.
build first-party plugins in the source checkout
c4d09ce0The 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.
plugins — supervisor, tokens and scopes, journal SSE, MCP bridge, status-file, Omarchy indicator
0af200f6Core: 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…
--help on a parent command exits 0
e9b2c87fM6 smoke 10: task/routine/kb/daemon/worker/proposal printed usage but exited 2 on --help; a help request is not bad input.
label the routine prompt in the assembled prompt
78984f4cRepeated 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.
on-the-fly registration records a base branch
0d62acdaA 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.
repositories on the fly (DESIGN 1.3)
a5e3dc8dM6 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…
task add waits for worker registration on a fresh home
3b86de7eM6 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).
keep the lock fd close-on-exec except across the exec restart
ccc29b9aM6 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.
drain-exec restart, SSE task logs, init, doctor, service units
b53bfba0daemon 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,…
require concrete after values in proposals
76c41adbM5 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).
proposal targets accept bare names
d4783d78Retro 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).
reflection — proposals end to end
77616070forge_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…
proposal store foundation
14b77228Proposal 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.
render subject claims into the verify prompt; enforce the envelope for every autonomy level
26f0c14bM4 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…
admit dependency-triggered verify follow-ups as interactive
0726bf8eAn 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.
eleven modes, verification L0-L3, greenfield, per-repo .forge/, UI browser tests
1662a629Modes: 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…
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.
budget policy, forge usage, queue interactions, human queue, stats, retro pack
80ae5ac4Budget: 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…
forge mcp, tool registry, knowledge base, prune
b43f2911forge 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…
reconcile falls back to a cleanup patch when complete needs a lease token
a3bea15eManifests 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).
a 204 empty claim is not a decode error
c81d82b6docs: record the local bare mirror as the Forge repo's origin
accept flags after positionals; pass FORGE_FAKE_* to the fake executor
b1dd82d9HTTP server on socket and loopback, sweeper, facts on complete, UI, thin CLI
3b346c5eServer: 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…
attempt runner, event batching, verification, poll loop, reconcile
f95fceb0Ten 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.
errcheck-clean worker, controlplane, and fake-claude tests
0c6e37afgit layer, manifests, supervisor, parser, executor, config, cleanup decision, client
67c46858feat(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
SQLite store with the full schema, journal, claim/lease/sweep, events, facts
2be79d3dOne 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.
ids, names, target state machine, work state, reasons, dependencies
390dd93afeat(protocol): wire types for register, claim (with policy and MCP token), heartbeat, events, complete, cleanup, handshake
cross-cutting logging standard and journal specification
bb440762One 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