Skip to content
Docs

Build a software factory with eve

Foreman, a software factory built on eve. It turns GitHub and Linear work items into reviewed draft pull requests through four agent stations, triaging issues and fixing its own red CI along the way.

Ben SabicContent Engineer

Foreman puts an agent on every stage of your development loop and keeps you on the judgment calls. Label a GitHub issue factory, @mention the bot on an issue or pull request, or delegate an issue in Linear, and Foreman moves the work through four stations, classifier, analyst, implementer, and reviewer, ending in a reviewed draft pull request on your repository. Along the way it triages incoming issues, fixes red CI on its own branches, and posts progress back where the work arrived. You mark ready and merge; those decisions never leave your hands.

The factory is built on eve, a filesystem-first framework for durable backend agents from Vercel. GitHub and Linear authenticate through Vercel Connect, and Vercel Blob and AI Gateway authenticate with your project's OpenID Connect (OIDC) token.

Deploy the template now, or read on for a deeper look at how it all works.

eve Software Factory Template

Four coding stations behind one orchestrator: triage, planning against a live checkout of your repo, implementation verified with your own checks, and independent review.

Deploy now

Copy link to headingQuick start with an AI coding agent

If you're working with an AI coding agent like Claude Code or Cursor, you can use this prompt to have it help you with building your factory:

Agent Prompt

I want to build a software factory with the eve framework, using the eve software factory template. Read the documentation at https://ask-foreman.dev/docs and follow it. It will cover deploying the template, building with eve, how the pipeline works overall, and more.

Copy link to headingVercel Plugin

The Vercel Plugin turns your AI coding agent (e.g., OpenAI Codex, Claude Code, or Cursor) into a Vercel expert. It adds skills, slash commands, and current knowledge of the tools this template uses, including Vercel Connect, Vercel Blob, Vercel Sandbox, and AI Gateway.

The plugin is optional; it isn't required to use eve or to follow this guide.

Terminal
npx plugins add vercel/vercel-plugin

Copy link to headingMeet the stations

Foreman, the orchestrator, routes each work item through four stations in order.

Each station is its own subagent with its own instructions, model, sandbox, and tools; it runs in a fresh session, inherits nothing from the orchestrator, and works only from the brief it's handed.

StationOwnsHands back
classifierTriage: type, priority, complexity, affected area, actionable or needs clarificationA structured classification the rest of the pipeline routes on
analystPlanning against a live checkout of your repository: approach, ordered steps, risksA plan with acceptance criteria, plus an analysis artifact for detail
implementerExecuting the plan in its own checkout, running your repo's checks, committing, pushing a branchThe branch name, a per-file change summary, and verification results
reviewerIndependent judgment of the pushed branch against the acceptance criteria, on a different vendorApprove, request changes, or reject, with evidence for each criterion

One more subagent, researcher, joins when a work item needs a fact that isn't in the repository or its issues, like an upstream bug or a library version. It runs web searches and returns cited findings with confidence levels, surfacing the gaps it couldn't close rather than papering them over.

Three rules hold the pipeline together:

  • The review is independent. The reviewer never sees the implementer's reasoning, only the pushed branch, and it runs on a different model vendor so it doesn't share the implementer's blind spots.
  • The acceptance criteria are the contract. The analyst writes them, and the reviewer judges the implementation against them verbatim.
  • Revisions are bounded. On a request_changes verdict, the orchestrator sends the reviewer's findings back to the implementer and re-runs the review, at most twice. If the work still doesn't pass, it halts the line and reports the unresolved findings rather than opening a pull request.

Copy link to headingSetup and deployment

Copy link to headingWhat you need before deploying

You need four things to deploy and run the factory:

  • A Vercel account.
  • A GitHub repository for the factory to work on, where you can install a GitHub App with write access to contents, issues, and pull requests.
  • A Linear workspace, if you want to delegate work from Linear.

For local development, you also need Node.js 24+, pnpm, and the Vercel CLI.

