--- title: Getting Started description: Install eforge and run your first delegated build. --- # Getting Started eforge turns normalized build source into reviewed, validated code changes. For your first build, start directly from a prompt, PRD, or file path; optional workflow surfaces such as first-party `eforge-playbooks`, session plans, and first-party planning extensions can prepare build source before it reaches the same kernel. ## Prerequisites - **Node.js 22+** - One of: [Pi](https://github.com/earendil-works/pi-mono), [Claude Code](https://claude.ai/code), or an npm-capable shell - An LLM credential for the runtime you choose: a provider-specific API key or OAuth token for the recommended `pi` harness, or an Anthropic API key for the supported secondary `claude-sdk` harness ## Install ### Pi package (recommended) Pi is the recommended harness for new users: you choose the providers, pay API prices directly, and keep orchestration local and inspectable. ```bash pi install npm:@eforge-build/pi-eforge /eforge:init ``` Add `-l` to write to project settings (`.pi/settings.json`) instead of your global Pi settings: ```bash pi install -l npm:@eforge-build/pi-eforge ``` ### Claude Code plugin Use the Claude Code plugin if Claude Code is already your daily environment. Claude Code can host the workflow while your active profile executes builds through the recommended Pi harness. Run these three commands inside Claude Code: ``` /plugin marketplace add eforge-build/eforge /plugin install eforge@eforge /eforge:init ``` The `/eforge:init` command creates `eforge/config.yaml` with sensible defaults and adds runtime-state entries such as `.eforge/` and `eforge/.active-profile` to your `.gitignore`. It walks you through a Quick setup (one harness/provider with suggested tier models, including an optional separate implementation model) or a Mix-and-match flow (different harness, provider, or model per tier). Choose Pi for the recommended provider-flexible path; `claude-sdk` remains available as a supported Anthropic-specific secondary path for users with Anthropic Claude Agent SDK credentials. Check your provider's current account and pricing terms before running large builds. After initialization, run `/eforge:workflow` to choose the workflow preset for this repository. It writes the landing action, pull-request auto-merge policy, and stacking setup to `eforge/config.yaml`, including optional automatic stack sync for git-spice stacks. ### CLI ```bash npx @eforge-build/eforge build "Add rate limiting to the API" ``` Or install globally: `npm install -g @eforge-build/eforge` The CLI has no init command yet: run `/eforge:init` once in Pi or Claude Code to create `eforge/config.yaml` and an agent runtime profile. After that one-time setup, the CLI drives builds without either host - suited to scripting and automation. ## Your First Build Once eforge is installed and initialized, enqueue directly with a prompt: ``` /eforge:build Add a dark mode toggle to the settings page ``` From the standalone CLI: ```bash eforge build "Add a dark mode toggle to the settings page" eforge build plans/my-feature-prd.md eforge build ./docs/my-feature.md eforge build --landing-action pr plans/my-feature-prd.md ``` The daemon normalizes the prompt, PRD, or file into build source, queues it, and runs the full pipeline in the background. Console at `http://localhost:/console/` (port deterministically assigned per project in the 4567-4667 range) tracks progress, cost, token usage, and efficiency metrics in real time. Use `--profile ` for a one-off agent runtime profile override, and `--landing-action pr|merge|leave` when one build should use a different landing action from `eforge/config.yaml`. ## Optional first-party workflow extensions Optional producers can prepare build source before the kernel sees it. The first-party [`eforge-playbooks`](./playbooks) extension provides reusable workflow artifacts: autonomous playbooks normalize to build source and enqueue a build, while planning-mode playbooks route to `eforge-plan` when that extension provides the `eforge.plan.planning-workstation` capability. Install either extension only when you want its workflow: ```bash eforge extension install @eforge-build/eforge-playbooks eforge extension install @eforge-build/eforge-plan eforge extension reload ``` The default installation scope is project-local. Committed project/team installs require inspection and an explicit trust record; follow the [eforge-playbooks](./playbooks#install) or [eforge-plan](./eforge-plan#install) guide for the complete flow. Discover extension actions through generic contribution list/show/invoke surfaces rather than host-specific workflow commands. For example, list playbook contributions with `eforge extension contributions list --extension-name eforge-playbooks`. When `eforge-plan` is loaded and trusted, use `eforge extension contributions list --search planning`, `eforge extension contributions show eforge-plan:open-planning-entry --kind command`, and `eforge extension contributions invoke eforge-plan:open-planning-entry --kind command`. The [eforge-plan guide](./eforge-plan) covers investigation, session-plan drafting and revision, and handoff; ready build source is still submitted through the ordinary build path. ## What Happens Next 1. **Formatting** - eforge normalizes your input into a structured PRD. 2. **Acceptance criteria inventory** - enqueue canonicalizes acceptance criteria and rejects vague, unverifiable, or duplicate criteria before the build is queued. [Concepts](./concepts#the-queue-and-daemon) covers the full validation rules. 3. **Compile preflight compaction** - eforge may compact generated or machine-readable bulk in planner prompts while preserving the full source for artifacts and validation. Oversized inputs decompose into bounded context-managed planning units governed by the `compile.planningUnit*` limits. 4. **Planning** - Planner-family agents enforce prompt and live context-budget guardrails before provider context-window failures. Pi-backed live context guards use ModelRegistry context metadata and effective output reserves when available, while prompt byte defaults remain static byte guards. The [bounded planner compiler](./concepts#one-planning-path-with-model-intent-and-deterministic-floors) combines model-proposed module intent with deterministic safety constraints and writes a detailed plan or set of plans. eforge validates the persisted `orchestration.yaml` and plan files before reporting compile success. 5. **Building** - Builder agents implement each plan in isolated git worktrees, in parallel where the dependency graph allows. 6. **Review** - Blind reviewers evaluate each plan's output without builder context. A fixer applies suggestions; an evaluator accepts only strict improvements. 7. **Merge** - Completed plans merge back to your branch in topological order. 8. **Validation** - Post-merge validation runs configured commands plus any queued PRD `postMerge` commands. On failure, a validation-fixer agent attempts repairs. ## Where to Look Next - [Concepts](./concepts) - How the pipeline works, what blind review means, and what harnesses do - [Configuration](./configuration) - The most important config options and how to tune them - [Profiles](./profiles) - Create and switch agent runtime profiles that control harness, model, and effort - [eforge-playbooks](./playbooks) - First-party extension for reusable playbook management and execution - [eforge-plan](./eforge-plan) - First-party extension for planning, backlog, recommendation, and revision workflows - [Integrations](./integrations) - How to use eforge from Claude Code, Pi, the CLI, and external issue trackers - [Troubleshooting](./troubleshooting) - Daemon startup, failed builds, and common error remedies - [Glossary](./glossary) - Definitions for eforge-specific terms such as profiles, worktrees, and playbooks - [CLI Reference](/reference/cli) - All CLI commands and flags - [Configuration Reference](/reference/config) - Full `eforge/config.yaml` schema --- title: Concepts description: How the eforge build engine, inputs, and extension surfaces fit together. --- # Concepts ## Core engine and extension surfaces eforge is a build-engine kernel with extensible workflow surfaces around it. The core engine consumes normalized build source, compiles it into plans, orchestrates dependency-aware worktrees, runs implementation/review/validation loops, records conservative gates, and emits typed events for consumers. Input authoring and workflow UX live outside that core. CLI prompts, rough notes, PRD files, `eforge-playbooks` playbook workflows, session plans, wrapper-app artifacts, host integrations, toolbelts, shell hooks, and native extensions can shape how work is prepared or governed before it reaches the engine. They are extension surfaces, not separate engines. That boundary is why different hosts can feel different while the build semantics remain consistent: every input surface eventually normalizes into build source, and the engine handles the same compile/build/landing lifecycle. ## What eforge builds Traditional build systems transform source code into artifacts. eforge transforms *build source* into source-code changes - then verifies its own output. The key quality insight: a single AI agent writing and reviewing its own code will almost always approve it. Quality requires **separation of concerns** - distinct agents for planning, building, reviewing, and evaluating. eforge applies build-system thinking to this multi-agent pipeline. ## The Pipeline Every eforge build runs two phases: **Compile phase** - Runs once per build. A deterministic compaction pass may summarize generated or machine-readable bulk in planner prompts while preserving the full source for artifacts and validation. The bounded planner compiler uses a single-atom fast path for small sources and decomposes large sources into planning units governed by `compile.planningUnit*` limits. Planner agents propose coherent module boundaries and typed execution intent, including test ownership and review depth. One canonical normalization pass then enforces context and module ceilings, criterion coverage, valid dependencies, exclusive file and test ownership, compatible stages, and minimum review floors without silently inventing replacement modules. Planner-family agents also enforce prompt and live context-budget guardrails before provider context-window failures. For Pi-backed agents, live context guard token limits use ModelRegistry context metadata and effective output reserves when available; prompt byte defaults remain static byte guards. The compiler produces plan files, an orchestration manifest, and supporting architecture, coverage, and diagnostics artifacts. A planning-quality review gate can propose bounded structural simplifications such as merging cohesive plans, removing redundant stages, or reducing excessive review depth; eforge applies those changes atomically, revalidates the same safety constraints, and validates all persisted artifacts before reporting compile success. Plans then build in parallel in dependency order. **Build phase** - Runs once per plan. Builder agents implement the plan in an isolated git worktree. When the build stage completes, a blind review cycle runs, then the result merges back. The compile phase produces `orchestration.yaml` - a dependency graph over the plans. Non-skipped compiles fail closed if `orchestration.yaml` or its referenced plan files are missing, invalid, mismatched, or empty. The orchestrator launches plans as soon as their dependencies have merged, not in fixed waves. Since agent execution is IO-bound, all ready plans run immediately in parallel. ## Normalized build-source boundary A **build source** is the normalized input eforge hands to the compile phase after an outside input surface has been resolved. It can start as a CLI prompt, rough notes, a PRD file, a wrapper-app artifact, an autonomous workflow artifact, or another host-provided file, but the build-engine kernel sees normalized build source rather than the original authoring surface. That boundary keeps producer UX optional. Hosts, wrapper apps, playbooks owned by the first-party `eforge-playbooks` extension, session-plan compatibility tools, and first-party extensions such as [eforge-plan](/docs/eforge-plan) may help collect intent, investigate scope, or format a handoff. They are producers around the kernel. Once a build is enqueued, the engine consumes the normalized source through the same compile/build/landing lifecycle. ## Build Artifact Provenance When a build runs, eforge commits source artifacts into the artifact branch before building: - A canonical PRD copy is written to `eforge/prds/{prdId}.md` at dispatch time. - Compiled plan files and `orchestration.yaml` are written to `eforge/plans/{planSet}/` during compile. These committed files are the shared, team-visible provenance record linking a build session to its originating requirements and plan. When `build.cleanupPlanFiles: true` (the default), eforge removes these artifacts from `HEAD` during the `pr` or `merge` landing flows after a successful build. Cleanup also strips temporary plan-ID eforge region marker comment lines from tracked JavaScript/TypeScript-family source files while preserving durable semantic markers and marked code. `landing.action: leave` does not run cleanup and leaves the artifact branch in place for inspection. Build artifacts are not permanently lost — when the artifact branch is landed with a merge commit (eforge's local `merge` action, or a GitHub PR merged via "Create a merge commit"), the commits that added the artifacts remain reachable in Git history. Use `git show :` with a commit-pinned reference to recover any artifact. PR bodies include an **Eforge provenance** section with these references when artifact commits are found. When `landing.action: pr` is used, provenance durability depends on the repository's chosen merge strategy. The durable provenance guarantee is Git history, not the final tree. Squash or rebase merge strategies applied after a PR is opened (for example, GitHub's "Squash and merge") can collapse intermediate commits and make artifact references unreachable. Use merge commits when preserving build provenance history matters to your team. ## One planning path with model intent and deterministic floors There is no compile-time scope classification. Every build runs the same compile pipeline - the bounded planner compiler followed by a planning-quality review gate - and the compiler adapts to input size through deterministic inventory chunking. A one-line fix takes the single-atom passthrough fast path; a large cross-cutting PRD fans out into bounded planning units that synthesize into one plan set. Planner agents propose the smallest coherent module set for the source and declare execution intent. Deterministic code does not choose module boundaries from lexical labels alone and does not silently split an oversized model proposal. Instead, reducers receive exact module ceilings and can repair an invalid proposal within their bounded run; final normalization rejects proposals that still violate budgets, coverage, dependencies, ownership, stage compatibility, or review floors. Model intent may deepen review but cannot relax the deterministic safety floor, and exactly one test-authoring owner is resolved for each module. The resulting `## Execution Intent` sections and compiler diagnostics make those decisions inspectable before implementation starts. ## Separation of Concerns Each pipeline stage uses a different agent with different context: - **Builder** - Has the plan, the codebase, and all tools. Writes code and commits changes. - **Reviewer** - Has only the code diff, not the builder's reasoning. Flags issues without being anchored to the builder's intent. - **Fixer** - Applies reviewer suggestions as unstaged changes. - **Evaluator** - Judges each fix against the original plan intent. Accepts strict improvements; rejects changes that alter intent. This three-step pattern (blind review - fix - evaluate) applies to code review and to the planning-quality review of compiled plan-set artifacts. The evaluator is the safety valve: it keeps the fixer from over-correcting. ## Harnesses eforge is harness-agnostic. A **harness** is the agent execution backend - the thing that runs the LLM and tools for each agent stage. Two harnesses ship with eforge: - **`pi`** - Recommended for new eforge setup. Uses pi-agent-core for provider-flexible execution across OpenAI, Anthropic, Google, Mistral, Groq, xAI, Bedrock, OpenRouter, local models, and more. - **`claude-sdk`** - Supported secondary path for users who intentionally want the Anthropic Claude Agent SDK with Anthropic API credentials. The harness you use to *drive* eforge (Claude Code or Pi) and the harness that *executes* builds are independent. You can plan in Claude Code and build with Pi, or plan in Pi and execute through the Anthropic-specific Claude Agent SDK. You can also switch harnesses mid-project by changing your active profile. ## Tiers A **tier** is a named configuration slot: `planning`, `implementation`, `review`, and `evaluation`. Each tier specifies a harness, model, and effort level. Agent roles are assigned to tiers by default - the planner uses `planning`, the builder uses `implementation`, reviewers use `review`, and evaluators use `evaluation`. This means you can say "use a fast cheap model for implementation, a thorough slow model for review" without listing every agent role individually. ## Agent Runtime Profiles A **profile** is a named YAML file that bundles tier recipes into a reusable unit. Profiles live at three scopes: - `~/.config/eforge/profiles/` - User scope, personal, cross-project - `eforge/profiles/` - Project scope, committed, team-canonical - `.eforge/profiles/` - Project-local scope, gitignored, personal override The active profile is resolved highest-priority-first: project-local beats project beats user. You can swap profiles without touching `eforge/config.yaml` - useful for switching between harnesses or experimenting with different models. See [Profiles](/docs/profiles) for a full walkthrough. **Playbooks** are an optional workflow surface owned by the first-party `eforge-playbooks` extension: reusable Markdown templates for recurring work that optionally pin a profile via their `profile` frontmatter field. Autonomous playbooks normalize to build source before enqueue; planning playbooks route to optional eforge-plan planning when that extension capability is available and return planning-entry metadata rather than enqueueing directly. See [Playbooks](/docs/playbooks). ## The Queue and Daemon When you run `/eforge:build` or `eforge build`, eforge writes a normalized PRD file to the configured queue directory (`.eforge/queue/` by default - gitignored, runtime state only). During enqueue, eforge also extracts a canonical acceptance-criteria inventory with stable `ac-###` ids and stores it in an eforge-owned hidden Markdown block in that queued PRD. The inventory is validated for schema, source grounding, confidence, duplicates, and item quality before the queue file is written. If extraction is malformed or criteria are vague, grouping labels, bare commands, manual-only, visual-only, low-confidence, or ungrounded, enqueue fails and no queue Markdown file is created. A long-running **daemon** watches the queue and, when `prdQueue.autoBuild` is enabled, processes PRDs automatically. The daemon runs in the background and survives terminal exit. `prdQueue.watchPollIntervalMs` controls how often the watcher polls for queued work. Queued PRD builds require the persisted inventory; if a queued file is missing the hidden block, has multiple blocks, or has malformed inventory JSON, the build fails before orchestration and the PRD must be re-enqueued. At dispatch time, the daemon also writes a canonical copy of the PRD to `eforge/prds/{prdId}.md` - a committed provenance record that links the build session to its originating requirements independently of queue state. Queue files in `.eforge/queue/` are ephemeral; `eforge/prds/` files are committed to the artifact branch and survive queue cleanup. The hidden inventory is stripped from planner, validator, dependency, staleness, profile-router, and provenance prose, while the loaded stable IDs are passed to acceptance validation. When `build.cleanupPlanFiles: true` (the default), these artifacts are removed from `HEAD` during the `pr` or `merge` landing flows after a successful build, temporary plan-ID eforge region marker comment lines are stripped from tracked JavaScript/TypeScript-family source files, and artifacts remain recoverable from Git history via commit-pinned references. `landing.action: leave` preserves the artifact branch in place. See [Build Artifact Provenance](#build-artifact-provenance) above. The queue supports dependencies, priority, hold state, and runtime controls. A PRD can declare `depends_on` to wait for upstream PRDs to complete before it starts; eforge validates that dependencies refer to pending, running, or waiting queue items, or to completed items with durable usable artifacts. Failed, skipped, unknown, or completed-without-artifact dependencies are rejected. Within each dependency wave, lower numeric `priority` values run first, PRDs without `priority` run last, and ties fall back to creation date. `eforge queue priority ` mutates pending or waiting PRD frontmatter, while failed and skipped items reject priority changes with a conflict until recovery or requeue makes them runnable. Queue hold state is runtime-only PRD frontmatter (`held`, `hold_reason`, `held_at`) on pending or waiting items; held items keep their file location and ordering metadata, but scheduler ticks do not dispatch them until they are unheld. Daemon projections expose hold state and queue-control capabilities, so Console can show hold/unhold, priority, remove, cascade, cancel, and disabled reasons from daemon-authored rules instead of inferring them locally. `eforge queue remove ` deletes non-running pending, waiting, failed, or skipped queue files and deletes matching failed-build recovery sidecars. The daemon API also supports targeted dependency overrides for pending or waiting PRDs: one `depends_on` id is removed, remaining blockers keep the PRD in `waiting/`, and clearing the last blocker moves a waiting PRD back to the queue root. Running items reject priority, hold, unhold, removal, and dependency override controls; daemon-owned cancellation requires live queue-lock and run/session ownership evidence. Legacy removal fails closed when live pending/waiting dependents exist and lists dependent ids. Cascade remove and cancel use a two-phase preview/apply flow that rechecks an expected affected token and requires explicit dependent confirmation before mutating dependents. These are runtime filesystem mutations under `.eforge/queue/`, are gitignored, produce no git commits, and notify the scheduler so it re-reads queue files before dispatch. Scheduler pause is distinct from disabling auto-build: pause keeps desired auto-build on but prevents new launches until resume, while already-running builds continue unless cancelled. If an upstream PRD fails or is cancelled, waiting dependents are skipped instead of cascading a broken build. The Console Now dashboard surfaces failed and skipped terminal rows in the Needs attention strip rather than the Queue card; the Queue card preview lists only forward pending and waiting rows, which expose set-priority, hold/unhold, and preview-first remove/cascade controls as capabilities allow. Failed enqueue attempts that never reached the queue appear as durable Needs attention rows with source label, reason, timestamp, fallback command, disabled reason when needed, and confirmed re-enqueue when source data is available. Failed upstreams with skipped descendants can be inspected before applying daemon-planned recovery, including projected pre-session dispatch blockers and explicit queue-cascade metadata repair choices when available. Recovery auto-resume is disabled by default; when explicitly enabled, only high-confidence `continue-repair` recommendations that pass compiled-artifact eligibility may auto-queue within the configured attempt budget after safety preflights pass, and repeated identical failures stop instead of looping while Console projects the latest automatic decision/attempt count/stop reason and manual recovery controls remain available. The **Console dashboard** (`http://localhost:/console/`) tracks cost, token usage, efficiency metrics, and pipeline progress in real time. Root UI requests redirect to Console, and the daemon keeps it available after the build completes so you can inspect results. ## Artifact Branches and Landing Actions Every eforge build produces an **artifact branch** - a named Git branch (`eforge/`) that holds the committed output. After all plans merge into the artifact branch and post-merge validation passes, the **landing action** determines what happens next. Configure the landing action via `landing.action` (values: `pr`, `merge`, `leave`). | `landing.action` | Behavior | |-----------------|----------| | `pr` | Opens a PR from the artifact branch targeting the resolved base branch. For direct non-stacked builds, eforge fetches `origin/`, rebases the artifact branch onto that fetched base before validation, and checks freshness again immediately before PR creation. For stacked builds, the root PR targets the resolved trunk branch and child PRs target their parent artifact branch unless eforge proves a missing parent base is already integrated into trunk and performs branch-scoped stale-parent landing repair; before submission, stacked landing runs provider repo sync, branch restack, and a remote-base freshness proof. | | `merge` | Merges the artifact branch into the base branch directly. | | `leave` | Leaves the artifact branch in place for manual inspection or cherry-picking. | ## Validation During each plan build, extensions may contribute validation providers that run in the per-plan `validate` stage after implementation and before review. Structured provider failures can be repaired before review: narrow issues use the review-fixer path, while `repairClass: 'structural'` issues route to an in-build validation-fixer path with evaluator-gated checkpoints. Review and test cycles also have a bounded same-plan recovery pass for active-plan blockers that remain after normal rounds; it emits `plan:build:recovery:*` evidence events, reruns the blocking checks, and falls back to normal failure/recovery when blockers remain or classification is unsafe. After all plans merge, eforge runs your configured `postMergeCommands` plus any queued PRD `postMerge` commands (compile, test, lint, etc.). On post-merge failure, a validation-fixer agent attempts repairs up to a configurable retry limit. This is the last line of defense before a build is marked complete and the landing action executes. ## Stacked PRs Stacked PR landing is optional. When `stacking.enabled: true` and `landing.action: pr` are set in `eforge/config.yaml`, builds form a **branch-per-PR stack**. The root artifact branch targets the resolved trunk branch, and each child artifact branch normally targets its parent artifact branch. If a parent branch was deleted after merge and the parent artifact commit is an ancestor of trunk, eforge can automatically land only the child artifact branch against trunk during landing. eforge currently uses git-spice to track branches and submit PRs into the stack. PRD frontmatter controls the stack topology: `stack_id` is a logical stack name shared by all PRDs in the stack; `stack_parent` is the parent PRD id. For single-dependency builds, `stack_parent` is inferred automatically from `depends_on`. See the [Stacked PRs](/docs/stacking) guide for setup instructions. ## Agent-Readable Artifacts eforge publishes machine-readable reference artifacts for use by AI coding assistants: - `/llms.txt` - Structured index of available documentation, getting-started guides, reference docs, packages, schemas, and optional context - `/llms-full.txt` - Full reference documentation bundle in a single file - `/docs/getting-started.md`, `/docs/concepts.md`, `/docs/configuration.md`, `/docs/profiles.md`, `/docs/playbooks.md`, `/docs/eforge-plan.md`, `/docs/stacking.md`, `/docs/extensions.md`, `/docs/extensions-api.md`, `/docs/integrations.md`, `/docs/troubleshooting.md`, `/docs/glossary.md` - Raw Markdown guide pages useful for onboarding, operations, optional workflows, first-party extensions, and terminology - `/reference/cli.md`, `/reference/api.md`, `/reference/events.md`, `/reference/config.md`, `/reference/tools.md` - Raw Markdown reference docs - `/schemas/events.schema.json`, `/schemas/config.schema.json` - JSON Schemas for wire types and config These are served byte-for-byte from the static `public/` directory and are regenerated from source on every release. --- title: Configuration description: Key configuration options for eforge and how to tune them. --- # Configuration eforge is configured via `eforge/config.yaml` (searched upward from cwd). All fields are optional - defaults work for most projects. This page covers the most commonly tuned options. For the full schema see the [Configuration Reference](/reference/config). ## The Three Config Tiers Config merges from three levels (lowest to highest priority): | Tier | Path | Committed? | Purpose | |------|------|-----------|---------| | User | `~/.config/eforge/config.yaml` | No | Cross-project, personal | | Project | `eforge/config.yaml` | Yes | Team-canonical | | Project-local | `.eforge/config.yaml` | No (gitignored) | Personal override | The project-local tier deep-merges over the others. Use it for personal tuning - different model choices, extra verbosity, or test commands you do not want to commit. ## Initialization The fastest way to set up config is `/eforge:init` in Claude Code or Pi. It scaffolds `eforge/config.yaml` with sensible defaults and walks you through harness and model selection. To edit config interactively after initialization: `/eforge:config --edit`. ## Workflow Presets Use `/eforge:workflow` after initialization when you want a guided choice for landing action, pull-request auto-merge, and stacking. Pi also exposes `/eforge:workflow:init` and `/eforge:workflow:reconfigure`; Claude Code uses `/eforge:workflow` and `/eforge:workflow --reconfigure`. | Preset | Config keys written | |--------|---------------------| | `solo-merge` | `landing.action: merge`, `build.allowLocalMergeToTrunk: true`, `stacking.enabled: false` | | `solo-pr` | `landing.action: pr`, `landing.pr.autoMerge: always`, `stacking.enabled: false` | | `team-pr` | `landing.action: pr`, `landing.pr.autoMerge: ask`, `stacking.enabled: false` | | `stacked-pr` | `landing.action: pr`, `stacking.enabled: true` | | `stacked-pr-autosync` | `landing.action: pr`, `stacking.enabled: true`, `stacking.sync.afterBuild: true` | For stacking presets, the wizard also writes `stacking.gitSpice.command` when you provide a custom git-spice path. Use `/eforge:config --edit` for fine-grained changes after applying a preset. ## Agent Tiers Tiers are the primary configuration axis. Each tier is a self-contained recipe: `harness + model + effort`. ```yaml agents: tiers: planning: harness: pi model: anthropic/claude-opus-4-6 effort: high pi: provider: openrouter implementation: harness: pi model: anthropic/claude-sonnet-4-6 effort: medium pi: provider: openrouter review: harness: pi model: anthropic/claude-opus-4-6 effort: high pi: provider: openrouter evaluation: harness: pi model: anthropic/claude-opus-4-6 effort: high pi: provider: openrouter ``` Pi is the recommended harness for new profiles. The engine still has current compatibility fallback defaults for omitted tiers; those defaults are `claude-sdk`, so Pi profiles should list all four tiers explicitly. **Effort levels**: `low`, `medium`, `high`, `xhigh`, `max`. Higher effort means more agent turns and more thorough output, at higher cost. **Thinking**: Add `thinking: true` to a tier to enable extended thinking. It is coerced to adaptive mode for models that only support adaptive thinking. ### Runtime choices The four built-in tiers remain the role-routing axis. The existing tier recipe equals that tier's implicit `default` choice. Named choices under `agents.tiers..choices` inherit from the tier default and override only the fields they list. Choices are selected after a role resolves to its tier. ```yaml agents: tiers: implementation: harness: pi model: anthropic/claude-sonnet-4-6 effort: medium pi: provider: openrouter choices: backend: model: qwen3-coder pi: provider: local toolbelt: none ui: effort: high toolbelt: browser-ui routing: rules: - name: ui-paths choice: ui when: pathGlobs: ["packages/console-ui/**", "web/**", "**/*.{tsx,jsx,css}"] keywords: ["ui", "frontend", "browser", "component"] - name: backend-paths choice: backend when: pathGlobs: ["packages/engine/**", "packages/client/**", "packages/monitor/**"] ``` Routing rules run in order before extension runtime-choice routers. If no rule or router selects a named choice, or a router errors, times out, or returns an invalid choice, eforge falls back to `default` for that invocation and does not fail the build. `registerProfileRouter` is still build-level profile selection before dispatch; `onAgentRun` can observe selected runtime-choice metadata but cannot change harness, model, provider, effort, or toolbelt. ## Using the Pi Harness Pi is the recommended provider-flexible execution harness for new eforge setup. Set `harness: pi` and add a `pi.provider` block: ```yaml agents: tiers: planning: harness: pi model: anthropic/claude-opus-4-6 effort: high pi: provider: openrouter implementation: harness: pi model: anthropic/claude-sonnet-4-6 effort: medium pi: provider: openrouter ``` Pi supports OpenAI, Google, Mistral, Groq, xAI, Bedrock, Azure, OpenRouter, and local models. Authentication resolves from provider-specific environment variables or `~/.pi/agent/auth.json`. For OAuth providers (OpenAI Codex, GitHub Copilot), run `pi auth login ` first. By default, eforge runs Pi harness sessions with `pi.resources: isolated` so ambient Pi resources (project/user/global extensions, skills, prompts, and themes) are not loaded into headless build agents. Set `pi.resources: ambient` on a tier only when you intentionally want those ambient Pi resources available during eforge agent runs. Pi-specific tier options also include `pi.thinkingLevel`, `pi.extensions`, `pi.compaction`, and `pi.retry`; keep provider selection in `pi.provider`. ## Optional Claude SDK Harness `claude-sdk` remains supported for Anthropic Claude Agent SDK users: ```yaml agents: tiers: implementation: harness: claude-sdk model: claude-sonnet-4-6 effort: medium claudeSdk: disableSubagents: true ``` Choose this path only when you intentionally want the Anthropic-specific Claude Agent SDK and have appropriate credentials for that provider. Claude SDK tiers disable Claude Code subagents by default by denying the `Task` tool; set `claudeSdk.disableSubagents: false` only when you intentionally want subagents available. ## Agent Runtime Profiles A profile bundles tier recipes into a reusable named file. This lets you switch between configurations - such as "use Claude for review, local model for implementation" - without editing `eforge/config.yaml`. Profiles live at three scopes (highest-priority-first): | Scope | Directory | Committed? | |-------|-----------|-----------| | Project-local | `.eforge/profiles/` | No (gitignored) | | Project | `eforge/profiles/` | Yes | | User | `~/.config/eforge/profiles/` | No | Set the active profile from Claude Code or Pi with: ``` /eforge:profile ``` The standalone CLI can override the active profile for one build with `eforge build --profile ...`; profile creation and switching are handled by the host skills. For a complete walkthrough covering profile creation, scope resolution, toolbelts inside profiles, and profile-router precedence, see [Profiles](/docs/profiles). ## Native Extensions Configuration is split between core build/daemon/profile settings and optional producer surfaces. The build-engine kernel consumes normalized build source; native extensions, the first-party `eforge-playbooks` extension, and session-plan compatibility tools can prepare, route, or govern that source before enqueue without becoming kernel capabilities. Native eforge extensions are TypeScript/JavaScript modules discovered from three scopes: | Scope | Directory | Trust default | |-------|-----------|---------------| | User | `~/.config/eforge/extensions/` | trusted | | Project/team | `eforge/extensions/` | skipped unless a matching local trust record exists | | Project-local | `.eforge/extensions/` | trusted | Precedence is `project-local > project-team > user`. Use project-local extensions for experiments, then promote to `eforge/extensions/` when the team should share them. Project/team extensions require a per-extension local trust record in `.eforge/extension-trust.json` created by `eforge extension trust ` after inspecting the code. Any code change invalidates the stored hash and blocks the extension until re-trusted. The content hash covers supported source files plus every regular file under `workstation-assets/`, so declared workstation bundle assets are covered; broad top-level `dist/` output, package sidecars, and files outside the extension unit remain outside the trust hash. ```yaml extensions: enabled: true # default eventHookTimeoutMs: 5000 # native onEvent and extension action timeout in ms agentContextHookTimeoutMs: 5000 # optional onAgentRun timeout; defaults to eventHookTimeoutMs profileRouterTimeoutMs: 5000 # optional registerProfileRouter timeout; defaults to eventHookTimeoutMs policyGateTimeoutMs: 5000 # optional policy gate timeout; defaults to eventHookTimeoutMs validationProviderTimeoutMs: 5000 # optional validation-provider timeout; defaults to eventHookTimeoutMs policyGateFailurePolicy: fail-closed # fail-closed blocks on failures; fail-open allows after diagnostics include: - build-notifier # optional allowlist by name exclude: - experimental-policy # optional denylist by name paths: - ./tools/eforge-audit.ts # explicit file/directory paths ``` Extension action handlers use `extensions.eventHookTimeoutMs`; `agentContextHookTimeoutMs`, `profileRouterTimeoutMs`, `policyGateTimeoutMs`, and `validationProviderTimeoutMs` remain scoped to their existing registration families. Supported extension entrypoints are `.ts`, `.mts`, `.js`, and `.mjs` files or directories with `index.*` / supported `package.json` entrypoints. TypeScript loads through `jiti`; JavaScript uses dynamic import. The loader executes the default-export factory in the eforge daemon/worker Node process without a sandbox, records registrations, and surfaces status, diagnostics, shadows, trust, source, strategy, registration counts, and event replay results through `eforge extension list/show/validate/test` and extension API routes. Current runtime support includes discovery, trust gating, loading, diagnostics, provenance output, registration capture for runtime-wired families, native `onEvent` dispatch and replay testing, `onAgentRun` prompt-context augmentation, per-run extension tool injection, per-run tool availability tuning, pre-build `registerProfileRouter` dispatch, runtime policy gates for `beforeQueueDispatch`, `beforePlanMerge`, and `beforeFinalMerge`, `registerInputSource` enqueue preprocessing, `registerPrdEnricher` content enrichment, reviewer perspective execution, validation-provider execution, engine-side extension action/contribution/workstation registry support, Console System rendering for declarative contributions, sandboxed Console workstation rendering from `srcDoc` or daemon-owned `frameBundle` frame/asset URLs, host discovery/detail/invocation for actions, integration commands, and action-backed deep links, daemon-owned `ctx.agentTasks` dispatch for supported single-shot read-only planner tasks, and management commands (`eforge extension list/show/validate/test/new/reload/trust/untrust/install/update/remove/promote/demote`). Package-managed extensions installed via `eforge extension install` carry nested `package.*` and `install.*` provenance fields such as `install.sourceKind`, `install.sourceSpec`, and `install.installedAt`; install sidecar files are excluded from the trust hash. `registerTool` records loader-time provenance; `onAgentRun({ tools: [...] })` is the per-run injection path. The first-party `eforge-playbooks` extension exposes playbook actions through the native extension contribution model and owns parser/storage/compiler/seed behavior locally; domain-neutral acceptance-criteria helpers and session-planning helpers remain separate from that playbook extension boundary. These are not user-authored native workflow registration points. `beforeEnqueue`, `beforeValidation`, approval workflow/state/UI, `modify` decisions, raw extension-owned HTTP routes, arbitrary frontend plugin bundles outside registered workstation iframes, direct React loading into the parent Console, private Console imports, extension-owned AI planning/chat APIs outside `ctx.agentTasks`, arbitrary raw prompt templates, multi-turn chat, and user-authored workflow registration for custom session-plan or playbook extraction are not supported by native extensions in the current release. See [Extensions](/docs/extensions) and [Extensions API Reference](/docs/extensions-api). ## Guided Toolbelt Presets Toolbelts let a tier opt into a named bundle of project MCP servers from `.mcp.json`. When creating a profile, Pi's native `/eforge:profile:new` wizard (and Claude Code's `/eforge:profile-new` fallback) includes an optional toolbelt step after tier configuration. **What the wizard asks:** - **Skip / default** — omit `toolbelt` from all tiers; all project MCP servers from `.mcp.json` pass through (original behavior). - **No project MCP access** — set all four tiers to `toolbelt: none`; no project MCP servers reach agents in any tier. - **Choose a preset** — configure a named toolbelt bundle with least-privilege tier assignments. **Least-privilege rule:** Presets explicitly assign `toolbelt: none` to tiers that do not need project MCP servers. An omitted `toolbelt` keeps the all-project-MCP default. ### Preset gallery | Preset | Typical MCP servers | Tiers receiving access | Missing-server behavior | |--------|--------------------|-----------------------|------------------------| | `browser-ui` | `playwright` | implementation, review | Show `.mcp.json` snippet; ask before adding | | `docs-research` | `fetch`, `context7` | planning, implementation | Show setup guidance; do not create tier references | | `issue-triage` | `github` | planning | Show setup guidance; do not create tier references | | `repo-review` | `github` | planning, review | Show setup guidance; do not create tier references | | `observability` | `datadog`, `sentry` | planning, evaluation | Show setup guidance; do not create tier references | | `database-readonly` | `postgres`, `sqlite` | planning | Show setup guidance; do not create tier references | | `api-testing` | `fetch` | implementation, review | Show setup guidance; do not create tier references | | `design-ui` | `figma` | planning, implementation, review | Show setup guidance; do not create tier references | Toolbelts filter only project MCP servers from `.mcp.json`. They do not affect Pi extensions, Claude Code plugins, engine-internal tools, or harness built-ins. ### browser-ui — Playwright setup The `browser-ui` preset is the only one that the profile wizard can auto-configure after explicit confirmation. For UI-heavy or browser-validation work, pair your profile with `browser-ui` backed by the Playwright MCP server. **Step 1 - Register the toolbelt in `eforge/config.yaml`:** ```yaml tools: toolbelts: browser-ui: description: Browser automation for UI implementation and review. mcpServers: - playwright ``` **Step 2 - Create `eforge/profiles/ui.yaml`:** ```yaml # eforge/profiles/ui.yaml description: UI-heavy feature work with browser validation. whenToUse: - Frontend features - Layout bugs - Screenshot-driven UI fixes tags: - ui - frontend - browser agents: tiers: planning: harness: pi model: anthropic/claude-opus-4-6 effort: high pi: provider: openrouter toolbelt: none implementation: harness: pi model: anthropic/claude-sonnet-4-6 effort: medium pi: provider: openrouter toolbelt: browser-ui review: harness: pi model: anthropic/claude-opus-4-6 effort: high pi: provider: openrouter toolbelt: browser-ui evaluation: harness: pi model: anthropic/claude-opus-4-6 effort: high pi: provider: openrouter toolbelt: none ``` **Step 3 - Add the Playwright MCP server to `.mcp.json`:** ```json { "mcpServers": { "playwright": { "command": "npx", "args": ["-y", "@playwright/mcp@latest"] } } } ``` **For other presets:** Add the required MCP servers to `.mcp.json` manually, declare `tools.toolbelts.` in `eforge/config.yaml`, then use `/eforge:profile-new` to create a profile referencing the toolbelt. **MVP constraints:** 1. Toolbelts filter only project MCP servers from `.mcp.json` - they do not affect Pi extensions, Claude Code plugins, engine-internal tools, or harness built-ins. 2. Each tier picks at most one toolbelt via the singular `toolbelt` field. 3. `toolbelt: none` passes no project MCP servers to agents in that tier. 4. An omitted `toolbelt` keeps the default: all servers from `.mcp.json` are passed through. 5. Pi extensions and Claude Code plugins are out of scope for this MVP - toolbelts are MCP-only and declarative. 6. Toolbelts are declarative MCP bundles; extensions are imperative lifecycle behavior. Extensions may inspect toolbelt and profile metadata when making routing decisions, but extensions should not redefine toolbelts or act as a hidden config layer. For the complete field schema and validation behavior, see the [Toolbelts](/reference/config#toolbelts) section in the Configuration Reference. For the extension/toolbelt boundary, see the [Extensions API Reference](/docs/extensions-api#toolbelt-vs-extension-boundary). ## Playbook Profiles Playbooks are optional workflow artifacts that resolve outside the build-engine kernel through `eforge-playbooks:run-playbook` before autonomous runs normalize to build source or planning runs route to eforge-plan handoff metadata. The extension owns playbook management/run behavior, parser, storage, validation, compilation, and seed generation locally; domain-neutral acceptance-criteria helpers remain separate input-layer utilities. Playbooks support an optional `profile` frontmatter field that names an agent runtime profile to use when the playbook runs: ```yaml --- name: docs-sync description: Sync project documentation scope: project-team mode: autonomous profile: docs-heavy # Optional — omit to allow router/active-profile/default resolution --- ## Goal Keep all documentation in sync with the latest code changes. ``` **Precedence**: an optional `profile` field on the `eforge-playbooks:run-playbook` action input overrides the playbook frontmatter for that run. When no action input override is supplied, the playbook `profile` field overrides the project's active-profile marker and any registered profile router. For session-plan builds, an explicit `--profile` flag or enqueue request field overrides the session plan's `agent_profile`. **Validation timing**: the named profile is validated at execution time, not when the playbook is saved. `agent_profile` values on session plans are validated when the session plan is enqueued. **Session-plan `agent_profile` metadata**: session-plan producers may set generic `agent_profile` frontmatter to carry a recommended agent runtime profile with the artifact. When that session plan is enqueued, `agent_profile` is used as the effective profile unless an explicit override is supplied. **Blank profile fallback**: omitting `profile` allows a registered profile router to select a profile first; if no router selects one, eforge uses the project's active-profile marker or engine defaults. ## Queue and Auto-Build The daemon watches `prdQueue.dir` for normalized PRDs. Enqueue stores a validated hidden canonical acceptance-criteria inventory in each queued PRD; missing, duplicated, or malformed inventories fail queued builds before orchestration and require re-enqueue. Queue mutations under `prdQueue.dir` are runtime filesystem operations in `.eforge/queue/` by default, are gitignored, and produce no git commits. Leave `prdQueue.autoBuild` enabled for normal usage so queued PRDs start automatically; disable it when you want to stage multiple queue items before running them. Scheduler pause is a separate runtime gate that leaves desired auto-build enabled but prevents new launches until resume. `prdQueue.watchPollIntervalMs` tunes how often the auto-build watcher polls for queue changes. ```yaml maxConcurrentBuilds: 2 # default: concurrent PRD builds across the queue prdQueue: dir: .eforge/queue autoBuild: true watchPollIntervalMs: 5000 ``` **PRD provenance**: when the daemon dispatches a PRD from `.eforge/queue/`, it writes a canonical copy to `eforge/prds/{prdId}.md`. Queue state is ephemeral and gitignored; `eforge/prds/` files are committed provenance artifacts that record what was built. The hidden acceptance-criteria inventory is consumed for validation IDs and stripped from the committed prose artifact. **Artifact cleanup and preserved history**: when `build.cleanupPlanFiles: true` (default), eforge removes committed build artifacts — the PRD copy in `eforge/prds/`, compiled plan files in `eforge/plans/{planSet}/`, and `orchestration.yaml` — from `HEAD` during the `pr` or `merge` landing flows after a successful build. Cleanup also strips temporary plan-ID eforge region marker comment lines from tracked JavaScript/TypeScript-family source files while preserving durable semantic markers and marked code. `landing.action: leave` does not run cleanup and leaves the artifact branch intact for inspection. These files are not permanently lost. When the artifact branch is landed with a merge commit (eforge's local `merge` action, or a GitHub PR merged via "Create a merge commit"), the commits that originally added the artifacts remain reachable in Git history. PR bodies include an **Eforge provenance** section with commit-pinned references (`git show :`) that can be used to recover any artifact. The durable guarantee is Git history, not the final tree — squash or rebase merge strategies applied after a PR is opened can collapse intermediate commits and make artifact references unreachable. When `landing.action: pr` is used, provenance durability depends on the repository's chosen merge strategy. Queue item frontmatter can also carry scheduling hints: ```yaml --- title: Add billing export priority: 10 # lower numbers run earlier within the same dependency wave depends_on: [api-v2] # wait for pending/running/waiting queue item ids --- ``` `depends_on` is validated at enqueue time. Dependencies may be active queue items (pending/running/waiting) or completed items with usable artifacts. Items blocked on active upstream dependencies live under the queue's `waiting/` subdirectory until all upstream items complete; items whose dependencies are already completed with usable artifacts are eligible immediately and remain in the queue root. If an upstream item fails or is cancelled, its waiting dependents move to `skipped/`. Queue controls operate on this runtime state. `eforge queue priority ` mutates pending or waiting PRD frontmatter; lower numeric priority values run earlier within each dependency wave, failed and skipped items reject priority mutation with a conflict until recovery/requeue makes them runnable, and running items reject priority changes because active cancellation requires live queue-lock and daemon run/session ownership evidence. Queue hold state is runtime-only PRD frontmatter (`held`, `hold_reason`, `held_at`) on pending or waiting items; held items keep their location and ordering metadata but scheduler ticks skip them until they are unheld. `eforge queue remove ` deletes non-running pending, waiting, failed, or skipped queue files; failed removal deletes matching `.recovery.md` and `.recovery.json` sidecars. The daemon API can also remove one `depends_on` id from a pending or waiting PRD; if the override clears the final dependency on a waiting PRD, the file moves back to the queue root. Legacy removal fails closed when live pending/waiting dependents exist and lists dependent ids. Cascade remove and cancel use preview/apply controls that recheck an expected affected token and require explicit dependent confirmation before mutating dependents. The daemon notifies the scheduler after successful priority, removal, dependency override, hold, unhold, or cascade mutations; when the scheduler is not explicitly paused, it re-reads queue files before dispatch. **Explicit deterministic handoff**: instead of writing `depends_on` in frontmatter, pass `--after ` to the CLI or `afterQueueId` to the `eforge_build` MCP/Pi tool to create an explicit dependency on an active or completed queue entry. Active upstream items (pending/running/waiting) are held in `waiting/` and unblocked when the upstream completes. Completed upstream items with a usable artifact are enqueued immediately as eligible dependents. Explicit `afterQueueId` takes precedence over automatic dependency detection, which remains best effort and is only used when no explicit dependency is supplied. Failed, skipped, and unknown IDs are rejected at enqueue time. ## Post-Merge Commands Commands to run after all plans merge - compile, test, lint, or any validation step: ```yaml build: postMergeCommands: - "pnpm type-check" - "pnpm test" postMergeCommandTimeoutMs: 300000 maxValidationRetries: 2 ``` `build.postMergeCommands` run in order after the merge. Queued PRD `postMerge` metadata, when present, is appended after the configured commands for that build. `build.postMergeCommandTimeoutMs` is the wall-clock timeout for each command in milliseconds (default 300000, five minutes). On failure, a validation-fixer agent attempts repairs up to `build.maxValidationRetries` times (default 2). When retries are exhausted, the build is marked failed. See [Troubleshooting - Validation-fixer retries exhausted](/docs/troubleshooting#validation-fixer-retries-exhausted) for recovery steps. Within a single build, plans run in parallel automatically as their dependencies are satisfied - no configuration needed there. ## Compile Planning Limits The top-level `compile` block tunes context-managed planning for compile inputs that are too large or risky for direct planning. The `planningUnit*` values only affect that overflow-risk planning path; ordinary direct planning does not use planning units. ```yaml compile: planningUnitParallelism: 2 planningUnitMaxDepth: 3 planningUnitMaxPromptSourceBytes: 40000 planningUnitMaxPromptBytes: 80000 planningUnitMaxObservedInputTokens: 120000 # planningUnitMaxObservedTurns is optional and unset by default planningUnitMaxCompactHandoffBytes: 12000 planningUnitMaxLocalExplorationToolUses: 24 planningUnitMaxCriteriaPerUnit: 20 planningUnitMaxSubsystemsPerUnit: 2 planningUnitMaxSplitAttemptsPerUnit: 2 ``` All compile planning limits are positive integers. Increase `planningUnitParallelism` to allow more decomposed planning units to run at once, or lower it to reduce concurrent planning pressure. The remaining `planningUnit*` keys cap recursive splitting, prompt/source size, observed budget pressure, handoff size, local exploration, criteria assignment, subsystem assignment, and split retries per planning unit. ## Landing Action `landing.action` controls what happens when a build completes successfully. | `landing.action` | Behavior | |-------|----------| | `merge` | Merges the artifact branch into the resolved base branch automatically. This is the engine default. | | `pr` | Opens a GitHub pull request from the artifact branch targeting the resolved base branch. For direct non-stacked builds, eforge fetches `origin/`, rebases the artifact branch before validation, and checks freshness again immediately before PR creation. For stacked builds the base is normally the parent artifact branch; landing can use trunk as the effective base when stale-parent repair proves the parent artifact is already integrated. Requires the `gh` CLI. | | `leave` | Leaves the artifact branch in place without merging or creating a PR. Useful when you want to inspect the output or handle the branch manually. | ```yaml landing: action: pr # pr | merge (default) | leave directPrBaseSync: conflictAttempts: 12 # Direct PR base-sync conflict attempts (clamped 1-100) ``` **`pr` prerequisite**: ensure `gh` is installed (`gh --version`) and authenticated (`gh auth status`). Builds configured with `landing.action: pr` will fail at the landing step if `gh` is unavailable. **Migrating from `build.onSuccess`**: if you have the old `build.onSuccess` key in your config, replace it with `landing.action`. The values map as follows: `issue-pr` → `pr`, `merge-to-base-branch` → `merge`, `leave-branch` → `leave`. New builds reject both the old `build.onSuccess` key and the legacy full-string values (`issue-pr`, `merge-to-base-branch`, `leave-branch`) with migration guidance - only `pr`, `merge`, and `leave` are valid for `landing.action`. ### PR auto-merge policy `landing.pr.autoMerge` controls whether GitHub PR auto-merge is enabled after a PR is opened. Only applies when `landing.action: pr`. Default: `ask`. | Value | Behavior | |-------|----------| | `ask` (default) | Enable auto-merge only when the per-run `landingAutoMerge` flag is explicitly `true`. | | `always` | Enable auto-merge on every PR unless the per-run `landingAutoMerge` flag is explicitly `false`. | | `never` | Never enable auto-merge; skips auto-merge and emits a skipped event. | Individual builds and extension-originated enqueue requests can override the policy with `--landing-auto-merge` or `--no-landing-auto-merge` (CLI), or by sending `landingAutoMerge: true/false` in the enqueue body. Omitting the flag defers to the configured policy. Note: `landing.pr.autoMerge` is distinct from `landing.action: merge`. The `action: merge` setting merges the artifact branch directly into the base branch without opening a PR. ```yaml landing: action: pr pr: autoMerge: ask # ask (default) | always | never ``` ## Stacked PRs When `stacking.enabled: true`, each build's artifact branch normally targets the parent artifact branch instead of the trunk, creating a stack of pull requests. Requires git-spice to be installed. During landing, eforge can repair a missing integrated parent by choosing trunk as the effective base for an initially untracked child or by retargeting a child that is already tracked, then gates PR submission on provider sync/restack and a remote-base freshness proof. ```yaml stacking: enabled: true # Default false gitSpice: command: git-spice # Default. Set to 'gs' if you use the short alias. sync: afterBuild: false # Default false. Set to true for daemon-owned after-build sync. landing: action: pr # Required for stacking ``` PRD frontmatter fields control the stack topology: - `stack_id` - logical stack name shared by all PRDs in the stack (optional; inferred from root PRD id) - `stack_parent` - parent PRD id (optional for single-dependency PRDs; required for multi-dependency PRDs) For single-dependency builds (`depends_on` has one entry), `stack_parent` is inferred automatically. For multi-dependency builds, set `stack_parent` explicitly to indicate the direct parent layer. Set `stacking.sync.afterBuild: true` to have the daemon automatically sync the stack after each queued build reaches a terminal state. When active builds overlap the stack candidates, sync is `deferred` and the daemon retries automatically. Prefer this over `build.postMergeCommands: ["eforge stack sync"]` for automatic sync. See [Stacked PRs](/docs/stacking) for the full guide including git-spice setup, stack sync, deferred retry, manual sync conflict recovery, automatic stacked PR landing conflict recovery, branch-scoped stale-parent landing repair, and landing-time sync/freshness. ## Trunk Branch Policy `build.trunkBranch` and `build.allowLocalMergeToTrunk` govern how eforge lands builds when you are on the project's trunk branch. eforge detects the trunk automatically from `origin/HEAD` during `/eforge:init` and writes the result to `eforge/config.yaml`. Override `build.trunkBranch` if the detected value is wrong or the repository uses a non-standard default branch name. ```yaml landing: action: merge # or 'pr' to open a pull request; 'leave' to skip both build: trunkBranch: main # detected from origin/HEAD; fallback: main allowLocalMergeToTrunk: false # default: false; set to true for solo/unprotected projects ``` **What each option does:** | Scenario | `allowLocalMergeToTrunk: false` (default) | `allowLocalMergeToTrunk: true` | |---|---|---| | On trunk, `landing.action: merge` | Rejected; CLI prompts to redirect | Merges directly to trunk | | On trunk, `landing.action: pr` | PR from artifact branch to trunk | PR from artifact branch to trunk | | On feature branch (either action) | Normal behavior, unaffected | Normal behavior, unaffected | When `allowLocalMergeToTrunk` is `false` and you run interactively on trunk with `landing.action: merge`, the CLI prompts before enqueue and offers four alternatives: switch to `pr`, cancel, create or switch to a feature branch, or enable the solo-dev opt-in in `eforge/config.yaml`. With `--auto`, the engine rejects the build at runtime with a clear error message. ## Pre-Compile Trunk Sync By default, eforge fetches the configured remote trunk before creating the merge worktree for a queued root build. This prevents stale-base builds when `origin/main` has advanced but the local branch has not been pulled. ```yaml build: trunkSync: enabled: true # default; set false for offline/local-only workflows remote: origin # remote to fetch trunk from strategy: fetchedRemoteRef # only supported strategy in v1 onDiverged: warn # warn | fail | use-remote ``` **What it does:** before compile, eforge runs `git fetch --no-tags origin main` (using your configured remote and trunk branch), resolves the fetched commit SHA, and compares it to the local trunk. When the remote is ahead or equal, the fetched SHA is used as the compile base. When local and remote have diverged, the `onDiverged` policy applies. **`onDiverged` options:** | Value | Behavior | |-------|----------| | `warn` (default) | Emit a `config:warning` diagnostic and fall back to the local trunk as the compile base. | | `fail` | Fail the build before compile begins. | | `use-remote` | Use the fetched remote SHA with a diagnostic. | **Fetch-unavailable fallback:** if the configured remote does not exist, the remote trunk branch is missing, the fetch fails, or FETCH_HEAD cannot be resolved, trunk sync is skipped. The build continues with the original candidate base and emits a `planning:progress` diagnostic. The `onDiverged` policy applies only to true local/remote divergence - not to network failures or unavailable remotes. **Validation and failure before compile:** the `remote` value is validated before the fetch runs. It must be a registered git remote name: non-empty, must not start with `-`, must contain no whitespace or control characters, and must not be a URL (containing `://`) or path (starting with `/`, `./`, or `../`). The resolved trunk branch must also be a valid git branch refname. Invalid values fail the build before compile - they do not fall back to the fetch-unavailable behavior. Use `enabled: false` to skip trunk sync for offline or local-only workflows. **What it does not do:** `trunkSync` only fetches and selects a base ref. It does not checkout, pull, reset, rebase, or move local branch refs or your working tree. Only FETCH_HEAD is updated as part of the fetch. Direct PR base sync is separate: direct non-stacked PR publication later fetches `origin/`, rebases the artifact branch before validation, and runs a final pre-PR freshness guard. **Scope:** only applies to queued root builds whose candidate base is the trunk branch. Child stacked PRDs use the parent artifact ref unchanged during `trunkSync`. Builds queued from a non-trunk feature branch are not retargeted by `trunkSync`. Direct PR base sync applies later only to direct non-stacked `landing.action: pr` publication, including non-trunk feature bases. ### Disabling trunk sync For offline workflows or repositories without a remote: ```yaml build: trunkSync: enabled: false ``` ### Distinction from stack sync `build.trunkSync` selects a fresh compile base before a build starts. It runs once, before the merge worktree is created, and does not affect the stack topology. Direct PR base sync is a later mutating publication gate for direct non-stacked `landing.action: pr` builds. After all plans merge and before validation, eforge fetches `origin/` and rebases the artifact branch onto that fetched base. The fixed conflict-resolution attempt budget comes from `landing.directPrBaseSync.conflictAttempts` (default `12`, clamped to `1`-`100`); the older `compile.directPrBaseSyncConflictAttempts` key is still accepted as a compatibility fallback when the landing key is unset. The resolved budget is used as-is for the operation and is not scaled by branch size. Immediately before PR creation, eforge fetches the base again; if it advanced after validation, eforge performs a bounded resync plus command validation and PRD/acceptance validation retry before attempting the PR again. If the retry budget is exhausted or sync cannot complete, landing fails closed with `landing:skipped` rather than opening a stale PR. Stacked PR landing does not use the direct non-stacked PR base sync path. Instead, it stays behind the stack provider boundary: eforge runs provider repo sync, branch restack, and a remote-base freshness proof for the branch being submitted. Manual `eforge stack sync` remains the separate whole-stack maintenance path after trunk or parent branches move. ## Per-Role Tuning Fine-tune individual agent roles without reassigning them to a different tier: ```yaml agents: roles: builder: effort: high maxTurns: 80 reviewer: promptAppend: | ## Project Rules - Flag raw SQL queries - Require error handling for all async operations formatter: effort: low ``` Available per-role fields: `tier`, `effort`, `thinking`, `maxTurns`, `allowedTools`, `disallowedTools`, `promptAppend`, `shards` (builder-only). Per-role overrides do not change the harness or model directly; move the role to a different tier with `tier` when one role should use a different tier recipe. ## Custom Prompts Override any bundled agent prompt by placing a `.md` file in `eforge/prompts/` with the same name as the bundled prompt file: ```yaml agents: promptDir: eforge/prompts ``` If `eforge/prompts/reviewer.md` exists, it replaces the bundled reviewer prompt entirely. Use `promptAppend` on a role for additive rules instead of full replacement. Prompt file names match the role name for most roles, but not all: the `formatter` role runs the intake prompt, so override it with `eforge/prompts/intake.md` (a `formatter.md` file has no effect). ## Hooks Hooks are fire-and-forget shell commands triggered by eforge events - useful for notifications, logging, and external integrations: ```yaml hooks: - event: plan:build:complete command: "notify-send 'Build complete'" timeout: 5000 - event: plan:build:failed command: "curl -X POST $SLACK_WEBHOOK -d '{\"text\": \"Build failed\"}'" ``` Hooks do not block the pipeline. See the [Hooks](/reference/config#hooks) section in the Configuration Reference for field details and the [Integrations](/docs/integrations#shell-hooks) page for examples. ## Full Reference For the complete `eforge/config.yaml` schema with all fields, types, and defaults, see the [Configuration Reference](/reference/config). --- title: Extensions description: TypeScript and JavaScript extensions that observe lifecycle behavior and publish typed actions, Console contributions, commands, and deep links. --- # Extensions eforge has a broad extension surface around a small build-engine kernel. Input surfaces, first-party `eforge-playbooks` playbook workflows, session plans, profile toolbelts, shell hooks, host integrations, wrapper apps, and native TypeScript extensions can shape how work is authored, routed, governed, observed, and integrated without moving those concerns into the engine. Native eforge extensions are one typed mechanism in that broader surface: TypeScript or JavaScript modules loaded by the eforge daemon/worker Node process. They are the typed, programmatic counterpart to shell hooks: extension factories can register event hooks, agent-run augmenters, policy gates, profile routers, runtime-choice routers, input sources, PRD enrichers, reviewer perspectives, validation providers, custom tools, typed actions, declarative Console contributions, sandboxed Console workstations, integration commands, and deep links with full TypeScript inference. Extensions are **not sandboxed**. A loaded native extension executes in the same Node process as eforge and has the same filesystem, environment, and network access as the daemon. Only enable extensions from sources you trust. ## What native extensions are (and are not) Native eforge extensions are distinct from other extensibility mechanisms in the broader eforge extension surface: | Mechanism | Language/shape | Runtime owner | Purpose | |-----------|----------------|---------------|---------| | Native eforge extensions | TypeScript/JavaScript modules in `extensions/` | eforge daemon/worker | Typed lifecycle registrations, per-run prompt/tool augmentation, and runtime hooks | | Claude Code plugins | Claude Code plugin package | Claude Code host | Slash commands, MCP proxy wiring, Claude Code UX | | Pi extensions | Pi extension package | Pi host | Native Pi commands, tools, and TUI surfaces | | Shell hooks | YAML + shell command | eforge hook runner | Fire-and-forget notifications/integrations | | Playbooks/session plans | Markdown input artifacts | First-party extensions and domain-neutral input adapters, then engine queue | Reusable build sources and planning artifacts | | Profile toolbelts | YAML MCP server bundles | agent runtime registry | Declarative project MCP server selection | Toolbelts answer "which project MCP servers from `.mcp.json` should this tier expose?" Extensions answer "what should eforge do when something happens?" and may contribute TypeScript-defined tools per agent run. Toolbelts do not filter extension-contributed tools, engine-internal custom tools, or harness built-ins. Extensions should not redefine toolbelts or act as a hidden profile/config layer. Playbooks and session plans are reusable input artifacts outside the engine kernel. The first-party `@eforge-build/eforge-playbooks` extension owns shipped playbook parsing, storage, validation, compilation, management actions, planning-mode handoff metadata, and autonomous queue handoff across project-local, project-team, and user scopes. Domain-neutral acceptance-criteria quality helpers and session-plan normalization are separate input-layer concerns; session plans remain project-local Markdown files under `.eforge/session-plans/` and are handled separately from the playbook extension boundary. Native extensions do not currently register custom playbook or session-plan extraction workflows. Custom playbook extraction remains deferred, custom session-plan extraction remains deferred, user-authored session-plan extraction remains unsupported, user-authored playbook extraction remains unsupported, and user-authored native workflow registration remains future/deferred work. ## Extension user workflow A typical extension workflow has four parts: 1. **Install or author** - use `eforge extension install ` for npm packages, local package directories, or tarballs, or scaffold a local TypeScript module with `eforge extension new `. Consumer integrations expose the same management actions through their host UIs. 2. **Configure loading** - keep `extensions.enabled: true`, then optionally use `extensions.include`, `extensions.exclude`, or `extensions.paths` to select discovered modules. Start experiments in `.eforge/extensions/` and promote to `eforge/extensions/` only when the team should share them. 3. **Trust and validate** - project/team extensions are skipped until each user runs `eforge extension trust ` after inspecting the code. Run `eforge extension validate` and, for event hooks, `eforge extension test` before reload or a real build. 4. **Use at runtime** - reload the daemon's extension registry with `eforge extension reload`, then run normal builds. Loaded extensions can observe events, add per-agent prompt/tool context, route profiles, enforce shipped policy gates, provide input sources and PRD enrichers, add reviewer perspectives, and run validation providers depending on what they registered. ## LLM-first extension authoring checklist When an LLM or assisted authoring flow creates an extension, keep the loop explicit and auditable: 1. **Scaffold locally** with `eforge extension new ` (usually project-local `.eforge/extensions/`) and keep the first version small. 2. **Author against supported V1 surfaces**: event hooks, agent-run context/tools, policy gates, profile routers, runtime-choice routers, input sources, PRD enrichers, reviewer perspectives, validation providers, actions, declarative System contributions, and source-authored sandboxed Console workstations with iframe `srcDoc` or `frameBundle` plus `window.eforge.invokeAction` / `@eforge-build/extension-sdk/browser`. 3. **Validate statically** with `eforge extension validate ` before reload. 4. **Test behavior** with `eforge extension test ` and, for event hooks, a fixture or `--run latest` replay. 5. **Trust only after inspection**: for project/team extensions, read the source and then run `eforge extension trust ` so the local `.eforge/extension-trust.json` records the reviewed hash. 6. **Reload deliberately** with `eforge extension reload` after validation, testing, and trust are complete. 7. **Re-check trust and boundaries** whenever code changes: changed project/team hashes must be re-trusted, workstation `srcDoc` HTML and bundle metadata/code are trusted iframe UI rather than sanitized declarative content, extension-owned raw routes, extension-authored arbitrary asset bundles outside the workstation frame/asset contract, React injection, and extension-owned AI planning/chat APIs outside the daemon-owned `ctx.agentTasks` boundary remain deferred. ## Configuration Native extension loading is controlled by the top-level `extensions` block in `eforge/config.yaml`, `~/.config/eforge/config.yaml`, or `.eforge/config.yaml`: ```yaml extensions: enabled: true # default: true eventHookTimeoutMs: 5000 # default: 5000; positive integer milliseconds # agentContextHookTimeoutMs: 5000 # default: inherits eventHookTimeoutMs; positive integer milliseconds # profileRouterTimeoutMs: 5000 # default: inherits eventHookTimeoutMs; positive integer milliseconds # policyGateTimeoutMs: 5000 # default: inherits eventHookTimeoutMs; positive integer milliseconds # validationProviderTimeoutMs: 5000 # default: inherits eventHookTimeoutMs; positive integer milliseconds # policyGateFailurePolicy: fail-closed # fail-closed (default) or fail-open # include: [build-notifier] # optional allowlist by extension name # exclude: [experimental] # optional denylist by extension name # paths: # optional explicit extension modules/directories # - ./tools/eforge-audit.ts ``` Fields: | Field | Default | Meaning | |-------|---------|---------| | `extensions.enabled` | `true` | Enables native extension loading at runtime. When `false`, extension directories and `paths` are not loaded; management commands may still report discovered candidates with `enabled: false` for visibility. | | `extensions.include` | unset | Optional allowlist for auto-discovered extension names. If set, only listed auto-discovered names are considered. | | `extensions.eventHookTimeoutMs` | `5000` | Timeout in milliseconds for each native `onEvent` handler invocation and extension-authored action invocation. Must be a positive integer. | | `extensions.agentContextHookTimeoutMs` | inherits `eventHookTimeoutMs` | Timeout in milliseconds for each `onAgentRun` handler invocation. Must be a positive integer when set. Defaults to `extensions.eventHookTimeoutMs` when omitted. | | `extensions.profileRouterTimeoutMs` | inherits `eventHookTimeoutMs` | Timeout in milliseconds for each profile-router handler invocation. Must be a positive integer when set. Defaults to `extensions.eventHookTimeoutMs` when omitted. | | `extensions.policyGateTimeoutMs` | inherits `eventHookTimeoutMs` | Timeout in milliseconds for each policy-gate handler invocation. Must be a positive integer when set. Defaults to `extensions.eventHookTimeoutMs` when omitted. | | `extensions.validationProviderTimeoutMs` | inherits `eventHookTimeoutMs` | Timeout in milliseconds for validation-provider handlers and commands. Must be a positive integer when set. Defaults to `extensions.eventHookTimeoutMs` when omitted. | | `extensions.policyGateFailurePolicy` | `fail-closed` | Failure policy for policy-gate throws, timeouts, or invalid decisions. `fail-closed` blocks the gated operation; `fail-open` records diagnostics and allows it to continue. | | `extensions.exclude` | unset | Optional denylist for auto-discovered extension names. Applied after `include`. | | `extensions.paths` | unset | Additional explicit extension file or directory paths. Relative paths resolve from the current project root. Explicit paths are validated even when outside standard extension directories. | Only the documented `extensions` keys are accepted in config files and profiles. Remove stale or obsolete project/team trust compatibility settings during upgrades; project/team extension loading now uses local trust records instead. Loading project/team extensions is controlled by explicit per-extension local trust records in `.eforge/extension-trust.json`, created with `eforge extension trust ` after inspecting the extension code. User and project-local extensions are trusted when loading is enabled. ## Discovery scopes and precedence Auto-discovery scans three directories: | Scope | Directory | Trust default | Purpose | |-------|-----------|---------------|---------| | User | `~/.config/eforge/extensions/` | trusted | Personal extensions reusable across projects | | Project/team | `eforge/extensions/` | untrusted unless a matching local trust record exists | Shared, committed team extensions | | Project-local | `.eforge/extensions/` | trusted | Local experiments and personal project overrides | Precedence for same-name auto-discovered extensions is: ```text project-local > project-team > user ``` The highest-precedence candidate wins; lower-precedence candidates with the same name are reported as `shadowed`. Project-local is the recommended starting point for new extensions. Promote an extension to `eforge/extensions/` only once it is intended for the team and document that users must inspect and trust the project/team extension locally. CLI scaffold scopes map to discovery directories as follows: local -> `.eforge/extensions/`, project -> `eforge/extensions/`, and user -> `~/.config/eforge/extensions/` by default (`$XDG_CONFIG_HOME/eforge/extensions/` when configured). ## Package-managed extensions Extensions can be distributed as npm packages, local package directories, or tarballs and installed with `eforge extension install`. A package declares itself as an eforge extension using the `eforge.extension` field in `package.json`: ```json { "name": "acme-build-notifier", "version": "1.0.0", "eforge": { "extension": { "name": "build-notifier", "entrypoint": "./dist/index.js", "capabilities": [{ "name": "acme.notifications", "version": "1.0.0" }], "dependencies": { "optional": [{ "name": "acme-backlog", "version": ">=1.0.0, <2.0.0", "capabilities": [{ "name": "acme.backlog", "version": ">=1.0.0" }] }] } } } } ``` Fields: | Field | Required | Meaning | |-------|----------|---------| | `eforge.extension.name` | Yes | Extension name used for discovery, trust records, and management commands. | | `eforge.extension.entrypoint` | Yes | Relative path from the package root to the extension module entry point. | | `eforge.extension.capabilities` | No | Public capabilities provided by the extension. Capability versions are exact semantic versions. | | `eforge.extension.dependencies.required` | No | Provider/capability requirements that must resolve before this extension is imported. Missing, shadowed, untrusted, changed, errored, version-incompatible, or capability-incompatible providers skip the dependent and emit `extension:dependency-*` diagnostics. | | `eforge.extension.dependencies.optional` | No | Provider/capability requirements that populate availability state but do not block importing the dependent. | Version constraints accept exact semantic versions, comparators (`>`, `>=`, `<`, `<=`), and comma-separated AND constraints. Invalid dependency or capability metadata is reported as `extension:invalid-package-manifest`. Contribution manifests expose `availability` for actions, commands, deep links, Console contributions, and workstations; unavailable actions return `{ ok: false, error: { code: "unavailable" } }`. Action contexts expose immutable `ctx.dependencies` and `ctx.capabilities` lookup data. They report availability only — there is no cross-extension invocation API or invocation loop state. ### Install, update, and remove ```bash # Install from an npm package name (defaults to --scope local) eforge extension install acme-build-notifier # Install from a local package directory or tarball eforge extension install ./packages/acme-build-notifier eforge extension install ./dist/acme-build-notifier-1.0.0.tgz # Install to a specific scope eforge extension install acme-build-notifier --scope project # Install and immediately trust (project/team scope only) eforge extension install acme-build-notifier --scope project --trust eforge extension install acme-build-notifier --scope project --trust --trusted-by "Alice " # Update an installed extension to the latest version eforge extension update build-notifier # Update an npm-installed extension to a version, range, or dist-tag eforge extension update build-notifier --version latest eforge extension update build-notifier --version 1.2.3 # Remove an installed extension eforge extension remove build-notifier eforge extension remove build-notifier --force ``` Install scope follows the CLI scaffold labels: `local` targets `.eforge/extensions/`, `project` targets `eforge/extensions/`, and `user` targets `~/.config/eforge/extensions/`. The default scope is `local`. Package acquisition uses the local `npm` CLI for npm specs/tarball URLs and the system `tar` command for tarball extraction, so ensure those commands are on `PATH` when using those source types. `eforge extension update --version ` forwards the specifier to npm-installed extensions only. Use it for npm versions, ranges, or dist-tags. Local package directory and tarball installs update from their recorded sidecar source rather than resolving a registry version specifier. Non-JSON output prints concrete next steps after install. When the returned entry has `trustState: "untrusted"` or `"changed"`, the CLI prints a trust command (`eforge extension trust `), a validate command, and a reload command. JSON output (`--json`) prints the daemon response directly. ### Optional first-party eforge-plan and eforge-playbooks packages `@eforge-build/eforge-plan` is the optional first-party planning package. `@eforge-build/eforge-playbooks` is the first-party playbooks package; it declares playbook management/run capabilities and optionally uses the `eforge.plan.planning-workstation >=1.0.0` capability from eforge-plan when available. This generic extension guide intentionally covers only package installation and the platform boundary; product behavior such as backlog workflows, recommendation refresh, workstation planning UX, revision flows, explicit planning-store maintenance, playbook management, and playbook run handoff is documented in [eforge-plan](/docs/eforge-plan), [Playbooks](/docs/playbooks), and the extension-owned READMEs. ```bash # Local install (trusted by default) eforge extension install @eforge-build/eforge-plan eforge extension validate eforge-plan eforge extension reload # Project/team install with post-inspection trust eforge extension install @eforge-build/eforge-plan --scope project eforge extension validate eforge-plan eforge extension trust eforge-plan eforge extension reload # Project/team install with immediate trust eforge extension install @eforge-build/eforge-plan --scope project --trust eforge extension reload # Install the first-party playbooks package eforge extension install @eforge-build/eforge-playbooks eforge extension validate eforge-playbooks eforge extension reload # Project/team install with post-inspection trust eforge extension install @eforge-build/eforge-playbooks --scope project eforge extension validate eforge-playbooks eforge extension trust eforge-playbooks eforge extension reload # Update or remove eforge extension update eforge-plan eforge extension update eforge-plan --version latest eforge extension remove eforge-plan eforge extension update eforge-playbooks eforge extension remove eforge-playbooks ``` These packages remain unsandboxed arbitrary code like any native extension. `eforge-plan` ships runtime entrypoints in `dist/` and its planning workstation browser bundle in `workstation-assets/plans/`; `eforge-playbooks` ships its runtime entrypoint in `dist/` and declarative Console contribution metadata without a workstation bundle. Local installs under `.eforge/extensions/` are trusted by default, while project/team installs under `eforge/extensions/` require each user to inspect and trust the package before loading. ### Promote and demote Installed and source-authored extensions can be promoted from project-local scope to project-team scope or demoted back: ```bash eforge extension promote build-notifier eforge extension promote build-notifier --force eforge extension demote build-notifier ``` `promote` moves the extension from `.eforge/extensions/` to `eforge/extensions/`. `demote` moves it back. After promotion, project/team scope trust requirements apply: users must run `eforge extension trust ` before the extension loads. After demotion, the extension returns to project-local scope and loads without an explicit trust record. ### Trust and supply-chain safety Package-managed extensions are **unsandboxed arbitrary code**. Installing from npm, a tarball, or a local package directory introduces supply-chain risk: a package may contain malicious code, its published source may not match the distributed artifact, and transitive dependencies are executed without a sandbox. Always inspect installed extension code before trusting it, especially before promoting to project/team scope where other team members will be asked to trust the same artifact. The content hash covers extension source files and directory-layout browser assets placed under `workstation-assets/`. Install sidecar files - package metadata, lockfile records, and other install-generated artifacts written alongside the extension module - are excluded from the trust hash. This means that reinstalling without changing the source files or trusted workstation assets does not invalidate an existing trust record. Git URL installs are unsupported. Accepted install sources are npm package specifiers (including tarball URLs), local package directories, and local `.tgz`/`.tar.gz` tarball paths. ## Supported layouts Auto-discovered and explicit extension paths support file and directory layouts. ### File layout ```text eforge/extensions/build-notifier.ts eforge/extensions/build-notifier.mts eforge/extensions/build-notifier.js eforge/extensions/build-notifier.mjs ``` The extension name is the filename without the known extension. ### Directory layout ```text eforge/extensions/build-notifier/index.ts eforge/extensions/build-notifier/index.mts eforge/extensions/build-notifier/index.js eforge/extensions/build-notifier/index.mjs ``` A directory may also provide `package.json` with a supported root `exports`, `exports["."].import`, `exports["."].default`, or `main` entrypoint pointing at `.ts`, `.mts`, `.js`, or `.mjs`. The extension name is the directory name. Unsupported files or directories are skipped during auto-discovery with a warning diagnostic. Unsupported explicit paths are errors. ## Loader strategy The loader chooses a strategy from the resolved entrypoint format: | Format | Strategy | |--------|----------| | `.js`, `.mjs` | native dynamic `import()` | | `.ts`, `.mts` | `jiti` runtime loader | The module must default-export an extension factory function. The factory is called once at load time with an `EforgeExtensionAPI` recorder. Registration methods must be called during factory execution; registrations made later are not guaranteed to be captured. ```ts import type { EforgeExtensionAPI } from "@eforge-build/extension-sdk"; export default function extension(eforge: EforgeExtensionAPI) { eforge.onEvent("plan:build:*", async (event, ctx) => { ctx.logger.info(`Build event: ${event.type}`); }); } ``` You can also use `defineEforgeExtension` for parameter inference. ## Statuses, diagnostics, and provenance The daemon and CLI expose candidates, loaded extensions, diagnostics, shadows, and registration summaries through `eforge extension` commands and daemon API routes. Statuses: | Status | Meaning | |--------|---------| | `pending` | Candidate discovered and awaiting load. Usually transient in internal results. | | `loaded` | Factory loaded successfully and registration capture completed. | | `shadowed` | Auto-discovered candidate lost to a higher-precedence extension with the same name. | | `skipped` | Candidate was intentionally skipped - most commonly because it is an untrusted project/team extension (`extension:untrusted`) or because its content changed since the last trust operation (`extension:trust-changed`). | | `excluded` | Candidate was filtered out by extension include/exclude configuration. | | `error` | Discovery, validation, import, export, or factory execution failed. | Diagnostics include severity (`warning` or `error`), stable code, message, and when available name/path/scope/source. Common diagnostics include unsupported layouts, duplicate explicit names, untrusted project extensions (`extension:untrusted`), changed content since last trust (`extension:trust-changed`), invalid default exports, and factory errors. Trust-related diagnostics also include `currentHash` and, for changed extensions, `trustedHash`. Provenance fields identify where an extension came from: - `scope`: `user`, `project-team`, `project-local`, or `external` - `source`: `auto` or `explicit` - `path` and `entrypoint` - `format`, `layout`, and `strategy` - `trust`: `trusted` or `untrusted` - `trustState`: `trusted`, `untrusted`, `changed`, or `not-required` (project-team candidates only; `not-required` for all other scopes) - `currentHash`: SHA-256 content hash at discovery time (project-team candidates) - `trustedHash`: SHA-256 hash recorded at trust time (project-team candidates with a trust record) - `trustedAt`: ISO-8601 timestamp of the most recent trust operation (project-team candidates with a trust record) - `trustedBy`: optional annotation set at trust time - `shadows`: lower-precedence candidates hidden by this candidate - `registrations`: counts captured by registration family - `package`: package provenance from `package.json`, including `packageName`, `version`, `description`, `eforgeExtensionName`, `eforgeEntrypoint`, `repository`, and `homepage` when present - `install`: install provenance from `.eforge-install.json`, including `sourceKind`, `sourceSpec`, `resolvedVersion`, `integrity`, `installedAt`, and `targetScope` for eforge-managed installs Use: ```bash eforge extension list eforge extension contributions list --kind command --search planning --limit 20 eforge extension contributions show --kind command --include-schema eforge extension contributions invoke --kind command eforge extension show build-notifier eforge extension validate eforge extension validate ./tools/eforge-audit.ts eforge extension test [nameOrPath] eforge extension test build-notifier --fixture events.json eforge extension test ./tools/eforge-audit.ts --run latest --event plan:build:failed eforge extension new eforge extension reload eforge extension trust eforge extension untrust eforge extension install eforge extension install --scope project --trust eforge extension update eforge extension update --version eforge extension remove eforge extension promote eforge extension demote ``` `eforge extension test [nameOrPath]` validates the selected extension set and dry-runs matching `onEvent` hooks against replayed events. Omit `nameOrPath` to test configured extensions, pass a configured extension name to test one loaded extension, or pass an extension file/directory path for an ad-hoc test. Path detection matches `extension validate`: `./tools/eforge-audit.ts` is a path, while `build-notifier` is a configured extension name. Replay sources: - no source: static validation and registration summary only - `--fixture `: read project-local fixture events through the daemon - `--run latest`: replay events from the latest monitor DB session - `--run `: replay events from a specific monitor session or run - `--event `: filter replay input to an exact event type before matching hooks - `--json`: print the raw `ExtensionTestResponse` Fixture files may contain one JSON event object, a JSON array of event objects, or JSONL with one event object per non-empty line. Every event is validated against the canonical eforge event schema before replay. Non-JSON output is summary-first. It reports whether the test passed, the source (`none`, `fixture`, or `run`), replay counts (`inputEventCount`, `filteredEventCount`, `emittedEventCount`, and `diagnosticEventCount`), match count, emitted event-handler diagnostics, and non-event registration family counts. Zero matching hooks are valid when the response is otherwise valid; the CLI prints a clear zero-match message and exits 0. The process exits 1 only when the daemon response has `valid: false`. `eforge extension new ` scaffolds a TypeScript extension. Defaults are `--scope local` (project-local `.eforge/extensions/`), `--template event-logger`, and no overwrite. Pass `--scope project` for committed team extensions, `--scope user` for personal cross-project extensions, `--template blank` for a minimal module, or `--force` to overwrite an existing scaffold target. Non-JSON output prints the created path, canonical daemon scope (`project-local`, `project-team`, or `user`), template, overwrite state, and next validation/reload steps. `eforge extension reload` refreshes daemon extension discovery and restarts the runtime watcher when it is currently running. JSON output is the raw daemon response, including refreshed extension entries, diagnostics, registration totals, and watcher restart metadata. Non-JSON output summarizes watcher state and diagnostic counts. MCP and Pi `eforge_extension` management calls return compact projections by default for list, show, validate, reload, test, install/update/remove, trust/untrust, promote/demote, and new-extension actions: identity, status, trust, path, scope, source, registration counts, diagnostic counts, sample summaries, and next steps are kept, while raw daemon discovery objects are omitted unless you use CLI `--json` or daemon/client HTTP APIs for explicit debug inspection. List/show projections preserve every action detail identity, label/name, and output profile, but omit large schemas and raw diagnostic detail from compact defaults. List/show output includes `enabled`, a derived boolean for whether the entry is selected by the current extension config and is not shadowed or excluded. It is `false` when extensions are globally disabled, when include/exclude filters leave the entry out, or when a higher-precedence extension shadows it. A selected entry can still have `enabled: true` with status `skipped` or `error`; use status, trust, and diagnostics to see why it did not load. Add `--json` to CLI commands for machine-readable provenance. The same data is exposed via `/api/extensions/list`, `/api/extensions/show`, `/api/extensions/validate`, `/api/extensions/new`, `/api/extensions/reload`, `/api/extensions/test`, `/api/extensions/trust`, `/api/extensions/untrust`, `/api/extensions/install`, `/api/extensions/update`, `/api/extensions/remove`, `/api/extensions/promote`, and `/api/extensions/demote`. `eforge extension trust` and `eforge extension untrust` discover and hash project/team candidates, then update `.eforge/extension-trust.json`; they do not import the extension module or execute its factory. The trust decision takes effect when a later validate, test, reload, or build operation loads the extension. `extension enable` and `extension disable` workflows are not implemented in the current release. ## Runtime support today The runtime foundation is shipped: discovery, trust gating, loader strategy selection, factory execution, registration capture, diagnostics, status reporting, CLI/API/MCP/Pi inspection and management tooling, native `onEvent` dispatch, `onAgentRun` prompt-context augmentation, per-run extension tool injection, per-run tool availability tuning, pre-build `registerProfileRouter` dispatch, and per-invocation `registerRuntimeChoiceRouter` dispatch after declarative runtime-choice rules are available. Event hooks run for real CLI, queue worker, and daemon watcher event streams. Dispatch is non-blocking with respect to the engine pipeline: handlers receive matching events but cannot alter or stop the triggering work. Handler failures and timeouts emit `extension:event-handler:*` diagnostics with the extension name, matched pattern, triggering event type, and available `sessionId`/`runId` correlation fields. Those diagnostics are recorded by the monitor before shell hooks run, so shell-hook matching has parity with normal engine and extension diagnostic events. Event replay testing is also available through `eforge extension test`. Replay execution is a dry run for `onEvent` hooks only: it invokes matching event handlers against fixture or monitor DB events and records emitted handler diagnostics, but it does not execute custom tools, policy gates, profile routers, runtime-choice routers, input sources, reviewer perspectives, validation providers, or agent-run hooks. Those non-event registrations are summarized separately in the test result; this replay limitation does not reflect whether the capability runs during normal engine execution. ### Actions, Console contributions, commands, and deep links Extension authors can use `registerAction`, `registerConsoleContribution`, `registerIntegrationCommand`, and `registerDeepLink`, and `registerConsoleWorkstation` to publish typed, manifest-backed control surfaces. Local IDs in extension source are projected as effective namespaced IDs (`:`) in the daemon manifest. These contribution registrations may declare dependency/capability `requirements`; manifest entries include `availability`, and unavailable actions are rejected with error code `unavailable`. Console contributions render only with closed renderer IDs (`text`, `markdown`, `status-badge`, `link`, `action-button`, and `action-form`) inside `/console/system`; outside registered workstation `srcDoc` or `frameBundle` sources, arbitrary Console JavaScript is unsupported, direct parent-Console React/components are unsupported, and independently loaded frontend plugins are unsupported. Actions, integration commands, and action-backed deep links are discoverable through CLI `eforge extension contributions list|show|invoke`, MCP/Claude `eforge_extension_contribution` (`mcp__eforge__eforge_extension_contribution` in Claude tool-call form), Pi `eforge_extension_contribution`, and Pi `/eforge:extensions`. URL-only deep links can be listed for host navigation, but generic contribution invocation requires an action binding. Contribution discovery surfaces return compact, formatted list text by default rather than raw full manifests. MCP and Pi coding-agent host text is capped to a 12,000-character budget; contribution list rendering stops on whole-entry boundaries and reports `returned`, `total`, and `nextOffset` continuation guidance when more entries remain. Narrow broad lists with `--kind`, `--extension-name`, `--search`, `--id-prefix`, `--output-profile`, `--limit`, and `--offset` (or the matching MCP/Pi tool fields), page through list results, then use `show ` / `action: "show"` for focused detail. Opt into heavier payloads with CLI `--include-schema`, `--include-diagnostics`, or `--full` (MCP/Pi fields: `includeInputSchema`, `includeDiagnostics`, or `full`) when a human or script needs schemas or diagnostics. Pi interactive browsing lists compact entries first, then fetches selected contribution detail with input schema only when it needs to prompt for required fields. Failed MCP/Pi invocations return a summarized envelope with action identity, target identity, error code/message, and input key/size summary; raw `target.input` is omitted. Use CLI `--json` or typed daemon/client HTTP helpers when you intentionally need full raw manifests or debug-rich action payloads. Action input schemas must be TypeBox object-root schemas (`Type.Object(...)`). Handler outputs must be JSON-safe; when an optional output schema is present, eforge validates the returned output before reporting success. Action handlers run as trusted, unsandboxed Node code in the daemon/worker process and reuse `extensions.eventHookTimeoutMs` for timeout enforcement. Throw `ExtensionActionInputValidationError` for schema-like field issues or `ExtensionActionUserError` for user-fixable domain/precondition failures; both surface as `invalid-input` with sanitized message/details. Unexpected exceptions remain generic `handler-error` responses. The daemon owns manifest projection and action invocation; TypeScript consumers should use `fetchExtensionContributionManifest`, `invokeExtensionAction`, and client-owned `API_ROUTES` helpers from `@eforge-build/client` rather than constructing raw `/api/...` paths. Extension-owned raw HTTP route registration is unsupported. Action lifecycle diagnostics/events include provenance, duration, status, and error metadata, but omit raw input payloads and raw output payloads. This keeps daemon logs and event streams useful for observability without leaking action arguments or results. ### Designing bounded contribution actions Contribution actions should be bounded by default because the same action may be invoked from a rich Console UI, a CLI, an MCP/Pi tool call, or a coding-agent context window. Prefer separate contracts for list, detail, and search operations: use compact `search-*` or `list-*` actions for summaries, targeted `get-*` actions for one record, and explicit opt-in flags for raw Markdown bodies, long evidence, trace rows, lifecycle links, recommendations, or other UI-only fields. Keep workstation-rich payloads separate from agent-safe payloads rather than making every host pay for the full UI projection. Actions may also declare an optional `outputProfile` so hosts can choose safe formatting without re-inferring payload shape. Supported profiles are `agent-compact`, `agent-paginated`, `markdown`, `ui-rich`, and `debug-rich`. Use agent profiles for coding-agent-safe summary/detail payloads, `markdown` for actions that return exactly `{ markdown: string }`, `ui-rich` for workstation-oriented rich payloads, and `debug-rich` for compatibility/debug surfaces that intentionally remain large. The engine treats output profiles as manifest metadata only; they do not change action invocation success/failure response shapes. Host integrations format invocation output defensively. CLI non-`--json`, MCP/Claude, Pi tools and panels, and Console previews render exact `{ markdown: string }` outputs as Markdown/plain text instead of escaped JSON. MCP and Pi apply the shared 12,000-character host-output budget to final coding-agent tool text: MCP returns capped text, while Pi tool details replace raw detail payloads with bounded metadata such as budget, raw length, summary/truncation flags, output kind, warnings, and guidance. Oversized JSON output is summarized semantically before any final character cap: top-level keys remain visible, arrays retain `count`, representative entries keep identity fields such as `id`, `itemId`, `epicId`, `title`, `name`, `status`, `state`, `kind`, and `lane`, and summaries preserve pagination/continuation fields such as `limit`, `offset`, `nextOffset`, `cursor`, and `nextCursor` with omitted counts. Hosts warn when output is summarized or truncated, and they warn for `ui-rich`/`debug-rich` profiles even when the current payload fits. Use CLI `--json` or daemon/client raw invocation helpers only when a human explicitly needs the full raw payload; coding-agent workflows should prefer compact, paginated, or Markdown actions. The extension SDK exports small helpers for paginated actions: `createContributionPaginationInputFields()`, `createContributionPageOutputSchema(itemSchema)`, `resolveContributionPagination()`, `paginateContributionItems()`, `CONTRIBUTION_OUTPUT_PROFILES`, and `contributionOutputProfile()`. They standardize `limit`/`offset`, cap excessive limits, return the common `{ items, total, limit, offset }` shape, and keep profile strings typed and JSON-safe while leaving domain filters and projection fields to the extension. Broad list/search/board-style read action registrations remain valid, but validation emits warning diagnostics for id-shaped broad reads with array-shaped outputs when their input schemas lack limit or cursor/page controls, or when array-shaped output schemas omit an explicit output profile. `agent-paginated` actions with limit and cursor/page controls are considered bounded for agent use without separate projection controls. These warnings are advisory for extension ergonomics; invalid registration shapes still use error diagnostics and are not recorded. ```ts const Summary = Type.Object({ id: Type.String(), title: Type.String() }, { additionalProperties: false }); const SearchInput = Type.Object({ query: Type.Optional(Type.String()), ...createContributionPaginationInputFields({ maxLimit: 50 }), }, { additionalProperties: false }); const searchItems = defineExtensionAction({ id: "search-items", title: "Search items", inputSchema: SearchInput, outputSchema: createContributionPageOutputSchema(Summary), outputProfile: CONTRIBUTION_OUTPUT_PROFILES.agentPaginated, sideEffects: ["local-read"], handler: (input) => paginateContributionItems(findMatches(input.query), input, { defaultLimit: 20, maxLimit: 50 }), }); ``` ### Daemon-owned agent tasks from actions Action handlers can inspect immutable dependency and capability availability through `ctx.dependencies` and `ctx.capabilities`, start/read/cancel daemon-owned single-shot agent tasks through `ctx.agentTasks`, and submit trusted build-queue handoffs through `ctx.buildQueue.enqueue({ source, ... })`, which uses the same daemon queue path as `POST /api/enqueue`, validates producer-agnostic metadata such as `postMerge` command arrays, and performs session-plan submission bookkeeping. Actions that enqueue should declare the `build-queue` side effect. Dependency/capability lookup reports availability only and does not invoke another extension. The action never imports provider SDKs or `AgentHarness`; the daemon owns task persistence, profile/runtime resolution, cancellation, and lifecycle events. Task records are stored under `.eforge/storage/agent-tasks`, and task events include only sanitized metadata rather than raw task input, prompt context, or result payloads. The MVP task runner is intentionally narrow. It resolves the existing `planner` role and enforces read-only agent tools for the run. Extensions cannot supply arbitrary raw prompt templates, register custom task kinds, expose multi-turn chat, or bypass the daemon-owned task lifecycle. The first supported task kind is a single-shot planning-draft task; extension-specific UI and higher-level eforge-plan actions can build on this boundary without owning the agent runtime directly. A planning-draft result is either a ready result that carries at least one applicable structured output section for a supported first-party workflow, or a `needs-input` decision that carries structured clarification questions and a rationale instead of output sections. Product-specific output-section names and preview/apply semantics belong to the extension that requested the task; the daemon owns only task records, status, result validation, cancellation, sanitized progress, and error lifecycle. While a task runs, the agent may report telemetry-only `sectionProgress` (current, covered, and remaining sections) through a read-only progress tool. The daemon sanitizes and length-caps that progress before persisting it on the running record and emitting it on sanitized progress events; section progress is advisory telemetry only and never determines readiness or apply eligibility. ### Console workstations Extension authors can also register sandboxed Console workstations for richer trusted extension UI under `/console/workstations`. V1 source-authored workstations are manifest-backed entries registered with `registerConsoleWorkstation` and declare exactly one source: inline iframe `srcDoc` or `frameBundle` bundle metadata. Bundle roots must be `workstation-assets` or a child directory under `workstation-assets/`; `entrypoint`, `styles`, and `assets` paths are relative to that root. Bundle entries render as sandboxed iframe `src` navigations to the manifest `frameBundle.frameUrl` with the bridge token in the URL fragment, not in the daemon route query string. Declared `frameBundle` assets are supported and served only through eforge-owned frame/asset routes. The daemon serves a generated frame shell through eforge-owned frame routes with no-cache semantics and a `Content-Security-Policy` header, and serves declared bundle files through immutable, content-addressed asset URLs from eforge-owned asset routes; browsers never provide filesystem-relative asset paths. Workstations are not sanitized declarative content and they are not rendered through the closed `/console/system` block renderer. Treat workstation `srcDoc` HTML and bundle metadata/code as trusted extension UI from the same unsandboxed extension codebase, isolated in the browser by the Console-owned iframe sandbox rather than by HTML sanitization. A workstation can call the parent-owned action bridge with `window.eforge.invokeAction(actionId, input)` or use `invokeAction` from the browser-safe `@eforge-build/extension-sdk/browser` subpath. Console validates the request against the workstation's manifest `allowedActions`, invokes the daemon-owned action dispatcher, and posts a success or failure response back into the iframe. Authors may list explicit local action IDs in `allowedActions`; when the allowlist is omitted, the manifest projection defaults to actions registered by the same extension. The iframe may request a local ID such as `render-board-markdown` or an effective manifest ID such as `my-extension:render-board-markdown` when that effective ID is allowed. Minimal workstation pattern: ```ts import { Type, defineConsoleWorkstation, defineEforgeExtension, defineExtensionAction } from "@eforge-build/extension-sdk"; const renderSummary = defineExtensionAction({ id: "render-summary", title: "Render summary", inputSchema: Type.Object({}), outputSchema: Type.Object({ markdown: Type.String() }), sideEffects: ["none"], handler: () => ({ markdown: "# Extension summary\nReady." }), }); export default defineEforgeExtension((eforge) => { eforge.registerAction(renderSummary); eforge.registerConsoleWorkstation(defineConsoleWorkstation({ id: "summary-workstation", title: "Summary workstation", srcDoc: `

