Skip to content

Forge — Complete Learn-by-Building Course

Generated from the individual lesson files for convenient continuous reading. Edit the individual lessons, then run bun run docs:course.

Table of contents

  • Lesson 00 — Orientation and repository contract
  • Lesson 01 — Toolchain and process boundaries
  • Lesson 02 — Typed RPC and desktop/core separation
  • Lesson 03 — Model engineering work, not screens
  • Lesson 04 — Small state machines and invariants
  • Lesson 05 — SQLite, migrations, and repositories
  • Lesson 06 — Why database transactions cannot control the outside world
  • Lesson 07 — Operation workers, idempotency, and recovery
  • Lesson 08 — One safe process-launch boundary
  • Lesson 09 — Git as code truth
  • Lesson 10 — Worktrees and Execution Environments
  • Lesson 11 — Driver, Instance, Strategy, Capability
  • Lesson 12 — Task vs Run vs Turn vs process
  • Lesson 13 — Build the programmable fake agent
  • Lesson 14 — Raw before parse
  • Lesson 15 — Normalize events without lying
  • Lesson 16 — Electron shell and secure renderer
  • Lesson 17 — React data ownership and the first Task workspace
  • Lesson 18 — Probe a real provider safely
  • Lesson 19 — First real adapter
  • Lesson 20 — Revisions and diffs
  • Lesson 21 — Verification that becomes stale correctly
  • Lesson 22 — Human review and change requests
  • Lesson 23 — Immutable Plans and review rounds
  • Lesson 24 — Decomposition and simple dependencies
  • Lesson 25 — Decisions and Needs You
  • Lesson 26 — Injected escalation and capability preflight
  • Lesson 27 — Involvement modes and host gating
  • Lesson 28 — Dependency readiness
  • Lesson 29 — Bounded scheduler and parallel agents
  • Lesson 30 — Facts, claims, and meaningful activity
  • Lesson 31 — Assumptions ledger
  • Lesson 32 — Realtime sync, coalescing, and bounded replay
  • Lesson 33 — Crash recovery and reconciliation
  • Lesson 34 — Performance, diagnostics, migrations, packaging
  • Lesson 35 — Add a second adapter/environment without breaking the architecture

Lesson 00 — Orientation and repository contract

Outcome

Understand how to use this repository as both a course and a codebase. You will not add product code yet; you will make sure you can explain Forge's purpose, boundaries, vocabulary, and documentation workflow.

Why this comes now

A complex systems project becomes much harder when every coding session starts by rediscovering what the project means. Before code, establish the small set of documents that are allowed to define current truth.

Understand

Forge is a control plane around engineering work. The Task is primary; agent sessions are subordinate. The repository is intentionally layered: short current-truth documents for daily work, ADRs for reasons, research for evidence, and archive for history.

The most important habit is that architecture is allowed to change, but changes must become explicit. We do not keep known-wrong canonical text and expect future humans or agents to apply mental patches.

Build the real project

  1. Run bun install, then bun run docs:dev. Use the VitePress site as the main human reading surface.
  2. Read docs/start-here.md, docs/context.md, docs/product/PRD.md, and docs/architecture/OVERVIEW.md.
  3. In your own words, add a short entry to docs/status/LEARNING_LOG.md answering: “What is Forge, and what is it not?”
  4. Initialize Git if this starter is not yet a repository.
  5. Make the initial documentation/scaffold commit.
  6. Confirm that docs/archive/ is understood as historical input, not authority.

Completion gate

You can explain these distinctions without looking them up: Task vs Agent Run; Agent Adapter vs Execution Environment; current docs vs ADRs vs archive. bun run docs:build succeeds, the Course/Architecture/Research/Status navigation works, and git status is clean after your initial commit.

Pitfalls to avoid

Do not begin “improving” the architecture in Lesson 00. Do not copy the archived schema into code. The point is to establish the contract that makes later change manageable.

References

Required: Forge docs home, Documentation site guide, System overview, VitePress getting started. Optional: Architecture Decision Records by Michael Nygard.

Checkpoint

Set docs/status/CURRENT.md current lesson to 01 and record any genuinely unclear vocabulary as an open question rather than silently inventing a meaning.


Lesson 01 — Toolchain and process boundaries

Outcome

Extend the real toolchain skeleton: keep the Go core independent, use the existing Bun root workspace for the desktop package, and establish clear commands for running/checking each half independently.

Why this comes now

Tooling becomes invisible infrastructure. A messy bootstrap spreads platform assumptions everywhere before you have product code.

Understand

The Go core is a separate executable. Electron is not the backend; it is a desktop shell that starts and monitors the core. Bun manages the JavaScript workspace because it is fast and convenient, but the architecture must not depend on Bun-specific runtime behavior in the renderer.

The core should be runnable from a terminal. This gives us a clean test/debug path and makes future remote control possible without redesigning ownership.

Build the real project

  1. Create core/go.mod with the chosen supported Go 1.26 toolchain.
  2. Add core/cmd/forge-core/main.go that starts, logs its version, and exits cleanly on context cancellation.
  3. Extend the existing Bun workspace with the desktop TypeScript package, then add its TypeScript config and Vite/Electron entry points.
  4. Add root commands (Makefile, scripts, or simple documented commands) for core-test, desktop-check, and check.
  5. Add .editorconfig and .gitignore.
  6. Keep the scaffold small: do not add an ORM, DI framework, or logging ecosystem before there is a need.

Completion gate

go test ./... passes. go run ./cmd/forge-core starts and can be stopped cleanly. bun install, bun run docs:build, and the desktop typecheck/build command succeed once desktop dependencies are installed. The core does not import desktop code and vice versa.

Pitfalls to avoid

Avoid giant monorepo tooling for two packages. Avoid using Electron main as a place for business logic. Avoid pinning tool versions in six different files.

References

Go modules, Go context package, Bun workspaces, Electron process model.

Checkpoint

Update docs/status/CURRENT.md; write down the exact developer commands in the root README if they differ from the starter placeholders.


Lesson 02 — Typed RPC and desktop/core separation

Outcome

Make a tiny typed request from a client to the Go core, such as GetServerInfo, without exposing filesystem/process power to the renderer.

Why this comes now

Before product behavior, prove the most important ownership boundary. Once UI code starts directly reading files or spawning commands, extracting that logic later is painful.

Understand

The RPC API is not remote database access. Mutations will eventually be domain commands. For now one read-only health/version method is enough. Protobuf gives one contract source and generated types; Connect works over normal HTTP and supports Go and web clients.

Electron main owns starting the core and handing the renderer a narrow connection configuration. The renderer remains unprivileged.

Build the real project

  1. Define a minimal forge.v1.SystemService/GetServerInfo protobuf message/service.
  2. Configure Buf/code generation.
  3. Implement the Connect handler in Go.
  4. Generate the TypeScript client.
  5. From a tiny renderer/test client, show core version/status.
  6. Bind the core to loopback only and add a random per-launch/session credential before exposing command APIs.

Completion gate

Generated Go and TS code comes from one schema. A round trip succeeds. Renderer code contains no child_process, filesystem, or arbitrary Electron IPC command runner.

Pitfalls to avoid

Do not design 50 RPCs now. Do not hand-maintain duplicate DTOs. Do not add a generic ExecuteCommand(string) endpoint.

References

Required: Connect for Go, Connect for Web, Buf. Read also docs/architecture/RPC_AND_APP_BOUNDARY.md.

Checkpoint

Advance to Lesson 03 only after you can draw the renderer → RPC → application/core boundary from memory.


Lesson 03 — Model engineering work, not screens