Copy link to headingDeploy to Vercel

Deploying with the one-click flow provisions everything the factory needs:

ProvisionedSets
GitHub connector, with its trigger pointed at /eve/v1/githubGITHUB_CONNECTOR
Linear connector, with its trigger pointed at /eve/v1/linearLINEAR_CONNECTOR
Public Vercel Blob store for preferences, brain, and artifactsBlob credentials
Prompt for your target repository and intake labelFACTORY_REPO, FACTORY_LABEL

FACTORY_REPO is required at build time. If the value is missing, a clear error will indicate that. Everything else has a sensible default:

VariableRequiredDefaultWhat it does
FACTORY_REPOYesThe owner/repo the factory works on
FACTORY_SETUP_COMMANDNoRuns once inside the sandbox checkout at build time (e.g. pnpm install), so every run starts with dependencies installed
FACTORY_LABELNofactoryThe issue label that hands an issue to the factory
FACTORY_BRANCH_PREFIXNofactory/Branch prefix marking the factory's own PRs, the only branches automated CI fixes touch
FACTORY_BOT_NAMENothe GitHub App's slugThe @mention name, resolved from the connector automatically when unset

Copy link to headingConnect GitHub

The one-click flow installs the GitHub App and points its event trigger at the route the agent serves (/eve/v1/github), so there's nothing to wire up. The connector subscribes to issues, issue_comment, pull_request_review_comment, pull_request, and check_suite events, and the installation needs write access to contents, issues, and pull requests on FACTORY_REPO.

The factory answers to whatever you named your GitHub App: the @mention name is resolved from the connector's app slug at runtime, so a hardcoded handle can't collide with an unrelated GitHub user. After the deploy finishes, label an issue factory or @mention the bot on an issue to start work.

You can't test GitHub or Linear events locally, because they’re both forwarded to Vercel Connect, which verifies them and delivers them to your deployed project rather than to a local URL. Test the inbound webhook paths against a preview or production deployment; everything else runs in the local dev terminal UI (TUI).

Copy link to headingConnect Linear

Linear is optional. When connected, users delegate issues to the factory through Linear Agent Sessions; the factory works the item, posts progress as Agent Activities, and reports the pull request link back on the session. The Linear connection is app-scoped rather than per-user, with scopes limited to read, write, issues:create, and comments:create, and every write on it is denied during unattended runs so a prompt-injected issue can't fan out into the tracker.

Not using Linear? Delete agent/channels/linear.ts and agent/connections/linear.ts and remove LINEAR_CONNECTOR.

Copy link to headingHow work arrives

Work reaches the factory six ways:

  • Label an issue factory. The pipeline runs on its own, posts progress as stations complete, and ends with a draft PR linked to the issue. Only labelers with at least triage permission on the repository can trigger it.
  • @mention the bot on an issue or PR. Mentions from repo owners, members, and collaborators start an interactive session. Everyone else's mentions are acknowledged without a session.
  • Delegate in Linear. Linear Agent Sessions run the same pipeline and report progress back in Linear.
  • The dev TUI. Hand it a task locally. Changes to GitHub wait for your approval.
  • Red CI on a factory PR. Foreman diagnoses the failure and pushes a fix to its own branches, never yours, capped at 2 attempts.
  • Someone opens a pull request. Foreman posts one orienting comment for reviewers: a summary with a changed-files table, not a review.

Copy link to headingLocal development

You can run the factory locally in the dev TUI.

First, clone the repository that the one-click deploy flow created:

Terminal
git clone https://github.com/your-username/eve-software-factory-template.git
cd eve-software-factory-template

Then link it to the Vercel project you deployed and pull the environment variables:

Terminal
vercel link
vercel env pull

vercel env pull writes a short-lived OIDC token into .env.local. Local runs use it to reach Vercel Blob, provision the station sandboxes, and call the model through AI Gateway. Start the dev server to open the TUI:

Terminal
pnpm dev

