---
title: Build a software factory with eve
description: 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.
url: /kb/guide/eve-software-factory
canonical_url: "https://vercel.com/kb/guide/eve-software-factory"
published: 2026-08-25
last_updated: 2026-08-25
authors: Ben Sabic
related:
  - /docs/connect
  - /docs/vercel-blob
  - /docs/agent-resources/vercel-plugin
  - /docs/cli
  - /docs/ai-gateway
  - /docs/cli/connect
  - /docs/sandbox
  - /docs/oidc
install_vercel_plugin: npx plugins add vercel/vercel-plugin
---

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](https://eve.dev/), a filesystem-first framework for durable backend agents from Vercel. GitHub and Linear authenticate through [Vercel Connect](https://vercel.com/docs/connect), and [Vercel Blob](https://vercel.com/docs/vercel-blob) and [AI Gateway](https://vercel.com/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.

## Quick 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

```txt
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.
```

### Vercel Plugin

The [Vercel Plugin](https://vercel.com/docs/agent-resources/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.

```bash
npx plugins add vercel/vercel-plugin
```

## Meet 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.

| Station       | Owns                                                                                             | Hands back                                                            |
| ------------- | ------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------- |
| `classifier`  | Triage: type, priority, complexity, affected area, actionable or needs clarification             | A structured classification the rest of the pipeline routes on        |
| `analyst`     | Planning against a live checkout of your repository: approach, ordered steps, risks              | A plan with acceptance criteria, plus an analysis artifact for detail |
| `implementer` | Executing the plan in its own checkout, running your repo's checks, committing, pushing a branch | The branch name, a per-file change summary, and verification results  |
| `reviewer`    | Independent judgment of the pushed branch against the acceptance criteria, on a different vendor | Approve, 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.
  

## Setup and deployment

### What you need before deploying

You need four things to deploy and run the factory:

- A [Vercel account](https://vercel.com/signup).
  
- 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](https://vercel.com/docs/cli).

### Deploy to Vercel

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

| Provisioned                                                    | Sets                            |
| -------------------------------------------------------------- | ------------------------------- |
| GitHub connector, with its trigger pointed at `/eve/v1/github` | `GITHUB_CONNECTOR`              |
| Linear connector, with its trigger pointed at `/eve/v1/linear` | `LINEAR_CONNECTOR`              |
| Public Vercel Blob store for preferences, brain, and artifacts | Blob credentials                |
| Prompt for your target repository and intake label             | `FACTORY_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:

| Variable                | Required | Default               | What it does                                                                                                               |
| ----------------------- | -------- | --------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| `FACTORY_REPO`          | Yes      | —                     | The `owner/repo` the factory works on                                                                                      |
| `FACTORY_SETUP_COMMAND` | No       | —                     | Runs once inside the sandbox checkout at build time (e.g. `pnpm install`), so every run starts with dependencies installed |
| `FACTORY_LABEL`         | No       | `factory`             | The issue label that hands an issue to the factory                                                                         |
| `FACTORY_BRANCH_PREFIX` | No       | `factory/`            | Branch prefix marking the factory's own PRs, the only branches automated CI fixes touch                                    |
| `FACTORY_BOT_NAME`      | No       | the GitHub App's slug | The @mention name, resolved from the connector automatically when unset                                                    |

### Connect 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).

### Connect 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`.

### How 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.
  

### Local development

You can run the factory locally in the dev TUI.

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

```bash
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:

```bash
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:

```bash
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:

| Command                   | Does                                                                  |
| ------------------------- | --------------------------------------------------------------------- |
| `pnpm dev`                | Start the eve dev TUI                                                 |
| `pnpm validate`           | Lint, typecheck, and discovery diagnostics in one                     |
| `pnpm check` / `pnpm fix` | Check formatting and lint rules, and auto-fix what it can             |
| `pnpm typecheck`          | `tsc --noEmit`                                                        |
| `pnpm eval --tag fast`    | Run the cheap eval loop guarding routing and safety behavior          |
| `npx eve info`            | Print every discovered tool, skill, connection, channel, and subagent |
| `eve deploy`              | Ship to production, the same as `vercel deploy --prod`                |

Linting and formatting come from [Ultracite](https://www.ultracite.ai/), 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.

### How 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
  

## Code 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.

### The GitHub surface

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

```typescript
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.

### The Linear surface

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

```typescript
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.

### The orchestrator that routes, not writes

The orchestrator's runtime configuration is minimal:

```typescript
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:

```typescript
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](https://vercel.com/docs/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.

### Stations 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:

```typescript
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.

### Approval 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`:

```typescript
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:

| Action                               | Unattended run (labeled issue) | Trusted mention or Linear session | Dev TUI                 |
| ------------------------------------ | ------------------------------ | --------------------------------- | ----------------------- |
| Open a draft pull request            | Runs                           | Runs                              | Runs                    |
| Open a non-draft PR, mark a PR ready | Denied                         | Waits for approval                | Waits for approval      |
| Merge a pull request                 | Not in the tool surface        | Not in the tool surface           | Not in the tool surface |
| Apply or remove labels               | Runs                           | Runs                              | Waits for approval      |
| Comment on the intake issue          | Runs                           | Runs                              | Waits for approval      |
| Comment anywhere else                | Denied                         | Runs                              | Waits for approval      |
| Close or reopen an issue             | Runs                           | Runs                              | Runs                    |
| Write the factory brain              | Denied                         | Runs                              | Waits for approval      |
| Write to Linear                      | Denied                         | Runs                              | Runs                    |
| `clear_user_preferences`             | Denied                         | Waits for approval                | Waits 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.

### Git 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:

```typescript
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`:

```typescript
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.

### The 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.

```typescript
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.

### Handoff 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:

```typescript
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.

### No self-delegation

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

```typescript
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.

### Coding 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:

| Skill                    | Loads when                                                                                                                                                                                               |
| ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `triaging-issues`        | A 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-bridging` | An item spans both trackers: check whether a GitHub issue is already tracked in Linear, backlink both directions, and mirror only meaningful state                                                       |
| `writing-quality`        | Any 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.

### Evals 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.

```bash
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.

## Customize 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 change                                 | Edit                                                                                         | Notes                                                                                                                  |
| ----------------------------------------- | -------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| The target repository                     | `FACTORY_REPO` in the project's environment                                                  | Changing it rebuilds the station sandboxes and gives the new repo its own factory brain                                |
| The orchestrator's behavior               | `agent/instructions.ts`                                                                      | Grounding, routing, the review loop, delivery                                                                          |
| A station's craft                         | Its `instructions.md` and `outputSchema` in `agent.ts`                                       | The `description` is all the orchestrator sees when routing                                                            |
| Models                                    | `agent/lib/models.ts`                                                                        | Any [Gateway model ID](https://vercel.com/ai-gateway/models) works; keep implementer and reviewer on different vendors |
| Approval gates                            | `agent/lib/github/approval.ts` and the `requireApproval` map in `agent/extensions/github.ts` | Write tools not listed keep the SDK's approval-by-default                                                              |
| What the orchestrator can reach on GitHub | `include` in `agent/extensions/github.ts`                                                    | An allowlist with no preset; prefer adding a name here over loosening a gate                                           |
| The intake label and branch prefix        | `FACTORY_LABEL` and `FACTORY_BRANCH_PREFIX` environment variables                            | The implementer's instructions carry the default prefix as prose, so keep them in sync on an override                  |
| Triage, bridging, or prose conventions    | The skill folders under `agent/skills/`                                                      | A skill's frontmatter `description` decides when it loads                                                              |
| The safety floor                          | `evals/`                                                                                     | 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 using    | Delete                                                       |
| ------------ | ------------------------------------------------------------ |
| Linear       | `agent/channels/linear.ts` and `agent/connections/linear.ts` |
| Web research | `agent/subagents/researcher/`                                |

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

## Troubleshooting

Each item below lists a symptom, its cause, and the fix.

### Labeling an issue doesn't start the pipeline

Symptom: You add the `factory` label to an issue and nothing happens.

Cause: The labeler holds less than triage permission on the repository, or the GitHub connector isn't subscribed to the `issues` event. GitHub fires the `labeled` action even for labels attached at issue creation, so the channel verifies the labeler's permission against the API and acknowledges anything below triage without a session.

Fix: Confirm the labeler is at least a triage-level collaborator, and confirm 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 response

Symptom: You @mention the bot on an issue, but it doesn't reply.

Cause: The commenter's `author_association` isn't OWNER, MEMBER, or COLLABORATOR, or you're mentioning a name the App doesn't answer to. Mentions from accounts the repo hasn't granted write access to are acknowledged without a session by design.

Fix: Mention 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 error

Symptom: `eve build` or the deployment fails complaining `FACTORY_REPO` is not set or not an `owner/repo` pair.

Cause: The variable is required at module load so a misconfigured factory fails discovery instead of producing a factory with no target.

Fix: Set `FACTORY_REPO` to a plain `owner/repo` pair (like `acme/widgets`) in the project's environment and redeploy.

### A write appears to stall in the dev TUI

Symptom: You trigger a GitHub write, a factory brain update, or `clear_user_preferences`, and the call pauses without completing.

Cause: The call is gated on approval and is waiting for a decision. The dev principal is untrusted by design, so approval cards surface in the TUI; on GitHub, the same request appears as a comment awaiting a reply to a mention.

Fix: Approve or deny the pending call. To change which tools are gated, edit the policies in `agent/lib/github/approval.ts` and the `requireApproval` map in `agent/extensions/github.ts`.

### CI fixes stop after two attempts

Symptom: Checks are still red on one of the factory's pull requests, and Foreman posts that it's pausing its automated fixes.

Cause: The red-CI loop is bounded on purpose. Each fresh session counts earlier fix-attempt comments on the pull request (the thread is the only durable record unattended runs share) and stops after 2 so the factory can't loop on a failure it doesn't understand.

Fix: Troubleshoot the failure yourself, or @mention the bot on the PR with a diagnosis to run an attended revision. The loop only ever touches branches carrying the `factory/` prefix, so a person's red PR never triggers an uninvited fix.

### Connectors work locally but fail in production

Symptom: The factory works in the dev TUI, but the deployed bot can't reach GitHub or Linear.

Cause: `.env.local` is local-only; the deployed app reads the Vercel project's environment.

Fix: Set `GITHUB_CONNECTOR`, `LINEAR_CONNECTOR`, and `FACTORY_REPO` in the project's production environment. The Deploy button sets them for you.

### A file you added doesn't show up

Symptom: The tool, skill, or station you added isn't discovered.

Cause: eve walks `agent/` at build time and only registers files that classify as an authored slot; a naming or placement problem drops the file from discovery.

Fix: Run `npx eve info` to see what eve discovered, and check `.eve/discovery/diagnostics.json` for why a file was skipped.

## Related resources

- [eve Software Factory template source](https://github.com/vercel-labs/eve-software-factory-template) and the [Foreman docs](https://ask-foreman.dev/docs)
  
- [eve documentation](https://eve.dev/docs), including [subagents](https://eve.dev/docs/subagents), [skills](https://eve.dev/docs/skills), and [human in the loop](https://eve.dev/docs/human-in-the-loop)
  
- [Vercel Connect](https://vercel.com/docs/connect) and [Vercel Connect CLI](https://vercel.com/docs/cli/connect)
  
- [Vercel Blob](https://vercel.com/docs/vercel-blob)
  
- [Vercel Sandbox](https://vercel.com/docs/sandbox)
  
- [Vercel AI Gateway](https://vercel.com/docs/ai-gateway)
  
- [Vercel OIDC](https://vercel.com/docs/oidc)
  
- [GitHub Tools eve extension](https://github-tools.com/frameworks/eve-extension)
  
- [eve Marketing Team template](https://vercel.com/templates/eve/eve-marketing-team), [eve Sanity Copilot template](https://vercel.com/templates/eve/eve-sanity-copilot), and [eve Personal Agent template](https://vercel.com/templates/nuxt/eve-personal-agent)