Outcome

Implement the first pure domain types for Project, Task, and Acceptance Criterion without SQLite or RPC concerns.

Why this comes now

Screens are tempting to model first (“Kanban column”, “review pane”), but that makes UI structure leak into business state. We want the same objects to support Kanban, List, Tree, CLI, and future clients.

Understand

A domain type should represent a concept and protect rules that remain true regardless of UI or storage. IDs are stable; titles are mutable. A Task owns its engineering goal and status, not provider protocol state.

Use plain Go structs and methods. Do not turn “DDD” into ceremony. The test is whether a rule has one obvious home.

Build the real project

  1. Add domain packages/types for Project ID, Task ID, Feature ID (even if Feature behavior comes later), Task, and AcceptanceCriterion.
  2. Add constructors that reject invalid required fields.
  3. Use explicit enum-like typed strings/constants for status/modes.
  4. Keep persistence tags/adapters minimal; domain constructors should not require a DB handle.
  5. Write table-driven tests for valid/invalid construction.

Completion gate

Domain tests pass without opening files, DBs, or network sockets. You can instantiate a Task in a unit test with no infrastructure.

Pitfalls to avoid

Do not create TaskViewModel in the core. Do not put agent PID/session fields directly on Task. Do not use Task title as identity.

References

docs/architecture/DOMAIN_MODEL.md, Go Code Review Comments.

Checkpoint

Record any field you were tempted to add but could not explain as Task-owned. That is useful design evidence.


Lesson 04 — Small state machines and invariants

Outcome

Implement and test the canonical Task lifecycle without agent code.

Why this comes now

If state transitions are left as arbitrary string updates, every future screen and service can create impossible combinations.

Understand

The Task lifecycle is deliberately small: BACKLOG → READY → WORKING ⇄ NEEDS_YOU, WORKING → REVIEW ⇄ WORKING, REVIEW → DONE. Planning, verification, review progress, agent startup, and worktree preparation are separate lifecycles.

Methods should express intent: MakeReady, StartWork, NeedHuman, ResumeWork, SubmitForReview, RequestChanges, Approve rather than SetStatus.

Build the real project

  1. Encode allowed transitions in domain methods.
  2. Return typed/actionable errors for illegal transitions.
  3. Write exhaustive transition tests.
  4. Add placeholder policy input where approval will later need current Review/Verification evidence, but do not fake those subsystems yet.
  5. Keep a clear test showing that “agent finished” is not a Task transition because there is no agent yet.

Completion gate

Every valid transition is tested; every invalid edge is rejected. There is no public unrestricted status setter used by application code.

Pitfalls to avoid

Avoid one giant transition table if named methods remain clearer. Avoid adding statuses to represent loading spinners.

References

STATE_MACHINES.md, Making illegal states unrepresentable (concept).

Checkpoint

Update the current status. You should be able to explain why Review is a Task state but “human_review_in_progress” is not.


Lesson 05 — SQLite, migrations, and repositories

Outcome

Persist Projects/Tasks safely in SQLite, survive restart, and establish migration discipline.

Why this comes now

We now have real state worth persisting. Adding SQLite after domain rules lets storage conform to the model rather than define it accidentally.

Understand

SQLite is an in-process transactional database, but database/sql may use multiple connections. Connection-scoped PRAGMAs must therefore be configured correctly for every connection (the driver DSN is the preferred route here). Starting defaults: foreign keys ON, WAL, synchronous NORMAL, and a reasonable busy timeout.

Migrations are permanent release history. Repository methods translate between storage rows and domain values; they do not decide business transitions.

Build the real project

  1. Add modernc.org/sqlite.
  2. Implement DB open and XDG data-path helper.
  3. Add embedded ordered migrations and schema_migrations.
  4. Create initial Project/Task/Acceptance Criterion tables with foreign keys.
  5. Configure SQLite pragmas through the DSN/driver-supported mechanism.
  6. Add repository methods and transaction helpers.
  7. Add migration idempotence, foreign-key, and restart persistence tests.
  8. Decide whether to introduce sqlc now or after a few hand-written queries; either is acceptable if SQL remains explicit.

Completion gate

A Task created through the application/repository survives DB close/reopen. Foreign-key enforcement is proven in a test. Re-running migrations changes nothing. go test ./... is green.

Pitfalls to avoid

Do not run one PRAGMA foreign_keys=ON on a pooled connection and assume all connections are configured. Do not edit an already-released migration later. Do not put raw provider transcripts in SQLite.

References

Required: modernc.org/sqlite, SQLite WAL, SQLite foreign keys. Optional: sqlc SQLite tutorial.

Checkpoint

Advance to M2. Note any storage constraint that exposed a flaw in the domain model and fix the canonical docs now, not later.


Lesson 06 — Why database transactions cannot control the outside world

Outcome

Add the Operation concept and use a fake external effect to understand durable intent.

Why this comes now

The next milestones touch Git and processes. Before that, learn the failure mode that causes many orchestrators to get stuck after crashes.

Understand

SQLite can atomically change SQLite. It cannot atomically start Claude, create a directory, or run Git. If you mutate DB state and then perform I/O, a crash between them creates disagreement. If you perform I/O first and DB fails, you get the opposite disagreement.

Forge records an Operation in the same transaction as the domain intent, commits it, then a worker performs the side effect. “Command accepted” means intent is durable—not that the outside world finished.

Build the real project

  1. Add Operation types: kind, aggregate target, idempotency key, status, attempt, timestamps, structured payload/result/error.
  2. Add operations table/migration/repository.
  3. Create a simple application command that updates a harmless domain/test record and inserts a fake WRITE_MARKER Operation transactionally.
  4. Prove that the Operation exists even before a worker runs it.
  5. Document which future actions will become Operations.

Completion gate

A test commits domain change + Operation atomically. Inject a transaction failure and prove neither commits. Explain in a test name/comment why the external effect is not run inside the transaction.

Pitfalls to avoid

Do not build Kafka-style infrastructure. Do not turn every database update into an Operation. Operations exist for external side effects/recovery boundaries.

References

OPERATIONS_AND_RECOVERY.md, T3 Code architecture: https://github.com/pingdotgg/t3code/blob/main/docs/internals/overview.md

Checkpoint

Write one short Learning Log note describing a crash boundary you now understand better.


Lesson 07 — Operation workers, idempotency, and recovery

Outcome

Execute pending Operations with a bounded worker and prove safe retry/restart behavior.

Why this comes now

Recording intent is useful only if execution and recovery have a disciplined lifecycle.

Understand

An Operation handler may run more than once. Idempotency means repeating the same intent either has no duplicate effect or can discover that the effect already happened. Recovery means startup can inspect unfinished Operations and decide whether to retry, reconcile, fail, or wait for a human.

Tests need a Drain/quiescence concept: queue length zero does not mean a worker is not executing its last item.

Build the real project

  1. Implement a bounded Operation worker.
  2. Claim PENDING work safely and mark RUNNING.
  3. Implement one fake idempotent handler.
  4. Add retry/requeue mechanics intentionally rather than a global “retry 3 times” rule.
  5. Add Drain for deterministic tests.
  6. Simulate restart by closing/reopening the DB with a PENDING Operation.
  7. Simulate “effect happened but result persistence failed” and reconcile using the fake handler's external marker.

Completion gate

No duplicate fake effect occurs across retry. Restart processes/reconciles PENDING work. Drain waits for in-flight work, not only an empty queue.

Pitfalls to avoid

Do not hold a DB transaction open while slow I/O runs. Do not busy-loop. Do not retry permanent validation failures.

References