In the TUI, hand Foreman a task ("users report the password reset email arrives twice, fix it") and watch the four stations fire in order, ending in a draft PR on FACTORY_REPO.

Local runs are untrusted by design, so writes to GitHub wait for your approval in the TUI. Use it to confirm the gates stop work before it lands.

These are the commands you'll use day to day:

CommandDoes
pnpm devStart the eve dev TUI
pnpm validateLint, typecheck, and discovery diagnostics in one
pnpm check / pnpm fixCheck formatting and lint rules, and auto-fix what it can
pnpm typechecktsc --noEmit
pnpm eval --tag fastRun the cheap eval loop guarding routing and safety behavior
npx eve infoPrint every discovered tool, skill, connection, channel, and subagent
eve deployShip to production, the same as vercel deploy --prod

Linting and formatting come from Ultracite, a Biome preset.

npx eve info is the fastest way to confirm a change landed: if a file you added doesn't appear, discovery didn't classify it as an authored slot, and .eve/discovery/diagnostics.json says why.

Copy link to headingHow the factory works

The orchestrator runs one loop: ground itself in the work item and the factory's memory, move the item through the stations in order, and deliver a reviewed draft pull request.

  1. Work arrives: a maintainer labels an issue factory, @mentions the bot, or delegates an issue in Linear. The channel turns the event into a session and stamps the caller's trust at dispatch, on the signed webhook, before the model sees anything.
  2. Ground in memory and the real issue. The orchestrator calls get_user_preferences for the requester's standing notes (a default base branch, how they like PR descriptions structured) and read_factory_brain for the factory's shared, durable notes about the target repository: build quirks, verification gotchas, recurring review findings. It then reads the actual GitHub or Linear issue in full, loads the triaging-issues skill, checks for duplicates against open and closed issues, and mirrors the classification onto the issue with labels from the repo's own vocabulary, never one it invented.
  3. Route through the stations, in order, always. Classifier, then analyst, then implementer, then reviewer. Every delegation message is self-contained: stations run in fresh sessions and never see the orchestrator's history, so it includes the original work item verbatim, along with every prior stage output the station needs. Never skipping a station is a rule that doesn't bend; the classifier decides what is trivial, not the orchestrator.
  4. Stop for clarification when the classifier says so. If the classifier returns needs_clarification, the pipeline stops. With a person on the other end, the orchestrator asks them the classifier's questions and waits. During an unattended run, it posts the questions as comments on the issue and stops; since nobody is watching, it never leaves a run waiting on input.
  5. Long documents travel by id, not by pasting. The researcher and analyst save full memos as handoff artifacts in Vercel Blob and return an id; the orchestrator relays the id into later stations' briefs, and the implementer and reviewer open the document themselves with read_artifact. An SEO-length analysis never passes through the orchestrator's context or a PR body.
  6. Deliver, and remember what the run taught. When the reviewer approves, the orchestrator opens a draft pull request (head set to the branch the implementer pushed), writes the PR body from the pipeline's outputs including the reviewer's pass/fail against each acceptance criterion, and reports back on the originating thread. If the run surfaced a durable fact about the repository, it merges the note into the factory brain so every future run starts from it.

Every credential in this loop is brokered at runtime:

  • GitHub and Linear through Vercel Connect
  • Vercel Blob, Vercel Sandbox, and AI Gateway through the OIDC token
  • Station git operations through installation tokens injected at the sandbox firewall, never inside the sandbox

Copy link to headingCode walkthrough

The whole factory is defined under agent/, and eve discovers each capability from the filesystem. Every tool's name is its filename, every subagent's name is its directory name, and every extension's namespace is its filename, so there's no central registry to wire up. These are the pieces that matter.

Copy link to headingThe GitHub surface

The GitHub channel lives in agent/channels/github.ts, and its intake hooks are where trust is decided:

agent/channels/github.ts
export default githubChannel({
botName: resolveBotName,
credentials: githubCredentials,
onComment: async (ctx, comment) => {
const botName = await resolveBotName().catch(() => null);
if (botName === null) {
return null;
}
return !isIgnoredComment(comment, botName) &&
mentionPattern(botName).test(comment.body) &&
isTrustedCommenter(comment)
? { auth: stampTrusted(defaultGitHubAuth(ctx)) }
: null;
},
onIssue: async (ctx, issue) => {
// ...dispatches only on the `labeled` action for the factory label,
// and only after verifying the labeler holds at least triage permission
return {
auth: stampAutonomous(defaultGitHubAuth(ctx), issue.issueNumber),
context: [FACTORY_INTAKE_TASK],
};
},
// onCheckSuite: red CI on factory/* PRs -> unattended fix loop, capped at 2
// onPullRequest: one summary comment on opened PRs, bot senders skipped
});

Two details in the intake hooks carry the security model:

  • onComment dispatches only for commenters whose author_association is OWNER, MEMBER, or COLLABORATOR, and stamps the trusted auth attribute at dispatch. Mentions from anyone else never start a session, so arbitrary accounts on a public repository cannot drive the agent's write tools.
  • onIssue verifies the labeler's repository permission against the API before dispatching, with triage as the floor. GitHub fires the labeled action even for labels attached at issue creation, which issue templates let unauthenticated reporters do, so checking the sender's permission is what keeps the unattended pipeline maintainer-triggered. The session's auth is then rewritten to a constructed autonomous principal carrying the intake issue number, so the run never executes as the labeler.

The channel also renders human-in-the-loop prompts as comments with a mention-based reply instruction, and answers route back through the same onComment gate. That's what makes a comment reply an authorization signal rather than a race anyone on a public repo can win: an untrusted account's "approve" never reaches the waiting session.

Copy link to headingThe Linear surface

The Linear channel in agent/channels/linear.ts is the second intake, and its trust decision is simpler on purpose:

agent/channels/linear.ts
export default linearChannel({
credentials: connectLinearCredentials(
process.env.LINEAR_CONNECTOR ?? "linear/foreman-agent"
),
onAgentSession: (_ctx, event) => {
if (event.action !== "created" && event.action !== "prompted") {
return null;
}
const requester = event.agentActivity?.user ?? event.agentSession.creator;
const context: string[] = [];
const requesterName = requester?.displayName ?? requester?.name;
if (requesterName) {
context.push(`The requesting user is ${requesterName}.`);
}
return { auth: stampTrusted(defaultLinearAuth(event)), context };
},
});

Every Agent Session is stamped trusted at dispatch, because workspace membership is the gate on Linear: only members can open a session, so there's no association check to run the way the GitHub channel does for public commenters. The hook also injects the requester's name as session context, so progress notes and the final report carry attribution. Credentials are brokered by Vercel Connect, which supplies the app token and verifies inbound webhooks by their Vercel OIDC signature, so none of that lives in your code.

Linear sessions run the same pipeline as GitHub ones, and when an item spans both trackers (a Linear issue about a GitHub bug, or the reverse), the github-linear-bridging skill supplies the conventions: check whether the GitHub issue is already tracked in Linear before creating anything, backlink both directions, and mirror only state that changes decisions, not every comment or label.

Copy link to headingThe orchestrator that routes, not writes

The orchestrator's runtime configuration is minimal:

import { defineAgent } from "eve";
import { MODELS } from "./lib/models.js";
export default defineAgent({
compaction: { thresholdPercent: 0.75 },
limits: {
maxOutputTokensPerSession: 100_000,
},
model: MODELS.orchestrator,
});

Everything else about the orchestrator is agent/instructions.ts, and its core rule is that Foreman is a routing layer, not a fifth station. It grounds the work item, delegates to exactly one station at a time with a complete brief, verifies handoffs, and assembles the result. It never writes code or performs deep analysis itself; if a station fails or returns something malformed, it retries once with a clarified message rather than doing the work.