`,
    allowedActions: ["render-summary"],
  }));
});
```

A bundle-backed source can use `frameBundle` instead of `srcDoc` when the declared files already exist under `workstation-assets/`:

```ts
eforge.registerConsoleWorkstation(defineConsoleWorkstation({
  id: "bundle-workstation",
  title: "Bundle workstation",
  frameBundle: {
    root: "workstation-assets/demo",
    entrypoint: "index.js",
    styles: ["index.css"],
    assets: ["logo.svg"],
    browserSdkVersion: 1, // optional; omitted means browser SDK v1
  },
  allowedActions: ["render-summary"],
}));
```

Browser bundle modules use the browser-safe SDK subpath rather than private Console modules:

```ts
import { invokeAction } from "@eforge-build/extension-sdk/browser";

const result = await invokeAction("render-summary", {});
```

V1 intentionally keeps several boundaries deferred: extension-authored arbitrary frontend asset bundles remain deferred beyond the daemon-owned workstation frame/asset manifest contract; direct React component loading into the parent Console, parent-Console plugins, parent Console context imports, and private Console React/components/CSS imports remain unsupported; raw extension-owned HTTP routes remain unsupported; extension-owned AI planning/chat APIs remain unsupported outside the daemon-owned `ctx.agentTasks` single-shot task boundary; arbitrary independently loaded frontend plugins remain deferred. Authors may bundle React or another browser framework inside a `frameBundle` iframe, but that code executes inside the workstation iframe boundary, not in the parent Console realm. Iframe bundle code should use the versioned `@eforge-build/extension-sdk/browser` helpers or host-rendered slots rather than private Console React imports.