T3 drainable-worker note: https://github.com/pingdotgg/t3code/blob/main/docs/internals/overview.md ; Go context.

Checkpoint

M2 is complete when you trust the fake Operation worker enough to let Git use it next.


Lesson 08 — One safe process-launch boundary

Outcome

Create one controlled process-launch API used by Git and later agent adapters.

Why this comes now

Git, setup scripts, verification, and some agent strategies all spawn processes. If each package invents its own environment/signals/quoting behavior, bugs multiply.

Understand

Prefer executable + argv to shell strings. A ProcessRunner centralizes cwd, environment, stdout/stderr handling, cancellation, deadlines, process groups, and future redaction/logging rules.

This boundary is infrastructure, not the Agent Run model. A managed OS process is an implementation detail.

Build the real project

  1. Define a small SpawnSpec/runner interface only for current needs.
  2. Implement direct exec.CommandContext-style launching with argv.
  3. Capture output with explicit size/stream behavior.
  4. Propagate cancellation and test a long-running helper process.
  5. Decide how environment variables are inherited/overridden; keep secrets out of debug logs.
  6. Make GitService depend on this runner in the next lesson.

Completion gate

Tests prove arguments containing spaces/special characters remain single argv entries, cancellation terminates the child, and cwd/environment work as expected.

Pitfalls to avoid

Do not call bash -c merely for convenience. Do not expose this runner to the Electron renderer. Do not make PID a domain identity.

References

os/exec, Emdash contributor guidance: https://github.com/generalaction/emdash/blob/main/CONTRIBUTING.md

Checkpoint

Add any platform-specific process behavior you encounter to RISK_REGISTER.md only if it could affect product safety/reliability.


Lesson 09 — Git as code truth

Outcome

Build a narrow GitService around the real Git executable and test it against real temporary repositories.

Why this comes now

Worktree/review correctness depends on exact Git semantics. It deserves a dedicated boundary before worktrees are introduced.

Understand

Forge asks Git questions in domain terms—repository valid? current revision? dirty? diff? branch checked out elsewhere?—rather than letting command strings spread through the code. Git is external mutable reality: another terminal can change it, so important Operations should re-check preconditions close to execution.

Build the real project

  1. Add real-temp-repository test helpers (git init, configured test identity, initial commit).
  2. Implement repository validation, rev-parse, status/dirty check, branch/ref resolution, and worktree list parsing needed next.
  3. Return structured errors with command context but avoid logging secrets.
  4. Ensure all calls use ProcessRunner.
  5. Add tests with paths containing spaces.

Completion gate

All Git tests use real repositories and the installed Git binary. No core behavior test mocks Git output. Invalid repository and ambiguous/missing ref errors are actionable.

Pitfalls to avoid

Do not embed libgit2/go-git because it looks convenient. Do not assume .git is always a directory; linked worktrees commonly use a file.

References

Required: Git worktree documentation, git rev-parse. Vibe Kanban worktree failure: https://github.com/BloopAI/vibe-kanban/issues/287

Checkpoint

You are ready for worktrees only when Git facts come through one service boundary.


Lesson 10 — Worktrees and Execution Environments

Outcome

Create, verify, persist, and safely refuse destructive cleanup of a Task worktree; wrap it as the first Execution Environment.

Why this comes now

Isolation is central to parallel delegated work, but mature tools show that worktree lifecycle has many sharp edges. We introduce the abstraction and safety rules before agents depend on it.

Understand

Worktree creation is a pipeline, not one command: inspect → resolve base → choose stable path/branch → add → verify → optional trusted setup. Paths use immutable Project/Task IDs so renaming a Task cannot invalidate provider state tied to cwd.

ExecutionEnvironment is separate from AgentAdapter. Today it means LocalWorktree; tomorrow it can mean Docker/SSH without rewriting Claude/Codex adapters.

Build the real project

  1. Add Worktree record/lifecycle and migration.
  2. Define stable XDG worktree path from IDs.
  3. Implement CREATE_WORKTREE Operation handler.
  4. Verify branch/path/base revision after creation.
  5. Add LocalWorktreeEnvironment exposing prepared cwd/environment.
  6. Add cleanup eligibility checks and a removal Operation.
  7. Test renaming Task does not change path.
  8. Create dirty file and prove automatic removal is refused.
  9. Add a minimal Project Trust flag; do not run project-defined setup scripts yet.

Completion gate

All worktree tests use real temp repos. Dirty work is never deleted. Re-running creation reconciles expected existing state. Worktree path is stable across title changes.

Pitfalls to avoid

Do not use age as deletion permission. Do not create hidden user-visible “autosave” commits just to make cleanup easier. Be aware a target branch can be checked out in another worktree.

References

Required: git-worktree, Emdash worktree pipeline: https://github.com/generalaction/emdash/blob/main/agents/workflows/worktrees.md . Pitfalls: https://github.com/BloopAI/vibe-kanban/issues/1897 and https://github.com/BloopAI/vibe-kanban/issues/2993

Checkpoint

M3 is complete when you would trust Forge to create and refuse unsafe deletion of a worktree containing your own uncommitted code.


Lesson 11 — Driver, Instance, Strategy, Capability

Outcome

Implement the provider identity/configuration model without starting a real agent.

Why this comes now

Agent products expose multiple protocols/accounts with different guarantees. “if agent == Claude” is not an architecture.

Understand

Driver is the integration family. Instance is one configured account/installation lifecycle. Strategy is one concrete protocol. Capabilities belong to Strategy and describe semantics, not marketing features.

Two instances of the same driver must not accidentally share mutable session/catalog/account state. A richer strategy must not silently fall back to a weaker one when doing so changes product guarantees.

Build the real project

  1. Add DriverID, AgentInstance, StrategyID, and CapabilitySet types.
  2. Create an in-memory/compiled strategy registry.
  3. Persist Agent Instances only as much as current settings need.
  4. Define the first capability names needed by the fake adapter (structured events, interrupt, interactive decision later, etc.).
  5. Add capability snapshots to future Run creation model.
  6. Write tests showing feature checks ask capabilities rather than driver names.

Completion gate

No domain/application branch checks a literal provider name to enable a behavior. Two instances can exist with the same driver ID. Capability snapshot is copyable/immutable for a Run.

Pitfalls to avoid

Avoid a giant capability list before behavior exists. Avoid supportsEverything bool. Avoid silent fallback.

References

T3 provider constraints: https://github.com/pingdotgg/t3code/blob/main/docs/internals/providers.md ; Mux policy/runtime behavior: https://github.com/coder/mux/blob/main/docs/config/policy-file.mdx

Checkpoint

Before next lesson, explain Driver/Instance/Strategy using a concrete Claude/Codex example.


Lesson 12 — Task vs Run vs Turn vs process

Outcome

Add AgentRun and AgentTurn domain/storage models and make their lifetimes explicit before the fake adapter starts doing work.

Why this comes now

Without this separation, later features such as resume, review follow-ups, long-lived provider servers, and crash recovery all become awkward or misleading.

Understand

A Task can outlive every provider execution. A Run is one supervised provider/session lifetime attached to the Task. A Turn is one input → work cycle within a Run. An OS process is merely how one adapter strategy may implement a Run or Turn.

Provider completion is also not the same as Task readiness for Review: revision capture and other follow-up Operations may still be pending.

Build the real project

  1. Implement AgentRun with selected Agent Instance/Strategy, Execution Environment reference, status, stopped reason, external session ID, capability snapshot, and policy snapshot placeholder.
  2. Implement AgentTurn sequence/status/timestamps.
  3. Persist them.
  4. Add domain/application APIs that can create a Run/Turn without exposing PID as identity.
  5. Add explicit stopped-reason classification type.
  6. Write timeline tests with multiple Turns in one Run and multiple Runs for one Task.