Model assignments are centralized in agent/lib/models.ts so a swap is a one-line edit:

agent/lib/models.ts
export const MODELS = {
analyst: "openai/gpt-5.6-terra-fast",
classifier: "openai/gpt-5.6-terra-fast",
implementer: "anthropic/claude-fable-5", // the station that writes the code gets the strongest coding model
orchestrator: "openai/gpt-5.6-terra-fast",
researcher: "openai/gpt-5.6-terra-fast",
reviewer: "openai/gpt-5.6-terra-fast", // different vendor than implementer on purpose: independent review
} as const;

The model IDs route through the Vercel AI Gateway, which authenticates through the linked project, so there's no provider API key to set. One split is deliberate: the implementer runs the strongest coding model on a different vendor than the reviewer, and keeping those two apart is what keeps the review independent.

Copy link to headingStations as task-mode subagents

Each station is a directory under agent/subagents/ with the same shape: an agent.ts whose description is all the orchestrator sees when routing, an instructions.md, its own sandbox.ts where it needs a repo checkout, and a tools/ directory. Subagents inherit nothing from the orchestrator: no conversation history, no skills, tools, or connections. That's why every brief has to be complete.

The routing description is a contract, not a label. Here's the reviewer's:

export default defineAgent({
description:
"Independently review a pushed factory branch against the original work item and its " +
"acceptance criteria: fetch the branch, read the real diff, re-run cheap checks, and " +
"return approve, request_changes, or reject with specific findings. Never modifies " +
"code. The caller passes the work item, the analysis with acceptance criteria, the " +
"branch name, and the implementer's report in the message, plus an artifact id when " +
"the analyst saved its full detail as one.",
model: MODELS.reviewer,
outputSchema: {
// verdict, criteria_results (pass/fail with evidence per criterion),
// blocking_findings, suggestions, summary
},
});

The outputSchema on each station's agent.ts is what makes every delegation run in task mode: the station must run to completion and return structured output, and it cannot stop to ask a question or wait on an approval card. That's a design constraint, not an accident. Hitting an approval gate within a task-mode child would strand the run forever, so nothing within a station may need one; anything that needs approval lives on the orchestrator, and station-side effects must be inert by construction.

The analyst, implementer, and reviewer each hold their own sandboxed clone of FACTORY_REPO, provisioned once per template build through a shared bootstrap in agent/lib/github/repo-sandbox.ts. The clone and your FACTORY_SETUP_COMMAND (like pnpm install) are paid once at build; each session pays only a fetch to the repository's current default branch. That's why the analyst's plan names real files and the implementer verifies with your repo's own checks, not guesses.

Copy link to headingApproval policies as the authorization model

The orchestrator's GitHub surface is mounted as an eve extension in agent/extensions/github.ts: an explicit allowlist with no preset, so reads, triage writes, and PR authoring are in, and merge tools are deliberately absent from the surface entirely. Merging stays a human act, done in the GitHub UI.

The requireApproval map doubles as the authorization policy, with per-tool predicates in agent/lib/github/approval.ts. The interesting one is createPullRequest:

agent/lib/github/approval.ts
export function createPullRequestPolicy(ctx: ApprovalContext): ApprovalStatus {
const input = ctx.toolInput as { draft?: unknown } | undefined;
if (input?.draft === true) {
return "not-applicable";
}
return shipPolicy(ctx);
}
export function shipPolicy(ctx: ApprovalContext): ApprovalStatus {
if (isAutonomous(ctx.session.auth.current)) {
return {
reason:
"Unattended factory runs stop at a draft pull request; a person marks it ready.",
type: "denied",
};
}
return "user-approval";
}

The policy receives the tool's input as well as its name, so it gates on what the call does rather than only what it's called: a draft PR cannot merge, so it runs for every caller, while anything that can ship, a non-draft PR or marking one ready, parks for a person no matter who asks. Draft pull requests are the unattended ceiling.