Agent-run hooks fire before each agent invocation. Handlers can inspect `ctx.role`, `ctx.tier`, `ctx.phase`, and `ctx.stage` to scope their contribution, then return `{ promptAppend, tools, allowedTools, disallowedTools }` to inject additional prompt context, expose extension tools for that run, and tune harness availability lists. Prompt fragments are appended after any config-level `promptAppend` already resolved by the engine, wrapped in a named provenance section identifying the contributing extension. Multiple extensions contribute in registration order. The runtime is fail-open: a handler that throws or exceeds `extensions.agentContextHookTimeoutMs` emits a typed diagnostic event but does not abort the agent run.

Policy gates run at three blocking points: `beforeQueueDispatch` runs before a queued PRD is dispatched, `beforePlanMerge` runs before a plan worktree is merged, and `beforeFinalMerge` runs before the completed feature branch is merged into the base branch. Gate contexts are read-only snapshots and include `ctx.logger` and `ctx.exec`; `beforeQueueDispatch` contexts also include optional `continueRepair` metadata for continue-and-repair queue items backed by complete compiled artifacts. They are not a sandbox. Extensions remain trusted, unsandboxed code running in the daemon/worker process.

Policy decisions are strictly validated. `{ decision: 'allow' }` lets the operation continue. `{ decision: 'block', reason }` blocks it and surfaces the reason. `{ decision: 'require-approval', reason }` blocks the gated operation; eforge does not provide approval workflow, approval state, or Console approval UI in the current release. Thrown errors, timeouts, and invalid decisions emit `extension:policy:*` diagnostics and follow `extensions.policyGateFailurePolicy`: `fail-closed` blocks, while `fail-open` allows continuation after recording diagnostics.

Unsupported extension capability families are still recorded as registration metadata when applicable so provenance and validation output remain complete. User-authored session-plan extraction remains unsupported, and user-authored playbook extraction remains unsupported. For tools, `registerTool(tool)` records loader-time provenance and validation metadata; returning `tools: [tool]` from `onAgentRun` is the per-run injection path. Extension-authored actions, Console contributions, integration commands, and deep links are captured in the engine registry with safe management details and manifest projection; the daemon exposes contribution manifest and action invocation routes, Console renders declarative Console contributions under `/console/system`, and CLI, MCP/Claude, and Pi host integrations can discover actions, integration commands, and action-backed deep links through the shared contribution dispatcher. Action invocations reuse `extensions.eventHookTimeoutMs`, receive the daemon-owned `ctx.agentTasks` API for supported single-shot read-only planner tasks and `ctx.buildQueue.enqueue` for trusted queue handoffs, and emit daemon-scoped `extension:action:*` lifecycle events without raw input payloads or raw output payloads. The shipped playbook action surface lives in the first-party `eforge-playbooks` extension, and the session-planning adapter remains internal/built-in; neither is a user-authored native extension registration point. `beforeEnqueue`, `beforeValidation`, `modify` decisions, custom session-plan extraction into extensions, custom playbook extraction into extensions, raw extension-owned HTTP routes, arbitrary Console JavaScript outside registered workstation documents, direct React loading into the parent Console, private Console React/components/CSS imports, parent Console context imports, extension-authored arbitrary frontend asset bundles outside the workstation frame/asset contract, extension-owned AI planning/chat APIs outside `ctx.agentTasks`, multi-turn chat, arbitrary raw prompt templates, and independently loaded frontend plugins are unsupported runtime phases. Approval workflows, approval state, and Console approval UI are not provided in the current release.

| Capability | Type contract | Loader-time registration capture | Runtime execution today |
|-----------|---------------|----------------------------------|-------------------------|
| `onEvent` - typed event subscriptions | Yes | Yes | Yes |
| `onAgentRun` - agent prompt/tool augmentation and availability tuning | Yes | Yes | Yes |
| `registerTool` - custom agent tool provenance | Yes | Yes | Provenance only; inject per run via `onAgentRun` |
| `beforeQueueDispatch` - policy gate before queued PRD dispatch | Yes | Yes | Yes (blocking policy gate) |
| `beforePlanMerge` - policy gate before plan worktree merge | Yes | Yes | Yes (blocking policy gate) |
| `beforeFinalMerge` - policy gate before final feature merge | Yes | Yes | Yes (blocking policy gate) |
| `registerProfileRouter` | Yes | Yes | Yes (pre-build dispatch) |
| `registerRuntimeChoiceRouter` | Yes | Yes | Yes (per-invocation runtime-choice dispatch after declarative rules) |
| `registerInputSource` | Yes | Yes | Yes (extension-aware enqueue preprocessing) |
| `registerPrdEnricher` | Yes | Yes | Yes (fail-open content enrichment before queue write) |
| `registerReviewerPerspective` | Yes | Yes | Yes (parallel review-cycle dispatch) |
| `registerValidationProvider` | Yes | Yes | Yes (per-plan `validate` build stage) |
| `registerAction` | Yes | Yes | Engine action dispatcher via daemon action invocation route; action context includes dependency/capability lookup, daemon-owned `ctx.agentTasks`, and `ctx.buildQueue` |
| `registerConsoleContribution` | Yes | Yes | Daemon contribution manifest projection; Console renders declarative panels under `/console/system` |
| `registerIntegrationCommand` | Yes | Yes | Daemon contribution manifest projection; host integrations can invoke action-backed commands |
| `registerDeepLink` | Yes | Yes | Daemon contribution manifest projection; host integrations can invoke action-backed deep links |
| `registerConsoleWorkstation` | Yes | Yes | Daemon contribution manifest projection; Console renders sandboxed iframe workstations under `/console/workstations` from `srcDoc` entries or source `frameBundle` entries projected to daemon frame/asset URLs |