Completion gate

Tests show Task ID, Run ID, Turn ID are independent. A Run can obtain external session ID after creation. Stopping a Run does not itself mark the Task Done.

Pitfalls to avoid

Do not put exit_code on Task. Do not assume every Run has one PID. Do not call a provider Turn “Task completed.”

References

AGENT_SYSTEM.md, T3 turn-completion notes: https://github.com/pingdotgg/t3code/blob/main/docs/internals/overview.md

Checkpoint

Update your vocabulary in code/comments if you catch yourself using “session” where Run or Turn is meant.


Lesson 13 — Build the programmable fake agent

Outcome

Run a deterministic fake agent against a real Task worktree through the real orchestration boundaries.

Why this comes now

A reliable fake gives us end-to-end confidence without model API cost, network variability, or provider changes. It also becomes the reference for every later adapter contract.

Understand

The fake should be programmable. It is not merely return success. A script can emit events, write a file, ask a Decision later, crash, delay, duplicate an event, or finish. This allows deterministic tests for timing and failure paths that are hard to force from a live model.

Build the real project

  1. Define the smallest Adapter Strategy interface required to start/drive/stop the fake.
  2. Implement a registry lookup by Strategy ID.
  3. Create Fake Adapter scripts/steps such as EmitMessage, EmitToolEvent, WriteFile, Sleep/Wait, Fail, Complete.
  4. Compose Task + Operation + LocalWorktreeEnvironment + Fake Adapter in the application/orchestrator.
  5. Have the fake edit a real file in the test worktree.
  6. Add contract tests for normal completion, cancellation, crash, and multiple Turns.
  7. Keep the fake deterministic by default—no random sleeps.

Completion gate

One integration test starts a Task, prepares its worktree, runs Fake Adapter, changes a file, records Run/Turn outcome, and leaves the Task in a sensible post-work state without any real provider.

Pitfalls to avoid

Do not let the fake bypass the same interfaces real adapters will use. Do not write tests that depend on timing sleeps when a deterministic gate/channel works.

References

TESTING.md, Go testing: https://pkg.go.dev/testing

Checkpoint

M4 is complete when the fake can reproduce failure modes you expect from a real provider.


Lesson 14 — Raw before parse

Outcome

Persist every fake/provider raw frame before normalization and make raw history robust to a partial final write.

Why this comes now

Once real protocols arrive, parser bugs and version drift are inevitable. Keeping raw evidence makes them diagnosable and re-normalizable.

Understand

The safe order is: receive frame → append durable raw record → obtain RawRef → parse/normalize. Raw storage is append-only and lives outside SQLite. SQLite stores queryable normalized state.

Frame the raw JSONL with Forge metadata such as format version, sequence, timestamp, source, and provider payload. JSONL is simple, but it still needs a version contract.

Build the real project

  1. Add RawLog interface and local file implementation under XDG data path.
  2. Define a framed line schema with formatVersion/sequence/source/receivedAt/payload.
  3. Append with ordering per Run and return a RawRef.
  4. Change fake adapter ingestion so normalization cannot occur without a RawRef.
  5. Add reader/recovery behavior that ignores or reports a corrupt/truncated final line without losing earlier valid lines.
  6. Test parser failure after raw append.

Completion gate

Every normalized event test can point to non-empty raw evidence. A simulated truncated final line does not make valid earlier history unreadable.

Pitfalls to avoid

Do not keep the only raw copy in memory. Do not put giant transcripts into SQLite. Do not pretend “JSONL” means schema evolution is solved.

References

Forge ADR direction in DATA_MODEL.md; newline-delimited JSON overview: https://jsonlines.org/

Checkpoint

Record the on-disk raw format version in docs if implementation differs from the proposed example.


Lesson 15 — Normalize events without lying

Outcome

Create a small normalized AgentEvent vocabulary with mandatory RawRef and Fact/Claim provenance.

Why this comes now

The UI and domain cannot understand every provider's native event names. Normalization gives Forge one language, but normalization must not erase uncertainty or invent conclusions.

Understand

A normalized event is a projection over retained raw evidence. Observed tool/process/file results are Facts. Agent-authored intent/conclusions are Claims. Unknown complete messages remain recoverable; unknown types that Forge would act on must fail closed. High-frequency token deltas do not automatically deserve permanent normalized rows.

Build the real project

  1. Define a deliberately small AgentEvent set needed by the fake: assistant message completed, tool call lifecycle, file/action fact, provider result, error, optional intent claim.
  2. Require EvidenceKind and RawRef.
  3. Persist/query normalized events.
  4. Implement adapter-to-canonical mapping for fake events.
  5. Add unknown-message diagnostic behavior.
  6. Keep live partial text as a separate transient path or clearly mark it non-historical.

Completion gate

Compile/tests make it difficult or impossible to create a normalized event without provenance and RawRef. Unknown action/control event does not get guessed into a known command.

Pitfalls to avoid

Do not create 100 canonical event types by mirroring one provider. Do not label an agent statement as an observed Fact. Do not persist every text chunk forever.

References

Sculptor structured integration: https://github.com/imbue-ai/sculptor/blob/main/docs/help/integrated_harnesses.md ; T3 architecture: https://github.com/pingdotgg/t3code/blob/main/docs/internals/overview.md

Checkpoint

M5 is complete when raw and normalized histories have clearly different responsibilities.


Lesson 16 — Electron shell and secure renderer

Outcome

Turn the desktop scaffold into a safe shell that starts/stops the Go core and renders a basic Forge window.

Why this comes now

We intentionally bring UI in now instead of waiting until every backend feature exists. This validates whether the architecture maps naturally to real usage.

Understand

Electron has privileged main-process capabilities and an untrusted-style renderer boundary. The renderer should behave like a web client: no direct local command/filesystem access. Main starts the Go core, monitors it, and supplies only the narrow connection/bootstrap information needed by the renderer.

Build the real project

  1. Configure BrowserWindow with context isolation, no Node integration, and sandbox.
  2. Implement main-process Go core lifecycle for development (or connect to a separately started core if that is simpler first).
  3. Generate/hand off a random local session credential securely.
  4. Add preload only for narrowly required desktop features; RPC can remain normal loopback HTTP from renderer when policy permits.
  5. Show server/core status in the app.
  6. Handle core startup failure visibly.

Completion gate

Electron security settings are asserted/reviewed. The renderer cannot import Node filesystem/process APIs. Close/reopen starts/stops or reconnects to core predictably.

Pitfalls to avoid

Do not put Task business rules in Electron main. Do not add a generic IPC “run command” bridge. Do not enable Node integration to save five minutes.

References

Required: Electron security, Electron process model. Read SECURITY.md.

Checkpoint

Note any Linux/Wayland/AppImage lifecycle issue in the Learning Log; desktop platform behavior is evidence-driven.


Lesson 17 — React data ownership and the first Task workspace

Outcome

Use the desktop app to create/list/open a Task and start the Fake Adapter while seeing bounded live status/activity.

Why this comes now

This is the first moment Forge becomes something you can actually use. The goal is not visual polish; it is validating state ownership and workflow.

Understand

TanStack Query owns authoritative core/server data. Zustand is only ephemeral presentation state such as selected pane or sidebar width. A command mutation can show “accepted/pending” without pretending an external Operation already succeeded.

Start with Task List + Task Workspace, not Kanban. The Task workspace is where we will later add review, plans, decisions, and activity.