The full matrix, by caller class:

ActionUnattended run (labeled issue)Trusted mention or Linear sessionDev TUI
Open a draft pull requestRunsRunsRuns
Open a non-draft PR, mark a PR readyDeniedWaits for approvalWaits for approval
Merge a pull requestNot in the tool surfaceNot in the tool surfaceNot in the tool surface
Apply or remove labelsRunsRunsWaits for approval
Comment on the intake issueRunsRunsWaits for approval
Comment anywhere elseDeniedRunsWaits for approval
Close or reopen an issueRunsRunsRuns
Write the factory brainDeniedRunsWaits for approval
Write to LinearDeniedRunsRuns
clear_user_preferencesDeniedWaits for approvalWaits for approval

Two choices in that table are worth pausing on. Unattended runs are denied rather than parked, because nobody is watching an autonomous turn to answer an approval card, and a server-side denial costs one step instead of a stranded session. And closing or reopening an issue runs ungated for every caller, because it's reversible triage, a reopen undoes it, and the only sessions that reach the tools at all are trusted mentions, autonomous label runs, and the local dev TUI. updateIssue with state set follows the same close/reopen policy, so the two paths to the same action always behave alike.

Copy link to headingGit credentials never enter a sandbox

The implementer's one side effect, push_branch, is safe to run ungated inside a task-mode station because it is inert by construction:

export function validateBranch(branch: string): string | null {
if (
!BRANCH_PATTERN.test(branch) ||
branch.includes("..") ||
branch.includes("//")
) {
return `"${branch}" is not a valid branch name.`;
}
if (branch.startsWith("refs/") || branch === "HEAD") {
return `"${branch}" is not a plain branch name. Pass the branch name without a refs/ prefix.`;
}
if (PROTECTED_BRANCHES.has(branch)) {
return `Direct pushes to ${branch} are not allowed. Push a feature branch and open a pull request.`;
}
return null;
}