Event-hook, agent context/tool injection, profile-router, shipped policy-gate, input-source fetching, PRD enrichment, reviewer perspective, validation provider, daemon-owned action agent tasks, daemon-owned action build queue handoffs, and contribution-family examples can be loaded and validated at runtime. Event-oriented examples include [`examples/extensions/minimal-event-logger.ts`](https://github.com/eforge-build/eforge/blob/main/examples/extensions/minimal-event-logger.ts) and the safe [`examples/extensions/slack-webhook-notifier.ts`](https://github.com/eforge-build/eforge/blob/main/examples/extensions/slack-webhook-notifier.ts), which only sends a Slack-compatible webhook when `EFORGE_SLACK_WEBHOOK_URL` is set. [`examples/extensions/agent-tools.ts`](https://github.com/eforge-build/eforge/blob/main/examples/extensions/agent-tools.ts) demonstrates defining a TypeBox tool, registering it for provenance, and returning it only for builder runs with `ctx.effectiveToolName(...)` in prompt text. Profile routers run before each PRD build is dispatched from the queue: routers are invoked in registration order with `extensions.profileRouterTimeoutMs` timeout/fail-open semantics, and the first valid profile selection persists to the PRD's frontmatter before `session:start` is emitted. When a router selects a profile, a `queue:profile:selected` event is emitted with the PRD id, selected profile, router name, and optional reason/confidence fields. An explicit `profile:` field in the PRD's frontmatter takes absolute precedence — no routers are consulted. See [`examples/extensions/profile-router.ts`](https://github.com/eforge-build/eforge/blob/main/examples/extensions/profile-router.ts) for a Claude → Codex → local fallback example. [`examples/extensions/protected-paths.ts`](https://github.com/eforge-build/eforge/blob/main/examples/extensions/protected-paths.ts) demonstrates runtime-supported plan/final merge policy enforcement for protected paths. [`examples/extensions/issue-tracker.ts`](https://github.com/eforge-build/eforge/blob/main/examples/extensions/issue-tracker.ts) demonstrates runtime-supported input source adapters for GitHub, Linear, and Jira. [`examples/extensions/reviewer-perspective.ts`](https://github.com/eforge-build/eforge/blob/main/examples/extensions/reviewer-perspective.ts) demonstrates a runtime-supported accessibility reviewer perspective with declarative `appliesTo.fileGlobs` applicability for UI/TSX files. [`examples/extensions/validation-provider.ts`](https://github.com/eforge-build/eforge/blob/main/examples/extensions/validation-provider.ts) demonstrates runtime-supported validation providers in both function form (programmatic) and command form (subprocess). [`examples/extensions/action-contribution.ts`](https://github.com/eforge-build/eforge/blob/main/examples/extensions/action-contribution.ts) demonstrates an action-backed Console contribution, integration command, and deep link without raw HTTP routes or browser code. Raw extension-owned HTTP routes are unsupported. `beforeEnqueue`, `beforeValidation`, `modify` decisions, user-authored custom session-plan extraction, user-authored custom playbook extraction, arbitrary frontend plugin bundles outside registered workstation iframes, extension-authored arbitrary frontend asset bundles outside the workstation frame/asset contract, direct React component loading into the parent Console, private Console React/components/CSS imports, parent Console context imports, extension-owned AI planning/chat APIs outside `ctx.agentTasks`, multi-turn chat, and arbitrary raw prompt templates are unsupported runtime phases. Approval workflow/state/UI is not provided in the current release.

### Input sources and PRD enrichers

Input sources and PRD enrichers run during the enqueue preprocessing stage, before the build source artifact is written to the queue. For usage examples from each host surface (Claude Code, Pi, CLI), see [Integrations - Input source adapters](/docs/integrations#input-source-adapters-github-linear-jira).

**`registerInputSource` — URI-based artifact fetching**

Input source adapters are selected by `name` against the `` segment of an `eforge://input//` URI. The runtime calls `adapter.fetch(id, ctx)` with the remaining `` path and an `InputTransformContext`. During enqueue preprocessing this context is limited to cwd/provenance metadata plus stub helpers: `ctx.exec.run` is unavailable and throws, and `ctx.logger` is a no-op logger rather than event-hook logging.

URI examples:
- `eforge://input/github/acme/backend#42` — adapter `github`, id `acme/backend#42`
- `eforge://input/linear/ENG-42` — adapter `linear`, id `ENG-42`
- `eforge://input/jira/ENG-42` — adapter `jira`, id `ENG-42`

Adapters may return a raw content string, an `InputSourceResult` object `{ content, title? }`, or `null` to signal that the identifier was not found. Returning `null` is fatal to enqueue (`FatalPreprocessingError`). Throwing is also fatal. Design adapters to be safe-by-default: when required credentials are absent, return an `InputSourceResult` with instructional content rather than throwing.

Provenance events emitted per adapter call:
- `extension:input-source:fetched` — adapter returned content successfully.
- `extension:input-source:failed` — adapter threw or returned `null`.

**`registerPrdEnricher` — content augmentation before queue write**

PRD enrichers run in registration order after input source preprocessing completes. Each enricher receives `{ content, sourceId, ctx }` and may return `{ content }` to replace the content, or `null`/`undefined` to pass it through unchanged. Enrichers always run for every preprocessed source; gate behavior inside `enrich` using `ctx.sourceKind`, `ctx.adapterId`, or `ctx.sourcePath` if needed. The preprocessing context has the same limits as input-source adapters: `ctx.exec.run` throws, and `ctx.logger` is a no-op logger.

Enricher failures are fail-open: a thrown error emits `extension:prd-enricher:failed` with the enricher name, source id, and error message, and the unchanged content carries forward.

Provenance events emitted per enricher call:
- `extension:prd-enricher:applied` — enricher returned modified content.
- `extension:prd-enricher:failed` — enricher threw (content unchanged; build continues).

### Validation providers

Validation providers execute during the per-plan `validate` build stage, after the implement stage completes and before the review stage, when `validate` is included in the build pipeline. Each registered provider is invoked in registration order for every plan that reaches the validate stage. Providers are fail-closed gates: normal validation failures can be repaired first, but unresolved failures still fail the current plan and emit `plan:build:failed`.

Normal validation-provider failures are recoverable before fail-closed terminal failure: structured `{ status: 'failed' }` function-form results and command-form non-zero exits enter the plan's recovery loop. Recovery is bounded by the plan's `review.maxRounds` budget. After each recovery attempt, eforge reruns the provider suite from the first provider so earlier gates can re-check changes made during recovery.

Hard provider failures bypass recovery and emit terminal `plan:build:failed` immediately: thrown exceptions or rejected promises, provider timeouts, non-empty string returns from function-form providers, and unexpected return shapes. Use these hard-failure paths for extension bugs or unavailable infrastructure, not ordinary quality-gate findings.

A function-form provider returns `null` or `undefined` to pass, or a structured `ValidationProviderResult` object with `status` (`'passed'`, `'failed'`, or `'skipped'`), an optional `message`, optional extended `details`, and optional per-file `annotations`. Non-empty string failure returns are not part of the function-form contract; they are treated as unexpected return shapes.

Structured annotations are the best path to precise recovery issues. Include `file` and `line` whenever possible so the repair agent can target the relevant location instead of inferring it from free-form output. Each annotation may include `details`, `fix`, `retryGuidance`, provider-authored `failureKind`, `repairClass` (`narrow`, `structural`, `manual`, or `followup`), and small JSON-safe `metadata`.

Use `repairClass: 'narrow'` or omit it for localized fixes. Use `repairClass: 'structural'` when the correct fix requires extraction, file splitting, or broader code organization changes. Use `manual` when the provider should fail closed without automated repair. Use `followup` for findings that should only fail closed when every remaining issue is follow-up-only; mixed follow-up plus automatable issues route according to the narrow/structural guidance.

Function-form annotations are normalized into review issues. Narrow or unspecified issues go through the review-fixer path first. Structural issues route to the validation-fixer path. If the same validation failure signature survives a prior narrow repair attempt, eforge escalates the next attempt to structural repair. Any manual annotation disables automated repair, and an all-follow-up failure set fails closed without an automated attempt; mixed follow-up plus narrow or structural issues routes according to the remaining automatable issues.

Before each automated validation-provider repair attempt, eforge writes a checkpoint under `.eforge/validation-recovery///attempt--/` with `checkpoint.patch` and `metadata.json`. The repair prompt references these paths, and the evaluator receives the same validation repair context. Every narrow or structural validation repair is evaluator-mediated: a candidate patch must be judged as a strict improvement before the provider suite reruns.

Command-form providers are useful for subprocess exit-code gates. A non-zero exit code is a recoverable generic subprocess failure with stderr (or stdout if stderr is empty) as the message. Command form cannot attach annotations, `repairClass`, `retryGuidance`, `failureKind`, or `metadata`; use function form when you need structured guidance.

## Schema language

The SDK uses [TypeBox](https://github.com/sinclairzx81/typebox) as its schema language for custom tools:

```ts
import { defineExtensionTool, Type } from "@eforge-build/extension-sdk";

const myTool = defineExtensionTool({
  name: "my-tool",
  description: "Does something useful",
  inputSchema: Type.Object({ path: Type.String() }),
  handler: async ({ path }) => `processed: ${path}`,
});
```

Supported authoring pattern:

1. Define the tool with `defineExtensionTool` and TypeBox.
2. Call `eforge.registerTool(tool)` during factory execution so loader/list output records provenance and validation metadata.
3. Return `{ tools: [tool] }` from `eforge.onAgentRun(...)` only for the roles/stages that should receive the tool.
4. Use `ctx.effectiveToolName(tool.name)` when prompt text names the tool, because harnesses may expose different visible names.
5. Use `allowedTools` and `disallowedTools` only for per-run harness availability tuning. They are not toolbelt configuration.

Tool sources stay distinct: engine-internal custom tools are owned by eforge, harness built-ins are owned by the selected harness, toolbelt-selected project MCP tools come from `.mcp.json`, and extension-contributed tools come from TypeScript extensions returned for a run.

Zod does not appear in the SDK public surface. If you use Zod internally, adapt it at the extension boundary.

## Event patterns

Event subscriptions accept glob-style patterns using `*` as a wildcard. The wildcard matches any characters including `:`:

| Pattern | Matches |
|---------|---------|
| `plan:build:failed` | Exact match only |
| `plan:build:*` | `plan:build:start`, `plan:build:complete`, `plan:build:failed`, etc. |
| `*:complete` | `planning:complete`, `plan:build:complete`, `merge:finalize:complete`, etc. |
| `*` | Every event |

Pattern semantics match shell hooks. See the [Events Reference](/reference/events) for public event types.

## Trust and security

- Extensions run in the eforge daemon/worker Node process without a sandbox.
- User (`~/.config/eforge/extensions/`) and project-local (`.eforge/extensions/`) extensions load when `extensions.enabled` is true.
- Project/team extensions (`eforge/extensions/`) are unsandboxed arbitrary code committed to the repository. They require an explicit per-extension local trust record in `.eforge/extension-trust.json` — created by `eforge extension trust ` — before loading. Any code change invalidates the stored hash and blocks the extension until re-trusted.
- The content hash covers the entrypoint for file-layout extensions and, for directory-layout extensions, `package.json` plus `.ts`, `.mts`, `.js`, and `.mjs` files under the extension directory (excluding top-level `node_modules/`, `dist/`, and `.git/`). It also covers every regular file under `workstation-assets/`, including nested `dist/`, `node_modules/`, or `.git/` directories there, so trusted workstation bundle assets are covered. Files imported from outside the extension directory — and non-source/data files outside `workstation-assets/` — are not covered; keep implementation code inside the extension directory and in hashed source files to ensure relevant code changes are captured.
- Explicit paths outside standard scopes are treated as `external` and trusted when enabled, so use them only for code you control.
- Do not load extensions from unreviewed repositories or package artifacts.
- Treat `eforge extension test` as code execution, not static analysis. The replay path is a dry run with respect to eforge engine state, but matching `onEvent` handlers still execute in the daemon process and can perform filesystem, environment, and network operations.

## API reference

For full type signatures and method documentation, see [`docs/extensions-api.md`](./extensions-api.md). For upgrade expectations, see the [SDK stability and migration guidance](./extensions-api.md#sdk-stability-and-migration-guidance).




---
title: Extensions API Reference
description: Type-level reference for extension hooks, actions, Console contributions, commands, and deep links in @eforge-build/extension-sdk.
---

# Extensions API Reference

This document is the type-level reference for `@eforge-build/extension-sdk`. For conceptual background, scope model, management commands (`eforge extension list/show/validate/test/new/reload`), and example walkthroughs, see [Extensions](/docs/extensions). For the user-facing profile creation, switching, and scope model that `registerProfileRouter` interacts with, see [Profiles](/docs/profiles).

## Entrypoint

An extension is a TypeScript module with a default-export factory function:

```ts
import type { EforgeExtensionAPI } from "@eforge-build/extension-sdk";

export default function extension(eforge: EforgeExtensionAPI): void | Promise {
  // register handlers on eforge
}
```

The factory is called once when the extension is loaded. All registrations must happen synchronously during the factory call (or within the awaited `Promise` if the factory is async). Registrations made after the factory resolves are not guaranteed to take effect.

### `defineEforgeExtension(factory)`

A no-op identity helper for TypeScript inference. Useful when you want parameter inference without explicitly importing `EforgeExtensionAPI`:

```ts
import { defineEforgeExtension } from "@eforge-build/extension-sdk";

export default defineEforgeExtension((eforge) => {
  // eforge is inferred as EforgeExtensionAPI
});
```

**Type:** `(factory: EforgeExtensionFactory) => EforgeExtensionFactory`

**Runtime cost:** none (returns the factory unchanged).

---

## Dependency and capability contracts

Directory-layout extensions may declare public capabilities and dependency requirements in `package.json#eforge.extension` before any extension code is imported:

```json
{
  "name": "acme-workflow",
  "version": "1.2.0",
  "eforge": {
    "extension": {
      "name": "acme-workflow",
      "capabilities": [{ "name": "acme.workflow", "version": "1.2.0" }],
      "dependencies": {
        "required": [{ "name": "acme-core", "version": ">=1.0.0" }],
        "optional": [{ "capabilities": [{ "name": "acme.backlog", "version": ">=1.0.0" }] }]
      }
    }
  }
}
```

Capability declarations use `{ name, version? }` with exact semantic versions. Dependency entries use `{ name?, version?, capabilities? }`; omit `name` only for capability-only requirements. Version constraints support exact semantic versions, `>`, `>=`, `<`, `<=`, and comma-separated AND constraints. Required failures skip the dependent extension; optional failures keep it loaded and surface availability metadata.

Actions, Console contributions, workstations, commands, and deep links may also declare `requirements?: { dependencies?: [...], capabilities?: [...] }`. The contribution manifest includes `availability`; unavailable actions are rejected with error code `unavailable`.

Action handlers receive immutable lookup data:

```ts
eforge.registerAction({
  id: "inspect-backlog",
  title: "Inspect backlog availability",
  inputSchema: Type.Object({}),
  requirements: { capabilities: [{ name: "acme.backlog" }] },
  handler: (_input, ctx) => ({
    dependency: ctx.dependencies.get("acme-core").available,
    backlog: ctx.capabilities.get("acme.backlog", ">=1.0.0").available,
  }),
});
```

The lookup API reports availability only. It does not call, proxy, or invoke another extension.

---

## Configuration fields

Policy gate and validation-provider runtime behavior is controlled by native extension config:

| Field | Default | Meaning |
|-------|---------|---------|
| `extensions.policyGateTimeoutMs` | inherits `extensions.eventHookTimeoutMs` | Timeout in milliseconds for each `beforeQueueDispatch`, `beforePlanMerge`, and `beforeFinalMerge` handler. Must be a positive integer. |
| `extensions.validationProviderTimeoutMs` | inherits `extensions.eventHookTimeoutMs` | Timeout in milliseconds for each validation-provider function or command. Must be a positive integer. |
| `extensions.policyGateFailurePolicy` | `fail-closed` | Failure policy for thrown, timed-out, or invalid policy gates. `fail-closed` blocks the gated operation; `fail-open` records diagnostics and allows it to continue. |

---

## Scoped storage helpers

### `createEforgeProjectPaths(opts)`

Create scoped path helpers for eforge-owned storage locations. Use these helpers when extension tooling or runtime handlers need deterministic user, project-team, or project-local paths without depending on ad hoc string concatenation.

```ts
import { createEforgeProjectPaths } from "@eforge-build/extension-sdk";

const paths = createEforgeProjectPaths({
  cwd: process.cwd(),
  extensionName: "my-extension",
});

const tracePath = paths.extensionStoragePath("project-local", ["traces", "item-1.json"]);
// /.eforge/storage/extensions/my-extension/traces/item-1.json
```

**Type:**

```ts
type EforgeStorageScope = 'user' | 'project-team' | 'project-local';

interface EforgeProjectPathsOptions {
  cwd: string;
  configDir?: string;
  extensionName?: string;
}

interface EforgeProjectPaths {
  cwd: string;
  configDir: string;
  scopeRoot(scope: EforgeStorageScope): string;
  storageRoot(scope: EforgeStorageScope): string;
  storagePath(scope: EforgeStorageScope, segments: readonly string[]): string;
  extensionStorageRoot(scope: EforgeStorageScope, extensionName?: string): string;
  extensionStoragePath(scope: EforgeStorageScope, segments: readonly string[], extensionName?: string): string;
}
```

Scope roots are:

| Scope | Root | Storage root |
|-------|------|--------------|
| `user` | `~/.config/eforge/` (XDG-aware) | `~/.config/eforge/storage/` |
| `project-team` | `/eforge/` (or `configDir`) | `/eforge/storage/` |
| `project-local` | `/.eforge/` | `/.eforge/storage/` |

Extension-owned private metadata should live under `storage/extensions//`, resolved with `extensionStorageRoot(scope)` or `extensionStoragePath(scope, segments)`. For example, a project-local trace sidecar for `my-extension` should use `.eforge/storage/extensions/my-extension/traces/.json`. Built-in eforge workflow artifacts, such as `.eforge/session-plans/`, are not extension-owned private storage and may keep their established workflow locations.

Runtime contexts expose the same helper object as `ctx.paths`, initialized with the current `cwd`, `configDir`, and extension name:

```ts
eforge.onEvent("plan:build:failed", async (_event, ctx) => {
  const diagnosticPath = ctx.paths.extensionStoragePath("project-local", ["diagnostics", "latest.json"]);
  // Callers own mkdir/write/read behavior.
});
```

**Behavior:** helper methods validate each segment lexically, reject empty segments, `.`/`..`, path separators, absolute paths, and null bytes, then verify the resolved absolute path remains contained under the selected storage root. The helpers perform no filesystem I/O: they do not create directories, read files, write files, or test whether a path exists. Callers own all I/O.

The path helpers are not a sandbox boundary. Extensions remain trusted, unsandboxed Node code running in the daemon/worker process; these APIs only standardize path layout and guard against accidental traversal in helper inputs.

### `resolveScopedStoragePath(opts)`

One-shot wrapper around `createEforgeProjectPaths(opts).storagePath(opts.scope, opts.segments)`.

**Type:** `(opts: { cwd: string; configDir?: string; scope: EforgeStorageScope; segments: readonly string[] }) => string`

### `resolveExtensionStoragePath(opts)`

One-shot wrapper around `extensionStoragePath` for extension-owned storage.

**Type:** `(opts: { cwd: string; configDir?: string; scope: EforgeStorageScope; extensionName: string; segments: readonly string[] }) => string`

### `resolveProjectLocalStoragePath(opts)`

Compatibility helper that resolves safe path segments under the project-local `.eforge/` root.

**Type:** `(opts: { cwd: string; segments: readonly string[] }) => string`

Prefer `createEforgeProjectPaths` or `resolveExtensionStoragePath` for new extension-owned storage so the scope and `storage/extensions//` convention are explicit.

These helpers do not add a native workflow registration API. The first-party `eforge-playbooks` package exposes shipped playbook behavior through extension actions and owns parser/storage/compiler/seed behavior locally; domain-neutral acceptance-criteria helpers and session-planning helpers remain separate from that playbook extension boundary. User-authored custom playbook or session-plan extraction is not supported by native extensions in the current release.

---

## `EforgeExtensionAPI` methods

### `onEvent(pattern, handler)`

Subscribe to one or more event types using a glob pattern. The handler fires after the event is emitted; it does not block or influence the pipeline.

```ts
eforge.onEvent("plan:build:failed", async (event, ctx) => {
  ctx.logger.warn(`Build failed for plan ${event.planId}`);
});

eforge.onEvent("plan:build:*", async (event, ctx) => {
  ctx.logger.info(`Build lifecycle: ${event.type}`);
});
```

**Signature:**

```ts
onEvent(
  pattern: TType,
  handler: EventHookHandler,
): void

onEvent(
  pattern: EventPattern,
  handler: (event: EforgeEvent, ctx: EventHookContext) => void | Promise,
): void
```

**Handler type:**

```ts
type EventHookHandler = (
  event: EventOfType,
  ctx: EventHookContext,
) => void | Promise
```

The `event` parameter is narrowed to `EventOfType` when the pattern is an exact event type string. For glob patterns (containing `*`), the event type is `EforgeEvent`.

**Runtime status:** registration is captured at load time and matching events are dispatched at runtime. Dispatch is non-blocking with respect to the engine pipeline: handlers cannot alter, block, or stop the triggering work. Handler failures and timeouts emit `extension:event-handler:*` diagnostics with extension name, pattern, triggering event type, and available `sessionId`/`runId` correlation fields; monitor recording sees those diagnostics before shell hooks run.

**Replay testing:** `eforge extension test` executes matching `onEvent` handlers against fixture or monitor DB events. It reports replay counts, matched hooks, emitted `extension:event-handler:*` diagnostics, and non-event registration summaries. Replay testing does not execute `onAgentRun`, custom tools, policy gates, profile routers, runtime-choice routers, input sources, reviewer perspectives, or validation providers.

---

### `onAgentRun(handler)`

Register a handler invoked before each agent run starts. The handler receives an `AgentRunContext` (which itself extends `EforgeExtensionContext`, so logger and exec are available on the same object) and may return a `promptAppend` fragment, per-run extension `tools`, or additive `allowedTools` / `disallowedTools` tuning. Inspect `ctx.role`, `ctx.tier`, `ctx.phase`, and `ctx.stage` to scope behavior to specific agent roles or lifecycle positions.

```ts
eforge.onAgentRun(async (ctx) => {
  if (ctx.role !== "builder") return;
  return {
    promptAppend: "Check the design system before modifying UI components.",
  };
});
```

**Signature:**

```ts
onAgentRun(handler: AgentRunHandler): void
```

**Handler type:**

```ts
type AgentRunHandler = (
  ctx: AgentRunContext,
) => AgentRunAugmentation | undefined | void | Promise
```

**`AgentRunContext`** (extends `EforgeExtensionContext`):

```ts
interface AgentRunContext extends EforgeExtensionContext {
  role: AgentRole;
  tier: string;
  profile: string;
  planId?: string;
  changedFiles?: string[];
  // Lifecycle context (populated for pipeline runs):
  phase?: string;   // 'compile' | 'build' | 'standalone'
  stage?: string;   // e.g. 'implement', 'review', 'planner'
  // Runtime metadata (read-only):
  harness?: 'claude-sdk' | 'pi';
  runtimeChoice?: string;
  runtimeChoiceQualified?: string;
  runtimeChoiceSource?: 'default' | 'rule' | 'extension-router' | 'fallback';
  runtimeChoiceRule?: string;
  runtimeChoiceRouter?: string;
  runtimeChoiceFallbackReason?: 'no-match' | 'router-declined' | 'router-timeout' | 'router-error' | 'router-invalid-choice';
  toolbelt?: string | null;
  toolbeltSource?: 'tier' | 'role' | 'plan' | 'default';
  projectMcpSelection?: 'all' | 'none' | 'toolbelt';
  effectiveToolName(name: string): string;
}
```

**`AgentRunAugmentation`:**

```ts
interface AgentRunAugmentation {
  promptAppend?: string;
  /** Additional extension tools made available only for this run. */
  tools?: ExtensionTool[];
  /** Tool names additively allowed for this run when a harness allowlist is active. */
  allowedTools?: string[];
  /** Tool names additively disallowed for this run; deny wins. */
  disallowedTools?: string[];
}
```

**Prompt composition:** returned `promptAppend` fragments are appended *after* any config-level `promptAppend` already resolved by the engine, wrapped in a per-extension provenance section:

```
## Native extension context

### 

```

Multiple extensions append in registration order. Each handler runs with a configurable timeout (see `extensions.agentContextHookTimeoutMs`).

**Fail-open behavior:** a handler that throws an error emits an `extension:agent-context:failed` event; a handler that exceeds the timeout emits an `extension:agent-context:timeout` event. In both cases that handler's prompt/tool changes are skipped and the agent run continues. Diagnostic events carry metadata (extension name, role, tier, phase, stage, fragment count) but never the prompt fragment text.

**Tool injection and availability tuning:** returning `tools` injects extension-defined tools only for the current run. Returning `allowedTools` and `disallowedTools` additively tunes the harness allow/deny lists for the current run; deny wins when the same name appears in both. Use `ctx.effectiveToolName(name)` when prompt text needs to mention the harness-visible name for an extension tool.

**Runtime-choice boundary:** `onAgentRun` can observe the selected runtime choice through the read-only `runtimeChoice*` fields, but it runs after runtime selection. It cannot change the harness, model, provider, effort, toolbelt, or selected choice for the current invocation; use declarative `agents.tiers..routing.rules` or `registerRuntimeChoiceRouter` for runtime-choice selection.

**Runtime status:** Yes. Prompt context, per-run extension tool injection, and per-run tool availability tuning are applied at runtime.

---

### `registerTool(tool)`

Register a custom agent tool independently of an `onAgentRun` return value. This records loader-time provenance and validation metadata so list/show/validate tooling can report the contribution. It does not globally expose the tool to every agent run; return the tool from `onAgentRun` for the roles or stages that should receive it.

```ts
import { Type, defineExtensionTool } from "@eforge-build/extension-sdk";

const lookupComponent = defineExtensionTool({
  name: "lookup-component",
  description: "Looks up a design-system component by name",
  inputSchema: Type.Object({
    name: Type.String(),
  }),
  handler: async ({ name }) => `Component: ${name}`,
});

eforge.registerTool(lookupComponent);

eforge.onAgentRun((ctx) => {
  if (ctx.role !== "builder") return;
  const toolName = ctx.effectiveToolName(lookupComponent.name);
  return {
    tools: [lookupComponent],
    promptAppend: `Use ${toolName} when you need design-system component details.`,
  };
});
```

**Signature:**

```ts
registerTool(tool: ExtensionTool): void
```

**Runtime status:** registration is captured at load time for provenance. Agent tool injection and execution happen only when an `onAgentRun` handler returns the tool for a specific run.

---

### Extension contribution contracts

The SDK also exposes registration methods for extension-authored actions and host-facing contributions. These methods are available on `EforgeExtensionAPI` for author-facing type safety; the daemon exposes safe manifest projection and action invocation routes, Console renders declarative contributions under `/console/system`, and host integrations can list and invoke actions, integration commands, and action-backed deep links through the shared contribution dispatcher.

#### `registerAction(action)`

Registers an extension-authored action handler. Action handlers run as trusted unsandboxed Node code. Action inputs require object-root TypeBox input schemas (`Type.Object(...)`). Action handlers must return JSON-safe outputs; optional output schemas are validated before eforge reports a successful invocation. Actions may declare an optional `outputProfile` (`agent-compact`, `agent-paginated`, `markdown`, `ui-rich`, or `debug-rich`) as manifest metadata so hosts can choose safe formatting without changing invocation success/failure response shapes. CLI non-`--json`, MCP/Claude, Pi, and Console previews use that metadata when formatting invocation output: exact `{ markdown: string }` outputs render as Markdown/plain text, oversized JSON is summarized with warnings while preserving identity fields, counts, omitted counts, and cursor/offset hints, MCP/Pi coding-agent tool text is capped at 12,000 characters, and `ui-rich`/`debug-rich` outputs warn in coding-agent hosts even when they fit. Broad list/search/board-style read action registrations remain valid, but validation emits warning diagnostics for id-shaped broad reads with array-shaped outputs when they lack limit or cursor/page controls, or when array-shaped output schemas omit an explicit profile. `agent-paginated` actions with limit and cursor/page controls are considered bounded for agent use without separate projection controls.

Action contexts include dependency/capability lookup plus daemon-owned APIs. `ctx.dependencies` and `ctx.capabilities` expose immutable availability data only; they do not invoke another extension. `ctx.agentTasks` starts, reads, and cancels supported single-shot agent tasks; the daemon owns task persistence, profile/runtime resolution, cancellation, and lifecycle events, and extensions never import provider SDKs or `AgentHarness`. `ctx.buildQueue.enqueue({ source, ... })` submits normalized build source through the same daemon queue path as `POST /api/enqueue`, including session-plan submission bookkeeping and producer-agnostic queue metadata such as `postMerge` command arrays. The MVP task runner resolves the `planner` role and enforces read-only tools. Product-specific output sections, transcript storage, annotations, and preview/apply semantics belong to the extension using the task API rather than to the daemon task API itself. Extensions cannot provide arbitrary raw prompt templates, register custom task kinds, or implement multi-turn chat through this API.

#### `registerConsoleContribution(contribution)`

Registers declarative Console metadata rendered under `/console/system`. Blocks must use one of the closed renderer IDs: `text`, `markdown`, `status-badge`, `link`, `action-button`, or `action-form`. Action blocks bind to a local action ID with optional JSON-safe input defaults.


#### `registerConsoleWorkstation(workstation)`

Registers a sandboxed Console workstation rendered under `/console/workstations`. The source SDK shape is trusted extension UI delivered as exactly one source: iframe `srcDoc` or `frameBundle` bundle metadata. Bundle roots must be `workstation-assets` or a child directory under `workstation-assets/`; `entrypoint`, `styles`, and `assets` paths are relative to that root. Bundle entries are projected as sandboxed iframe `src` navigations to the manifest `frameBundle.frameUrl` with the bridge token in the URL fragment, not in the daemon route query string. Declared `frameBundle` assets are supported and served only through eforge-owned frame/asset routes. The daemon serves bundle workstations through a generated frame shell with no-cache semantics and a `Content-Security-Policy` header plus declared, immutable, content-addressed asset URLs; the browser never supplies filesystem-relative asset paths. Workstation UI is isolated by the Console-owned iframe sandbox and bridge checks, but the `srcDoc` HTML or bundle metadata/code is not sanitized declarative content and should be reviewed like the extension source that produced it.

```ts
import { CONTRIBUTION_OUTPUT_PROFILES, Type, defineConsoleWorkstation, defineExtensionAction } from "@eforge-build/extension-sdk";

const listPlanningArtifacts = defineExtensionAction({
  id: "list-planning-artifacts",
  title: "List planning artifacts",
  inputSchema: Type.Object({
    includeSubmitted: Type.Optional(Type.Boolean()),
    includeBoard: Type.Optional(Type.Boolean()),
    includeArchive: Type.Optional(Type.Boolean()),
    epic: Type.Optional(Type.String()),
    limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 100 })),
    offset: Type.Optional(Type.Integer({ minimum: 0 })),
  }),
  outputSchema: Type.Object({
    artifacts: Type.Array(Type.Unknown()),
    plans: Type.Array(Type.Unknown()),
    planSets: Type.Array(Type.Unknown()),
    total: Type.Integer({ minimum: 0 }),
    limit: Type.Integer({ minimum: 1 }),
    offset: Type.Integer({ minimum: 0 }),
    board: Type.Optional(Type.Unknown()),
  }),
  outputProfile: CONTRIBUTION_OUTPUT_PROFILES.agentPaginated,
  sideEffects: ["local-read"],
  handler: () => ({ artifacts: [], plans: [], planSets: [], total: 0, limit: 50, offset: 0 }),
});

eforge.registerAction(listPlanningArtifacts);
eforge.registerConsoleWorkstation(defineConsoleWorkstation({
  id: "planning-workstation",
  title: "Planning workstation",
  description: "Interactive planning UI backed by extension actions.",
  frameBundle: {
    root: "workstation-assets/plans",
    entrypoint: "index.js",
    styles: ["style.css"],
    browserSdkVersion: 1,
  },
  allowedActions: ["list-planning-artifacts"],
}));
```

**Bridge protocol:** Console injects a small browser helper at `window.eforge.invokeAction(actionId, input)`. The helper posts an invocation request to the parent frame. The parent validates the source frame, resolves the requested local action ID to the effective manifest ID when allowed, calls the daemon-owned action invocation route, and posts a response back to the iframe. Successful responses resolve the promise with the action output; failed or disallowed responses reject the promise with an error. Iframe bundle code can import `getEforgeConsoleBridge`, `assertEforgeConsoleBridgeVersion`, `invokeAction`, and `EFORGE_WORKSTATION_BROWSER_SDK_VERSION` from the browser-safe `@eforge-build/extension-sdk/browser` subpath; those helpers are intentionally not exported from the package root.

**Allowed actions:** `ConsoleWorkstation.allowedActions` lists local action IDs registered by the same extension. The manifest carries effective namespaced action IDs. When `allowedActions` is omitted, projection uses the same-extension default behavior for the current V1 contract; authors who need a narrow bridge should specify the allowlist explicitly. Console rejects bridge calls for actions outside the manifest allowlist.

**Signature:**

```ts
registerConsoleWorkstation(workstation: ConsoleWorkstation): void
```

```ts
interface ConsoleWorkstationBase {
  id: string;
  title: string;
  description?: string;
  allowedActions?: string[];
}

interface ConsoleWorkstationFrameBundle {
  root: string;
  entrypoint: string;
  styles?: string[];
  assets?: string[];
  /** Optional; omitted means browser SDK v1. */
  browserSdkVersion?: 1;
}

type ConsoleWorkstation =
  | (ConsoleWorkstationBase & { srcDoc: string; frameBundle?: never })
  | (ConsoleWorkstationBase & { srcDoc?: never; frameBundle: ConsoleWorkstationFrameBundle });
```

**Runtime status:** Yes. Registrations are captured at load time, projected into the daemon contribution manifest, listed in management output, and rendered by Console as sandboxed iframe workstations under `/console/workstations` with the parent-owned action bridge. Manifest entries render from either `srcDoc` or bundle-backed `frameBundle.frameUrl` sources.

#### `registerIntegrationCommand(command)`

Registers a host-discoverable command. Commands are manifest metadata plus an action binding; CLI, MCP/Claude, and Pi hosts invoke the bound action through the daemon-owned dispatcher.

#### `registerDeepLink(deepLink)`

Registers a host-discoverable deep link. Action-backed deep links can be invoked through generic contribution surfaces; URL-only deep links are listable navigation metadata and are not generic invocations.

```ts
registerAction(
  action: ExtensionAction,
): void
registerConsoleContribution(contribution: ConsoleContribution): void
registerConsoleWorkstation(workstation: ConsoleWorkstation): void
registerIntegrationCommand(command: IntegrationCommand): void
registerDeepLink(deepLink: ExtensionDeepLink): void
```

Use the identity helpers to preserve TypeBox inference when defining contributions:

```ts
import { Type, defineExtensionAction } from "@eforge-build/extension-sdk";

const sayHi = defineExtensionAction({
  id: "say-hi",
  title: "Say hi",
  inputSchema: Type.Object({ name: Type.String() }),
  outputSchema: Type.Object({ greeting: Type.String() }),
  outputProfile: "agent-compact",
  sideEffects: ["none"],
  handler: (input) => ({ greeting: `Hello ${input.name}` }),
});
```

Browser bundle authors can import the dedicated browser SDK subpath from iframe code:

```ts
import {
  EFORGE_WORKSTATION_BROWSER_SDK_VERSION,
  assertEforgeConsoleBridgeVersion,
  getEforgeConsoleBridge,
  invokeAction,
} from "@eforge-build/extension-sdk/browser";

assertEforgeConsoleBridgeVersion(1);
const bridge = getEforgeConsoleBridge();
const result = await invokeAction("render-board-markdown", {});
```

`EFORGE_WORKSTATION_BROWSER_SDK_VERSION` is the helper package's current browser SDK version. `getEforgeConsoleBridge()` returns the injected `window.eforge` bridge and throws if it is unavailable. `assertEforgeConsoleBridgeVersion(expected)` verifies the injected bridge major version before app code depends on it. `invokeAction(actionId, input)` is a convenience wrapper around the bridge's action invocation. These helpers are for code running inside the workstation iframe only; browser bundles must not import private Console React/components/CSS modules, parent Console context, or parent-Console plugins.

Exported contribution types include `ExtensionAction`, `ExtensionActionOutputProfile`, `ExtensionActionContext`, `ExtensionContributionRequirements`, `ExtensionContributionAvailability`, `ExtensionAgentTasksApi`, `ExtensionActionBinding`, `ConsoleContribution`, `ConsoleContributionBlock`, `ConsoleWorkstation`, `ConsoleWorkstationBase`, `ConsoleWorkstationFrameBundle`, `ConsoleWorkstationFrameBundleWorkstation`, `ConsoleWorkstationSrcDoc`, `EforgeConsoleBridge`, `IntegrationCommand`, and `ExtensionDeepLink`. `EforgeConsoleBridge` is the browser-side shape for `window.eforge`. `ExtensionActionContext.requestedBy` uses the client-owned `ExtensionActionRequestedBy` provenance type. Host integrations that display contribution lists, details, invocation output, or failed-invocation summaries use the client formatter APIs `formatExtensionContributionListText()`, `formatExtensionContributionDetailText()`, `formatExtensionContributionOutput()`, `formatExtensionContributionOutputText()`, and `formatExtensionContributionFailedInvocationEnvelopeText()` from `@eforge-build/client` or the browser-safe `@eforge-build/client/browser` entrypoint. Shared host contribution projection and daemon dispatch helpers are exported from the main `@eforge-build/client` entrypoint only: `EXTENSION_HOST_CONTRIBUTION_KINDS`, `summarizeExtensionContributionManifest()`, `showExtensionContributionManifestEntry()`/`getExtensionContributionManifestEntry()`, `resolveExtensionContributionInvocation()`, `listEforgeExtensionContributions()`/`listEforgeExtensionContributionsIfRunning()`, `invokeEforgeExtensionContribution()`/`invokeEforgeExtensionContributionIfRunning()`, `summarizeExtensionContributionInvocationInput()`, and `createExtensionContributionFailedInvocationEnvelope()`, plus `ExtensionHostContributionKind`, `ExtensionHostContributionProjection`, `ExtensionHostContributionProjectionOptions`, `ExtensionHostContributionDetailOptions`, `ExtensionHostContributionDetailResponse`, `ExtensionHostContributionEntry`, `ExtensionHostContributionListResponse`, `ExtensionHostContributionInvokeParams`, `ExtensionHostContributionInvokeResult`, `ExtensionHostContributionInvokeTarget`, `ExtensionHostContributionInputSummary`, and `ExtensionHostContributionFailedInvocationEnvelope`. Browser-specific contribution access should call `fetchExtensionContributionManifest()` / `invokeExtensionAction()` and use the browser-safe formatter helpers.

**Runtime status:** engine registry/runtime support plus daemon manifest/action routes, Console System rendering for declarative contributions, Console workstation rendering under `/console/workstations`, and CLI, MCP/Claude, and Pi host dispatch for action-backed contributions, plus sandboxed iframe workstation rendering. Registrations are captured at load time, local IDs are namespaced as `:`, invalid or duplicate registrations produce extension diagnostics, manifest/management projection omits handlers, and action dispatch validates object-root TypeBox input schemas plus JSON-safe outputs. Contribution `requirements` are projected with `availability`; unavailable actions are rejected with error code `unavailable`. Action invocations reuse `extensions.eventHookTimeoutMs`, receive dependency/capability lookup data, `ctx.agentTasks` for supported daemon-owned single-shot planner tasks, and `ctx.buildQueue.enqueue` for trusted queue handoffs, and emit daemon-scoped `extension:action:start`, `extension:action:complete`, `extension:action:failed`, and `extension:action:timeout` events without raw input payloads or raw output payloads.

---

### `beforeQueueDispatch(handler)`

Policy gate that fires before a queued PRD is dispatched to a build worker. Return `{ decision: 'block', reason }` to prevent dispatch.

```ts
eforge.beforeQueueDispatch(async (ctx) => {
  if (ctx.priority !== undefined && ctx.priority > 100) {
    return { decision: "block", reason: "Priority is outside the team-approved range" };
  }
  return { decision: "allow" };
});
```

**Signature:**

```ts
beforeQueueDispatch(handler: QueueDispatchPolicyGateHandler): void
```

**Runtime status:** registration is captured at load time and executed at runtime before queue dispatch. Decisions are blocking; `require-approval` currently blocks because no approval workflow exists.

---

### `beforePlanMerge(handler)`

Policy gate that fires before a plan's worktree is merged into the integration branch. Return `{ decision: 'block', reason }` to prevent the merge.

```ts
eforge.beforePlanMerge(async (ctx) => {
  if (ctx.diff.files.some((f) => f.path === ".env")) {
    return { decision: "block", reason: "Do not merge .env changes" };
  }
  return { decision: "allow" };
});
```

**Signature:**

```ts
beforePlanMerge(handler: PlanMergePolicyGateHandler): void
```

**Runtime status:** registration is captured at load time and executed at runtime before each plan merge. Decisions are blocking; `require-approval` currently blocks because no approval workflow exists.

---

### `beforeFinalMerge(handler)`

Policy gate that fires before the completed feature branch is merged into the base branch. Return `{ decision: 'block', reason }` to prevent the final merge.

```ts
eforge.beforeFinalMerge(async (ctx) => {
  if (ctx.diff.files.some((f) => f.path.startsWith("infra/"))) {
    return { decision: "block", reason: "Final merge touches infra/" };
  }
  return { decision: "allow" };
});
```

**Signature:**

```ts
beforeFinalMerge(handler: FinalMergePolicyGateHandler): void
```

**Handler types:**

```ts
type PolicyGateHandler = (
  ctx: TContext,
) => PolicyDecision | Promise

type QueueDispatchPolicyGateHandler = PolicyGateHandler;
type PlanMergePolicyGateHandler = PolicyGateHandler;
type FinalMergePolicyGateHandler = PolicyGateHandler;
```

**Runtime status:** registration is captured at load time and executed at runtime before the final merge. Decisions are blocking; `require-approval` currently blocks because no approval workflow exists.

---

### `registerProfileRouter(spec)`

Register a function that selects an agent runtime profile for each build dispatched from the queue. Called before a queued PRD build begins.

**Signature:**

```ts
registerProfileRouter(spec: ProfileRouterSpec): void
```

**`ProfileRouterSpec`:**

```ts
interface ProfileRouterSpec {
  name: string;
  /** Canonical method — receives full build/queue context. */
  selectBuildProfile?: (
    ctx: ProfileRouterContext,
  ) => ProfileRouterResult | null | undefined | Promise;
  /**
   * @deprecated Use `selectBuildProfile` instead.
   * Receives limited agent-run context rather than build/queue context.
   */
  resolve?: (
    ctx: AgentRunContext,
  ) => ProfileRouterResult | null | undefined | Promise;
}

interface ProfileRouterResult {
  profile: string;
  reason?: string;
  confidence?: 'low' | 'medium' | 'high';
}
```

At least one of `selectBuildProfile` or `resolve` must be provided. The `selectBuildProfile` method is canonical and receives `ProfileRouterContext` with PRD id, title, body, priority, dependencies, available profiles, and usage statistics. The PRD body and summary exclude eforge's hidden acceptance-criteria inventory block.

Return `null` or `undefined` from the handler to defer to the next registered router (or the default profile if no router selects one). The optional `reason` and `confidence` fields flow into the `queue:profile:selected` wire event.

**Runtime status:** `Yes (pre-build dispatch)`. Routers are invoked sequentially in registration order before each queued PRD build, with per-router timeouts controlled by `extensions.profileRouterTimeoutMs` (defaulting to `extensions.eventHookTimeoutMs`) and fail-open semantics:

- **Dispatch-time routing.** Routers run after a PRD is dequeued and before `session:start` is emitted. The selected profile is persisted to the PRD's frontmatter via a `chore(queue): route  to profile ` commit before the build subprocess starts.
- **Explicit-override precedence.** When the PRD's `frontmatter.profile` is already set, routing is skipped entirely — no `queue:profile:*` events are emitted and no router is invoked.
- **Fail-open.** A router that throws emits `queue:profile:router-failed` and the next router is consulted. A timeout emits `queue:profile:router-timeout`. A returned profile name that cannot be loaded (not found in any scope) emits `queue:profile:invalid-selection`. If no router yields a valid selection, the build proceeds under the default profile (unchanged from current behavior).
- **First-valid-wins.** Returning `null` or `undefined` defers to the next router. The first non-null result whose profile name successfully loads wins.
- **`queue:profile:*` event family.** Four event types are emitted during dispatch:
  - `queue:profile:selected` — a valid profile was selected (includes `prdId`, `profile`, `baseProfile`, `routerName`, `extensionName`, `extensionPath`, optional `reason`/`confidence`).
  - `queue:profile:router-failed` — a router threw (includes `message`, optional `stack`).
  - `queue:profile:router-timeout` — a router exceeded its timeout (includes `timeoutMs`).
  - `queue:profile:invalid-selection` — a router returned a profile that could not be loaded (includes `requestedProfile`, `reason: 'not-found' | 'load-error'`).
- **Exact-quota caveat.** `ctx.usage.profile(name)` returns best-effort data from daemon event history. It does not query provider APIs for exact quota state. Use it for heuristic decisions (cooldown detection, token accumulation trends) rather than hard quota enforcement.

**Example using `selectBuildProfile`:**

```ts
eforge.registerProfileRouter({
  name: 'quota-aware-router',
  async selectBuildProfile(ctx) {
    const usage = ctx.usage.profile('primary-profile');
    if (usage.cooldownActive || usage.nearLimit) {
      // Fall back to secondary when primary is throttled
      return { profile: 'secondary-profile', reason: 'primary in cooldown', confidence: 'medium' };
    }
    if (ctx.availableProfiles.some((p) => p.name === 'primary-profile')) {
      return { profile: 'primary-profile', reason: 'primary available', confidence: 'high' };
    }
    return null; // Defer to next router or default profile
  },
});
```

See [`examples/extensions/profile-router.ts`](https://github.com/eforge-build/eforge/blob/main/examples/extensions/profile-router.ts) for a complete three-tier fallback example with env-var-driven profile names.

---

### `registerRuntimeChoiceRouter(spec)`

Register a function that can select a tier-local runtime choice for an individual agent invocation after role-to-tier resolution and after declarative routing rules decline to match. This is a separate layer from `registerProfileRouter`: profile routers select the active profile before build dispatch, while runtime-choice routers select among choices already declared inside the selected tier.

**Signature:**

```ts
registerRuntimeChoiceRouter(spec: RuntimeChoiceRouterSpec): void
registerRuntimeChoiceRouter(name: string, handler: RuntimeChoiceRouterHandler): void
```

**`RuntimeChoiceRouterSpec`:**

```ts
interface RuntimeChoiceRouterSpec {
  name: string;
  resolveRuntimeChoice: (ctx: RuntimeChoiceRouterContext) => RuntimeChoiceRouterResult | string | null | undefined | Promise;
}

interface RuntimeChoiceRouterContext extends EforgeExtensionContext {
  role: AgentRole;
  tier: string;
  profile: string;
  availableChoices: Array<{ name: string; qualified: string }>;
  phase?: string;
  stage?: string;
  planId?: string;
  planName?: string;
  planSummary?: string;
  prdTitle?: string;
  prdSummary?: string;
  taskSummary?: string;
  keywordText?: string;
  pathHints?: string[];
  changedFiles?: string[];
  shardIds?: string[];
  shardRoots?: string[];
  shardFiles?: string[];
}

interface RuntimeChoiceRouterResult {
  choice?: string;
  decline?: boolean;
  reason?: string;
  confidence?: number;
}
```

Return a choice name, `{ choice }`, `null`, `undefined`, or `{ decline: true }`. Choice names must resolve within the already-selected tier: use `default`, a tier-local name such as `ui`, or the same-tier qualified form such as `implementation.ui`. Cross-tier choices and unknown choices are invalid.

**Runtime status:** `Yes (per agent invocation)`. Declarative `agents.tiers..routing.rules` run first; if a rule matches, runtime-choice routers are not invoked. If no rule matches, routers are invoked in registration order. Declines continue to the next router. A router error, timeout, invalid result, or unknown choice falls back to the tier `default` choice for that invocation and does not fail the build. Non-secret selection metadata is exposed on `agent:start` events and on `onAgentRun` context as `runtimeChoice*` fields.

---

### `registerInputSource(adapter)`

Register a custom input source that produces PRD/build-source artifacts for the queue. Adapters are selected at enqueue time by matching the adapter `name` against the `` segment of an `eforge://input//` URI.

**Signature:**

```ts
registerInputSource(adapter: InputSourceAdapter): void
```

**`InputSourceAdapter`:**

```ts
interface InputSourceAdapter {
  /** Unique adapter name matched against the URI's  segment (e.g. `github`, `linear`). */
  name: string;
  /** Human-readable description of where this source retrieves input from. */
  description: string;
  /**
   * Fetch the build input for the given identifier.
   *
   * Returns raw content (string), a structured InputSourceResult, or null if
   * the identifier was not found. Returning null is fatal to enqueue.
   * The optional ctx argument provides cwd and source provenance during enqueue preprocessing.
   */
  fetch: (id: string, ctx?: InputTransformContext) => Promise;
}
```

**`InputSourceResult`:**

```ts
interface InputSourceResult {
  /** The raw build-input artifact content. */
  content: string;
  /** Optional human-readable title for the fetched item. */
  title?: string;
}
```

**`InputTransformContext`** (extends `EforgeExtensionContext`):

```ts
interface InputTransformContext extends EforgeExtensionContext {
  /** Absolute path to the project working directory. */
  cwd: string;
  /** The raw input content as originally provided (before any transformations). */
  originalSource: string;
  /**
   * How the source was supplied:
   * - 'inline' - raw text provided directly.
   * - 'file' - content read from a local file path.
   * - 'extension-reference' - a symbolic reference resolved by a registered adapter.
   */
  sourceKind: 'inline' | 'file' | 'extension-reference';
  /** Absolute path to the source file when sourceKind is 'file'. */
  sourcePath?: string;
  /** The adapter that produced this input when sourceKind is 'extension-reference'. */
  adapterId?: string;
  /** The remaining URI id passed to the input source adapter when sourceKind is 'extension-reference'. */
  sourceId?: string;
  /** Name of the extension that registered the adapter when sourceKind is 'extension-reference'. */
  extensionName?: string;
  /** Path of the extension that registered the adapter when sourceKind is 'extension-reference'. */
  extensionPath?: string;
}
```

**Preprocessing context limits:** although `InputTransformContext` extends `EforgeExtensionContext` for typing convenience, enqueue preprocessing receives only cwd/provenance metadata plus stub helpers. `ctx.exec.run` is unavailable during preprocessing and throws if called. `ctx.logger` is a no-op logger, so its behavior is not equivalent to event-hook or policy-gate logging.

**URI dispatch:** the runtime parses `eforge://input//` URIs and looks up the registered adapter whose `name` exactly matches ``. The remaining `` path is passed to `fetch`. Example URIs:
- `eforge://input/github/acme/backend#42` — adapter `github`, id `acme/backend#42`
- `eforge://input/linear/ENG-42` — adapter `linear`, id `ENG-42`
- `eforge://input/jira/ENG-42` — adapter `jira`, id `ENG-42`

**Failure policy:** returning `null` or throwing is fatal to enqueue (`FatalPreprocessingError`). Design adapters to be safe-by-default: when credentials are absent, return an `InputSourceResult` with instructional content rather than throwing.

**Provenance events:** `extension:input-source:fetched` (success) and `extension:input-source:failed` (null return or throw).

**Runtime status:** Yes (extension-aware enqueue preprocessing). Registration is captured at load time; adapters are invoked during enqueue preprocessing when a matching `eforge://input//` URI is supplied.

See [`examples/extensions/issue-tracker.ts`](https://github.com/eforge-build/eforge/blob/main/examples/extensions/issue-tracker.ts) for a worked example with GitHub, Linear, and Jira adapters.

---

### `registerPrdEnricher(spec)`

Register a PRD enricher that mutates or augments PRD/build-source content before it is written to the queue. Enrichers run in registration order after all input source preprocessing completes.

**Signature:**

```ts
registerPrdEnricher(spec: PrdEnricher): void
```

**`PrdEnricher`:**

```ts
interface PrdEnricher {
  /** Unique enricher name used for logging, duplicate detection, and provenance. */
  name: string;
  /** Human-readable description of what this enricher does. */
  description: string;
  /**
   * Enrich the given PRD content.
   *
   * Return a PrdEnrichmentResult to replace the content, or null/undefined to
   * pass the content through unchanged.
   */
  enrich: (input: PrdEnrichmentInput) => Promise | PrdEnrichmentResult | null | undefined;
}
```

**`PrdEnrichmentInput`:**

```ts
interface PrdEnrichmentInput {
  /** The PRD/build-source content to be enriched. */
  content: string;
  /** The source identifier (e.g. file path, issue id) for this PRD content. */
  sourceId: string;
  /** Runtime context providing cwd and source provenance during preprocessing. */
  ctx: InputTransformContext;
}
```

**`PrdEnrichmentResult`:**

```ts
interface PrdEnrichmentResult {
  /** The enriched PRD/build-source content. */
  content: string;
}
```

**Behavior:** enrichers always run for every preprocessed source. Gate behavior inside `enrich` using `input.ctx.sourceKind`, `input.ctx.adapterId`, or `input.ctx.sourcePath` if you need to act only for specific source types. The preprocessing context has the same limits as input-source adapters: `ctx.exec.run` throws, and `ctx.logger` is a no-op logger rather than event-hook logging.

**Failure policy:** enricher failures are fail-open. A thrown error emits `extension:prd-enricher:failed` with the enricher name, source id, and error message; the unchanged content carries forward.

**Provenance events:** `extension:prd-enricher:applied` (content replaced) and `extension:prd-enricher:failed` (enricher threw).

**Runtime status:** Yes (fail-open content enrichment before queue write). Registration is captured at load time; enrichers are invoked during enqueue preprocessing in registration order.

---

### `registerReviewerPerspective(spec)`

Register a custom reviewer perspective that executes during parallel review-cycle perspective dispatch alongside built-in eforge perspectives (`review.strategy: parallel`, or `auto` once the diff crosses the parallel-review thresholds). When a perspective is applicable, eforge dispatches it as its own review perspective using the generic reviewer prompt with `promptFragment` appended as an extension-provenance section. Multiple extensions may register perspectives; each applicable perspective is dispatched separately and its findings are aggregated with the other review results.

**Signature:**

```ts
registerReviewerPerspective(spec: ReviewerPerspectiveSpec): void
```

**`ReviewerPerspectiveSpec`:**

```ts
interface ReviewerPerspectiveSpec {
  /** Unique perspective key used as the review perspective identifier. */
  key: string;
  /** Human-readable label shown in review output and management tooling. */
  label: string;
  /**
   * Human-readable description of what this perspective reviews.
   * Exposed in management projections (eforge extension show, list, validate, test).
   */
  description: string;
  /** Prompt fragment appended to the generic reviewer prompt when this perspective is active. */
  promptFragment: string;
  /** Optional applicability rules. Omit to run on every parallel review cycle. */
  appliesTo?: ReviewerPerspectiveApplicability;
}

interface ReviewerPerspectiveApplicability {
  /** Glob patterns matched against changed file paths. */
  fileGlobs?: string[];
  /** Path prefixes matched against changed file paths. */
  paths?: string[];
  /** File extensions, with or without a leading dot. */
  extensions?: string[];
  /** Built-in file categories that must have at least one changed file. */
  categories?: Array<'code' | 'api' | 'docs' | 'config' | 'deps' | 'test'>;
  /** Minimum number of changed files. */
  minChangedFiles?: number;
  /** Minimum number of added + deleted lines. */
  minChangedLines?: number;
  /** Optional predicate called after all declarative rules pass. */
  fn?: (changedFiles: string[], changedLines: number) => boolean | Promise;
}
```

**Applicability rules:**

- `appliesTo.fileGlobs`: glob patterns matched against changed file paths in the review diff. The perspective runs when at least one changed file matches.
- `appliesTo.paths`: path prefixes matched against changed file paths.
- `appliesTo.extensions`: file extensions, with or without a leading dot.
- `appliesTo.categories`: built-in file categories (`code`, `api`, `docs`, `config`, `deps`, `test`).
- `appliesTo.minChangedFiles` / `appliesTo.minChangedLines`: minimum diff-size thresholds.
- `appliesTo.fn(changedFiles, changedLines)`: optional predicate called only after all declarative rules pass. Return `true` to include the perspective or `false` to skip it.
- Neither: omit `appliesTo` to run on every review cycle.

All specified declarative rules are ANDed together. Function-form applicability receives copies of the changed-file list and changed-line count; it does not receive a mutable orchestration context.

**Events:**

- `extension:reviewer-perspective:applied` — the perspective was evaluated as applicable and dispatched.
- `extension:reviewer-perspective:skipped` — the perspective was skipped because it was not applicable, its function-form predicate threw or timed out, an explicit key was unknown, or the predicate returned an invalid value.

Diagnostic events for reviewer perspectives include: perspective key, optional extension name/path when the skipped key maps to a registered extension perspective, optional plan id, skip reason, timeout milliseconds when applicable, and an error message for applicability failures. `unknown-key` skips omit extension provenance because no extension owns the key. There is no separate `extension:reviewer-perspective:failed` event; failures are reported as `extension:reviewer-perspective:skipped` with reason `applicability-error` or `applicability-timeout`.

**Trust model:**

Applicability inputs are read-only API snapshots (changed file paths and changed-line count). Reviewer perspectives cannot mutate orchestration state, block the review cycle, or call agent tool APIs. The extension module itself is unsandboxed trusted code running in the daemon/worker process; the read-only constraint applies to applicability inputs, not to extension code in general.

**Management projections:**

`eforge extension show` and JSON list/show/validate/test responses include registered reviewer perspectives with: `key`, `label`, `description`, extension name/path, and a normalized applicability summary. Function source text is never included in management projections.

**Limits:**

- Reviewer perspectives run during parallel review-cycle perspective dispatch only. They do not run during planning, building, merge stages, `review.strategy: single`, or `auto` reviews that stay below the parallel-review thresholds.
- `appliesTo.fn` is evaluated once per review cycle per registered perspective after declarative rules pass. Expensive synchronous work blocks the review dispatch; prefer declarative `fileGlobs`, `paths`, `extensions`, or `categories` for file-pattern-based rules.

**Runtime status:** registration is captured at load time. Perspectives execute at runtime during parallel review-cycle perspective dispatch. See [`examples/extensions/reviewer-perspective.ts`](https://github.com/eforge-build/eforge/blob/main/examples/extensions/reviewer-perspective.ts) for a worked example.

---

### `registerValidationProvider(spec)`

Register a custom validation step that runs during the per-plan `validate` build stage, after the implement stage and before the review stage.

**Signature:**

```ts
registerValidationProvider(spec: ValidationProviderSpec): void
```

**`ValidationProviderSpec`:**

Provide exactly one of `validate` (function form) or `commands` (command form). Registering both or neither is rejected at load time.

```ts
interface ValidationProviderSpec {
  /** Unique provider name. */
  name: string;
  /** Human-readable description of what this provider validates. */
  description: string;

  /**
   * Function form: run custom validation logic for the plan.
   *
   * Receives the absolute path to the plan worktree and an optional
   * `ValidationProviderContext` with richer build facts (planId, paths, logger,
   * exec, signal, changedFiles).
   *
   * Return values:
   * - `null` or `undefined` — passed
   * - `ValidationProviderResult` — explicit structured outcome; `status: 'failed'`
   *   is recoverable before terminal failure, and annotations improve recovery targeting
   *
   * Throwing/rejecting, timing out, returning a non-empty string, or returning
   * an unexpected shape is a hard failure that bypasses recovery.
   *
   * Mutually exclusive with `commands`. Provide exactly one.
   */
  validate?: (
    planOutputDir: string,
    context?: ValidationProviderContext,
  ) => Promise
     | null | undefined | ValidationProviderResult;

  /**
   * Command form: shell commands to run in the plan worktree, one per entry.
   *
   * Each command string is split on whitespace into `[executable, ...args]`
   * and run via `execFile` (no shell interpretation — quoted args, env-var
   * expansion, redirects, and pipes are not supported). A non-zero exit code
   * is a recoverable generic subprocess failure using the command's stderr
   * (or stdout if stderr is empty) as the failure message.
   *
   * Mutually exclusive with `validate`. Provide exactly one.
   */
  commands?: string[];
}
```

**`ValidationProviderContext`:**

```ts
interface ValidationProviderContext {
  /** The plan ID being validated. */
  planId: string;
  /** Absolute path to the worktree root for the plan. */
  planOutputDir: string;
  /** Same as `planOutputDir` — the worktree root path. */
  worktreePath: string;
  /** Scoped eforge project path helpers initialized for this extension. */
  paths: EforgeProjectPaths;
  /** Structured logger routed through the eforge daemon's log pipeline. */
  logger: ExtensionLogger;
  /** Shell-exec API for running subprocesses from a validation provider. */
  exec: ExtensionExecApi;
  /** AbortSignal for the current build, if available. */
  signal?: AbortSignal;
  /** Files changed in the plan worktree, if available. */
  changedFiles?: string[];
}
```

**`ValidationRepairClass` and metadata:**

```ts
type ValidationRepairClass = 'narrow' | 'structural' | 'manual' | 'followup';
type ValidationJsonPrimitive = string | number | boolean | null;
type ValidationJsonValue =
  | ValidationJsonPrimitive
  | ValidationJsonValue[]
  | { [key: string]: ValidationJsonValue };
type ValidationProviderMetadata = Record;
```

Metadata values must be JSON-safe primitives, arrays, or objects. Keep metadata small and factual so repair agents can use it without parsing prose.

**`ValidationProviderResult`:**

```ts
interface ValidationProviderResult {
  /** Validation outcome. */
  status: 'passed' | 'failed' | 'skipped';
  /** Optional human-readable message describing the outcome. */
  message?: string;
  /** Optional extended details (e.g. full command output). */
  details?: string;
  /** Optional structured annotations for individual files. */
  annotations?: ValidationProviderAnnotation[];
}

interface ValidationProviderAnnotation {
  severity: 'info' | 'warning' | 'error';
  message: string;
  file?: string;
  line?: number;
  details?: string;
  fix?: string;
  retryGuidance?: string;
  /** Provider-authored domain failure signature; runtime failures use a separate classification. */
  failureKind?: string;
  repairClass?: ValidationRepairClass;
  metadata?: ValidationProviderMetadata;
}
```

Use `repairClass: 'narrow'` or omit it for localized fixes. Use `repairClass: 'structural'` for extraction, file splitting, or broader code organization changes. Use `manual` when the provider should fail closed without automated repair. Use `followup` for findings that should only fail closed when every remaining issue is follow-up-only; mixed follow-up plus automatable issues route according to the narrow/structural guidance.

**Worked example:**

```ts
import type { EforgeExtensionAPI, ValidationProviderResult } from '@eforge-build/extension-sdk';

export default function validationProviders(eforge: EforgeExtensionAPI): void {
  // Function form: programmatic validation with full context access.
  eforge.registerValidationProvider({
    name: 'type-check-gate',
    description: 'Runs TypeScript type checking and fails the plan on type errors.',
    validate: async (planOutputDir, ctx): Promise => {
      const result = await ctx!.exec.run('pnpm', ['type-check'], { cwd: planOutputDir });
      if (result.exitCode !== 0) {
        const output = result.stderr.trim() || result.stdout.trim();
        return {
          status: 'failed',
          message: 'TypeScript type checking failed',
          details: output,
          annotations: [{
            severity: 'error',
            message: 'TypeScript diagnostics must be resolved before review can continue.',
            details: output,
            fix: 'Run pnpm type-check locally and fix the reported TypeScript errors.',
            retryGuidance: 'Make the smallest type-safe change that resolves the diagnostic.',
            failureKind: 'typescript-diagnostics',
            repairClass: 'narrow',
            metadata: { command: 'pnpm type-check' },
          }],
        };
      }
      return null; // passed
    },
  });

  // Command form: exit-code-is-failure subprocess dispatch.
  eforge.registerValidationProvider({
    name: 'lint-gate',
    description: 'Runs the project linter and fails the plan on lint errors.',
    commands: ['pnpm lint'],
  });
}
```

**Failure semantics, recovery, and timeout:**

Providers are fail-closed gates. Normal validation failures — structured `{ status: 'failed' }` results and command-form non-zero exits — enter bounded in-plan recovery before terminal failure. Recovery uses the `review.maxRounds` budget and reruns the provider suite from the first provider after each recovery attempt. If recoverable failures remain unresolved when the budget is exhausted, the current plan fails and emits `plan:build:failed`.

Structured annotations are normalized into review issues. Narrow or unspecified annotations go through the review-fixer path first. Structural annotations route to the validation-fixer path. If the same validation failure signature survives a prior narrow repair attempt, eforge escalates the next attempt to structural repair. Any manual annotation disables automated repair, and an all-follow-up failure set fails closed without automated repair; mixed follow-up plus narrow or structural issues routes according to the remaining automatable issues. Before each automated repair attempt, eforge writes `.eforge/validation-recovery///attempt--/checkpoint.patch` and `metadata.json`; the repair prompt and evaluator both receive those checkpoint references. Every narrow or structural validation repair is evaluator-mediated before the provider suite reruns.

Command-form failures are recoverable but generic: the command output becomes the message, with no annotations, `repairClass`, `retryGuidance`, `failureKind`, or `metadata`. Use function form when a provider can supply structured repair guidance.

Hard provider failures bypass recovery and emit terminal `plan:build:failed` immediately: thrown exceptions/rejections, provider timeouts, non-empty string returns, and unexpected return shapes. The timeout is controlled by `extensions.validationProviderTimeoutMs` (falls back to `extensions.eventHookTimeoutMs`).

**Runtime events:**

- `extension:validation-provider:start` — provider invocation has begun.
- `extension:validation-provider:complete` — provider completed with a passed or skipped outcome; carries `status`.
- `extension:validation-provider:error` — provider completed with a failed outcome; carries provider name and error message. This includes recoverable normal failures (structured failed results and command-form non-zero exits) as well as hard exception/rejection, non-empty string return, and unexpected-return-shape failures.
- `extension:validation-provider:timeout` — timeout exceeded; carries provider name and elapsed milliseconds. This is a hard failure that bypasses recovery.

**Runtime status:** registration is captured at load time. Providers execute at runtime during the per-plan `validate` build stage. See [`examples/extensions/validation-provider.ts`](https://github.com/eforge-build/eforge/blob/main/examples/extensions/validation-provider.ts) for a worked example with both function-form and command-form providers.

---

## Context types

### `EforgeExtensionContext`

The base context passed to all handlers. Provides logging and command execution.

```ts
interface EforgeExtensionContext {
  logger: ExtensionLogger;
  exec: ExtensionExecApi;
  /** Scoped path helpers for resolving eforge-owned storage locations. */
  paths: EforgeProjectPaths;
}
```

Action handlers receive an `ExtensionActionContext`, which extends the base context shape with invocation provenance, immutable dependency/capability lookup, daemon-owned task controls, and daemon-owned queue handoff controls:

```ts
interface ExtensionActionContext {
  invocationId: string;
  actionId: string;
  requestedBy: ExtensionActionRequestedBy;
  cwd: string;
  signal: AbortSignal;
  logger: ExtensionLogger;
  paths: EforgeProjectPaths;
  dependencies: ExtensionDependencyLookup;
  capabilities: ExtensionCapabilityLookup;
  agentTasks: ExtensionAgentTasksApi;
  buildQueue: ExtensionBuildQueueApi;
}
```

#### `ctx.agentTasks`

`ctx.agentTasks` lets an action delegate a supported single-shot agent run to the daemon while keeping the extension out of provider/runtime internals.

```ts
interface ExtensionAgentTasksApi {
  start(request: ExtensionAgentTaskStartInput): Promise;
  get(taskId: string): Promise;
  cancel(taskId: string, reason?: string): Promise;
}

type ExtensionAgentTaskStartInput = {
  kind: "eforge-plan.planning-draft";
  input: EforgePlanPlanningDraftInput;
};
```

`start` persists a task record before the background run is queued and returns it as `{ task }`. The planning-draft input may request structured output sections owned by the calling first-party workflow; completed output-bearing results are validated by the shared client schema before persistence and count as output-bearing task results. Workflow-specific unresolved cases live inside the workflow output section; the top-level `needs-input` result variant carries no output sections. For first-party eforge-plan output sections and workflow semantics, see the [eforge-plan guide](/docs/eforge-plan). `get` returns `{ task }` for the persisted record. `cancel` requests cancellation for a running task, records `status: "cancelled"` when accepted, and returns the updated record as `{ task }`. Task records use `status: "queued" | "running" | "completed" | "failed" | "cancelled"`; completed records contain a JSON-safe result, failed records contain a sanitized error message, and lifecycle events never include raw task input or raw result payloads.

The action dispatcher binds task provenance to the invoking extension and host request. Do not pass secrets or raw prompt templates through task metadata. First-party workflow packages may layer their own storage, UI state, annotations, or preview/apply semantics above this API while `ctx.agentTasks` remains a daemon-owned single-shot task API. The MVP supports only the daemon-owned read-only planner task kind shown above; custom task-kind registration, arbitrary prompt templates, write-capable tools, and multi-turn chat are unsupported.

#### `ctx.buildQueue`

`ctx.buildQueue` lets an action hand a ready build source to the daemon queue without constructing raw HTTP requests.

```ts
interface ExtensionBuildQueueApi {
  enqueue(request: EnqueueRequest): Promise;
}
```

`enqueue` follows the daemon enqueue route semantics: it validates profile/landing/dependency fields and `postMerge` command arrays, spawns the enqueue worker, returns `{ sessionId, pid, autoBuild }`, and applies built-in session-plan submission bookkeeping when the source is a flat session plan under `.eforge/session-plans/`. Per-enqueue `postMerge` commands are persisted as queued PRD metadata and appended after configured `build.postMergeCommands` for that build. Actions using it should declare `build-queue` in `sideEffects`.

**`ExtensionLogger`:**

```ts
interface ExtensionLogger {
  debug(message: string, ...args: unknown[]): void;
  info(message: string, ...args: unknown[]): void;
  warn(message: string, ...args: unknown[]): void;
  error(message: string, ...args: unknown[]): void;
}
```

**`ExtensionExecApi`:**

```ts
interface ExtensionExecApi {
  run(
    command: string,
    args?: string[],
    options?: { cwd?: string; env?: Record },
  ): Promise<{ stdout: string; stderr: string; exitCode: number }>;
}
```

### `EventHookContext`

Context for `onEvent` handlers. Extends `EforgeExtensionContext` and adds an `event` field carrying the raw `EforgeEvent` that triggered the hook (the same object as the handler's first argument, exposed here for convenience in shared helpers). Runtime event hooks receive the enriched event object, including available `sessionId` and `runId` correlation fields:

```ts
interface EventHookContext extends EforgeExtensionContext {
  event: EforgeEvent;
}
```

### Policy gate contexts

Policy gate contexts are read-only snapshots for the gated operation. They include `ctx.logger` and `ctx.exec`, but those helpers do not sandbox extension code; loaded extensions remain trusted, unsandboxed code running in the daemon/worker process.

```ts
type PolicyGateKind = "queue-dispatch" | "plan-merge" | "final-merge";

interface QueueDispatchContinueRepairMetadata {
  mode: "compiled";
  sourcePrdId: string;
  setName: string;
  featureBranch: string;
  baseBranch: string;
}

interface QueueDispatchPolicyGateContext extends EforgeExtensionContext {
  gateKind: "queue-dispatch";
  prdId: string;
  prdTitle?: string;
  priority?: number;
  profile?: string;
  dependsOn: string[];
  /** Present only for continue-and-repair queue items backed by complete compiled artifacts; omitted for normal PRDs. */
  continueRepair?: QueueDispatchContinueRepairMetadata;
}

interface PlanMergePolicyGateContext extends EforgeExtensionContext {
  gateKind: "plan-merge";
  planId: string;
  diff: ExtensionDiff;
}

// Backward-compatible alias for the original plan-merge context.
type PolicyGateContext = PlanMergePolicyGateContext;

interface FinalMergePolicyGateContext extends EforgeExtensionContext {
  gateKind: "final-merge";
  featureBranch: string;
  baseBranch: string;
  planIds?: string[];
  diff: ExtensionDiff;
}

type AnyPolicyGateContext =
  | QueueDispatchPolicyGateContext
  | PlanMergePolicyGateContext
  | FinalMergePolicyGateContext;

interface ExtensionDiff {
  files: Array<{
    path: string;
    status: "added" | "modified" | "deleted" | "renamed";
  }>;
}
```

`continueRepair` is available only on `beforeQueueDispatch` contexts for continue-and-repair PRDs backed by complete compiled artifacts. The SDK exposes the parsed camelCase shape shown above. Normal PRDs omit the property entirely.

---

## Hook result types

### `PolicyDecision`

Returned by policy gate handlers. A discriminated union with three variants:

```ts
type PolicyDecision =
  | { decision: "allow" }
  | { decision: "block"; reason: string }
  | { decision: "require-approval"; reason: string };
```

- `allow` - the operation proceeds normally.
- `block` - the operation is rejected. `reason` is surfaced in logs and Console.
- `require-approval` - currently blocks the operation because no approval workflow, approval state, or Console approval UI exists in this MVP.

A `modify` variant (mutating the diff inline) is intentionally absent. `modify` decisions are not supported; no policy gate in the current scope explicitly allows mutation.

---

## Event types and `EventPattern` glob semantics

All event types are exposed through `packages/client/src/events.schemas.ts` as the `EforgeEvent` discriminated union and implemented in focused modules under `packages/client/src/events/`. The SDK re-exports `EforgeEvent`, `EforgeEventSchema`, `AgentRole`, and `safeParseEforgeEvent` from `@eforge-build/client`. The lower-level `@eforge-build/client/events` subpath also exports `DaemonStreamSnapshotSchema` for validating the daemon `stream:hello` snapshot shape, including queue hold/capability metadata and failed-enqueue projections.

Policy gate execution emits `extension:policy:decision`, `extension:policy:failed`, and `extension:policy:timeout` diagnostics with extension name/path, registration index, gate kind, method (`beforeQueueDispatch`, `beforePlanMerge`, or `beforeFinalMerge`), the configured failure policy, and target identifiers such as `prdId`, `planId`, or final-merge branch names.

Daemon-owned agent tasks emit `extension:agent-task:start`, `extension:agent-task:progress`, `extension:agent-task:complete`, `extension:agent-task:failed`, and `extension:agent-task:cancelled`. These events carry `taskId`, `taskKind`, duration/error fields where applicable, and sanitized metadata only; raw task context and result payloads stay in the persisted task record instead of the event stream.

### `EventOfType`

Extract a specific event variant by type string:

```ts
import type { EventOfType } from "@eforge-build/extension-sdk";

type FailedEvent = EventOfType<"plan:build:failed">;
// resolves to the exact discriminant variant from EforgeEvent
```

### Pattern semantics

`EventPattern` is a string type alias. Patterns use `*` as a wildcard that matches any characters including `:`. The semantics are identical to shell hook patterns in `eforge/config.yaml`.

| Pattern | Matches | Does not match |
|---------|---------|----------------|
| `plan:build:failed` | `plan:build:failed` | `plan:build:complete` |
| `plan:build:*` | `plan:build:start`, `plan:build:failed`, ... | `planning:complete` |
| `*:complete` | `plan:build:complete`, `merge:finalize:complete`, `planning:complete` | `plan:build:failed` |
| `*` | Every event type | - |
| `plan.build:start` | `plan.build:start` (literal dot) | `plan:build:start` |

The last row illustrates that `.` in a pattern is a literal dot, not a regex wildcard. Only `*` is special.

### Pattern helpers

```ts
import { compileEventPattern, matchesEventPattern } from "@eforge-build/extension-sdk";

// Compile a pattern once and reuse the RegExp
const re = compileEventPattern("plan:build:*");
re.test("plan:build:failed"); // true

// One-shot test
matchesEventPattern("*:complete", "wave:complete"); // true
```

`compileEventPattern` produces an anchored `RegExp` (`^...$`) using the same algorithm as `packages/engine/src/hooks.ts::compilePattern`. The SDK ports this algorithm internally so it stays engine-independent; behavioral parity is tested in `test/extension-sdk-example.test.ts`.

---

## TypeBox schema usage and `ExtensionTool`

The SDK uses TypeBox as its schema language. Import `Type`, `TSchema`, `TObject`, and `Static` directly from `@eforge-build/extension-sdk` - you do not need a separate `@sinclair/typebox` dependency to write tools.

### `ExtensionTool`

```ts
interface ExtensionTool {
  name: string;
  description: string;
  inputSchema: TInput;
  handler: (input: Static) => Promise | string;
}
```

### `defineExtensionTool(tool)`

Identity helper for inference. Returns the tool unchanged at runtime:

```ts
import { defineExtensionTool, Type } from "@eforge-build/extension-sdk";

const lookupTool = defineExtensionTool({
  name: "lookup-component",
  description: "Looks up a design system component by name",
  inputSchema: Type.Object({
    name: Type.String({ description: "Component name" }),
  }),
  handler: async ({ name }) => {
    return `Component: ${name}`;
  },
});
```

`ExtensionTool` is a narrower public type than the engine's internal `CustomTool`. The loader captures `ExtensionTool` registrations at load time for provenance and validation; `onAgentRun` return values inject accepted tools for a specific run. The public shape stays narrow so the engine's internal representation can evolve without breaking extension authors.

---


## SDK stability and migration guidance

The canonical SDK stability and migration guidance lives here. Public exports from `@eforge-build/extension-sdk` are stability-promised within a major version, but runtime behavior is intentionally versioned through the daemon/client contract and documented in this reference. When upgrading:

1. Read this Extensions API reference first, especially the runtime support table and unsupported-boundary notes.
2. Run `eforge extension validate ` to catch registration-shape changes.
3. Run `eforge extension test ` for replayable event hooks or registration summaries.
4. Rebuild packaged extensions against the new SDK and avoid private imports from `packages/console-ui`, `packages/engine`, or daemon internals.
5. For Console workstations, target the documented `ConsoleWorkstation` source union and `window.eforge.invokeAction` bridge. Iframe bundle code can use the versioned `@eforge-build/extension-sdk/browser` helpers instead of private Console React imports; omitted `frameBundle.browserSdkVersion` means browser SDK v1.

Breaking changes to daemon HTTP routes, event schemas, manifest wire shapes, or workstation bridge semantics require a daemon API version bump and migration notes in this section. Bundle-backed workstation manifest metadata, daemon-owned workstation frame/asset routes, and daemon-owned agent-task routes are part of the client contract; extension-authored arbitrary asset bundle serving outside the workstation frame/asset contract, direct React component loading into the parent Console remains unsupported, raw extension-owned HTTP routes are unsupported, extension-owned AI planning/chat APIs outside `ctx.agentTasks` are unsupported, arbitrary raw prompt templates and multi-turn chat are not migration targets for V1 because they remain deferred or unsupported.

## Runtime support status

The daemon can discover, trust-check, import, and execute extension factories. During factory execution it records runtime-wired registrations and exposes counts through `eforge extension` CLI commands and extension daemon APIs. Direct React component loading into the parent Console is unsupported for extensions, private Console React/components/CSS imports are unsupported, parent Console context imports are unsupported, and parent-Console plugins are unsupported. Runtime dispatch and replay testing are available for `onEvent`; runtime wiring is also available for `onAgentRun` prompt-context augmentation, per-run extension tool injection, per-run tool availability tuning, `registerProfileRouter` pre-build dispatch, `registerRuntimeChoiceRouter` per-invocation dispatch after declarative runtime-choice rules, the shipped policy-gate subset (`beforeQueueDispatch`, `beforePlanMerge`, `beforeFinalMerge`), `registerInputSource` enqueue preprocessing, `registerPrdEnricher` content enrichment, `registerReviewerPerspective` parallel review-cycle dispatch, `registerValidationProvider` per-plan validate-stage execution, engine-side extension action/contribution/workstation registry support, daemon contribution manifest/action invocation routes, daemon-owned `ctx.agentTasks` dispatch for supported single-shot read-only planner tasks, daemon-owned `ctx.buildQueue.enqueue` dispatch for trusted queue handoffs, Console System rendering, and CLI/MCP/Pi host discovery/detail/invocation for action-backed contributions, plus sandboxed iframe workstation rendering. Replay invokes only matching event hooks and summarizes non-event registrations separately with their current runtime status. The first-party `eforge-playbooks` extension exposes shipped playbook behavior through native actions, and session-planning remains separate/built-in; neither is a user-authored native extension workflow registration API. `beforeEnqueue`, `beforeValidation`, approval workflow/state/UI, `modify` decisions, user-authored custom session-plan extraction, user-authored custom playbook extraction, raw extension-owned HTTP routes, extension-authored arbitrary frontend asset bundles outside the workstation frame/asset contract, direct React component loading into the parent Console, private Console React/components/CSS imports, parent Console context imports, extension-owned AI planning/chat APIs outside `ctx.agentTasks`, arbitrary raw prompt templates, multi-turn chat, and arbitrary frontend plugin bundles outside registered workstation iframes are intentionally deferred or unsupported runtime phases.

| Capability | Type contract | Loader-time registration capture | Runtime execution today |
|-----------|---------------|----------------------------------|-------------------------|
| `onEvent` | Yes | Yes | Yes |
| `onAgentRun` | Yes | Yes | Yes (promptAppend, per-run tools, allowedTools, disallowedTools)[^1] |
| `registerTool` / `ExtensionTool` | Yes | Yes | Provenance only; inject per run via `onAgentRun` |
| `beforeQueueDispatch` policy gate | Yes | Yes | Yes (blocking policy gate) |
| `beforePlanMerge` policy gate | Yes | Yes | Yes (blocking policy gate) |
| `beforeFinalMerge` policy gate | Yes | Yes | Yes (blocking policy gate) |
| `registerProfileRouter` | Yes | Yes | Yes (pre-build dispatch) |
| `registerRuntimeChoiceRouter` | Yes | Yes | Yes (per-invocation runtime-choice dispatch after declarative rules) |
| `registerInputSource` | Yes | Yes | Yes (extension-aware enqueue preprocessing) |
| `registerPrdEnricher` | Yes | Yes | Yes (fail-open content enrichment before queue write) |
| `registerReviewerPerspective` | Yes | Yes | Yes (parallel review-cycle dispatch) |
| `registerValidationProvider` | Yes | Yes | Yes (per-plan `validate` build stage) |
| `registerAction` / `ExtensionAction` | Yes | Yes | Engine action dispatcher via daemon action invocation route; action context includes dependency/capability lookup, daemon-owned `ctx.agentTasks`, and `ctx.buildQueue` |
| `registerConsoleContribution` / `ConsoleContribution` | Yes | Yes | Daemon contribution manifest projection; Console renders declarative panels under `/console/system` |
| `registerConsoleWorkstation` / `ConsoleWorkstation` | Yes | Yes | Daemon contribution manifest projection; Console renders sandboxed iframe workstations under `/console/workstations` from `srcDoc` entries or source `frameBundle` entries projected to daemon frame/asset URLs |
| `registerIntegrationCommand` / `IntegrationCommand` | Yes | Yes | Daemon contribution manifest projection; host integrations can invoke action-backed commands |
| `registerDeepLink` / `ExtensionDeepLink` | Yes | Yes | Daemon contribution manifest projection; host integrations can invoke action-backed deep links |

[^1]: `onAgentRun` handlers are fail-open: errors and timeouts emit `extension:agent-context:failed` / `extension:agent-context:timeout` diagnostics and do not abort the agent run. Tool names in prompt text should use `ctx.effectiveToolName(name)` when they refer to extension tools.

Loaded extensions appear in provenance and validation output, including registration summaries and diagnostics for runtime-wired families. Event-hook, agent context/tool injection, profile-router, runtime-choice-router, policy-gate, input-source fetching, PRD enrichment, reviewer perspective, validation-provider, daemon-owned action agent tasks, daemon-owned action build queue handoffs, and contribution-family examples can be loaded and validated at runtime. Action lifecycle diagnostics use the `extension:action:*` event family; daemon-owned task diagnostics use the `extension:agent-task:*` event family with sanitized metadata. Event-hook examples can also be dry-run with `eforge extension test --fixture ` or `eforge extension test --run latest`. `beforeEnqueue`, `beforeValidation`, approval workflow/state/UI, `modify` decisions, user-authored custom session-plan extraction, user-authored custom playbook extraction, raw extension-owned HTTP routes, extension-authored arbitrary frontend asset bundles outside the workstation frame/asset contract, direct React component loading into the parent Console, private Console React/components/CSS imports, parent Console context imports, extension-owned AI planning/chat APIs outside `ctx.agentTasks`, arbitrary raw prompt templates, multi-turn chat, and arbitrary frontend plugin bundles outside registered workstation iframes are future or unsupported runtime work.

---

## Toolbelt-vs-extension boundary

Profile toolbelts and extensions are complementary but intentionally separate:

| | Toolbelts | Extensions |
|-|-----------|-----------|
| **Language** | YAML (declarative) | TypeScript (imperative) |
| **Purpose** | "Which MCP servers does this tier get?" | "What should eforge do when X happens?" |
| **Can block pipeline** | No | Yes (policy gates) |
| **Can add custom tools** | Indirectly (MCP) | Yes (`ExtensionTool`) |
| **Scope model** | profiles/, user/project/local | extensions/, user/project/local |

Toolbelt filtering applies only to project MCP servers declared in `.mcp.json`. It does not filter engine-internal custom tools, harness built-ins, or extension-contributed custom tools. `registerTool` records loader-time provenance; `onAgentRun({ tools: [...] })` is the supported per-run injection path. `allowedTools` and `disallowedTools` tune harness availability for a single run and are not toolbelt configuration.

Profile routers receive available profile names and best-effort usage summaries through `ProfileRouterContext`; runtime-choice routers receive available tier-local choices after role-to-tier resolution; agent-run hooks also receive read-only runtime metadata such as `profile`, `harness`, runtime choice, and toolbelt selection. Extensions must not write profile marker files or redefine toolbelt declarations.




---
title: Agent Runtime Profiles
description: Create, switch, and manage named agent runtime profiles that control harness, model, and effort per build tier.
---

# Agent Runtime Profiles

An agent runtime profile is a named YAML file that bundles harness, model, and effort settings for each build tier into a reusable unit. Switching profiles changes how eforge executes builds without touching `eforge/config.yaml`.

## Profile anatomy

Each profile lives at one of three scope tiers and contains an `agents.tiers` block:

```yaml
# eforge/profiles/pi-anthropic.yaml
description: Anthropic models via Pi on OpenRouter.
whenToUse:
  - General-purpose feature work
  - Full-stack changes requiring review depth
tags:
  - pi
  - anthropic

agents:
  tiers:
    planning:
      harness: pi
      model: anthropic/claude-opus-4-6
      effort: high
      pi:
        provider: openrouter
    implementation:
      harness: pi
      model: anthropic/claude-sonnet-4-6
      effort: medium
      pi:
        provider: openrouter
    review:
      harness: pi
      model: anthropic/claude-opus-4-6
      effort: high
      pi:
        provider: openrouter
    evaluation:
      harness: pi
      model: anthropic/claude-opus-4-6
      effort: high
      pi:
        provider: openrouter
```

**Required fields per tier:** `harness`, `model`, `effort`.

**Optional per tier:** `pi.provider` (required when `harness: pi`), `pi.resources` (`isolated` by default, or `ambient` to opt into ambient Pi resources), `pi.thinkingLevel`, `pi.extensions`, `pi.compaction`, `pi.retry`, `thinking` (boolean, enables extended thinking), `fallbackModel`, `maxTurns`, `allowedTools`, `disallowedTools`, `promptAppend`, `toolbelt` (named MCP bundle or `none`), and tier-local runtime `choices`/`routing`.

**Metadata fields** (`description`, `whenToUse`, `tags`) are descriptive only - they surface in list and show commands but do not affect runtime behavior.

## Scope tiers

Profiles live at three scope directories, resolved highest-precedence-first:

| Scope | Directory | Committed? | Precedence |
|-------|-----------|-----------|-----------|
| Project-local | `.eforge/profiles/` | No (gitignored) | Highest |
| Project | `eforge/profiles/` | Yes | Middle |
| User | `~/.config/eforge/profiles/` | No | Lowest |

When two profiles share the same name, the project-local version wins over project, which wins over user.

The active profile is tracked by a marker file at the matching scope:

- `.eforge/.active-profile` - project-local marker
- `eforge/.active-profile` - project marker
- `~/.config/eforge/.active-profile` - user marker

The daemon resolves the active profile in the same precedence order: project-local marker first, then project marker, then user marker, then no profile (engine defaults apply).

## Create a profile

Use `/eforge:profile-new` in Claude Code or `/eforge:profile:new` in Pi to create a profile through a guided wizard. The wizard walks through:

1. **Scope** - project-local, project, or user
2. **Name** - e.g. `pi-anthropic`, `local-qwen`, `mixed`
3. **Tier configuration** - harness, model, effort per tier (planning, implementation, review, evaluation)
4. **Toolbelt preset** (optional) - focused MCP server access for UI, docs, or database work
5. **Activation** - optionally make the new profile active immediately

There is no standalone CLI profile wizard today. The host skills call the daemon's `eforge_profile` MCP tool with `action: "create"` after collecting the tier recipes.

Profile names must match `[A-Za-z0-9._-]+`.

## Switch the active profile

```
/eforge:profile 
```

On success, the daemon writes the active-profile marker at project scope (`eforge/.active-profile`) by default. Ask the skill to use local or user scope when you need the marker at `.eforge/.active-profile` or `~/.config/eforge/.active-profile` instead.

The standalone CLI does not have profile-management subcommands yet, but build enqueue supports a one-off override:

```bash
eforge build --profile pi-anthropic "Add rate limiting"
```

The next build picks up the new active profile immediately - no daemon restart needed.

## Inspect the active profile

```
/eforge:profile
```

Reports the active profile name, source (local, project, or user), resolved harness, metadata (description, tags), and per-tier toolbelt assignments if configured.

The same command also lists all available profiles. Output includes name, scope, harness, description, and a marker for the active profile.

## Profile precedence over other selection mechanisms

The active profile sets the baseline. Other mechanisms can override it in specific contexts:

1. **Explicit `--profile` flag** - `eforge build --profile ` or the enqueue `profile` field overrides the active-profile marker for that single build.
2. **PRD frontmatter `profile:`** - a profile set directly in a PRD file takes absolute precedence; no profile router is consulted.
3. **Playbook `profile:` frontmatter** - applied by `eforge-playbooks:run-playbook` before generic queue handoff; it overrides the active-profile marker and any profile router for autonomous runs and is included in eforge-plan handoff metadata for planning runs. See [Playbooks](/docs/playbooks).
4. **Registered profile router** - an extension can register a `selectBuildProfile` function that selects a profile per-PRD from queue context. Routers run only when no explicit profile is set in the PRD frontmatter. See [Extensions API - registerProfileRouter](/docs/extensions-api).
5. **Active-profile marker** - the fallback when no higher-precedence mechanism applies.
6. **Engine defaults** - used when no profile is configured at all.

When a profile router selects a profile, a `queue:profile:selected` event is emitted. If the router selects a profile name that does not exist, `queue:profile:invalid-selection` is emitted and the build proceeds under the active profile or defaults.

Build-level profile routing is separate from per-invocation runtime-choice routing. `registerProfileRouter` chooses which profile is active before build dispatch; runtime choices select among `choices` declared inside the selected tier for each agent invocation. `onAgentRun` can observe selected choice metadata, but it cannot change the harness, model, provider, effort, or toolbelt for that run.

## Runtime choices inside profiles

The four built-in tiers remain the role-routing axis. Existing tier recipe fields equal that tier's implicit `default` choice. Named choices inherit from the tier default and override only the fields they list. Choices are selected after a role resolves to a tier.

```yaml
agents:
  tiers:
    implementation:
      harness: pi
      model: anthropic/claude-sonnet-4-6
      effort: medium
      pi:
        provider: openrouter
      choices:
        backend:
          model: qwen3-coder
          pi:
            provider: local
          toolbelt: none
        ui:
          effort: high
          toolbelt: browser-ui
      routing:
        rules:
          - name: ui-paths
            choice: ui
            when:
              pathGlobs: ["packages/console-ui/**", "web/**", "**/*.{tsx,jsx,css}"]
              keywords: ["ui", "frontend", "browser", "component"]
          - name: backend-paths
            choice: backend
            when:
              pathGlobs: ["packages/engine/**", "packages/client/**", "packages/monitor/**"]
```

Declarative rules run in order before extension runtime-choice routers. If a rule matches, routers are skipped. If no rule or router selects a named choice, or a router errors, times out, or returns an invalid choice, eforge falls back to `default` for that invocation and does not fail the build. Events expose non-secret runtime-choice metadata such as selected choice, source, rule/router name, and fallback reason.

## Harnesses

Two harnesses ship with eforge:

- **`pi`** - recommended for new profiles; provider-flexible execution across Anthropic, OpenAI, Google, Mistral, Groq, xAI, Bedrock, OpenRouter, and local models. Requires `pi.provider` per tier. Defaults to `pi.resources: isolated` so ambient Pi extensions, skills, prompts, and themes are not loaded into headless eforge agents; set `ambient` only when you intentionally want those resources.
- **`claude-sdk`** - supported secondary path for Anthropic Claude Agent SDK users. Does not use a `pi` block.

You can mix harnesses across tiers within a single profile:

```yaml
agents:
  tiers:
    planning:
      harness: pi
      model: anthropic/claude-opus-4-6
      effort: high
      pi:
        provider: openrouter
    implementation:
      harness: pi
      model: qwen3-coder
      effort: medium
      pi:
        provider: local
    review:
      harness: claude-sdk
      model: claude-opus-4-7
      effort: high
    evaluation:
      harness: pi
      model: gemini-flash
      effort: high
      pi:
        provider: google
```

## Toolbelts inside profiles

A toolbelt filters which project MCP servers from `.mcp.json` reach agents in a given tier. Set `toolbelt: ` on a tier to use a named bundle, or `toolbelt: none` to pass no project MCP servers to that tier.

```yaml
agents:
  tiers:
    implementation:
      harness: pi
      model: anthropic/claude-sonnet-4-6
      effort: medium
      pi:
        provider: openrouter
      toolbelt: browser-ui      # only the playwright MCP server reaches this tier
    planning:
      harness: pi
      model: anthropic/claude-opus-4-6
      effort: high
      pi:
        provider: openrouter
      toolbelt: none             # no project MCP servers for planning
```

Omitting `toolbelt` keeps the default: all servers from `.mcp.json` pass through.

The Claude Code `/eforge:profile-new` wizard and Pi `/eforge:profile:new` wizard include an optional toolbelt step with a preset gallery covering `browser-ui`, `docs-research`, `issue-triage`, `repo-review`, `observability`, `database-readonly`, `api-testing`, and `design-ui`. See [Configuration - Guided Toolbelt Presets](/docs/configuration#guided-toolbelt-presets) for the full setup instructions and [Configuration Reference - Toolbelts](/reference/config#toolbelts) for the schema.

## Move profiles between scopes

Profiles can live at any of the three scope directories, but there are no standalone `promote` or `demote` profile commands today. To share a personal profile with the team, create a project-scope profile with `/eforge:profile-new` in Claude Code or `/eforge:profile:new` in Pi, or move the YAML file from `.eforge/profiles/` to `eforge/profiles/` and then switch to it with `/eforge:profile `.

User-scope profiles (`~/.config/eforge/profiles/`) apply across all projects on the machine and are never committed.

## Where to look next

- [Configuration](/docs/configuration) - `eforge/config.yaml` team defaults that profiles override
- [Playbooks](/docs/playbooks) - run recurring workflows with a profile baked in
- [Extensions API - registerProfileRouter](/docs/extensions-api) - automate profile selection per build
- [Extensions API - registerRuntimeChoiceRouter](/docs/extensions-api) - automate tier-local runtime choice selection per invocation
- [Configuration Reference - Toolbelts](/reference/config#toolbelts) - full toolbelt schema




---
title: eforge-playbooks
description: Install the first-party extension and run reusable workflow templates for recurring eforge builds.
---

# eforge-playbooks

`eforge-playbooks` is the optional first-party extension for reusable Markdown workflow templates. Instead of re-describing recurring work each time, you write a playbook once and run it on demand. The extension resolves the playbook, optionally routes it to a specific agent runtime profile, and either normalizes it to build source for enqueue or routes it to an investigation-first planning extension before a later handoff.

## Install

Install the package in the default project-local extension scope, validate it, and reload extension discovery:

```bash
eforge extension install @eforge-build/eforge-playbooks
eforge extension validate eforge-playbooks
eforge extension reload
```

A project-local install lives under `.eforge/extensions/` and does not require a team trust record. For a committed project/team installation, inspect the installed package before trusting it:

```bash
eforge extension install @eforge-build/eforge-playbooks --scope project
# Inspect the installed package, then trust and reload it.
eforge extension trust eforge-playbooks
eforge extension reload
```

Confirm that the extension is loaded and discover its commands before creating a playbook:

```bash
eforge extension show eforge-playbooks
eforge extension contributions list --extension-name eforge-playbooks
```

Extensions run as trusted, unsandboxed code in the daemon process. Team installations require each user to trust the reviewed package, and code changes invalidate the stored trust hash until the extension is trusted again.

## Boundary and ownership

`eforge-playbooks` owns playbook management and run behavior. The first-party extension exposes the canonical actions `eforge-playbooks:list-playbooks`, `eforge-playbooks:show-playbook`, `eforge-playbooks:save-playbook`, `eforge-playbooks:validate-playbook`, `eforge-playbooks:copy-playbook`, `eforge-playbooks:promote-playbook`, `eforge-playbooks:demote-playbook`, and `eforge-playbooks:run-playbook` through generic extension contribution/action invocation. Hosts discover and invoke these contributions through their generic extension integration surfaces rather than dedicated playbook commands or tools.

`eforge-playbooks` keeps parse, serialize, list, load, write, move, copy, validate, compile, and seed helpers locally, with named-set storage resolved through `@eforge-build/scopes`. Domain-neutral acceptance-criteria helpers remain separate input-layer utilities. Autonomous playbooks enqueue through `ctx.buildQueue.enqueue(...)` via generic extension action handoff. Planning playbooks check the `eforge.plan.planning-workstation` capability from eforge-plan and return planning-entry metadata or unavailable diagnostics; they do not create session plans or enqueue PRDs directly. Console playbook management is displayed through extension contributions and workstations, not a core Console playbooks section.

If `eforge-playbooks` is unavailable, hosts report extension-unavailable diagnostics and guidance to install, trust, validate, or reload the extension before retrying.

## Modes

Every playbook has a `mode` field in its YAML frontmatter:

**`mode: autonomous`** - running the playbook compiles it into normalized build source and enqueues a build, like any other producer input. The daemon picks it up and runs the full pipeline without further interaction. Use this for mechanical, predictable workflows where the build agent does not need to consult you mid-run.

**`mode: planning`** - running the playbook checks the `eforge.plan.planning-workstation` capability from optional [eforge-plan](/docs/eforge-plan) and returns generic planning entry metadata when that capability is available. Continue through generic extension contribution list/show/invoke or the eforge-plan workstation deep link; the extension owns the investigation-first flow, session-plan drafting, revision, and handoff before build submission. The daemon does not create the session plan directly or enqueue a PRD.

When you invoke the `eforge-playbooks:run-playbook` extension action for a planning playbook, the extension action returns `{ kind: "requires-agent", mode: "planning", planningEntry, requiredCapability }` when eforge-plan is available, or `{ kind: "planning-unavailable", requiredCapability, diagnostics }` when the required capability is unavailable. The required capability is provider `eforge-plan`, id `eforge.plan.planning-workstation`, range `>=1.0.0`. Available planning output includes contribution `eforge-plan:open-planning-entry`, workstation id `eforge-plan:planning-workstation`, and workstation URL `/console/workstations/eforge-plan%3Aplanning-workstation`.

Planning-mode playbooks produce session plans through the eforge-plan planning entry, not by directly enqueueing a PRD. The planning workstation creates or resumes a file in `.eforge/session-plans/`, records confirmed investigation findings as context/evidence in context-oriented sections, and makes Scope, Code Impact, and Acceptance Criteria describe concrete implementation targets, actions, and validation criteria. Then `/eforge:build` submits the ready session-plan file as build source. If the playbook declares `profile`, the planning flow can set the session plan's generic `agent_profile`; the profile is validated when that session plan is enqueued. If the playbook declares `postMerge`, those commands are forwarded as generic queued PRD `postMerge` metadata only when an autonomous playbook is converted directly to build source.

## Scope tiers

Playbooks live at three scope directories, shadowed by higher-precedence tiers:

| Scope | Directory | Committed? |
|-------|-----------|-----------|
| User | `~/.config/eforge/playbooks/` | No |
| Project-team | `eforge/playbooks/` | Yes |
| Project-local | `.eforge/playbooks/` | No (gitignored) |

A project-local playbook with the same name shadows the project-team version, which shadows the user version. eforge always resolves the most-specific tier.

## Playbook file format

Playbooks are Markdown files with a YAML frontmatter block:

```yaml
---
name: docs-sync
description: Keep all documentation in sync with the latest code changes.
scope: project-team
mode: autonomous
# profile: docs-heavy  # Optional - omit to allow router/active-profile/default resolution
---

## Goal
Keep documentation current with code changes in every PR.

## Out of scope
Do not create new docs sections; only update existing content.

## Acceptance criteria
- All code examples in docs compile or run without errors
- API surface descriptions match the current implementation
- No stale version references

## Notes for the planner
Focus on packages/ and web/content/. Cross-check against generated reference.
```

**Required frontmatter fields:** `name`, `description`, `scope`, `mode`.

**Optional frontmatter fields:**
- `profile` - agent runtime profile name to use when the playbook runs
- `postMerge` - list of post-merge commands to forward as queued PRD metadata for autonomous builds

## Use a profile with a playbook

The optional `profile` frontmatter field names an agent runtime profile to use when the playbook runs:

```yaml
---
name: ui-regression
description: Automated UI regression sweep
scope: project-team
mode: autonomous
profile: browser-ui
---
```

**Precedence:** an optional `profile` field on the `eforge-playbooks:run-playbook` action input overrides the playbook frontmatter for that run. When no action input override is supplied, the playbook `profile` field overrides the project's active-profile marker and any registered profile router.

**Validation timing:** the named profile is validated at execution time, not when the playbook is saved.

**Planning playbooks:** when a planning-mode playbook has a `profile` field and the eforge-plan planning flow creates a session plan from it, the flow can set the session plan's generic `agent_profile` frontmatter field. When the session plan is enqueued, `agent_profile` is used as the effective profile unless an explicit override is supplied.

**Blank profile fallback:** omitting `profile` allows a registered profile router to select a profile first; if no router selects one, eforge uses the project's active-profile marker or engine defaults.

## Create a playbook

Draft a Markdown playbook with the required frontmatter, then invoke the generic extension contribution command:

```bash
eforge extension contributions invoke eforge-playbooks:save-playbook --kind command --input-json '{"scope":"project-team","raw":"---\nname: docs-sync\ndescription: Keep docs current\nscope: project-team\nmode: autonomous\n---\n\n## Goal\nKeep docs current"}'
```

The action validates and saves the playbook. Use `scope` values `user`, `project-team`, or `project-local`; include `profile` in the playbook frontmatter when the playbook should pin a runtime profile. Besides raw Markdown, `save-playbook` accepts the nested `{ playbook: { frontmatter, body } }` form or flattened fields such as `name`, `description`, `mode`, `profile`, `postMerge`, `goal`, `outOfScope`, `acceptanceCriteria`, and `plannerNotes`.

## Run a playbook

Invoke the generic extension contribution command:

```bash
eforge extension contributions invoke eforge-playbooks:run-playbook --kind command --input-json '{"name":"docs-sync"}'
```

After a successful autonomous enqueue, the extension action returns `{ kind: 'enqueued', id }` and the build appears in Console. Planning playbooks instead return eforge-plan planning entry metadata or unavailable capability diagnostics.

## List playbooks

Discover playbook contributions through the generic extension contribution list command:

```bash
eforge extension contributions list --extension-name eforge-playbooks
```

Then invoke the list contribution when you need the playbook inventory:

```bash
eforge extension contributions invoke eforge-playbooks:list-playbooks --kind command
```

Output groups playbooks by scope tier and marks shadowed entries.

## Copy a playbook

Copy an existing playbook between scopes through the generic extension contribution command:

```bash
eforge extension contributions invoke eforge-playbooks:copy-playbook --kind command --input-json '{"name":"docs-sync","targetScope":"project-local","overwrite":true}'
```

The action validates the source and destination, writes through extension-owned storage helpers, and returns the copied playbook metadata.

## Edit a playbook

Load the playbook, edit its Markdown, then save it through the generic extension contribution commands:

```bash
eforge extension contributions invoke eforge-playbooks:show-playbook --kind command --input-json '{"name":"docs-sync"}'
eforge extension contributions invoke eforge-playbooks:save-playbook --kind command --input-json '{...}'
```

## Promote and demote playbooks

Move a project-local playbook to project-team scope so the whole team benefits:

```bash
eforge extension contributions invoke eforge-playbooks:promote-playbook --kind command --input-json '{"name":"release-prep"}'
eforge extension contributions invoke eforge-playbooks:demote-playbook --kind command --input-json '{"name":"release-prep"}'
```

After promotion, the playbook moves into the committed project-team directory; review and commit it with the rest of your change. Demotion moves it back to project-local scope, where it shadows any team version of the same name.

## Dependency on queue items

For autonomous playbooks, you can schedule a playbook to run after an in-flight build completes by passing `afterQueueId` to the generic extension contribution command:

```bash
eforge extension contributions invoke eforge-playbooks:run-playbook --kind command --input-json '{"name":"docs-sync","afterQueueId":""}'
```

## Where to look next

- [eforge-plan](/docs/eforge-plan) - the first-party planning extension used by planning-mode playbooks
- [Profiles](/docs/profiles) - agent runtime profiles that playbooks can reference
- [Configuration](/docs/configuration#playbook-profiles) - playbook profile frontmatter and precedence
- [Integrations](/docs/integrations) - how to discover and invoke extension contributions from Claude Code, Pi, and the CLI
- [Extensions](/docs/extensions) - extension scopes, package management, and the trust model
- [Glossary](/docs/glossary) - short definitions for playbook, session plan, and PRD




---
title: Stacked PRs
description: Build and submit stacked pull requests with eforge and git-spice.
---

# Stacked PRs with git-spice

Stacked PR landing is an optional, opt-in mode for teams that want a branch-per-PR review flow. eforge currently supports stacked pull requests via git-spice. When `stacking.enabled: true` and `landing.action: pr`, the root artifact branch targets the resolved trunk branch, and each child artifact branch normally targets its parent artifact branch, forming a linear stack of pull requests that reviewers can merge in order. During landing, eforge can repair a missing integrated parent by choosing trunk as the effective base before tracking an untracked child, or by retargeting a child that was already tracked earlier in the landing flow. It also runs provider repo sync, branch restack, and a remote-base freshness proof before submitting the PR.

## Artifact branches

Every eforge build produces an **artifact branch** - a named Git branch (`eforge/`) that holds the committed output from that build. When `landing.action: pr`, eforge opens a pull request from this artifact branch targeting its resolved base.

For non-stacked builds, the resolved base is the branch eforge builds from (often the project trunk, but it can be an active feature branch), and direct PR base sync fetches `origin/` before validation and again immediately before PR creation. For stacked builds, the root PRD targets the resolved trunk branch and child PRDs target the parent PRD's artifact branch. Stacked landing uses provider-owned repo sync/restack plus a remote effective-base ancestor proof instead of the direct non-stacked PR publication path:

```mermaid
graph TD
    main --> A["eforge/prd-a
(PR #1, targets main)"] A --> B["eforge/prd-b
(PR #2, targets eforge/prd-a)"] B --> C["eforge/prd-c
(PR #3, targets eforge/prd-b)"] ``` ## stack_id and stack_parent PRD frontmatter carries two optional stacking fields: **`stack_id`** - a logical stack name shared by all PRDs in the same stack. If omitted, defaults to the first PRD id in the chain. **`stack_parent`** - the PRD id of the immediate parent layer. Controls which artifact branch this PRD's PR targets, and must name an entry from `depends_on` when stacking is enabled. ```yaml --- title: Auth service - part 2 stack_id: auth-refactor stack_parent: q-abc123 depends_on: [q-abc123] --- ``` ## Single-dependency inference When a PRD has exactly one `depends_on` entry and stacking is enabled, eforge automatically infers `stack_parent` from that dependency at dispatch time. For linear stacks you do not need to set `stack_parent` explicitly. When a PRD has multiple `depends_on` entries, eforge cannot infer the stack parent. You must set `stack_parent` explicitly to indicate which dependency is the direct parent layer. If `stack_parent` is missing, or if it is set to an id that is not listed in `depends_on`, dispatch fails before `session:start` with a durable `queue:prd:dispatch-failed` event. ### Explicit handoff and stack parent When you use `--after ` (CLI) or `afterQueueId` (MCP/Pi tool) to create an explicit dependency, the resulting single `depends_on` entry participates in the same stack parent inference described above. If stacking is enabled and the explicit dependency is the only `depends_on` entry, eforge infers `stack_parent` from it at dispatch time - no extra configuration is needed. The explicit handoff is deterministic: dependency detector inference is not used when `afterQueueId` is supplied. ## Enable stacking The guided path is `/eforge:workflow`. Choose a stacked workflow preset to opt in to stacked PR landing and write the required `landing.action: pr` and `stacking.enabled: true` keys to `eforge/config.yaml`. The `stacked-pr-autosync` preset also writes `stacking.sync.afterBuild: true` for daemon-owned automatic stack sync. eforge currently uses git-spice for stacked PR operations. To configure the same settings by hand, add these fields to `eforge/config.yaml`: ```yaml stacking: enabled: true landing: action: pr # stacking requires action: pr ``` If git-spice is not installed to a standard PATH location, set the command explicitly: ```yaml stacking: enabled: true gitSpice: command: /usr/local/bin/git-spice # or 'gs' if you have the alias on PATH ``` ## git-spice setup Install git-spice from [https://abhinav.github.io/git-spice/](https://abhinav.github.io/git-spice/), then initialize it in your repository once: ```bash git-spice repo init ``` This writes a local tracking file that git-spice uses to maintain branch relationships. If git-spice is not available, eforge fails the build with a clear error message. ## Stacked PR landing conflict recovery During stacked builds with `landing.action: pr`, eforge restacks the artifact branch before submitting it. If the stack provider classifies that restack failure as a recoverable conflict, eforge attempts automatic provider-encapsulated recovery before failing the landing step. Recovery first cleans up deterministic temporary plan-ID region marker conflicts. If unmerged files remain, eforge falls back to the merge-conflict resolver agent. The stack provider owns the continue and abort operations; eforge records provider commands as events without hard-coding git-spice arguments. If recovery succeeds, eforge proves remote-base freshness and then submits the PR normally. Manual recovery is still required for non-recoverable provider failures, failed automatic recovery, and conflicts from `eforge stack sync`. ## Stack sync Landing-time sync/freshness is automatic and scoped to the branch being submitted: eforge runs provider repo sync, branch restack, and a remote-base ancestor proof immediately before PR submission. If the fetched effective base is not contained in `HEAD`, eforge retries that sync/restack/proof cycle once before failing closed. When an upstream PR merges, GitHub updates downstream PR bases, but your local artifact branches still need to sync and restack outside a landing run. Use one of these task surfaces: | Surface | Command | |---------|---------| | Claude Code | `/eforge:stack` | | Pi | `/eforge:stack:sync` | | Standalone CLI | `eforge stack sync` | Use `--dry-run` to preview what commands would run without executing them: ```bash eforge stack sync --dry-run ``` `eforge stack sync` calls the daemon's stack sync route, which runs `git-spice repo sync` followed by `git-spice stack restack` to update the full local stack. This is different from automatic landing-time sync, which is branch-scoped and gates PR submission on a freshness proof for that branch's effective base. The sync executes from the project root and returns a structured report: | Field | Description | |-------|-------------| | `outcome` | One of `skipped`, `complete`, `deferred`, `failed`, `conflict` | | `restackCandidates` | Artifact branches eligible for restack | | `activeBuildSkips` | Branches excluded because active builds are using their worktrees | | `providerCommands` | git-spice commands that ran (or would run in dry-run mode) | | `fastForward` | Whether local trunk is at or behind `origin/` | | `error` | Error message when outcome is `failed` or `conflict` | ### Automatic after-build sync To run stack sync automatically after every queued build reaches a terminal state, set `stacking.sync.afterBuild: true` in `eforge/config.yaml`: ```yaml stacking: sync: afterBuild: true ``` When enabled, the daemon triggers a sync from the project root after each build reaches a terminal state (completed, failed, or skipped). The after-build path uses `activeBuildPolicy: "defer"` — if other active builds still overlap the stack candidates, the sync records a `deferred` outcome rather than running. > **Avoid `build.postMergeCommands: ["eforge stack sync"]` for automatic sync.** That path bypasses active-build overlap detection. Use `stacking.sync.afterBuild: true` instead. ### Active-build deferral When sync runs while active builds are in progress, branches whose worktrees overlap active builds are excluded and reported in `activeBuildSkips`. The outcome depends on the `activeBuildPolicy` in the request: - **`skip` (default for manual sync)** — returns `skipped` immediately without mutating any branch state. Re-run `eforge stack sync` manually after active builds complete. - **`defer` (used by the after-build trigger)** — returns `deferred`, recording that candidates were blocked. When `stacking.sync.afterBuild: true` is configured, the daemon fires another sync attempt after each build reaches a terminal state, which proceeds if the stack is no longer blocked. ### Pre-landing reconciliation Before a stacked build lands, eforge checks whether the child artifact branch's stacked base still exists on the remote. This remote-base preflight protects git-spice submission from stale parent branches that were deleted after their PR merged. After preflight and any repair, eforge runs provider repo sync and branch restack, rechecks the effective base, fetches the latest remote effective base, and proves that fetched commit is an ancestor of `HEAD` before PR submission. If the parent remote branch is missing and eforge can prove that the parent artifact commit is already an ancestor of trunk, stale-parent landing repair is automatic and branch-scoped. For an initially untracked child, eforge treats trunk as the effective base, tracks the child against trunk, restacks, and submits the child PR against trunk. If the parent branch disappears after the child has already been tracked, eforge retargets and restacks only the child artifact branch onto trunk. This avoids running a whole-stack restack while preserving the proof that the parent layer is already integrated. If eforge cannot prove the parent artifact commit is an ancestor of trunk, landing fails closed with an actionable error instead of guessing or mutating the rest of the stack. Restore, submit, or repair the parent branch, or verify the parent changes are integrated before rerunning the build. `eforge stack sync` remains the command for normal whole-stack maintenance when parent branches move or upstream PRs merge. Stale-parent landing repair and landing-time sync/freshness are automatic and branch-scoped during landing; use stack sync when you intentionally want git-spice to reconcile the full local stack. ### Conflict recovery When sync returns `outcome: conflict`, a merge conflict occurred during manual stack sync restack. This is separate from stacked PR landing, which attempts automatic provider-encapsulated recovery for provider-classified recoverable restack conflicts. To recover a sync conflict: 1. Run `git status` to see the conflicting files. 2. Resolve the conflicts in the affected files. 3. Run `git add ` to stage the resolved files. 4. Run `git rebase --continue` (or the git-spice equivalent) to resume the restack. 5. Once the restack finishes, run `eforge stack sync` again to sync remaining branches. ### Fast-forward-only trunk policy Sync uses a fast-forward-only policy for trunk. When `fastForward` is `false`, the local trunk is ahead of `origin/`. Push or align the local trunk with origin before running sync. ## Note on GitHub inline comments When a PR's base branch changes after an upstream PR merges, GitHub marks existing inline review comments as "outdated". This is a known GitHub limitation. The comment content remains accessible in the PR timeline. ## Migration: build.onSuccess to landing.action `landing.action` is the current canonical config key. If your config uses the old `build.onSuccess` key, migrate by replacing it with `landing.action` under the `landing:` block: | Old `build.onSuccess` | New `landing.action` | |----------------------|---------------------| | `issue-pr` | `pr` | | `merge-to-base-branch` | `merge` | | `leave-branch` | `leave` | The old `build.onSuccess` key and the legacy full-string values (`issue-pr`, `merge-to-base-branch`, `leave-branch`) are both rejected at validation with migration guidance. Replace `build.onSuccess` with `landing.action` and update the values to `pr`, `merge`, or `leave` before running new builds. ## Where to look next - [Configuration](/docs/configuration) - full config reference including `stacking`, `stacking.sync.afterBuild`, and `landing` fields - [Configuration Reference](/reference/config) - machine-readable schema - [Concepts](/docs/concepts) - artifact branches and the build pipeline --- title: eforge-plan description: Optional first-party planning, backlog, recommendation, and revision extension for eforge. --- # eforge-plan `eforge-plan` is an optional first-party extension package around the eforge build-engine kernel. The kernel still consumes normalized build source and produces reviewed, validated code; `eforge-plan` owns planning-product workflows that help teams decide what build source to hand to the kernel. This public guide covers installation, the first planning handoff, storage and trust, host invocation, and the extension-owned workflow boundary. The package [`README.md`](https://github.com/eforge-build/eforge/blob/main/eforge/extensions/eforge-plan/README.md) is the exhaustive reference for action inputs and maintainer-level implementation details. ## Install ```bash eforge extension install @eforge-build/eforge-plan eforge extension validate eforge-plan eforge extension reload ``` For a team/project install, inspect the package and use the normal extension trust flow: ```bash eforge extension install @eforge-build/eforge-plan --scope project # Inspect the installed package, then trust and reload it. eforge extension trust eforge-plan eforge extension reload ``` Extensions run as trusted, unsandboxed code in the daemon process. A project-local install does not require a team trust record. Each user must trust a committed project/team installation, and package changes invalidate the stored trust hash until it is reviewed and trusted again. ![The eforge-plan planning workstation showing an epic-grouped backlog, planning search, fresh recommendations, and parallel planning lanes.](/screenshots/eforge-plan-workstation.png) *The sandboxed eforge-plan workstation brings backlog organization, cross-artifact search, recommendations, and planning handoff into Console.* ## What it adds Install `@eforge-build/eforge-plan` when you want first-party planning UX in addition to direct prompt, PRD, or file builds: - Project-local backlog capture, body-safe backlog item updates, SQL/FTS search, board rendering, epics, dependencies, and promotion, backed by canonical private SQLite rows, with direct compact agent operations (`search-items`, `search-planning-records`, `get-item`, `get-epic`, `capture-item`, and `update-item`) and projection flags for smaller payloads (epics, lane counts, sections, lifecycle rows, dependencies, dependents, selected search fields, and body text remain opt-in or omittable where supported). Compact item and detail projections include backend `planEligible` and eligibility reason/link fields. For title or section edits, callers read `bodySha256` with `get-item` and send it back as `expectedBodySha256` on `update-item`; metadata-only updates preserve body content and do not require that lock. - Recommendation refresh and backlog curation workflows backed by daemon-owned agent tasks, with server-derived recommendation actionability dispositions, SQL lifecycle evidence, backend planning eligibility, and duplicate planning guards. - A Console planning workstation for investigation-first planning and handoff, including a Backlog all-domain planning search panel and Roadmap store status/maintenance card backed only by extension actions. - Session-plan creation, including one automatic apply attempt for eligible ready creation drafts, persistence of the task summary as a leading `## Executive Summary`, visible failed apply attempts, readiness checks with cache/Markdown freshness metadata, handoff into ordinary eforge builds, and resubmission of submitted or removed plans with terminal failed or removed queue/build evidence. - Revise with AI workflows for existing flat session plans, including durable annotations and revision turns. - Explicit local store import and maintenance actions for dry-run-first legacy import, status, dry-run-first retention compaction, FTS rebuild/optimize, and SQLite `VACUUM`. These are extension-owned product semantics, not kernel behavior. The engine receives the resulting normalized build source the same way it receives a prompt, PRD file, playbook output, or wrapper-app artifact. ## Start a planning workflow Confirm that the extension loaded, then discover its planning contribution: ```bash eforge extension show eforge-plan eforge extension contributions list --extension-name eforge-plan --search planning eforge extension contributions show eforge-plan:open-planning-entry --kind command ``` Invoke the planning entry from the CLI, or open the contributed `eforge-plan:planning-workstation` from Console's Workstations surface: ```bash eforge extension contributions invoke eforge-plan:open-planning-entry --kind command ``` The workstation investigates the change, drafts or resumes a Markdown session plan under `.eforge/session-plans/`, supports annotations and Revise with AI, and checks readiness before handoff. A ready session plan is submitted through the ordinary `/eforge:build` or `eforge build ` flow, so the kernel receives the same normalized build source as any other build. If a previously submitted plan has terminal failed or removed queue/build evidence, use the contributed `resubmit-session-plan` action to preserve the plan's identity and source provenance while creating a new handoff. ## Storage and trust boundary `eforge-plan` runs as trusted extension code in the daemon process. Its private planning state lives under `.eforge/storage/extensions/eforge-plan/`, including the normalized SQLite store at `.eforge/storage/extensions/eforge-plan/eforge-plan-private.sqlite`, backlog records, recommendation runs/models, backlog curation previews, planning task indexes, lifecycle evidence, accepted-analysis baselines, and plan revision threads. Treat that directory as local/private project metadata. Runtime planning mutations write canonical SQLite rows for queryable metadata, provenance, item/plan joins, lifecycle timestamps, lifecycle evidence, search documents, and queue/build/session/landing links. Body-safe `update-item` writes canonical backlog rows, recomputes section rows, updates Markdown mirrors, marks search documents dirty, and marks recommendation metadata stale. FTS-backed `search-items` and `search-planning-records` return bounded ranked/snippet results, counts by type, pagination, selected refs, and dirty-index metadata rather than scanning legacy Markdown, recommendation JSON, or session-plan bodies. Dirty indexes are reported to callers; browser and host clients do not rebuild them implicitly. Markdown mirrors such as `.backlog/items/.md` and `.eforge/storage/extensions/eforge-plan/backlog/items/.md` are compatibility/import outputs, not normal mutation targets. Session plans created for handoff live under `.eforge/session-plans/` and are submitted to eforge as build source when ready; SQLite records metadata, canonical session-plan status, readiness summaries, readiness cache freshness/source indicators, submitted handoff/resubmit state, lifecycle timestamps, lifecycle projection reasons, and links, not the Markdown body as canonical content. Resubmission preserves the existing session-plan identity and source provenance while recording fresh submitted lifecycle evidence. AI-created session plans preserve the task summary as a leading `## Executive Summary` before readiness dimensions. They are local and gitignored; committed build provenance is still the engine's artifact-branch PRD and plan records. ### Retention and compaction Planning-store maintenance is explicit local extension behavior. `get-store-status` reports whether the private SQLite store exists, file sizes, table counts, retention eligibility counts, FTS status, and recent maintenance runs without creating a missing store. `compact-planning-store` is dry-run-first and only applies when called with `dryRun: false`; it may compact prunable lifecycle event payloads, terminal planning-task raw payloads, and superseded non-current recommendation runs. Compaction preserves canonical backlog items, epics, dependencies, session plans and joins, current lifecycle evidence summaries, current recommendation state, actionability projections, associated links, and duplicate-coverage policy. Optional JSONL archives are written under `.eforge/storage/extensions/eforge-plan/archives/maintenance//` before mutation and are reported by path/count rather than returned inline. Search maintenance stays explicit through `rebuild-search-index` and `optimize-search-index`; SQLite file reclamation stays separate through `vacuum-planning-store`. The planning workstation surfaces these as bounded, explicit controls: dry-run compaction by default, explicit FTS rebuild/optimize buttons, and a confirmation step before vacuum. ## Host invocation CLI, MCP, Claude Code, Pi, and other hosts should discover and invoke the same eforge-plan action IDs through generic extension contribution tooling. For direct backlog item edits, use `get-item` to read the current lock token and `update-item` with `expectedBodySha256`, `sections`, or `sectionOperations` rather than editing Markdown mirrors. Submitted or removed session-plan recovery uses `resubmit-session-plan` through the same generic action surface when terminal failed or removed queue/build evidence makes the plan recoverable. This module does not add dedicated host-specific commands for FTS search, lifecycle/actionability projections, body editing, or maintenance; use actions such as `get-store-status`, `search-planning-records`, `compact-planning-store`, `rebuild-search-index`, `optimize-search-index`, and `vacuum-planning-store` directly through the host's generic extension-action surface. ## Product semantics owned here `eforge-plan` owns product-specific concepts that generic core and extension-platform docs intentionally do not describe in detail: - `backlogCurationDraft` outputs from backlog curation tasks. - Generated recommendations plus read-time freshness/staleness and actionability projections from current SQLite recommendation runs, canonical lifecycle evidence, and queue/build/session/landing links. - `planRevisionTurn` output for Revise with AI, including answer-only and patch-bearing turns. - Annotation-backed revision sessions and durable quote-context targets. - Daemon-owned `ctx.agentTasks` execution boundaries: the extension owns product storage and apply semantics, including one-attempt workstation auto-apply for eligible ready `sessionPlanCreationDraft` tasks and summary-to-Executive-Summary persistence, while the daemon owns task records, status, cancellation, and sanitized results. - Workstation routing, planning-entry contributions, and backlog promotion UX. `eforge-plan` does not turn the daemon into a generic multi-turn chat runtime. Its planning and revision workflows are bounded extension UX built on daemon-owned single-shot tasks. Use the package README for the exhaustive action-input reference and maintainer-level storage and workstation details. --- title: Integrations description: How to use eforge from Claude Code, Pi, the standalone CLI, extension contributions, and external issue trackers. --- # Integrations eforge can be driven from three host surfaces: the Claude Code plugin, the Pi extension, and the standalone CLI. All three talk to the same daemon and share the same core queue and profiles. Optional workflow surfaces such as `eforge-playbooks`, session-plan compatibility tools, and first-party extensions prepare or route build source around the kernel. This page covers how each surface works and how to connect eforge to external systems. ## Claude Code plugin The Claude Code plugin installs eforge's skills as slash commands, wires up an MCP proxy so Claude Code can call eforge daemon tools directly, and includes the marketplace install flow. ### Install Run these three commands inside Claude Code: ``` /plugin marketplace add eforge-build/eforge /plugin install eforge@eforge /eforge:init ``` `/eforge:init` creates `eforge/config.yaml` with sensible defaults and walks you through harness and model selection. Choose Quick setup with Pi for the recommended provider-flexible path. ### MCP proxy The Claude Code plugin communicates with the daemon through an MCP stdio proxy. When the plugin loads, it launches: ```bash eforge mcp-proxy ``` The proxy translates MCP tool calls from Claude Code into HTTP requests to the local daemon HTTP API. The daemon auto-starts on first use; you do not need to start it manually. The MCP tool surface includes build enqueueing, status, config/profile/session-plan management, recovery, extension management, extension contribution discovery/detail/invocation through `eforge_extension_contribution` (`mcp__eforge__eforge_extension_contribution` in Claude tool-call form), existing queue controls, and auto-build state. Playbook behavior is available only when the first-party extension contributes actions to that generic surface. Extension-management and contribution tools use compact default projections and share a 12,000-character host-output budget for returned tool text; contribution lists stop on whole-entry boundaries and report continuation offsets instead of dumping raw daemon objects into the coding-agent context. `eforge_queue_priority` updates pending/waiting queue-item priority, and `eforge_queue_remove` removes non-running pending, waiting, failed, or skipped queue items. The richer hold/unhold, scheduler pause/resume, failed-enqueue re-enqueue, and cascade preview/apply controls are Console and daemon/client API surfaces unless a host implementation intentionally exposes them. The `eforge_auto_build` tool reads or updates the daemon's auto-build desired state; Console uses the same daemon API state. ### Skills (slash commands) All eforge workflows are available as slash commands: | Command | Purpose | |---------|---------| | `/eforge:build` | Enqueue a build from a prompt, PRD, file path, or optional session-plan artifact | | `/eforge:profile` | Inspect and switch agent runtime profiles | | `/eforge:profile-new` | Create a new profile through a guided wizard | | `/eforge:workflow` | Choose or reconfigure landing action, PR auto-merge policy, stacking, and automatic stack sync | | `/eforge:stack` | Synchronize a git-spice stack; accepts `--dry-run` | | `/eforge:recover` | Inspect a failed build's recovery verdict and apply it | | `/eforge:restart` | Safely restart the daemon | | `/eforge:status` | Show current build queue and daemon status | | `/eforge:init` | Initialize eforge in the current project | | `/eforge:config` | View or edit `eforge/config.yaml` | | `/eforge:extend` | Manage native extensions | | `/eforge:update` | Check for and install eforge updates | Use `/eforge:workflow` to choose one of the workflow presets. The stacked preset with automatic sync is `stacked-pr-autosync`; it writes `landing.action: pr`, `stacking.enabled: true`, and `stacking.sync.afterBuild: true` so the daemon owns stack sync instead of relying on a post-merge shell command. When after-build sync is enabled, overlapping active builds produce a `deferred` stack sync outcome and the daemon retries after later terminal queue events. ## Pi extension The Pi extension provides the same capabilities as the Claude Code plugin through Pi's native command system and interactive TUI surfaces. ### Install ```bash pi install npm:@eforge-build/pi-eforge /eforge:init ``` Add `-l` to install to project settings instead of global: ```bash pi install -l npm:@eforge-build/pi-eforge ``` The Pi extension communicates directly with the daemon HTTP API rather than through a proxy, and supports richer UI patterns such as searchable selectors plus scrollable panels for variable-length read-only content. Native Pi tools mirror the Claude Code MCP surface, including core build/status/queue/config tools plus optional workflow tools such as `eforge_session_plan`, `eforge_extension`, and `eforge_extension_contribution`. Playbook behavior is reached through generic contribution invocation when the first-party extension is loaded. Pi extension-management and contribution tool text uses the same 12,000-character host-output budget as MCP, including whole-entry contribution list continuation. Pi also exposes `/eforge:extensions` for browsing, showing, and invoking extension-provided actions, commands, and deep links without dumping raw manifests by default, including optional [eforge-plan](/docs/eforge-plan) planning entries, SQLite store/search/maintenance actions, and `eforge-playbooks` contributions when those extensions are loaded. ### Pi commands | Command | Purpose | |---------|---------| | `/eforge:workflow` | Open the workflow setup/reconfigure chooser | | `/eforge:workflow:init` | Run the full workflow preset wizard from scratch | | `/eforge:workflow:reconfigure` | Show current workflow config, then run the preset wizard | | `/eforge:stack:sync` | Synchronize a git-spice stack; accepts `--dry-run` | ## Standalone CLI For shell-based workflows or CI environments where a host is not available: ```bash # Install globally npm install -g @eforge-build/eforge # Or run without installing npx @eforge-build/eforge build "Add rate limiting to the API" ``` Daemon management, extension commands, and one-off build profile overrides are available from the CLI. Playbook actions are discovered and invoked through the generic extension contribution dispatcher: ```bash eforge build "Add dark mode toggle" eforge build --profile pi-anthropic plans/my-feature-prd.md eforge build --landing-action pr plans/my-feature-prd.md eforge queue run --all eforge queue priority eforge queue remove eforge extension contributions list --kind command --search playbook eforge extension contributions invoke eforge-playbooks:run-playbook --kind command --input-json '{"name":"docs-sync"}' eforge daemon status eforge daemon start eforge daemon stop eforge daemon restart eforge extension list eforge extension contributions list --kind command --search planning --limit 20 eforge extension contributions show --kind command --include-schema eforge extension contributions invoke --kind command eforge stack sync eforge stack sync --dry-run ``` For standalone use, run `/eforge:init` in Claude Code or Pi first to create `eforge/config.yaml` and an agent runtime profile. The CLI then reads the same config. Profile creation and switching are currently exposed through the Claude Code and Pi skills rather than standalone `eforge profile` subcommands. Documented CLI queue controls match host tools: priority applies to pending/waiting items, removal applies to non-running pending, waiting, failed, and skipped items, running queue-item cancellation requires daemon ownership evidence, and failed removal cleans up recovery sidecars. ## Extension host contributions Native extensions can publish shared manifest metadata for actions, declarative Console panels, integration commands, and deep links. The same daemon-owned manifest feeds CLI `eforge extension contributions list|show|invoke`, MCP/Claude `eforge_extension_contribution`, Pi `eforge_extension_contribution`, and Pi `/eforge:extensions`, so hosts discover the same action, command, and deep-link IDs. Optional first-party eforge-plan planning, SQLite status, FTS search, and maintenance actions are discovered through this generic routing rather than through kernel-owned or host-specific planning commands. Manifest entries also carry dependency/capability availability metadata; unavailable actions are rejected with error code `unavailable`. Actions, action-backed commands, and action-backed deep links can be invoked generically through those host surfaces. Contribution list output is compact by default; page through list filters using returned continuation offsets, then use `show ` / `action: "show"` for focused detail, and opt into schemas, diagnostics, or full projections only when needed. MCP and Pi host output is capped to 12,000 characters for coding-agent tool text, and list rendering stops on complete entries with returned/total/next-offset guidance. Non-JSON host output is formatted for bounded display: exact `{ markdown: string }` outputs render as Markdown/plain text, oversized JSON is summarized with warnings and preserved identity/count/continuation fields, and rich/debug output profiles warn in coding-agent hosts. Use CLI `--json` or direct client/HTTP invocation only when you intentionally need the full raw action result. URL-only deep links are listable navigation entries for hosts that know how to open the URL, but they are not generic invocations unless the extension also supplies an action binding. Console contribution rendering stays inside `/console/system` and uses closed renderer IDs; richer extension UI uses registered sandboxed workstations (`srcDoc` or daemon-owned `frameBundle` assets), not arbitrary parent-Console frontend bundles. ## Daemon HTTP API The daemon exposes a local HTTP API and SSE event streams used by the Claude Code MCP proxy, the Pi extension, Console, and wrapper apps. Use the generated [HTTP API Reference](/reference/api) for route shapes and the [Events Reference](/reference/events) for streamed event variants. For TypeScript integrations, import typed route helpers from `@eforge-build/client` instead of hard-coding `/api/...` paths; queue and recovery helpers include hold/unhold, queue cascade preview/apply, failed-enqueue list/re-enqueue, recovery-guidance preparation, and scheduler pause/resume. Direct playbook-specific daemon routes are absent: integrations must discover `eforge-playbooks:*` through the generic extension contribution manifest and invoke actions through generic extension action routes. Browser/Console integrations should use `holdQueueItem`, `unholdQueueItem`, `previewQueueCascade`, `applyQueueCascade`, `fetchFailedEnqueues`, `reenqueueFailedEnqueue`, `prepareRecoveryGuidance`, `pauseScheduler`, `resumeScheduler`, `fetchExtensionContributionManifest`, `invokeExtensionAction`, and client-owned `API_ROUTES` helpers rather than raw route construction. For normal day-to-day usage, prefer the host commands and tools above; direct API calls are intended for integrations and automation. ## Shell hooks Shell hooks let you trigger external commands on eforge events without writing a TypeScript extension. Configure them in `eforge/config.yaml`: ```yaml hooks: - event: plan:build:complete command: "notify-send 'Build complete'" timeout: 5000 - event: plan:build:failed command: "curl -X POST $SLACK_WEBHOOK -d '{\"text\": \"Build failed\"}'" - event: session:end command: "./scripts/notify-team.sh" ``` Hooks are fire-and-forget - they do not block the pipeline. See [Configuration - Hooks](/docs/configuration#hooks) and [Configuration Reference - Hooks](/reference/config#hooks) for field details and available event patterns. ## Input source adapters (GitHub, Linear, Jira) Native extensions can register input source adapters that resolve `eforge://input//` URIs. When you supply such a URI as the build source, eforge fetches the issue or PR content and uses it as the PRD. ```bash eforge build "eforge://input/github/acme/backend#42" eforge build "eforge://input/linear/ENG-42" eforge build "eforge://input/jira/ENG-42" ``` URI dispatch: the `` segment selects a registered adapter by name. The `` path is passed to the adapter's `fetch` function. The adapter returns Markdown content that eforge uses as build input. Example adapters are available at `examples/extensions/issue-tracker.ts` in the eforge repository. The example GitHub adapter reads `GITHUB_TOKEN` (and optional `GITHUB_API_BASE` for Enterprise Server), the Linear adapter reads `LINEAR_API_KEY`, and the Jira adapter reads `JIRA_BASE_URL` plus `JIRA_TOKEN` in `:` format. See [Extensions - Input sources and PRD enrichers](/docs/extensions#input-sources-and-prd-enrichers) for the full adapter API. ## Observability with Langfuse eforge sends agent trace data to Langfuse when both a public key and secret key are configured. Set them in `eforge/config.yaml` under `langfuse.publicKey`, `langfuse.secretKey`, and optional `langfuse.host`, or use the environment variables `LANGFUSE_PUBLIC_KEY`, `LANGFUSE_SECRET_KEY`, and optional `LANGFUSE_BASE_URL`. The default host is `https://cloud.langfuse.com`. The `langfuse` field is listed in the [Configuration Reference](/reference/config#top-level-fields) top-level fields table. ## Console dashboard A web-based Console dashboard runs locally alongside the daemon. Access it at the canonical Console URL: ``` http://localhost:/console/ ``` Root UI requests on the same port redirect to Console. The port is deterministically assigned per project in the 4567-4667 range. The same port persists across daemon restarts for a given project. Console shows: - Active and queued builds with live progress - Pending/waiting queue row actions to set priority, hold/unhold rows, show daemon capability disabled reasons, and confirm preview-first remove/cascade flows - Header controls that distinguish disabling auto-build from pausing/resuming scheduler launches - Per-plan stage breakdown (plan, implement, review, merge, validate) - Token usage and cost per build - Live efficiency metrics and a historical efficiency analytics Now rail card with selectable 1d, 7d, 14d, 30d, and 90d windows - Runtime agent decisions (effort, thinking mode) on stage hover - Console Needs attention strip for failed builds, compile scope/context failure banners with optional guard diagnostics or decomposition-exhaustion evidence, projected dispatch blockers, durable failed-enqueue rows with confirmed re-enqueue when source data exists, root-hosted recovery dialog actions with read-only compile guidance and explicit queue-cascade repair controls, and queue refresh, plus untrusted/changed project-team extension alerts with inline Trust/Re-trust actions - Extension inventory, status, and diagnostics, plus a System extension management surface (under `/console/system`) for reloading extensions, validating a selected extension, and trusting/re-trusting, untrusting, promoting, and demoting discovered extensions through confirmation-gated actions Efficiency metrics are telemetry proxies from eforge event data, not provider benchmark measurements: | Metric | Formula | Notes | |--------|---------|-------| | Output generation rate | `sum(output tokens) / sum(API duration seconds)` | Historical rows show p50/p95 over eligible finalized samples. | | Token traffic | Live: `(input + output tokens) / elapsed wall-clock minute`; historical: `total tokens / provider API duration seconds` for eligible finalized samples | Use this to understand total token movement, not just generated output. | | Cost burn | Live: `total cost / elapsed wall-clock minute`; historical cost/min: `finalized cost / provider API duration minutes` for eligible finalized samples | Distinct from total spend. | | Output tokens / $ | `output tokens / total cost` | Unavailable when cost is missing or zero. | | Cache context | `cache read tokens / input tokens` | Cache creation tokens are separate context, not cache hits. | The historical analytics card groups model rows by model plus harness/provider and profile rows by session profile. Rows include p50/p95 output generation rate, cost/run, cost/min, output tokens/$, success/failure counts, and sample counts when available. Sparse data is labeled explicitly: missing values render as `—`, and partial rows call out missing or excluded samples instead of displaying zero. Multi-model builds can split model attribution while still counting once at the profile/run level, so compare model and profile rollups as directional signals rather than exact benchmark rankings. The daemon keeps Console available after a build completes so you can inspect results and costs. ## Where to look next - [Getting Started](/docs/getting-started) - install and first build - [Configuration](/docs/configuration) - configure hooks, extensions, and daemon settings - [Extensions](/docs/extensions) - write TypeScript extensions for richer integrations - [Troubleshooting](/docs/troubleshooting) - daemon startup issues and common errors --- title: Troubleshooting description: Common eforge failure modes and how to resolve them. --- # Troubleshooting ## Daemon won't start or port is in use eforge assigns each project a deterministic port in the 4567-4667 range. If the daemon fails to start, check whether another process is holding that port or if a previous daemon instance did not exit normally. **Diagnose:** ```bash eforge daemon status ``` **Restart the daemon:** ```bash eforge daemon restart ``` `daemon restart` stops the daemon before starting it again. The stop phase runs the active-build safety check: in interactive terminals it prompts before stopping active builds, while non-interactive runs proceed to avoid blocking scripts. The `--force` flag skips that check and prompt: ```bash eforge daemon restart --force ``` **Last resort - SIGKILL:** ```bash eforge daemon kill ``` `daemon kill` sends SIGKILL to the daemon process and is the last resort when the daemon is unresponsive. Any in-progress builds will be interrupted. Use `eforge daemon restart` or the `/eforge:restart` skill from Claude Code or Pi for an active-build safety check. After a forced restart or kill, check `eforge daemon status` before starting a new build to confirm the daemon is healthy. ## `pnpm docs:check` reports drift or broken links The drift check compares on-disk generated reference outputs against freshly regenerated outputs. If any generated file is out of date, the check fails. **Fix:** ```bash pnpm docs:generate pnpm docs:check ``` Run `pnpm docs:generate` any time you edit hand-authored guide pages under `web/content/docs/` or change source files that feed into generated reference pages (`packages/engine/src/config.ts`, CLI source, client event schema modules, MCP tools). The generator updates `web/content/reference/*.md`, `web/public/reference/*.md`, `web/public/docs/*.md`, `web/public/schemas/*.json`, `web/public/llms.txt`, and `web/public/llms-full.txt`. Never edit those files by hand; the drift check will catch it. If the check reports broken internal links, update the link target in the relevant `web/content/docs/*.md` file to point at a slug that exists under `/docs/`, `/reference/`, or `/schemas/`. ## Auto-build is disabled or paused If a PRD is queued but does not start, check whether daemon auto-build is disabled or the scheduler is paused. Auto-build starts from `prdQueue.autoBuild` in config and can be toggled at runtime through the host `eforge_auto_build` tool or Console. Scheduler pause is separate: it keeps desired auto-build enabled but prevents new launches until resume; already-running builds continue unless you explicitly cancel them. The daemon pauses launches after a queued build fails so dependents do not cascade. **Diagnose:** - Run `/eforge:status` in Claude Code or Pi and inspect the queue/auto-build summary. - In host tooling, call `eforge_auto_build` with `{ "action": "get" }` to read the daemon's current state. - Check the monitor event stream for `daemon:auto-build:disabled`, `daemon:auto-build:paused`, or `daemon:auto-build:transition`. **Resume safely:** 1. If the scheduler paused because a build failed, run `/eforge:recover` first and apply the recovery verdict, then resume the scheduler from Console. 2. If auto-build is disabled, re-enable desired auto-build with `eforge_auto_build` `{ "action": "set", "enabled": true }` or Console. 3. If you intentionally disabled auto-build to stage queue items, either re-enable it or run `eforge build --queue` / `eforge queue run --all` from the CLI. For persistent defaults, set `prdQueue.autoBuild: true` in `eforge/config.yaml`. If the watcher is slow to notice new PRDs, tune `prdQueue.watchPollIntervalMs` rather than manually editing queue files. ## Queue priority, removal, or dependency override returns conflict Queue controls are safe runtime filesystem mutations under `.eforge/queue/` and produce no git commits. Use `eforge queue priority ` to update pending or waiting PRD frontmatter; lower numeric priority values run earlier within each dependency wave. Failed and skipped priority changes return conflict until recovery or requeue makes the item runnable. Use `eforge queue remove ` to delete a non-running pending, waiting, failed, or skipped queue item. Removing a failed item also deletes matching `.recovery.md` and `.recovery.json` sidecars. The daemon API's dependency override removes one dependency id from a pending or waiting queue item; it returns conflict for running, failed, or skipped items, or when the target does not list the requested dependency. Queue hold state is stored as runtime-only PRD frontmatter (`held`, `hold_reason`, `held_at`) on pending or waiting items; held items keep their file location and ordering metadata, but scheduler ticks skip them until they are unheld. Console renders hold/unhold, priority, remove, cascade, cancel, and disabled reasons from daemon-authored queue capabilities. Running items reject priority, hold, unhold, removal, and dependency override controls because active work is owned by its build session; daemon-owned cancellation requires live queue-lock and run/session ownership evidence. If removal reports live dependents, eforge failed closed because pending or waiting queue items still depend on the target. The conflict lists dependent ids for target-only removal. Cascade remove and cancel use a preview/apply flow that rechecks an expected affected token and requires explicit dependent confirmation before mutating dependents. If a queue file was moved or deleted between listing and mutation, refresh the queue view and retry against the current item id. After successful priority, removal, dependency override, hold, unhold, or cascade mutations, the daemon notifies the scheduler; when the scheduler is not explicitly paused, it re-reads queue files before dispatch. Console exposes set-priority, hold/unhold, disabled capability reasons, and preview-first cascade remove/cancel actions on eligible queue rows. MCP and Pi expose the existing host tool names, `eforge_queue_priority` and `eforge_queue_remove`; priority applies to pending/waiting items and removal applies to non-running pending, waiting, failed, and skipped items. ## Stack sync skipped, failed, or conflicted Run stack sync from your host (`/eforge:stack` in Claude Code, `/eforge:stack:sync` in Pi) or from the CLI: ```bash eforge stack sync --dry-run eforge stack sync ``` Use the report's `outcome`, `reason`, `activeBuildSkips`, `fastForward`, and `providerCommands` fields to choose the fix: - **Skipped sync because stacking is disabled**: enable a stacked workflow with `/eforge:workflow`, or set `stacking.enabled: true` and `landing.action: pr` in `eforge/config.yaml`, then rerun sync. - **git-spice missing or uninitialized**: install git-spice, verify `git-spice --version`, run `git-spice repo init` once in the repository, and set `stacking.gitSpice.command` if the binary is not on `$PATH`. - **Local trunk not fast-forwardable**: when `fastForward` is `false`, push or otherwise align your local trunk with `origin/`; eforge will not force-push, reset, or rebase trunk for you. - **Active-build skips**: wait for the listed active eforge builds to finish, then run `eforge stack sync` again. The manual sync command uses skip semantics by default to avoid mutating worktrees that active builds are using. - **After-build sync keeps deferring**: confirm `stacking.sync.afterBuild: true` is set (or choose the `stacked-pr-autosync` preset in `/eforge:workflow`). Daemon-owned after-build sync uses `activeBuildPolicy: "defer"`; overlapping active builds produce a `deferred` outcome and the daemon retries after later terminal queue events. - **Conflict recovery for manual stack sync**: when `outcome` is `conflict`, run `git status`, resolve the conflicted files, `git add `, continue the rebase/restack with `git rebase --continue` or the git-spice equivalent, then rerun `/eforge:stack` or `/eforge:stack:sync` to finish remaining branches. Stacked PR landing has a separate recovery path: during `landing.action: pr`, eforge attempts automatic provider-encapsulated recovery for provider-classified recoverable restack conflicts before failing the landing step. It also preflights the remote base before git-spice submission: if a missing parent branch's artifact commit is an ancestor of trunk, eforge automatically performs branch-scoped stale-parent landing repair by tracking initially untracked children against trunk or retargeting already tracked children; if that ancestry cannot be proven, landing fails closed with manual stack-base repair guidance. Before submitting, it runs provider repo sync, branch restack, and a remote-base freshness proof, retrying that sync/restack/proof cycle once if the fetched effective base is not contained in `HEAD`. Manual recovery is still required for `eforge stack sync` conflicts, non-recoverable provider failures, failed automatic landing recovery, and stale parent branches whose changes are not proven integrated. If `outcome` is `failed` without a conflict, inspect the failed `providerCommands`, run the same git-spice command manually for more context, fix the repository state, and rerun with `--dry-run` before applying changes. ## Recover a failed enqueue If enqueue formatting, source loading, or pre-queue validation fails before a runnable queue file exists, Console shows a durable **Enqueue failed** row in Needs attention. The row is keyed by run id and includes the source label, reason, timestamp, fallback next command, and any disabled reason. When the daemon still has enough source data, use the confirmed **Re-enqueue…** action; otherwise copy the fallback command and fix the source or validation issue first. A successful re-enqueue resolves the failed-enqueue row. ## Oversized PRDs and compile scope/context failures Large or machine-generated PRDs can exhaust compile-stage prompt or provider context before any plan is built. eforge reports this through typed planning diagnostics instead of a generic manual failure. **What to look for:** - `planning:scope-context:failure` - a compile scope/context guard failure. It identifies the failure source, failure kind, stage, bounded explanation, observed prompt/token/turn metrics when available, artifact summary, and recovery action. Newer events may also include optional guard diagnostics such as provider/model, model-aware input-token limit, context window, reserves, safety margin, metadata source, and fallback reason, plus bounded decomposition evidence for `decomposition-exhausted` failures. **How to interpret recovery guidance:** - `bounded-decomposition` means eforge should use bounded context-managed planning units when eligible, or the operator should deliberately reduce/decompose the source before retrying. Context-managed planning-unit budgets come from the top-level `compile.planningUnit*` config keys; exhausted decomposition evidence is read-only and does not mean eforge auto-authored successor PRDs or queue items. - `manual-reduce-scope` means a human should remove generated bulk, duplicate context, or unrelated requirements and enqueue a reduced source. - `repair-existing-artifacts` means preserved compile artifacts may be usable through the compiled-artifact repair path when the sidecar also reports valid continue-and-repair eligibility. Compile scope/context recovery options in a sidecar are **read-only guidance**. They do not create Console or daemon `apply-recovery` actions for `bounded-decomposition`. When `decompositionEvidence` is present, CLI, Console, and sidecar markdown show bounded fields such as failed unit, depth, triggered limits, blockers, unresolved criteria, and split attempts, without raw source, prompts, transcripts, or agent output. Use the existing recovery verdict action, continue-and-repair when artifacts are valid, or manually reduce/decompose the PRD and enqueue any revised source deliberately. When guard diagnostics are present, inspect the rendered `limits`, `outputReserveTokens`, and any `fallbackReason` before deciding how to retry. `outputReserveTokens` is the effective output reserve used in the input-budget calculation; for planner-family Pi guards it can be capped below the model's raw output-token metadata. `metadataSource` can be `registry` (Pi ModelRegistry metadata), `builtin` (Pi built-in metadata), `synthetic` (provider sibling transport metadata for an unknown model), or `fallback` (conservative defaults after missing or unusable metadata). Registry, built-in, and synthetic metadata can still be incomplete or caveated; `fallbackReason` is the authoritative caveat text whenever it appears. If the reported `maxObservedInputTokens` is too low for the build source, reduce scope or choose/add a profile/model with complete context metadata before retrying. ## Recover from a failed build When a queued build fails, the PRD is marked `failed` in the queue while desired auto-build remains enabled. By default the scheduler pauses; if `recovery.autoResume.enabled` is true and the failed PRD passes the guarded high-confidence `continue-repair` policy, including compiled-artifact eligibility, the daemon may queue the continued repair instead of pausing. Do not re-enqueue manually; use the recovery workflow instead. **Check for failed builds:** ```bash eforge queue list ``` Or from Claude Code or Pi: ``` /eforge:status /eforge:recover ``` The recovery flow: 1. Call `eforge_queue_list` to find failed PRDs. 2. Read the recovery sidecar (`eforge_read_recovery_sidecar`) to get the recovery verdict and bounded evidence. 3. The verdict is one of: - `retry` - move the failed PRD back to the queue root and remove recovery sidecars so auto-build can try it again from scratch - `continue-repair` - prepare root-plan `## Recovery Guidance`, then queue the failed PRD through the compiled-artifact repair path, preserving existing queue controls and reactivating skipped descendants whose dependency chain reaches the parent - `abandon` - remove the failed PRD and recovery sidecars from the queue because the work should not continue - `manual` - make no queue changes; a human must inspect the recovery report and decide whether bounded manual replanning or a deliberately authored follow-up PRD is appropriate 4. Check the sidecar's continue-and-repair fields. If `continueRepairEligibility.eligible` is true or `recoveryOptions` recommends `continue-repair`, present one primary **Continue and repair build** action. If the sidecar says continue-and-repair is ineligible, show the bounded reason and do not infer eligibility manually from branch or artifact presence. Other `recoveryOptions` entries, such as `compile-scope-context`, are non-mutating guidance; surface their reason/action and any bounded `decompositionEvidence`, but keep the primary action tied to the verdict or continue-and-repair eligibility. 5. Confirm the action with the user. 6. Apply via `eforge_apply_recovery` / `eforge apply-recovery ` for `retry` and `abandon`. For `continue-repair`, call `eforge_continue_repair` (Pi), `mcp__eforge__eforge_continue_repair` (Claude Code), or `eforge continue-repair [--set-name ] [--profile ]` (CLI). These commands require current root-plan `## Recovery Guidance` before queueing the continued build and return queued metadata rather than a local worker session. When you are present in the Console Now dashboard, failed builds appear in the Needs attention strip with a **Recover…** action. Rows with daemon-projected pre-session dispatch blockers show the blocker stage and reason, and the root-hosted recovery dialog repeats that callout above the recovery report. The dialog leads with the recovery sidecar verdict and exactly one confirmed primary action: retry from scratch, continue and repair build from preserved compiled artifacts, abandon, or manual review / manual replanning guidance with no apply button. When recovery auto-resume is enabled, daemon audit events record evaluate/queued/stopped outcomes and stop reasons such as sidecar, compiled-artifact eligibility, worktree, queue preflight, active hold/gate, budget, or repeated-signature blockers; Console also shows the latest automatic decision/attempt count/stop reason from the auto-build projection and keeps manual controls available after automatic decisions. Continue-and-repair waits for scheduler dispatch under the same queue controls described above after the engine patches the failed root compiled plans with one canonical `## Recovery Guidance` section; if guidance cannot be applied, the action reports the blocker and leaves queue files unmoved. After dispatch and a successful continued build it retires the failed queue item and reactivates skipped descendants automatically, while an activated failed continue-and-repair run returns the PRD to `failed/` with refreshed or degraded recovery evidence when possible. Lower-level queue-cascade retry/reactivation - which moves the failed upstream back to the queue for explicit retry/repair and may reactivate skipped descendants - lives in a collapsed advanced section that loads its analysis only when opened. That section renders dependency classifications, dispatch preflight blockers/warnings, explicit dependency-removal and `stack_parent` repair controls, selected-repair summaries, and repair results; dependency removal and `stack_parent` persistence are never silently selected. When no sidecar exists yet the dialog shows `recovery pending` with a confirmed **Run recovery analysis** action. Every mutating, queueing, or worker-spawning action requires an explicit confirmation, and a successful apply refreshes the queue. ## Untrusted project extension blocks loading Project/team extensions (`eforge/extensions/`) require an explicit per-extension trust record before loading. The `extension:untrusted` diagnostic appears in `eforge extension list` output when the trust record is missing. **Trust the extension:** ```bash eforge extension trust ``` This hashes the current extension source and writes a record to `.eforge/extension-trust.json`. The trust record applies only on your machine; each team member must trust shared extensions independently. If the extension source changes after trust, `extension:trust-changed` appears. Re-run `eforge extension trust ` after reviewing the diff to accept the new version. If an extension management or contribution request fails with a stale-daemon/version-skew hint, restart the eforge daemon first. If the daemon and caller still report different `eforgeVersion` values, update or rebuild both from the same eforge version; compatible API version skew is diagnostic context, not a separate failure by itself. ## Profile router selected an invalid profile When a registered profile router returns a profile name that does not exist in any scope, eforge emits `queue:profile:invalid-selection` and the build proceeds under the active profile or engine defaults. **Diagnose:** check the Console event stream or run `eforge extension show ` to see recent diagnostics. **Fix:** update the profile router extension to return a profile name that exists, or create the missing profile with `/eforge:profile-new` in Claude Code or `/eforge:profile:new` in Pi. The `availableProfiles` field in `ProfileRouterContext` lists all currently loadable profile names - use it to guard against stale names. ## Queue lock files Queue lock files signal in-progress builds. Do not delete them by hand. If you suspect a lock file is stale (after an unexpected daemon restart or system crash), the scheduler reconciles stale locks automatically at startup - wait for the daemon to restart and check `eforge daemon status`. If a lock file persists after a confirmed full daemon restart, check whether another daemon instance is running on a different port (`eforge daemon status` reports the active port and PID). Force-stopping that instance will release the lock. ## Validation-fixer retries exhausted After all plans merge, eforge runs `build.postMergeCommands` plus any queued PRD `postMerge` commands and calls a validation-fixer agent on failure. The fixer retries up to `build.maxValidationRetries` times (default: 2). When retries are exhausted, the build is marked `failed`. **Adjust the retry budget:** ```yaml build: maxValidationRetries: 3 ``` Each retry runs the full fixer-evaluator cycle; higher values increase cost. If your validation commands are non-deterministic (e.g. flaky tests), fix the flakiness first rather than raising the retry limit. After an exhausted-retries failure, use `/eforge:recover` to apply the recovery verdict. The recovery sidecar captures what the fixer attempted and where it stopped, which helps identify the root cause. ## Extension policy gate `require-approval` blocks a build Policy gates can return `{ decision: 'require-approval', reason }`, but eforge does not provide an approval workflow, approval state, or Console approval UI. The decision blocks the gated operation. If a build is stuck on a policy gate, check Console for `extension:policy:decision` events with `decision: require-approval`. **Supported fix:** change the extension to return `{ decision: 'allow' }` or `{ decision: 'block', reason }`. Treat `require-approval` as unsupported for runtime approvals in the current release. See [Extensions API - Policy gates](/docs/extensions-api) and [Configuration - Native Extensions](/docs/configuration#native-extensions) for `policyGateFailurePolicy` and timeout configuration. ## Where to look next - [Configuration](/docs/configuration) - validation commands, retry limits, hooks - [Extensions](/docs/extensions) - trust model, diagnostics, and status codes - [Extensions API](/docs/extensions-api) - policy gate decisions and profile router contracts - [Integrations](/docs/integrations) - daemon startup, Console dashboard, and restart --- title: Glossary description: Definitions for eforge-specific terms used across the docs and agent-readable reference. --- # Glossary ## Agent runtime profile A named YAML file that selects the harness, model, and effort settings for eforge tiers. Profiles live at user, project, or project-local scope and can be switched without editing `eforge/config.yaml`. See [Profiles](/docs/profiles). ## Auto-build The desired daemon mode that automatically processes queued PRDs when `prdQueue.autoBuild` is enabled. Disabling auto-build prevents automatic dispatch until it is enabled again. Scheduler pause is separate: it can stop new launches while leaving desired auto-build enabled. ## Build source The normalized input handed to the engine. It may originate from a CLI prompt, rough notes, a session plan, a playbook, a wrapper app, an input-source URI, or a PRD file. ## Builder The agent stage that implements a plan in an isolated worktree and commits the result. ## Compile phase The once-per-build phase where eforge formats input, runs the bounded planner compiler to write the plan set and dependency graph, reviews the compiled artifacts through the planning-quality gate, and validates the persisted plan artifacts before reporting success. ## Compile preflight compaction A deterministic pass over the build source before planner-family agents run. It detects generated or machine-readable bulk (inventories, sidecars, large code fences) and replaces it with bounded summaries in the prompt source while preserving the full source for artifacts and validation. ## Compile scope/context failure A typed `planning:scope-context:failure` diagnostic for compile-stage context exhaustion or guard failures. It records source, failure kind, stage, bounded explanation, observed metrics, artifact summary, and recovery action so CLI, Console, and recovery sidecars can distinguish compile guidance from ordinary plan-build failures. Newer events may include optional guard diagnostics for provider/model, model-aware input-token limit, context window, reserves, safety margin, metadata source, and fallback reason, or bounded decomposition evidence for `decomposition-exhausted`; older events omit those fields without placeholders. ## Daemon The long-running background process that watches the queue, runs builds, exposes the HTTP API, and streams live events to the monitor and integrations. ## Evaluator The agent stage that judges proposed fixes against the original intent and accepts only strict improvements. ## Failed enqueue A durable daemon attention item for an enqueue attempt that failed before producing a runnable queue file. Console shows the source label, reason, timestamp, fallback next command, disabled reason when re-enqueue is unavailable, and a confirmed re-enqueue action when source data still exists. ## Fixer The agent stage that applies reviewer suggestions as candidate changes before evaluation. ## Harness The agent execution backend used by a stage. eforge recommends `pi` for provider-flexible execution through pi-agent-core, and also supports `claude-sdk` as an Anthropic-specific secondary path through the Claude Agent SDK. ## Hooks Fire-and-forget shell commands triggered by eforge events. Configured in `eforge/config.yaml` under `hooks`. See [Configuration - Hooks](/docs/configuration#hooks) and [Configuration Reference - Hooks](/reference/config#hooks). ## Input source A TypeScript extension adapter that resolves `eforge://input//` URIs into PRD content. Adapters fetch issues or PRs from GitHub, Linear, Jira, or any custom source. See [Extensions - Input sources and PRD enrichers](/docs/extensions#input-sources-and-prd-enrichers). ## Console dashboard The web UI running locally at `http://localhost:/console/` (port range 4567-4667, deterministically assigned per project). Shows live build progress, token usage, cost, live/historical efficiency metrics, queue management, failed-enqueue attention, recovery guidance flows, scheduler pause/resume, and preview-first cascade controls. Root UI requests redirect to Console. See [Integrations - Console dashboard](/docs/integrations#console-dashboard). ## Playbook A reusable Markdown workflow template for recurring work owned by the first-party `eforge-playbooks` extension. Has a `mode` of either `autonomous` (normalizes to build source and enqueues through generic extension action handoff) or `planning` (checks eforge-plan capability `eforge.plan.planning-workstation` and returns `planningEntry` metadata for `eforge-plan:open-planning-entry` / `eforge-plan:planning-workstation`). Optionally pins an agent runtime profile via a `profile` frontmatter field. See [Playbooks](/docs/playbooks). ## Planner The compile stage that writes implementation plans. It is the bounded planner compiler: deterministic inventory chunking sizes the work, bounded agents plan each unit, and synthesis writes the plan-set artifacts. This is separate from the driver-side planning conversation exposed by the generic eforge-plan planning entry. ## PRD Product Requirements Document. A PRD file is one supported input surface, but eforge can also accept prompts, notes, session plans, playbooks, and wrapper-app input. ## PRD provenance The `eforge/prds/` directory where the engine writes a canonical copy of each PRD at dispatch time. Each file is named `{prdId}.md` and serves as a committed artifact linking a build session to its originating requirements. Unlike queue state (`.eforge/queue/` — gitignored), PRD provenance files are committed to the artifact branch and survive queue cleanup. The hidden canonical acceptance-criteria inventory stored in the queued PRD is consumed for validation IDs and stripped from the committed prose artifact. When `build.cleanupPlanFiles: true` (default), the PRD copy and compiled plan artifacts in `eforge/plans/{planSet}/` may be removed from `HEAD` when cleanup runs during `pr` or `merge` landing. Cleanup also strips temporary plan-ID eforge region marker comment lines from tracked JavaScript/TypeScript-family source files while preserving durable semantic markers and marked code. `landing.action: leave` does not run cleanup and leaves the artifact branch in place for inspection. Artifacts removed by cleanup remain recoverable from Git history: when the artifact branch is landed with a merge commit (eforge's local `merge` action, or a GitHub PR merged via "Create a merge commit"), the commits that added these files stay reachable. Use `git show :` with a commit-pinned reference to recover any artifact. PR bodies include an **Eforge provenance** section with these references when artifact commits are found. When `landing.action: pr` is used, provenance durability depends on the repository's chosen merge strategy. The durable provenance guarantee is Git history, not the final tree. Squash or rebase merge strategies applied after a PR is opened can collapse intermediate commits and make artifact references unreachable. Session plans (`.eforge/session-plans/`) are local and gitignored — they are not the shared provenance mechanism. ## Post-merge validation The validation step after all plans merge. eforge runs `build.postMergeCommands` plus any queued PRD `postMerge` commands with `build.postMergeCommandTimeoutMs`; on failure it can invoke the validation-fixer up to `build.maxValidationRetries` times. ## Queue The `.eforge/queue/` directory where normalized PRDs wait for daemon processing. Queue state is runtime-only (gitignored) — queue mutations are filesystem operations and do not produce git commits. Each queued PRD includes an eforge-owned hidden canonical acceptance-criteria inventory; queued builds with missing, duplicated, or malformed inventory fail before orchestration and must be re-enqueued. Queue items can depend on earlier items with `depends_on`, can use numeric `priority` so lower-priority-number items run earlier within the same dependency wave, and can carry runtime-only hold frontmatter (`held`, `hold_reason`, `held_at`) that prevents scheduler dispatch without moving the file. `eforge queue remove ` deletes non-running pending, waiting, failed, or skipped queue files; failed removal also deletes matching recovery sidecars and live-dependent conflicts list dependent ids. The daemon API can remove a single dependency from a pending or waiting queue item, moves a waiting item to the queue root when no dependencies remain, and uses preview/apply cascade controls for dependent remove or cancel flows. ## Queue hold Runtime-only queue frontmatter (`held`, `hold_reason`, `held_at`) on pending or waiting PRDs. Held rows keep their queue order and file location but scheduler ticks skip them until unheld. Console renders hold/unhold availability from daemon-authored queue capabilities. ## Queue priority An optional PRD frontmatter number. Lower numbers run before higher numbers within the same dependency wave; PRDs without `priority` run after prioritized items. `eforge queue priority ` mutates pending or waiting PRD frontmatter; failed and skipped items return conflict until recovery or requeue makes them runnable, and running items require daemon-owned cancellation instead of reprioritization. ## Recovery guidance The canonical `## Recovery Guidance` section that eforge writes into failed root compiled plan artifacts before compiled-artifact continue/resume builders read them. Read-only recovery analysis does not mutate artifacts; explicit prepare/continue paths patch plan artifacts through engine git discipline. ## Recovery sidecar A structured recovery analysis artifact written for a failed build plan. It records whether eforge should retry, continue and repair from preserved compiled artifacts, abandon, or require manual review / manual replanning, and may include read-only `continueRepairEligibility`, optional auto-resume attempt state, plus recovery options for continue-repair or non-mutating compile scope/context guidance. Compile scope/context options such as `bounded-decomposition` and `manual-reduce-scope` are advisory; they do not map to `apply-recovery` mutations or Console apply buttons. When a compile scope/context option includes `decompositionEvidence`, sidecar markdown and Console render bounded failed-unit evidence as read-only decomposition context, not provider context-window evidence or generated successor PRD content. For compiled-artifact continue/resume, the sidecar is also the durable source used to patch the failed root compiled plans with `## Recovery Guidance` before builders read them. ## Recovery verdict The outcome of a recovery sidecar analysis: `retry`, `continue-repair`, `abandon`, or `manual`. Applied via `/eforge:recover`, `eforge_apply_recovery`, `eforge_continue_repair`, `eforge apply-recovery `, or `eforge continue-repair ` depending on the action. See [Troubleshooting - Recover from a failed build](/docs/troubleshooting#recover-from-a-failed-build). ## Reviewer The blind review agent stage that evaluates a diff without the builder's reasoning or conversation context. ## Scheduler pause A runtime launch gate for the daemon scheduler. Pausing the scheduler leaves desired auto-build enabled but prevents new queued builds from launching until resume; already-running builds continue unless explicitly cancelled. ## Session plan A driver-side planning artifact created by the generic eforge-plan planning entry under `.eforge/session-plans/`. It captures planning type/depth, an optional executive summary, scope, acceptance criteria, risks, assumptions, skipped dimensions, readiness, and other dimensions before `/eforge:build` converts a ready file into build source. ## Tier A configuration slot such as `planning`, `implementation`, `review`, or `evaluation`. Tiers map agent roles to harness/model/effort settings. ## Toolbelt A named declarative bundle of project MCP servers (from `.mcp.json`) that a tier can opt into via `toolbelt: ` in a profile. Filters project MCP access per tier without affecting engine tools or harness built-ins. See [Configuration - Guided Toolbelt Presets](/docs/configuration#guided-toolbelt-presets) and [Extensions API - Toolbelt-vs-extension boundary](/docs/extensions-api#toolbelt-vs-extension-boundary). ## Trunk branch policy The pair of config fields (`build.trunkBranch` and `build.allowLocalMergeToTrunk`) that control how eforge handles landing when the current branch is the project trunk. By default, `landing.action: merge` is rejected on trunk to prevent accidental direct commits to a protected branch; the policy must be explicitly opted into for solo or unprotected projects. See [Configuration - Trunk Branch Policy](/docs/configuration#trunk-branch-policy). ## Worktree An isolated git working tree used to build an individual plan without blocking or contaminating other concurrently running plans. # eforge CLI Reference Autonomous plan-build-review CLI for code generation. **Usage:** `eforge [command] [options]` ## Commands ### `enqueue` **Full command:** `eforge enqueue` Normalize input and add it to the PRD queue **Options:** | Flag | Description | |------|-------------| | `--name ` | Override the inferred PRD title | | `--verbose` | Stream agent output | | `--no-plugins` | Disable plugin loading | | `--profile ` | Override active profile for this enqueue + build | | `--landing-action ` | Landing action for this build (pr\|merge\|leave) | | `--landing-auto-merge` | Enable PR auto-merge for this build | | `--no-landing-auto-merge` | Disable PR auto-merge for this build | | `--after ` | Explicit upstream dependency: waits in waiting/ if the upstream is active; enqueues immediately as an eligible dependent if the upstream completed with a usable artifact | | `--post-merge ` | Per-enqueue post-merge validation command (repeatable) | ### `build` **Full command:** `eforge build` Compile + build + validate in one step **Alias:** `run` **Options:** | Flag | Description | |------|-------------| | `--auto` | Run without approval gates | | `--verbose` | Stream agent output | | `--name ` | Plan set name (inferred from source if omitted) | | `--queue` | Process all PRDs from the queue | | `--max-concurrent-builds ` | Max parallel queue PRDs | | `--dry-run` | Compile only, then show execution plan without building | | `--foreground` | Run in-process instead of delegating to daemon | | `--no-cleanup` | Keep plan files after successful build | | `--no-monitor` | Disable web monitor | | `--no-plugins` | Disable plugin loading | | `--watch` | Watch mode: continuously poll the queue for new PRDs | | `--poll-interval ` | Poll interval in milliseconds for watch mode | | `--profile ` | Override active profile for this build | | `--landing-action ` | Landing action for this build (pr\|merge\|leave) | | `--landing-auto-merge` | Enable PR auto-merge for this build | | `--no-landing-auto-merge` | Disable PR auto-merge for this build | | `--after ` | Explicit upstream dependency: waits in waiting/ if the upstream is active; enqueues immediately as an eligible dependent if the upstream completed with a usable artifact | ### `monitor` **Full command:** `eforge monitor` Start or connect to the monitor dashboard **Options:** | Flag | Description | |------|-------------| | `--port ` | Preferred port | ### `status` **Full command:** `eforge status` Check running builds ### `queue` **Full command:** `eforge queue` Manage PRD queue #### `priority` **Full command:** `eforge queue priority` Update the priority for a pending or waiting PRD queue item #### `remove` **Full command:** `eforge queue remove` Remove a non-running PRD queue item #### `list` **Full command:** `eforge queue list` Show PRDs in the queue #### `run` **Full command:** `eforge queue run` Process PRDs from the queue **Options:** | Flag | Description | |------|-------------| | `--all` | Process all pending PRDs | | `--auto` | Run without approval gates | | `--verbose` | Stream agent output | | `--no-monitor` | Disable web monitor | | `--no-plugins` | Disable plugin loading | | `--max-concurrent-builds ` | Max parallel queue PRDs | | `--watch` | Watch mode: continuously poll the queue for new PRDs | | `--poll-interval ` | Poll interval in milliseconds for watch mode | | `--landing-action ` | Landing action for this build (pr\|merge\|leave) | | `--landing-auto-merge` | Enable PR auto-merge for this build | | `--no-landing-auto-merge` | Disable PR auto-merge for this build | #### `exec` **Full command:** `eforge queue exec` Build a single PRD directly (subprocess entry point for the queue scheduler) **Options:** | Flag | Description | |------|-------------| | `--auto` | Run without approval gates | | `--verbose` | Stream agent output | | `--no-monitor` | Disable web monitor | | `--no-plugins` | Disable plugin loading | | `--session-id ` | Session ID injected by parent scheduler (skips child session:start emission) | | `--profile ` | Override active profile for this build | | `--landing-action ` | Landing action for this build (pr\|merge\|leave) | | `--landing-auto-merge ` | Enable PR auto-merge for this build (true\|false) | ### `extension` **Full command:** `eforge extension` Manage native eforge extensions #### `list` **Full command:** `eforge extension list` List discovered native extensions **Options:** | Flag | Description | |------|-------------| | `--json` | Output JSON | #### `show` **Full command:** `eforge extension show` Show one native extension by name **Options:** | Flag | Description | |------|-------------| | `--json` | Output JSON | #### `validate` **Full command:** `eforge extension validate` Validate configured native extensions, or a single extension name/path **Options:** | Flag | Description | |------|-------------| | `--json` | Output JSON | #### `test` **Full command:** `eforge extension test` Dry-run native extension event hooks against fixture or monitor events **Options:** | Flag | Description | |------|-------------| | `--run ` | Replay monitor DB events: latest or a session/run id | | `--event ` | Filter replay input by exact event type | | `--fixture ` | Replay project-local fixture events from a JSON or JSONL file | | `--json` | Output JSON | #### `new` **Full command:** `eforge extension new` Scaffold a native eforge extension **Options:** | Flag | Description | |------|-------------| | `--scope ` | Extension scope: local, project, or user | | `--template