Build the real project

  1. Add generated client/query layer.
  2. Build Project/Task list and Task detail route.
  3. Add create Task + criteria form.
  4. Add state actions through command RPCs.
  5. Start fake work through a real core command.
  6. Display Task state, Operation/start status, Run/Turn state, and a small live activity list.
  7. Add clear error and degraded/starting states.
  8. Keep server data out of global Zustand.

Completion gate

Manual acceptance: create a Task, make Ready, Start, watch fake work, close/reopen and find persisted Task state. Frontend tests cover at least the main route and mutation states.

Pitfalls to avoid

Do not build drag-and-drop Board first. Do not duplicate Task state in a client store and manually keep it in sync. Do not render raw protocol history as the activity UI.

References

TanStack Query, TanStack Router, Zustand, SCREEN_BUILD_ORDER.md.

Checkpoint

M6 is complete when you can use Fake Forge through the desktop without touching the CLI harness for the happy path.


Lesson 18 — Probe a real provider safely

Outcome

Detect whether the first real coding-agent strategy is available without accidentally opening a real session or running provider hooks.

Why this comes now

Provider setup can have side effects: starting MCP servers, hooks, login flows, or mutable sessions. Settings health checks must be cheaper and safer.

Understand

Separate Probe from Start. Probe should answer: binary found? version readable? supported protocol likely available? basic configuration/auth status if provider exposes a side-effect-minimal check? It should not be treated as absolute proof that a future Run will start successfully.

Build the real project

  1. Close OD-002 and document the first provider/strategy.
  2. Implement a side-effect-minimal probe behind the adapter strategy.
  3. Parse and store/report provider version diagnostics.
  4. Add capability availability output.
  5. Surface unavailable/unsupported state in Settings or a small provider diagnostic page.
  6. Add fixtures/tests for missing binary, unsupported version, and healthy probe.

Completion gate

Opening/reloading the Settings diagnostic repeatedly does not create provider sessions, start task work, or mutate repository/user config. Missing/unsupported provider produces an actionable explanation.

Pitfalls to avoid

Do not implement health check by launching a full chat session. Do not assume a cached model list proves current authentication. Do not silently select another strategy when probe fails.

References

T3 provider guidance: https://github.com/pingdotgg/t3code/blob/main/docs/internals/providers.md

Checkpoint

Record the exact live provider version/protocol surface used for development in a provider-specific README/fixture metadata.


Lesson 19 — First real adapter

Outcome

Run one real coding agent inside the prepared Task worktree through a structured protocol while retaining raw evidence and normalized state.

Why this comes now

The fake has proven our architecture. Now we integrate one real source of messy external behavior without letting it define the whole system.

Understand

The adapter owns provider protocol and Run semantics. The execution environment owns cwd/location. Raw frames are persisted before decode. External session identity may arrive after startup. Provider result semantics—not only exit code—classify Run stop. Capabilities are explicit and no feature silently degrades.

Build the real project

  1. Implement strategy startup through ProcessRunner or provider service protocol as appropriate.
  2. Apply per-run configuration/isolation rules without writing user/repo config.
  3. Feed raw protocol frames through RawLog before parse.
  4. Normalize only the event types Forge currently understands.
  5. Capture external session ID when assigned.
  6. Implement interrupt/cancel semantics.
  7. Implement correct provider completion/stopped-reason classification.
  8. Add captured protocol fixtures and adapter contract tests.
  9. Run a disposable live acceptance repository and inspect raw + normalized output.

Completion gate

A real agent edits the Task worktree, Forge displays structured progress, cancellation works, and completion is classified from provider semantics. Unknown protocol message remains diagnosable. No provider name branches appear in domain/UI.

Pitfalls to avoid

Do not scrape terminal text when a machine protocol exists. Do not modify global/repo provider config as a hidden setup step. Do not swallow parser errors. Do not treat exit 0 as definitive semantic success.

References

Sculptor integrated harness: https://github.com/imbue-ai/sculptor/blob/main/docs/help/integrated_harnesses.md ; T3 provider constraints: https://github.com/pingdotgg/t3code/blob/main/docs/internals/providers.md ; use the selected provider's official protocol/CLI docs too.

Checkpoint

M7 is complete only after one live task and the deterministic contract suite both pass.


Lesson 20 — Revisions and diffs

Outcome

Capture exact candidate revision identity after agent work and show a diff relative to the intended base.

Why this comes now

Review and verification are meaningless if Forge cannot answer “which exact code did we evaluate?”

Understand

A provider Turn completing does not automatically mean the Review candidate is settled. Forge may still need to inspect Git status, compute the revision/worktree state, and record the candidate. Git identity is the anchor for later validity.

Do not auto-commit user-visible history purely for Forge. If working-tree diffs are the candidate representation early on, define the identity carefully (base revision + worktree state fingerprint or a hidden checkpoint). Simpler first implementation may require/produce commits if your chosen provider workflow already does; document the exact rule.

Build the real project

  1. Define CandidateRevision representation.
  2. Add post-turn Operation that captures/settles candidate code state.
  3. Implement Git diff query against base/candidate.
  4. Store enough identity for stale checks.
  5. Expose diff file list and hunks through paged/on-demand RPC.
  6. Add first diff viewer to Task workspace.

Completion gate

Change code after capturing candidate A and prove Forge detects that the current code no longer matches A. Diff tests cover rename/add/delete/basic binary handling policy.

Pitfalls to avoid

Do not call Turn complete and instantly set Task Review before candidate capture settles. Do not create misleading autosave commits without an explicit policy.

References

git diff, T3 checkpoint/revision separation: https://github.com/pingdotgg/t3code/blob/main/docs/internals/overview.md

Checkpoint

Document the exact candidate-revision identity rule chosen for the first implementation.


Lesson 21 — Verification that becomes stale correctly

Outcome

Run deterministic project checks against an exact candidate revision and invalidate their usefulness when code changes.

Why this comes now

A green test badge is dangerous if it silently survives later code changes.

Understand

Verification Definition says what to run; Verification Run records what happened against a revision. Historical runs never mutate into having run against new code. “stale” is the relationship between a current candidate and an old run.

Project-defined commands cross the Project Trust boundary and must execute through ProcessRunner/ExecutionEnvironment.

Build the real project

  1. Add VerificationDefinition and VerificationRun storage/domain.
  2. Add project trust check before executing project-defined commands.
  3. Add RUN_VERIFICATION Operation.
  4. Bind run to CandidateRevision.
  5. Capture status/output summary without dumping unlimited output into memory/DB.
  6. Display Passed/Failed/Error and Current/Stale.
  7. Modify code after a pass and prove the UI/core shows it stale.

Completion gate

A pass for revision A is not considered current for B. Untrusted projects cannot silently run configured verification commands. Cancellation/error states are visible.

Pitfalls to avoid

Do not make verification exit code the only possible semantic result when a check protocol later carries richer status. Do not store megabytes of stdout blindly in a Task row.

References

SECURITY.md, DATA_MODEL.md.

Checkpoint

Keep OD-001 open unless using the loop gives enough evidence to decide whether failed required checks hard-block approval.


Lesson 22 — Human review and change requests

Outcome

Complete the core rinse-and-repeat quality loop: review candidate A, request changes, produce B, mark old evidence stale, and human-approve B to Done.

Why this comes now

This is the heart of the product. Everything before this was infrastructure needed to make this loop reliable.

Understand

Review is a record against one exact revision, not a Task status subtype. Human and AI reviewers are reviewer kinds. Requesting changes moves the Task back to Working and creates a later agent Turn/Run with review feedback as context. Approval applies only to the current candidate.