validateBranch refuses main, master, refs/*, HEAD, and anything outside a conservative character set, so shell metacharacters can never reach the command line and a feature branch alone ships nothing. The credential itself never enters the sandbox: clones, fetches, and pushes target the literal https://github.com/<FACTORY_REPO>.git URL, never the model-writable origin remote, and the installation token is injected at the sandbox firewall as a header transform on egress to github.com, then dropped again in a finally:

export function brokerPolicy(installationToken: string): SandboxNetworkPolicy {
const authorization = `Basic ${Buffer.from(
`x-access-token:${installationToken}`
).toString("base64")}`;
return {
allow: {
"*": [],
"github.com": [
{ transform: [{ headers: { Authorization: authorization } }] },
],
},
};
}

Git remote config inside a sandbox (pushurl, per-branch remotes) is model-writable, which is exactly why the git helpers never go through origin: nothing the model writes can redirect the brokered credential.

Copy link to headingThe factory brain and per-user memory

Two kinds of durable state live in Vercel Blob, and their key derivation is the part worth reading.

The factory brain is one shared document per target repository: durable notes like build quirks, verification gotchas, and recurring review findings that every run reads at the start of a task.

export const factoryBrainKey = (): string => {
const id = createHash("sha256").update(FACTORY_REPO).digest("hex");
return `${FACTORY_BRAIN_PREFIX}${id}.md`;
};

The Blob key is derived entirely from FACTORY_REPO, resolved at module load, never from model input, so a session can't redirect a read or write to another object. Reads are open to every run, but writes are gated by factoryBrainPolicy: unattended runs are denied outright, because a labeled issue's body is untrusted input that must not be able to poison the context every future run reads. Trusted callers write directly; the dev TUI waits on an approval card.

User preferences are the opposite: one file per person, keyed by the framework-resolved principal, hashed so the stored path carries no raw identifier. Each session can only ever read or write its own user's file, and clear_user_preferences is irreversible and gated on approval. The reserved-namespace registry in agent/lib/blob.ts is what keeps any general-purpose Blob tool from using either prefix as a side channel.

Copy link to headingHandoff artifacts

Long documents move between stations by id rather than by pasting text through the orchestrator's context. The researcher and analyst save Markdown documents (up to 200,000 characters) to a private Blob under a reserved artifacts/ prefix with save_artifact and return an id; the implementer and reviewer read them back with read_artifact, and the orchestrator holds only the reader.

Artifact ids are the one Blob address the model supplies, which is why the id format is validated strictly:

export const ARTIFACT_ID_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
export const artifactKey = (id: string): string | null =>
ARTIFACT_ID_PATTERN.test(id) ? `${ARTIFACTS_PREFIX}${id}.md` : null;

The pattern is anchored, with no dots or slashes permitted, so a validated id cannot traverse out of the reserved prefix; without that, a caller could pass a path like ../factory-brain/<hash>.md and read a managed document through a tool that was never meant to reach one. An invalid id reads as not found, indistinguishable from a missing one, so a probe learns nothing from the difference. Saves never overwrite and mint their own suffixed id (analysis-dedupe-reset-emails-k3f9qz), which is what lets both artifact tools live ungated inside task-mode stations.

Copy link to headingNo self-delegation

The root's tools/agent.ts disables eve's built-in agent tool:

tools/agent.ts
import { disableTool } from "eve/tools";
export default disableTool();

The built-in tool runs a fresh copy of the root agent, which would let the orchestrator delegate work to an undifferentiated clone of itself and bypass the four stations. All delegation goes through the declared subagents instead, so the pipeline is structural rather than a suggestion in the prompt.

Copy link to headingCoding judgment as skills

Three skills sit on the orchestrator, each a folder with a SKILL.md whose frontmatter description is the routing hint that decides when it loads:

SkillLoads when
triaging-issuesA work item arrives from a GitHub issue or mention: dedupe against open and closed issues, label from the repo's own vocabulary, decide whether to ask or proceed, and ask for reproduction details well
github-linear-bridgingAn item spans both trackers: check whether a GitHub issue is already tracked in Linear, backlink both directions, and mirror only meaningful state
writing-qualityAny prose meant for humans: PR descriptions, issue comments, review reports, Linear replies. Kills AI tells and swaps bloated wording for plain English from its references/ lookup tables

Loading a skill only adds instructions to the turn; it never adds a new action the agent can take. Skills are per-agent, so the stations don't see the orchestrator's skills, which is another reason every brief has to carry what the station needs.

Copy link to headingEvals as the safety floor

The template ships an eval suite under evals/, organized as a failure taxonomy: routing/ asserts the classifier always runs first and labels follow classification, safety/ asserts deny-by-default over the whole write-tool list (a read-only question calls no write tool, a write parks on approval, a prompt-injected issue body doesn't move the agent, nothing pushes to main, unattended runs can't write the factory brain), and pipeline/ holds an opt-in run of the whole line that pushes a real branch.

pnpm eval --tag fast # the cheap loop: routing + safety
pnpm eval pipeline/full-pipeline # deliberately runs the whole line; use a scratch repo

Evals cost real model tokens, and the full-pipeline run pushes a real branch, so run it deliberately. Instruction-following on injected text remains model judgment; the prompt-injection eval keeps a floor under it while the structural bounds (the autonomous principal's denials, the firewall-brokered credential, the human gate on shipping) don't depend on the model behaving.

Copy link to headingCustomize the factory

There is no registry file: a tool's name is its filename, and a subagent's or skill's name is its directory name. Add a file to add a capability, delete it to remove one. The agent picks up changes to these files as you save.

To changeEditNotes
The target repositoryFACTORY_REPO in the project's environmentChanging it rebuilds the station sandboxes and gives the new repo its own factory brain
The orchestrator's behavioragent/instructions.tsGrounding, routing, the review loop, delivery
A station's craftIts instructions.md and outputSchema in agent.tsThe description is all the orchestrator sees when routing
Modelsagent/lib/models.tsAny Gateway model ID works; keep implementer and reviewer on different vendors
Approval gatesagent/lib/github/approval.ts and the requireApproval map in agent/extensions/github.tsWrite tools not listed keep the SDK's approval-by-default
What the orchestrator can reach on GitHubinclude in agent/extensions/github.tsAn allowlist with no preset; prefer adding a name here over loosening a gate
The intake label and branch prefixFACTORY_LABEL and FACTORY_BRANCH_PREFIX environment variablesThe implementer's instructions carry the default prefix as prose, so keep them in sync on an override
Triage, bridging, or prose conventionsThe skill folders under agent/skills/A skill's frontmatter description decides when it loads
The safety floorevals/One file per case; helpers.ts carries the shared write-tool list read-only evals assert against

Two extensions are designed for, and the approval policies already anticipate them:

  • Merge behind approval. Add mergePullRequest to the extension's allowlist mapped to shipPolicy, and comment-driven merges park for a person the same way marking ready does today.
  • Continuous operation. A sweep schedule that dispatches one session per queued item inherits sensible write behavior automatically: the policies already recognize schedule turns via isScheduleAppAuth, so reversible writes run and anything that ships still parks for a person. Cross-run dedupe state needs an external store, since the template has no application database.

Not using a surface? Delete its file; nothing references one by name, so removing it is the whole job.

Not usingDelete
Linearagent/channels/linear.ts and agent/connections/linear.ts
Web researchagent/subagents/researcher/

Keep all four stations: the pipeline's guarantees, independent review above all, come from every item passing through them in order.

Copy link to headingTroubleshooting

SymptomLikely causeFix
Labeling an issue doesn't start the pipelineThe labeler holds less than triage permission on the repository, or the GitHub connector isn't subscribed to the issues event. The channel verifies the labeler's permission against the API and acknowledges anything below triage without a sessionConfirm the labeler is at least a triage-level collaborator and that the connector's trigger points at /eve/v1/github with the issues event subscribed, then remove and re-apply the label
@mentions don't get a responseThe commenter's author_association isn't OWNER, MEMBER, or COLLABORATOR, or the mention uses a name the App doesn't answer toMention it from an owner, member, or collaborator account, using the GitHub App's own slug (or FACTORY_BOT_NAME if you overrode it)
The build fails with a FACTORY_REPO errorThe variable is required at module load, so a misconfigured factory fails discovery instead of producing a factory with no targetSet FACTORY_REPO to a plain owner/repo pair (like acme/widgets) in the project's environment and redeploy
Writes appear to stall in the dev TUIThe call is gated on approval and waiting for a decision. The dev principal is untrusted by design, so approval cards surface in the TUIApprove or deny the pending call. To change which tools are gated, edit agent/lib/github/approval.ts and the requireApproval map in agent/extensions/github.ts
CI fixes stop after two attemptsThe red-CI loop is bounded on purpose. Each session counts earlier fix-attempt comments on the pull request and stops after 2, so the factory can't loop on a failure it doesn't understandTroubleshoot the failure yourself, or @mention the bot on the PR with a diagnosis to run an attended revision. The loop only touches branches carrying the factory/ prefix
Connectors work locally but fail in production.env.local is local-only, and the deployed app reads the Vercel project's environmentSet GITHUB_CONNECTOR, LINEAR_CONNECTOR, and FACTORY_REPO in the project's production environment. The Deploy button sets them for you
Files you added don’t show upeve walks agent/ at build time and only registers files that classify as an authored slot, so a naming or placement problem drops the file from discoveryRun npx eve info to see what eve discovered, and check .eve/discovery/diagnostics.json for why a file was skipped

Give your software factory a browser

Foreman can reproduce bugs behind a login, verify shipped fixes on preview deployments, and record findings for every future run.

Read the guide

Copy link to headingRelated resources

Related documentation

More eve guides