Acceptance criteria, verification, comments, and assumptions eventually appear together because they are different evidence for the same human judgement.

Build the real project

  1. Add Review and ReviewComment.
  2. Bind Review to CandidateRevision.
  3. Build Review workspace: file tree/diff, criteria, verification, comments.
  4. Add RequestChanges command that validates the Review is current and returns Task to Working.
  5. Feed review feedback into a new agent Turn (fresh Run is acceptable initially if resume semantics are complex).
  6. Capture candidate B and show review/verification A stale.
  7. Add ApproveTask command with current-review checks.
  8. Add optional reviewer-kind model but do not require AI review for human approval unless later policy says so.

Completion gate

End-to-end acceptance exactly follows A → review → changes → B → stale A evidence → review B → human approve → Done. Illegal approval of stale revision fails.

Pitfalls to avoid

Do not add agent_review or human_review Task statuses. Do not let UI call generic UpdateTask(status=DONE). Do not assume review comments belong forever to mutable line numbers after revision changes.

References

Plannotator (integration inspiration): https://github.com/backnotprop/plannotator ; PRD.md.

Checkpoint

M8 is complete. Use Forge on a small real change before adding Planning; write down every point where the review loop felt confusing or slow.


Lesson 23 — Immutable Plans and review rounds

Outcome

Create Feature/Task Plans whose reviewed content is immutable by revision and whose rejection produces a new revision.

Why this comes now

Planning comes after the work/review loop so it can reuse known revision/review ideas instead of creating a second ad-hoc system.

Understand

Plan is identity/lifecycle; PlanRevision is immutable content. A review belongs to the exact PlanRevision. Rejection returns the Plan to Draft for a new revision; it does not mutate the reviewed text. Feature and Task Execution Plans share machinery but produce different outcomes.

Build the real project

  1. Add Plan scope, Plan, PlanRevision, PlanReview/annotation models.
  2. Implement create revision, submit review, reject, approve commands.
  3. Build simple Markdown Plan workspace and revision history.
  4. On rejection, carry annotations as context to the next authoring pass without rewriting history.
  5. Add Task Execution Plan attachment/use after approval.
  6. Keep Feature decomposition for next lesson.

Completion gate

Revision 1 remains readable after revision 2 exists. Review 1 always points to revision 1. Approved Plan identifies the approved revision explicitly.

Pitfalls to avoid

Do not store only plans.content + round. Do not use “decomposed” as a replacement for approved plan status. Do not invent a different review engine for task plans.

References

Original workflow is preserved in docs/archive/00-original-agent-task-orchestrator.md; compare it with current DOMAIN_MODEL.md.

Checkpoint

Update docs/status/CURRENT.md and note whether immutable plan history feels useful in actual use or unnecessarily heavy; evidence can refine the model.


Lesson 24 — Decomposition and simple dependencies

Outcome

Turn an approved Feature Plan into an editable proposed Task set, confirm it, and persist simple acyclic dependencies.

Why this comes now

Plans become useful when they create executable work, but automatic decomposition should remain a proposal until a human confirms it.

Understand

A planner can propose Tasks plus dependsOn relationships. Forge shows the proposal, lets you edit it, validates the graph, then commits Tasks/dependencies idempotently. Dependency edges do not mean Forge needs a general workflow/DAG engine.

Decomposition is an action against an approved Plan Revision, not a new Plan status that erases “approved.”

Build the real project

  1. Add decomposition proposal/result model.
  2. Use Fake Planner or first real adapter to produce structured proposed Tasks.
  3. Build preview/edit UI.
  4. Validate no self-edge, missing target, or cycle (simple DFS/topological check is enough).
  5. Confirm in one transaction: Tasks + criteria + dependency edges + decomposition record/idempotency key.
  6. Support requires task execution plan as a Task property/policy if useful without changing Task status.

Completion gate

Reject a cyclic proposal. Confirming the same decomposition twice does not duplicate Tasks. Human can edit titles/descriptions/dependencies before commit.

Pitfalls to avoid

Do not let planner output write directly into production tables before confirmation. Do not build Airflow. Do not infer dependency completion from branch names.

References

Directed acyclic graph basics, original Plan loop archive: docs/archive/00-original-agent-task-orchestrator.md.

Checkpoint

M9 is complete when a reviewed Feature Plan can create real work without bypassing human confirmation.


Lesson 25 — Decisions and Needs You

Outcome

Create durable Decisions that survive restart and move a blocked Task into and out of NEEDS_YOU.

Why this comes now

Before injecting a tool into a real provider, the domain/UI path for human judgement must work independently.

Understand

A Decision is more than a transient question dialog. It has a reason, context, options, recommendation when applicable, origin Run/Turn, blocking semantics, and a Resolution. Blocking unresolved Decisions are why a Task shows NEEDS_YOU.

Persist the Decision before notifying the UI. The process/channel used to deliver an answer is temporary; the Decision is durable truth.

Build the real project

  1. Add Decision, Option, Resolution tables/domain.
  2. Define the initial escalation reason enum from current product vocabulary.
  3. Add application command to open a blocking Decision from Fake Adapter.
  4. Move Task Working → Needs You based on the durable blocked state.
  5. Build Task Decision panel and basic global Decision Inbox.
  6. Resolve with human action and return Task to Working.
  7. Restart while a Decision is open and prove it still appears.

Completion gate

Open Decision survives core/desktop restart. Human resolution is distinguishable from any future default resolution. Task returns to Working only after the blocking condition is resolved.

Pitfalls to avoid

Do not store pending questions only in channels. Do not mark agent recommendation as human judgement. Do not notify before persistence and risk a ghost question.

References

Sculptor pending questions: https://github.com/imbue-ai/sculptor/blob/main/docs/help/integrated_harnesses.md ; Forge vocabulary.

Checkpoint

Use the Fake Adapter to exercise several Decision shapes before deciding final visual density.


Lesson 26 — Injected escalation and capability preflight

Outcome

Let a capable real agent raise Forge Decisions through an injected tool and prove the tool is actually usable before enabling dependent modes.

Why this comes now

Watching stdout cannot reveal choices the model considered but never said. Structured escalation must be part of the interaction contract.

Understand

Forge registers its own escalation tool for the run and instructs the model to use it. Depending on provider, native question tools may be suppressed/replaced. Registration is not enough: a permission system can deny the tool while the agent silently continues by guessing, so Forge preflights the capability.

Forge configures the individual run, not repository/user-global agent config.

Build the real project

  1. Implement the escalation tool server/handler (MCP or provider-native mechanism).
  2. Map tool call → durable Decision before answering/returning.
  3. Support adapter-declared blocking or exit-and-resume semantics.
  4. Add preflight that verifies the tool is actually invocable for the selected Strategy/config.
  5. If preflight fails, disable/refuse escalation-dependent Involvement Modes with a clear reason.
  6. Live-test an ambiguity where the agent asks and obeys an answer contrary to its recommendation.
  7. Test identical task with capability disabled to understand the difference.

Completion gate

A real agent raises a Decision; it survives restart; answer is delivered/resumed correctly; a deliberately denied tool fails preflight rather than silently claiming support.

Pitfalls to avoid

Do not infer architectural decisions from text. Do not treat “MCP configured” as proof “MCP callable.” Do not modify ~/.claude/repo config to make the integration easy.

References

Sculptor substituted tools: https://github.com/imbue-ai/sculptor/blob/main/docs/help/integrated_harnesses.md ; current archived Forge ADR research in docs/archive/01-previous-forge-overview.md.

Checkpoint

Record actual escalation behavior/rate in the Learning Log. This is a product hypothesis that must be measured, not assumed.


Lesson 27 — Involvement modes and host gating

Outcome

Separate “how often should the agent consult me?” from “what actions may execute without host approval?”

Why this comes now

A delegated agent can still attempt a destructive command. Conversational autonomy and execution permission are different axes.

Understand

Execution Mode answers who writes code. Involvement Mode is a preset over escalation reasons. Gating Tier is host enforcement over semantic tool capability classes such as read/write/exec.

An agent saying “this is safe” is a Claim. Host observation/classification is enforcement evidence. Unknown classification should not silently become safe when the consequence matters.

Build the real project

  1. Implement policy snapshot captured immutably on Run start.
  2. Define minimal Involvement Mode presets based on observed escalation behavior, not old names alone.
  3. Add semantic host gating classes and decision path for blocked actions where the provider protocol exposes tool calls.
  4. Keep gating independent from involvement.
  5. Add tests: Delegate still blocks a destructive host-classified action; Teach/Collaborative mode unavailable on strategy without interactive Decision capability.
  6. Never allow destructive-action Decisions to auto-default.

Completion gate

Policy behavior is derived from explicit snapshot/capabilities. Changing global settings later does not rewrite what policy a historical Run used.

Pitfalls to avoid

Do not map permissions by provider-specific tool names throughout the domain. Do not assume the agent's self-classification is the only safety layer. Do not make Delegate mean “anything goes.”

References

OpenHands security/confirmation concepts: https://docs.openhands.dev/sdk/arch/security ; Mux fail-closed policy behavior: https://github.com/coder/mux/blob/main/docs/config/policy-file.mdx

Checkpoint

M10 is complete when human judgement is a real end-to-end path rather than a UI concept.


Lesson 28 — Dependency readiness

Outcome

Compute which Tasks are eligible to run from dependencies and Task state without adding a new orchestration language.

Why this comes now

Planning can now create dependency edges. The scheduler needs one trustworthy answer: “is this Ready Task eligible?”

Understand

A dependency is simple: Task C cannot be assigned until every required predecessor satisfies the chosen completion/integration condition. Early Forge uses Done (or a clearly documented integrated state if introduced later). Eligibility is derived; do not store a second mutable “is_unblocked” flag that can drift.

Build the real project

  1. Add repository/query for dependency satisfaction.
  2. Validate cycles on every edge insertion path, not only decomposition.
  3. Define EligibleTask query: status Ready + dependencies satisfied + no incompatible active Run/Operation + environment/provider available as needed.
  4. Surface blocked-by information in Task UI.
  5. Add tests for diamond graph, no dependencies, unfinished predecessor, and cycle rejection.

Completion gate

Eligibility changes automatically when predecessor state changes; no manual unblocking update is required.

Pitfalls to avoid

Do not use dependency graph as branch stacking yet. Do not persist derived eligibility unless profiling proves a need and invalidation is explicit.

References

DOMAIN_MODEL.md, topological sorting.

Checkpoint

Only proceed to scheduler when eligibility logic is readable and heavily tested.


Lesson 29 — Bounded scheduler and parallel agents

Outcome

Run multiple eligible Tasks concurrently with configurable limits and independent worktrees.

Why this comes now

Parallelism is useful only after isolation, recovery, and readiness are trustworthy. Earlier would multiply failure modes.

Understand

The scheduler is a small local coordinator, not a distributed queue. It repeatedly reacts to meaningful state changes, selects eligible Tasks, and starts work up to configured capacity. Every Task keeps its own Worktree/Run/Operation identities.

Use bounded concurrency. Cancellation and shutdown flow through contexts. Avoid aggressive auto-parallelism that makes human supervision harder.

Build the real project

  1. Add configurable max_concurrent_agent_runs with conservative default.
  2. Implement scheduler service driven by state notifications/ticks as simply as reliability allows.
  3. Select eligible Tasks deterministically/fairly enough for first version.
  4. Start through existing application/Operation path—scheduler must not bypass domain rules.
  5. Add tests A+B independent, C depends on both.
  6. Add graceful shutdown test with active fake Runs.
  7. Surface active capacity and blocked Tasks in UI.

Completion gate

A and B run concurrently up to limit, C waits, reducing limit does not kill valid work unexpectedly, shutdown is controlled, no duplicate Run starts for one Task.

Pitfalls to avoid

Do not create one goroutine forever per backlog Task. Do not poll the database at millisecond frequency. Do not start dependent branches from unmerged sibling work as an early optimization.

References

Go concurrency patterns: https://go.dev/blog/pipelines ; OPERATIONS_AND_RECOVERY.md.

Checkpoint

M11 is complete when parallel fake/real work remains understandable, not merely technically concurrent.


Lesson 30 — Facts, claims, and meaningful activity

Outcome

Build a human-readable activity feed that is useful without pretending agent narration is verified fact.

Why this comes now

The central UX problem is returning awareness without reading transcripts. We waited until real Runs exist so activity is based on evidence, not imagined schemas.

Understand

Two tiers work well: deterministic Steps from observed events, plus coarse agent-authored phases/intent/conclusions where the selected strategy can provide or accept them. The data model preserves Fact vs Claim. If narration is absent, Forge falls back to facts honestly.

Do not run a second summarizer model over every event by default; it adds cost/latency and can fabricate context.

Build the real project

  1. Define ActivityItem projection from normalized AgentEvents.
  2. Group low-level tool lifecycle into useful Steps.
  3. Add optional native-plan/intent events as Claims when strategy supports them.
  4. Add injected narration marker only for findings/phase boundaries not already provided natively.
  5. Build paged Activity UI with collapsed detail and raw-evidence drill-down.
  6. Preserve source Run/Turn and evidence kind.
  7. Add tests showing missing narration still produces a fact-only feed.

Completion gate

A real Task's activity can be understood at a glance; clicking deeper reaches normalized/raw evidence; a Claim is never rendered/persisted as Fact.

Pitfalls to avoid

Do not infer “Found reusable retry infrastructure” from a Bash command. Do not display every token/tool delta as a top-level feed row. Do not hide absence of narration by generating fake summaries.

References

Sculptor structured UX: https://github.com/imbue-ai/sculptor/blob/main/docs/help/integrated_harnesses.md ; archived Forge research ADR-0011/0012 in docs/archive/01-previous-forge-overview.md.

Checkpoint

Use the feed for real work and record which low-level steps are noise. Tune projection from evidence.


Lesson 31 — Assumptions ledger

Outcome

Ask capable agents to record important choices they made without escalating and surface those Claims in Review.

Why this comes now

The dangerous failure is often the missing Decision: the agent made an assumption silently. The ledger narrows where a reviewer should look without pretending to replace review.

Understand

The Assumptions Ledger is agent-authored and therefore a Claim. It should be captured at a natural Turn boundary through the same structured integration channel used for narration/escalation when available. Missing ledger capability must degrade honestly, not fabricate assumptions.

Build the real project

  1. Define a structured assumption entry: statement, reason/context, confidence/impact only if useful, related files/criteria optionally.
  2. Extend capable adapter prompt/tool contract.
  3. Store as Claims linked to Run/Turn/RawRef.
  4. Surface in Review beside diff/criteria/verification.
  5. Add fake scenarios with no assumptions, one assumption, and malformed output.
  6. Measure whether ledger is helpful or verbose in several real Tasks.

Completion gate

Review clearly labels assumptions as agent-authored. Absence is shown as unavailable/none, not filled by Forge.

Pitfalls to avoid

Do not treat the ledger as proof that all assumptions were disclosed. Do not block Done merely because a provider lacks the capability unless product evidence later justifies it.

References

Archived Forge ADR-0008: docs/archive/01-previous-forge-overview.md; UX_PRINCIPLES.md.

Checkpoint

Record whether the ledger changed your review behavior. This feature exists to improve human judgement, not to produce more text.


Lesson 32 — Realtime sync, coalescing, and bounded replay

Outcome

Make live activity responsive and reconnect safely without replaying an unbounded provider firehose.

Why this comes now

Comparable tools have frozen clients and displayed stale state even while transport remained connected. We now have enough real activity volume to build this correctly.

Understand

Separate raw storage, live stream, and historical read model. Initial state arrives as a snapshot/query; live events advance it. Small reconnect gaps can replay in batches; large gaps use a fresh snapshot. Track Connection Health separately from Sync Health because a subscription can die while the socket stays alive.

Coalesce updates before publishing to React. Replaying 2,000 token deltas as 2,000 full state publications is avoidable work.

Build the real project

  1. Add sequence/cursor semantics for relevant UI update stream.
  2. Implement bounded resume gap and snapshot fallback.
  3. Handle cursor-ahead invalidation.
  4. Subscribe/buffer around snapshot so no race loses events.
  5. Batch/coalesce replay and high-frequency live deltas.
  6. Add ConnectionStatus + SyncStatus.
  7. Create deterministic large-history perf test/playground.
  8. Simulate killed subscription while connection remains and prove client detects/re-hydrates.

Completion gate

Large-gap reopen does not replay every raw event. UI publication count is bounded/batched. CONNECTED + STALE can be represented and recovered. Performance test prevents obvious quadratic regression.

Pitfalls to avoid

Do not equate WebSocket/HTTP connection alive with data current. Do not use subscriptions as historical pagination API. Do not derive entire timeline from scratch on every token delta.

References

T3 replay bug: https://github.com/pingdotgg/t3code/issues/4596 ; stale subscription: https://github.com/pingdotgg/t3code/issues/4589 ; stale remote projection: https://github.com/pingdotgg/t3code/issues/5742 ; REALTIME_SYNC.md.

Checkpoint

M12 is complete after a long fake history and a real provider history both reopen smoothly.


Lesson 33 — Crash recovery and reconciliation

Outcome

Deliberately kill Forge at important boundaries and make startup converge to truthful recoverable state.

Why this comes now

A daily-driver orchestrator must survive crashes without losing work or lying. We introduced Operations early specifically so this lesson is tractable.

Understand

Recovery is not “set every Running row to Failed.” It asks external reality what happened and reconciles according to each subsystem's guarantees. Some provider strategies can resume/discover sessions; some cannot. Worktrees are inspectable. Verification effects can often be rerun.

A crash is expected at every boundary between durable state and external effect.

Build the real project

  1. Enumerate a recovery matrix for Operation kinds and Run states.
  2. Add startup reconciliation before normal scheduling.
  3. Add integration tests killing/restarting around: worktree create, agent start, open Decision, candidate capture, verification completion.
  4. Preserve dirty work regardless of uncertainty.
  5. Mark states needs intervention/failed honestly where automatic reconciliation is impossible.
  6. Ensure shutdown uses contexts and stops admitting new work before tearing down workers.

Completion gate

Defined crash-boundary tests pass. Restart never marks a Task Done merely because previous process disappeared. Uncertain provider state is explicit. Dirty worktree survives.

Pitfalls to avoid

Do not blanket-reset RUNNING → FAILED without asking what state means. Do not retry a provider blindly if duplicate execution could edit the same worktree concurrently.

References

OPERATIONS_AND_RECOVERY.md, T3 durable intent: https://github.com/pingdotgg/t3code/blob/main/docs/internals/overview.md

Checkpoint

Write the recovery matrix into the architecture doc rather than leaving knowledge only in tests.


Lesson 34 — Performance, diagnostics, migrations, packaging

Outcome

Turn the working system into a dependable Linux daily-driver build with measured performance and upgrade confidence.

Why this comes now

Only now do we know what needs hardening. Premature packaging/observability would have optimized architecture that was still moving.

Understand

Hardening includes database migration fixtures, structured diagnostics, performance regression scenarios, backup/recovery, Electron/core lifecycle, and packaging. Large raw logs/history require bounded retention/read behavior without deleting evidence unexpectedly.

Packaging should keep the Go core independently executable while bundling it with Electron.

Build the real project

  1. Add old-schema DB fixtures and upgrade tests.
  2. Version long-lived JSON payload schemas.
  3. Add structured logging + diagnostic export that avoids casually leaking secrets.
  4. Profile core memory/goroutines and UI long-history behavior.
  5. Add explicit limits/retention settings where evidence requires them.
  6. Add DB backup/restore guidance.
  7. Package AppImage first for the target Linux environment; then .deb/AUR only as useful.
  8. Test clean install, upgrade, crash/restart, and uninstall without destroying project repositories/worktrees unexpectedly.

Completion gate

A packaged build completes the core acceptance loop. Old DB fixtures migrate. Large-history perf scenario stays within documented expectations. Diagnostic bundle can explain a failed Operation/Run without reading the database manually.

Pitfalls to avoid

Do not add a self-updater before basic packaging is reliable. Do not log provider secrets/tokens. Do not load huge native modules into Electron main casually; isolate risky native capability if introduced.

References

Electron packaging docs: https://www.electronjs.org/docs/latest/tutorial/application-distribution ; modernc SQLite: https://modernc.org/sqlite ; Vibe migration failure example: https://github.com/BloopAI/vibe-kanban/issues/2972 ; memory/long-session example: https://github.com/BloopAI/vibe-kanban/issues/3352

Checkpoint

M13 is complete when you can install Forge on your normal Linux environment and trust it with a real small project.


Lesson 35 — Add a second adapter/environment without breaking the architecture

Outcome

Use a second provider strategy or execution environment as an architecture test: adding it should not require provider/runtime branches throughout Forge.

Why this comes now

An abstraction is not proven by its first implementation. The second implementation exposes where we accidentally coupled concepts.

Understand

Choose based on real need. A second Agent Strategy is likely more valuable first; a second Execution Environment (Docker/SSH) comes only if your workflow needs it. The existing contract suite should do most of the behavioral validation.

When capabilities differ, the UI/domain should naturally enable/disable behaviors through capability semantics rather than if provider == ....

Build the real project

  1. Choose one new Driver/Strategy or Environment.
  2. Document its actual protocol/lifecycle differences from the first.
  3. Implement through existing boundary.
  4. Run shared contract suite.
  5. Add only new capabilities that correspond to real semantic differences.
  6. Search codebase for provider/runtime-name conditionals; justify or remove them.
  7. Revisit interfaces that became awkward and simplify them while keeping canonical docs current.

Completion gate

Second integration works without changing Task lifecycle or React feature logic around provider names. Unsupported capabilities produce intentional UI behavior. Contract suite catches semantic gaps.

Pitfalls to avoid

Do not preserve a bad abstraction simply because “the ADR says so.” ADRs explain decisions; evidence from a second implementation can justify change. Do not create a public plugin API yet.

References

OpenHands environment separation: https://docs.openhands.dev/openhands/usage/sandboxes/overview ; Mux runtimes: https://github.com/coder/mux/blob/main/docs/config/policy-file.mdx ; Emdash remote/worktree overview: https://github.com/generalaction/emdash

Checkpoint

At this point the course becomes normal product development. Create feature plans from actual needs and continue using the documentation/status discipline rather than extending the course speculatively.

Forge is local-first. The docs are part of the product engineering system.