Skip to content

Feature Agent — Complete Setup & Configuration Guide

Using FA day to day? The full feature reference is the User Guide. Running it in production? See the Operations Runbook — systemd services, Cloudflare Access tunnel, health-watcher auto-heal, the self-build loop, and recovery playbook. Browsable docs site: run npm run docs:build to generate a static documentation site from these docs. See docs-site/README.md for build and self-hosting instructions, and the Operations Runbook §"Docs site — publishing" for the deploy runbook (Cloudflare Pages via CI, or self-hosting the static output). This guide covers everything you need to set up Feature Agent from scratch, including Claude Code configuration, agent permissions, model selection, and integrations.

Tenant/org scope: FA now models an internal tenant/org scope one level above projects — a single default org today, not yet user-configurable. The tenant_id field appears on project reads; multi-org management arrives with a future increment.


Table of Contents

  1. Prerequisites
  2. Installation
  3. Claude Code Setup
  4. Agent Configuration
  5. Permissions & Safety
  6. Model Selection
  7. WhatsApp Notifications (Twilio)
  8. GitHub Integration — incl. PR-merge tracking, manual PR creation, revisions
  9. GitLab Integration — draft MRs, merge detection, revisions on gitlab.com or self-hosted
  10. Bitbucket Cloud Integration — PRs, merge detection, revisions on bitbucket.org
  11. Spec-Kit Pipeline
  12. Workspace Confinement & Data Provisioning
  13. Inbound Triggers — receive webhooks from GitHub/GitLab/Linear/Sentry/Slack
  14. Webhook Configuration
  15. Users & Roles
  16. Protected Projects
  17. Self-Managed Development
  18. Diagnostic Tool
  19. Production Deployment
  20. Troubleshooting

Prerequisites

  • Node.js 18+Download
  • Git — for cloning target repos
  • Claude Code CLI — the autonomous agent engine

Installation

bash
# Clone the repository
git clone https://github.com/scottallan/featureagent.git
cd featureagent

# Install dependencies
npm install

# Create your environment file
cp .env.example .env

Edit .env with your configuration (detailed in sections below).

bash
# Start in development mode (hot-reload)
npm run dev

# Or build for production
npm run build
npm start

Claude Code Setup

Feature Agent uses the Claude Code CLI to autonomously implement features. You need Claude Code installed and authenticated on the machine running Feature Agent.

Install Claude Code

bash
# Install globally via npm
npm install -g @anthropic-ai/claude-code

# Verify installation
claude --version

Authenticate

bash
# Interactive login (opens browser)
claude login

# Or set API key directly
export ANTHROPIC_API_KEY=sk-ant-...

Verify It Works

bash
# Quick test — should return a response
claude --print "Say hello"

Custom CLI Path

If Claude Code is installed in a non-standard location:

env
CLAUDE_CLI_PATH=/usr/local/bin/claude

Agent Configuration

Core Settings

env
# How often the agent checks for new work (milliseconds)
AGENT_POLL_INTERVAL_MS=30000        # Default: 30 seconds

# Maximum parallel feature implementations
AGENT_MAX_CONCURRENCY=3             # Default: 1

# Maximum agent turns per implementation session
AGENT_MAX_TURNS=100                 # Default: 100

Concurrency

AGENT_MAX_CONCURRENCY controls how many features the agent works on simultaneously. Consider:

  • 1 — Safe default. Features are processed one at a time, in order.
  • 2-3 — Good for machines with ample CPU/RAM. Each implementation clones a repo and runs Claude Code.
  • 5+ — For dedicated servers. Each concurrent job uses significant resources.

Retry

env
AGENT_MAX_RETRIES=3                  # Default: 3 (set to 0 to disable auto-retry)

When a feature fails, the agent will automatically retry it up to AGENT_MAX_RETRIES times. Each retry resets the feature to queued, clears previous artifacts, and increments retry_count. You can also manually retry via the API (POST /api/features/:id/retry) or the dashboard's Retry button.

Spec-conformance reviewer auto-revise loop

env
REVIEWER_MAX_ROUNDS=2                # Default: 2 (set to 0 to disable auto-revise loop)

When reviewer: spec_conformance is enabled, REVIEWER_MAX_ROUNDS controls how many automatic revise-and-re-review rounds FA drives before escalating to a human. With the default of 2, FA will auto-revise up to twice before leaving the feature at implemented with a standing REQUEST_CHANGES review. Set to 0 to disable the loop entirely (FA posts the verdict once and stops — the original post-and-stop behavior).


How the Agent Works

Understanding what happens when the agent processes a feature helps with debugging, monitoring, and configuring your environment.

The Agent Loop

Feature Agent runs a polling loop inside the Node.js server process (no separate daemon or container). Every AGENT_POLL_INTERVAL_MS milliseconds (default: 30s), the loop:

  1. Picks up features in analyzing status and runs a single-shot Claude Code call to evaluate them
  2. Picks up features in queued status (up to AGENT_MAX_CONCURRENCY) and starts implementation
  3. Picks up failed features with retry_count < AGENT_MAX_RETRIES and re-queues them

What Happens During Implementation

Each feature job runs through these steps:

1. Clone          git clone --depth 50 -b main <repo_url>
                  into: WORKSPACE_DIR/<feature-id>/

2. Branch         git checkout -b feature/<slug>-<id>

3. Implement      claude --print --model sonnet --max-turns 100 \
                    --dangerously-skip-permissions \
                    "<implementation prompt>"
                  (spawned as a child process in the workspace dir)

4. Test           npm test
                  (runs the project's test suite)

5. Document       claude --print "<doc prompt>"
                  (generates docs/features/<slug>.md)

6. Push           git add -A && git commit && git push -u origin <branch>

7. PR (optional)  Creates a draft PR via GitHub API

8. Cleanup        rm -rf WORKSPACE_DIR/<feature-id>/

The Claude Code CLI is spawned as a child process using Node's child_process.spawn(). It runs with:

  • --print mode (non-interactive, outputs text)
  • --output-format text
  • --model set to your AGENT_MODEL (default: sonnet)
  • --max-turns set to your AGENT_MAX_TURNS (default: 100)
  • --dangerously-skip-permissions if configured
  • A 10-minute timeout per Claude Code invocation
  • Working directory set to the cloned repo

The stdout/stderr from Claude Code is captured and stored in the feature's implementation_log field.

Where Code Lives

PathPurposeLifetime
WORKSPACE_DIR/<feature-id>/Cloned repo for implementationTemporary — deleted after job completes or fails
data/featureagent.dbSQLite database with all features, projects, usersPersistent

Default WORKSPACE_DIR is ./workspaces relative to the Feature Agent install directory.

Monitoring the Agent

Server logs — The Feature Agent server logs all agent activity to stdout:

[agent] Starting agent loop (poll: 30000ms, concurrency: 1)
[agent] Processing feature: Build chess game (a1b2c3d4) [1/1 slots]
[agent] Feature implemented: a1b2c3d4 on feature/build-chess-game-a1b2c3d4 (PR: https://...)

Or on failure:

[agent] Implementation failed for a1b2c3d4: Claude Code exited with code 1: ...
[agent] Retrying failed feature: Build chess game (a1b2c3d4) attempt 2/3

Dashboard — The header shows active jobs / max concurrency. Feature cards show real-time status, and the detail view shows the full implementation log and test results after completion.

API — Poll the status endpoint and feature detail:

bash
# Agent status (active jobs, model, concurrency)
curl http://localhost:3100/api/status

# Feature detail (status, implementation_log, test_results, retry_count)
curl http://localhost:3100/api/features/:id \
  -H "Authorization: Bearer $API_KEY"

Watch the workspace — While a feature is being implemented, you can watch the workspace directory to see files being created in real-time:

bash
# Watch for new files
watch -n 2 "find ./workspaces -type f | head -30"

# Or tail the workspace for a specific feature
ls ./workspaces/
# Then watch the active one:
watch -n 2 "ls -la ./workspaces/<feature-id>/"

Watch Claude Code process — Since Claude Code runs as a child process, you can see it in your process list:

bash
ps aux | grep claude

Permissions & Safety

This is the most important configuration decision. It determines how much autonomy the Claude Code agent has when implementing features.

env
AGENT_PERMISSIONS=default

In default mode, Claude Code runs with standard permissions. The agent can:

  • Read and write files in the cloned workspace
  • Run commands (npm test, git, etc.)

The agent will prompt for permission on certain operations, which in a headless environment may cause the implementation to stall. For fully autonomous operation, see the next section.

Dangerously Skip Permissions (Full Autonomy)

env
AGENT_PERMISSIONS=dangerously-skip-permissions

WARNING: This flag gives Claude Code unrestricted access to execute any command without confirmation. Only use this when:

  • The agent runs in an isolated environment (container, VM, dedicated server)
  • You trust the feature descriptions being submitted
  • You have proper network isolation (the agent won't access sensitive internal systems)

What --dangerously-skip-permissions enables:

  • File read/write without confirmation
  • Shell command execution without confirmation
  • Package installation without confirmation
  • Any tool use without user approval prompts

Security recommendations when using full autonomy:

  1. Run in a container — Use Docker to isolate the agent:

    bash
    docker run -d \
      -e AGENT_PERMISSIONS=dangerously-skip-permissions \
      -e ANTHROPIC_API_KEY=sk-ant-... \
      -v /var/featureagent/data:/app/data \
      featureagent
  2. Limit network access — The agent only needs to reach:

    • api.anthropic.com (Claude API)
    • github.com (repo clone + push)
    • Notification endpoints: api.telegram.org, discord.com, hooks.slack.com, api.twilio.com, graph.facebook.com, SMTP host (depending on configured channels)
  3. Use fine-grained GitHub tokens — Only grant access to specific repositories the agent should modify. Never use tokens with admin/org-level access.

  4. Restrict workspace directory — Keep WORKSPACE_DIR on a separate partition or volume.

Allowlisted Tools (Future)

Claude Code also supports granular tool permissions via --allowedTools. This is a middle ground:

bash
# Example: allow file ops and bash, but not network
claude --allowedTools "Read,Write,Edit,Bash" --print "..."

This is not yet configurable via Feature Agent's .env but can be added to the CLI args in src/services/agent-engine.ts.


Model Selection

env
AGENT_MODEL=sonnet                  # Default

Available models:

ModelSettingBest For
Claude Sonnet 4.6sonnetBalanced speed and quality. Good default for most features.
Claude Opus 4.6opusComplex features requiring deep reasoning. Slower, higher cost.
Claude Haiku 4.5haikuSimple features, fast turnaround. Lower cost.

Recommendations

  • Start with sonnet — Good balance of capability and cost for most features
  • Use opus for — Large architectural changes, complex multi-file features, features requiring deep understanding of the codebase
  • Use haiku for — Simple bug fixes, config changes, documentation updates

Max Turns

env
AGENT_MAX_TURNS=100

Controls how many "turns" (tool calls) the agent can make per implementation. Higher values allow the agent to handle more complex features but increase cost and time.

  • 50 — Simple features (add a config option, small UI change)
  • 100 — Medium features (new API endpoint, component)
  • 200+ — Complex features (new subsystem, large refactor)

Notification Channels

Feature Agent supports multiple notification channels, configured per-project. Channels are stored in the project's notification_channels JSON array.

Supported Channels

ChannelCostSetupBest For
TelegramFree5 minBest free option. No limits, no verification.
DiscordFree2 minTeams already using Discord.
SlackFree tier5 minTeams already using Slack.
WhatsAppPaid (Twilio) or free tier (Meta)15-60 minDirect mobile notifications.
EmailFree (SMTP)10 minUniversal fallback.

Notification Rules

Each channel has a notify array controlling who receives messages:

Status ChangeDefault Recipient
awaiting_approvalPO
clarification_neededSubmitter (or PO in po_approval mode)
in_progressSubmitter
implementedSubmitter + PO
failedSubmitter + PO

Set notifications_enabled: false on a project to disable all notifications.

  1. Message @BotFather on Telegram
  2. Send /newbot, follow prompts, get your bot token
  3. Message your bot, then visit https://api.telegram.org/bot<TOKEN>/getUpdates to find your chat_id
  4. Configure:
bash
curl -X POST http://localhost:3100/api/projects \
  -H "Content-Type: application/json" \
  -d '{
    "name": "my-app",
    "repo_url": "https://github.com/org/my-app.git",
    "autonomy_mode": "auto_safe",
    "notification_channels": [
      {
        "type": "telegram",
        "bot_token": "123456:ABC-DEF1234ghIkl-zyx57W2v",
        "chat_id": "987654321",
        "notify": ["all"]
      }
    ]
  }'

For group notifications, add the bot to a group and use the group's chat_id (negative number).

Discord Setup

  1. In your Discord server: Server Settings → Integrations → Webhooks → New Webhook
  2. Copy the webhook URL
  3. Configure:
json
{
  "type": "discord",
  "webhook_url": "https://discord.com/api/webhooks/123456/abcdef...",
  "notify": ["all"]
}

Discord notifications include rich embeds with color-coded status.

Slack Setup

  1. Create a Slack app at api.slack.com/apps
  2. Enable Incoming Webhooks, add to a channel
  3. Copy the webhook URL
  4. Configure:
json
{
  "type": "slack",
  "webhook_url": "https://hooks.slack.com/services/T00/B00/xxxx",
  "notify": ["all"]
}

WhatsApp Setup

Via Twilio:

json
{
  "type": "whatsapp",
  "provider": "twilio",
  "account_sid": "ACxxxxxxxx",
  "auth_token": "your_token",
  "from_number": "whatsapp:+14155238886",
  "po_number": "whatsapp:+15551234567",
  "submitter_number": "whatsapp:+15559876543",
  "notify": ["all"]
}

Via Meta Cloud API (free tier — 1,000 conversations/month):

json
{
  "type": "whatsapp",
  "provider": "meta",
  "access_token": "EAAxxxxxxx",
  "phone_number_id": "123456789",
  "po_number": "15551234567",
  "submitter_number": "15559876543",
  "notify": ["all"]
}

Email Setup

json
{
  "type": "email",
  "smtp_host": "smtp.gmail.com",
  "smtp_port": 587,
  "smtp_user": "you@gmail.com",
  "smtp_pass": "app-specific-password",
  "from": "featureagent@example.com",
  "po_address": "po@company.com",
  "submitter_address": "dev@company.com",
  "notify": ["all"]
}

For Gmail, use an App Password (not your regular password).

Multiple Channels

You can combine any number of channels per project:

json
"notification_channels": [
  { "type": "telegram", "bot_token": "...", "chat_id": "...", "notify": ["all"] },
  { "type": "discord", "webhook_url": "...", "notify": ["po"] },
  { "type": "email", "smtp_host": "...", "po_address": "...", "notify": ["po", "submitter"] }
]

GitHub Integration

Feature Agent can automatically create draft pull requests when a feature is implemented.

Setup

  1. Create a GitHub Personal Access Token (or fine-grained token):

    • Go to GitHub → Settings → Developer settings → Personal access tokens
    • For fine-grained tokens, grant access to specific repos with:
      • Contents: Read and write
      • Pull requests: Read and write
  2. Configure:

env
GITHUB_TOKEN=ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
  1. Enable per-project:
bash
# Via API
curl -X PATCH http://localhost:3100/api/projects/:id \
  -H "Content-Type: application/json" \
  -d '{"auto_create_pr": true}'

# Or via the dashboard when enrolling/editing a project

What the Agent Creates

When a feature is successfully implemented with auto_create_pr enabled:

  • A draft pull request is created (not ready for review)
  • PR title: feat: <feature title>
  • PR body includes: feature description, test results, doc path, agent attribution
  • The PR targets the project's default_branch (or the feature's base_branch override)

Draft PRs allow your team to review, request changes, or merge at their discretion.

PR-Merge & Review Tracking

With GITHUB_TOKEN set, the agent polls open PRs for implemented features each tick (up to 5 at a time, throttled per-feature by PR_CHECK_INTERVAL_MS, default 10 min):

  • Merged PR → the feature auto-transitions to merged (a terminal state).
  • Review comments → the count is recorded and surfaced in the dashboard as a ! next to the PR, so you know there's feedback to address.

Polling is skipped entirely when GITHUB_TOKEN is unset.

GITHUB_TOKEN is read once at startup. After rotating the token, restart the server or PR creation/polling will keep using the old (expired) token.

Manual PR Creation

If the automatic PR creation failed (e.g. an expired token at implementation time), the branch is already pushed — recover with a pure API call once the token is fixed:

bash
curl -X POST http://localhost:3100/api/features/:id/create-pr \
  -H "Authorization: Bearer fa_your_api_key"

This opens a draft PR for an implemented feature that has a branch_name but no pr_url. Failures surface as 502 (unlike the auto-flow, which swallows errors).

Addressing Review Comments (Revisions)

After a PR is reviewed, nudge the agent to apply the requested changes on the same branch:

bash
curl -X POST http://localhost:3100/api/features/:id/revise \
  -H "Authorization: Bearer fa_your_api_key"

This flips the feature to revising. The agent fetches the PR's review feedback (review summaries + inline code comments + conversation comments), re-clones the branch, applies the changes, runs tests, and pushes to the same branch — updating the existing PR (no new PR). It returns to implemented. If there are no comments, or on error, it reverts to implemented with the PR left intact.


GitLab Integration

Feature Agent supports GitLab-hosted projects natively. When a project's repo_url points to gitlab.com (or a self-hosted GitLab instance), FA automatically creates draft Merge Requests, polls for merges, surfaces MR review comments, and posts spec-conformance reviews — all without any per-project configuration beyond the credentials below.

Setup

  1. Create a GitLab personal access token (or a project access token) with at minimum:

    • api scope (needed for MR creation, notes, and approvals)
  2. Configure:

env
GITLAB_TOKEN=glpat-xxxxxxxxxxxxxxxxxxxx
  1. For self-hosted GitLab (optional):
env
# Hostname only — no https:// prefix, no trailing slash
GITLAB_HOST=gitlab.mycompany.com

When GITLAB_HOST is set, any project whose repo_url matches that hostname is routed to the GitLab provider automatically.

How provider selection works

FA selects the VCS provider solely from the hostname in the project's repo_url (or pr_url). No per-project configuration is required:

Host in URLProvider used
github.comGitHub (Octokit, GITHUB_TOKEN)
gitlab.comGitLab (REST v4, GITLAB_TOKEN)
value of GITLAB_HOSTGitLab (REST v4, GITLAB_TOKEN)
anything elseGitHub (backward-compatible default)

What FA creates on GitLab

When a feature is implemented with auto_create_pr enabled on a GitLab-hosted project:

  • A draft Merge Request is created (Draft: feat: <title>)
  • MR description includes: feature description, test results, doc path, agent attribution
  • The MR targets the project's default_branch (or the feature's base_branch override)

MR-Merge & Review Tracking

With GITLAB_TOKEN set, FA polls open MRs for implemented features (same cadence as GitHub: up to 5 at a time, throttled per-feature by PR_CHECK_INTERVAL_MS, default 10 min):

  • Merged MR → feature auto-transitions to merged.
  • Review comments → count is recorded and surfaced in the dashboard as a ! next to the MR link.

Manual MR Creation & Revisions

The same /create-pr and /revise endpoints work for GitLab MRs:

bash
# Recover a failed auto-MR
curl -X POST http://localhost:3100/api/features/:id/create-pr \
  -H "Authorization: Bearer fa_your_api_key"

# Address MR review comments
curl -X POST http://localhost:3100/api/features/:id/revise \
  -H "Authorization: Bearer fa_your_api_key"

GITLAB_TOKEN is read once at startup. After rotating the token, restart the server.


Bitbucket Cloud Integration

Feature Agent supports Bitbucket Cloud (bitbucket.org) natively. When a project's repo_url points to bitbucket.org, FA automatically creates PRs, polls for merges, surfaces PR review comments, and posts spec-conformance reviews — all without any per-project configuration beyond the credentials below.

Bitbucket Server / Data Center is NOT supported. These products use a different REST API (v1.0, /rest/api/1.0) and are out of scope. Only bitbucket.org (Bitbucket Cloud) is supported.

Setup

Option A — App password (most common)

App passwords are per-user credentials that scope permissions without sharing your account password.

  1. Go to Bitbucket Cloud → Personal settings → App passwords → Create app password
  2. Name it (e.g. feature-agent) and grant these scopes:
    • Repositories: Read
    • Pull requests: Write
  3. Copy the generated password (shown once).
  4. Configure:
env
BITBUCKET_TOKEN=your-app-password
BITBUCKET_USERNAME=your-bitbucket-username

When BITBUCKET_USERNAME is set, FA uses HTTP Basic auth (username:app-password) — the standard Bitbucket Cloud app-password flow.

Option B — Repository or workspace access token

If your Bitbucket workspace has workspace/repository access tokens enabled (Bitbucket Cloud Premium):

  1. Go to Workspace settings → Security → Access tokens (or Repository settings → Access tokens)
  2. Create a token with repository:read and pullrequest:write scopes.
  3. Configure:
env
BITBUCKET_TOKEN=your-access-token
# BITBUCKET_USERNAME — leave unset when using a workspace/repo access token

Without BITBUCKET_USERNAME, FA sends the token as Authorization: Bearer <token>.

How provider selection works

FA selects the VCS provider solely from the hostname in the project's repo_url (or pr_url). No per-project configuration is required:

Host in URLProvider used
github.comGitHub (Octokit, GITHUB_TOKEN)
gitlab.comGitLab (REST v4, GITLAB_TOKEN)
value of GITLAB_HOSTGitLab (REST v4, GITLAB_TOKEN)
bitbucket.orgBitbucket Cloud (REST v2.0, BITBUCKET_TOKEN)
anything elseGitHub (backward-compatible default)

Both HTTPS and SSH repo URLs are recognized:

  • https://bitbucket.org/myworkspace/myrepo.git
  • git@bitbucket.org:myworkspace/myrepo.git

What FA creates on Bitbucket

When a feature is implemented with auto_create_pr enabled on a Bitbucket-hosted project:

  • A PR is created with the title Draft: feat: <title>
  • PR description includes: feature description, test results, doc path, agent attribution
  • The PR targets the project's default_branch (or the feature's base_branch override)

Draft PR limitation: Bitbucket Cloud's REST API does not expose a native draft PR flag. FA prefixes the title with Draft: to signal intent — the same approach used for GitLab. Merge or close the PR as usual; FA detects the merge via polling.

PR-Merge & Review Tracking

With BITBUCKET_TOKEN set, FA polls open PRs for implemented features (same cadence as GitHub/GitLab: up to 5 at a time, throttled per-feature by PR_CHECK_INTERVAL_MS, default 10 min):

  • Merged PR → feature auto-transitions to merged.
  • Review comments → count is recorded and surfaced in the dashboard as a ! next to the PR link.

Manual PR Creation & Revisions

The same /create-pr and /revise endpoints work for Bitbucket PRs:

bash
# Recover a failed auto-PR
curl -X POST http://localhost:3100/api/features/:id/create-pr \
  -H "Authorization: Bearer fa_your_api_key"

# Address PR review comments
curl -X POST http://localhost:3100/api/features/:id/revise \
  -H "Authorization: Bearer fa_your_api_key"

BITBUCKET_TOKEN and BITBUCKET_USERNAME are read once at startup. After rotating credentials, restart the server.


Spec-Kit Pipeline

Feature Agent integrates with GitHub Spec Kit for spec-driven development, enabled per project.

Enrolling a Project

bash
# Clone, run `specify init --ai claude`, generate a constitution, open a bootstrap PR
curl -X POST http://localhost:3100/api/projects/:id/spec-kit/enable

Status flows disabled → enrolling → awaiting_merge. Watch live enrollment logs:

bash
curl http://localhost:3100/api/projects/:id/spec-kit/logs
# or in the dashboard log viewer: ?id=enroll-<projectId>

After merging the bootstrap PR, finalize:

bash
curl -X POST http://localhost:3100/api/projects/:id/spec-kit/mark-enabled   # → 'enabled'

Already have Spec Kit configured? Skip enrollment — detection promotes the project straight to enabled if .specify/ and a constitution both exist:

bash
curl -X POST http://localhost:3100/api/projects/:id/spec-kit/check

To turn it off: POST /api/projects/:id/spec-kit/disable.

Running a Feature Through Spec-Kit

Submit a feature with "use_spec_kit": true (silently coerced to false if the project isn't enabled). The agent runs the pipeline in order:

/speckit.specify → /speckit.clarify → /speckit.plan → /speckit.tasks → /speckit.analyze → /speckit.implement

It pauses at clarification gates and resumes from the saved spec/plan/task files once the questions are answered — the same clarification flow as auto_safe mode.


Workspace Confinement & Data Provisioning

Each feature runs in an isolated workspace (workspaces/<feature_id>/), and the agent is instructed to stay inside it and ignore absolute paths in repo docs. To give features the external data they legitimately need without letting the agent reach outside its workspace, declare it on the project:

bash
curl -X PATCH http://localhost:3100/api/projects/:id \
  -H "Content-Type: application/json" \
  -d '{
    "data_dirs": ["/data/signal_cache", {"src": "/data/fixtures", "dest": "test/fixtures"}],
    "setup_command": "npm ci"
  }'

After cloning each workspace, the agent (provisionWorkspace()):

  1. Validates data_dirs exist (step one). Every declared source must exist on the agent host; a missing one fails the run up front with an actionable error instead of silently continuing.
  2. Runs git lfs pull (best-effort — fine if the repo has no LFS).
  3. Symlinks each data_dirs entry read-only into the workspace and adds it to .git/info/exclude so it's never committed (path-traversal guarded).
  4. Runs setup_command in the workspace (a non-zero exit fails the run).

data_dirs entries are either a path string ("/abs/src" — destination defaults to the basename) or { "src": "/abs/src", "dest": "rel/path" }.


Declared Gate Commands (gates: in .fa/environment.yml)

Declare an ordered list of shell commands in the committed .fa/environment.yml that FA runs inside the sandbox after implementation, after tests, and after the verify step — and before creating any commit or PR. FA reads only the exit code and captured output; it never interprets what the command does (§VII).

yaml
# .fa/environment.yml
image: node:20
test: npm test

gates:
  # Blocking gate (default: block: true) — non-zero exit fails the run,
  # no commit pushed, no PR opened. Workspace preserved for inspection.
  - name: lint
    command: npm run lint

  # Advisory gate — non-zero exit recorded as a warning, run continues.
  - name: advisory-scan
    command: ./my-scanner.sh
    block: false

Gate execution:

  • FA runs each gate via bash -lc "<command> 2>&1" inside the sandbox Runtime (the same isolated environment the agent used). If RUNTIME=docker, gates run inside the container.
  • Gates run in order. All gates execute (even after a blocking failure) so the full picture is captured before failing.
  • A failing blocking gate transitions the feature to failed and preserves the workspace. No commit is pushed and no PR is created.
  • Advisory gate failures (block: false) are recorded as warn but do not block.

Gate shape:

FieldTypeRequiredDefaultDescription
namestringyesNon-empty label used in logs, run-events, and the provenance artifact.
commandstringyesShell command to run. FA runs it and reads only the exit code.
blockbooleannotruetrue = blocking (fail the run on non-zero); false = advisory (warn, continue).

Unknown keys or malformed gate entries throw ManifestValidationError — the run aborts before any agent spend.

Observability: every gate result is recorded as a gate_result run-event (visible in the dashboard's run-events timeline) and committed into the provenance artifact (.fa/provenance/<id>.md).

No gates declared → strict no-op. A project with no gates: key behaves byte-identically to before — no extra cost, no behavior change.


Inbound Triggers

Feature Agent can receive webhooks from external tools (GitHub, GitLab, Linear, Sentry, Slack) and automatically create features from them. Each incoming event enters the project's standard autonomy-mode funnel — a po_approval project still requires human sign-off before any agent spend.

Enabling

Set the environment variable before starting the server:

bash
FA_INBOUND_TRIGGERS_ENABLED=true

Without this flag the ingest routes return 404. The discovery endpoint (GET /api/projects/:id/triggers) still returns the provider list with inbound_enabled: false so you can preview what URLs will be active.

Finding your webhook URLs

Open the dashboard, locate your project in the project table, and click Triggers. The modal shows:

  • An enabled/disabled indicator reflecting FA_INBOUND_TRIGGERS_ENABLED.
  • A list of all registered providers (GitHub, GitLab, Linear, Sentry, Slack) with their full absolute webhook URLs — copy them directly into the provider's webhook settings.
  • A signing-secret hint: set the provider's webhook secret to this project's API key.

You can also query the endpoint directly:

bash
curl http://localhost:3100/api/projects/<projectId>/triggers \
  -H "Authorization: Bearer <project-api-key>"

Response:

json
{
  "inbound_enabled": true,
  "providers": [
    { "key": "github",  "webhook_path": "/api/projects/<id>/triggers/github" },
    { "key": "gitlab",  "webhook_path": "/api/projects/<id>/triggers/gitlab" },
    { "key": "linear",  "webhook_path": "/api/projects/<id>/triggers/linear" },
    { "key": "sentry",  "webhook_path": "/api/projects/<id>/triggers/sentry" },
    { "key": "slack",   "webhook_path": "/api/projects/<id>/triggers/slack" }
  ]
}

Per-provider webhook setup

The signing secret for every provider is the project API key (fa_...). Paste it into the provider's "webhook secret" or "signing secret" field.

GitHub — Settings → Webhooks → Add webhook

  • Payload URL: https://your-fa-host/api/projects/<id>/triggers/github
  • Content type: application/json
  • Secret: your project API key
  • Events: Issues → "Let me select individual events" → check Issues

GitLab — Settings → Webhooks → Add new webhook

  • URL: https://your-fa-host/api/projects/<id>/triggers/gitlab
  • Secret token: your project API key
  • Trigger: check Issues events

Linear — Settings → API → Webhooks → New webhook

  • URL: https://your-fa-host/api/projects/<id>/triggers/linear
  • Secret: your project API key
  • Resources: check Issues

Sentry — Settings → Integrations → Webhooks → Add to Project

  • Webhook URL: https://your-fa-host/api/projects/<id>/triggers/sentry
  • Secret: your project API key
  • Events: check issue (issue-alert created)

Slack — Use a Slack workflow or custom app that POSTs a JSON body:

json
{
  "title": "Feature title",
  "description": "Optional details",
  "event_id": "<unique-id>",
  "channel": "#general",
  "user": "@alice"
}
  • URL: https://your-fa-host/api/projects/<id>/triggers/slack
  • Set the x-fa-token header to your project API key (or include "token": "..." in the body)

Webhook Configuration

Feature Agent fires HTTP POST webhooks to your project's callback_url on every status change.

Setup

Set callback_url when enrolling a project:

bash
curl -X POST http://localhost:3100/api/projects \
  -H "Content-Type: application/json" \
  -d '{
    "name": "my-app",
    "repo_url": "...",
    "autonomy_mode": "auto_safe",
    "callback_url": "https://my-app.com/api/webhooks/featureagent"
  }'

Payload Format

json
{
  "event": "feature.status_changed",
  "feature_id": "550e8400-e29b-41d4-a716-446655440000",
  "project_id": "660e8400-e29b-41d4-a716-446655440000",
  "title": "Dark mode support",
  "old_status": "queued",
  "new_status": "in_progress",
  "branch_name": "feature/dark-mode-support-550e8400",
  "pr_url": null,
  "timestamp": "2025-01-15T10:30:00.000Z"
}

Security

Webhooks include an HMAC-SHA256 signature in the X-FeatureAgent-Signature header, using your project's API key as the secret:

X-FeatureAgent-Signature: sha256=abc123...

Verify on your end:

javascript
const crypto = require('crypto');
const expectedSig = 'sha256=' + crypto
  .createHmac('sha256', projectApiKey)
  .update(JSON.stringify(requestBody))
  .digest('hex');

if (receivedSignature !== expectedSig) {
  return res.status(401).send('Invalid signature');
}

Retry Policy

  • 3 retries with exponential backoff (1s, 2s, 4s)
  • Configurable via WEBHOOK_MAX_RETRIES and WEBHOOK_TIMEOUT_MS
  • Non-2xx responses trigger a retry

Local Email + Password Login

FA supports native email/password login as a second human-login method (alongside Google and API keys). It uses the same session cookie infrastructure as Google Sign-In.

Requirements

  • SESSION_SECRET must be set (already required by Google login — same secret).
  • SMTP configuration (SMTP_HOST etc.) is needed for password-reset emails; without it, the endpoint still returns 200 and logs a warning (graceful degradation).

Environment variables

env
# Required for any human login (Google or local)
SESSION_SECRET=<at-least-32-random-bytes-hex>

# Optional: SMTP for password-reset/invite emails
SMTP_HOST=smtp.example.com
SMTP_PORT=587                          # default 587; use 465 for TLS
SMTP_USER=notify@example.com
SMTP_PASS=<app-password>
SMTP_FROM=Feature Agent <notify@example.com>

# Base URL used in reset-link emails (default: http://localhost:3100)
APP_BASE_URL=https://your-fa-domain.com

Generate a strong SESSION_SECRET:

bash
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"

Inviting a user (setting a first password)

Local login is invite-only — users cannot self-register. The operator creates the account, then the user sets their own password via the reset flow:

  1. Create the user account (admin API):

    bash
    curl -s -X POST https://your-fa-domain.com/api/users \
      -H "Authorization: Bearer $ADMIN_KEY" \
      -H "Content-Type: application/json" \
      -d '{"email": "alice@company.com", "name": "Alice", "role": "admin"}'
  2. Send Alice the login page URL: https://your-fa-domain.com/login.html

  3. Alice clicks "Set / forgot password?", enters her email, and receives a reset link by email (if SMTP is configured). She clicks the link and sets her password.

Without SMTP: Alice can't receive the email automatically. You can instead generate the reset link manually via POST /auth/local/reset-request (which logs a warning and returns 200) and then extract the token from server logs or use the API — or simply set the password directly via the API in a dev environment.

How it works

  • Passwords are hashed with scrypt (N=16384, r=8, p=1, 32-byte output, random per-user salt). The stored form is self-describing: scrypt$N$r$p$<salt>$<hash>.
  • Constant-time compare prevents timing attacks. The login handler always runs scrypt regardless of whether the email exists, keeping timing consistent (prevents user enumeration).
  • Password reset tokens are stateless HMACs over {userId, exp, fingerprint} where fingerprint is derived from the current password_hash. Setting a new password invalidates all prior tokens (single-use).
  • Tokens expire after 24 hours.
  • Failed logins are tracked in-memory per email+IP. After 5 consecutive failures, the account+IP is locked out for 15 minutes (configurable via LOCAL_AUTH_MAX_ATTEMPTS and LOCAL_AUTH_LOCKOUT_MS).

API endpoints

EndpointMethodBodyDescription
/auth/local/loginPOST{email, password}Issue a session cookie. 401 on wrong credentials (uniform, no enumeration).
/auth/local/reset-requestPOST{email}Send reset email. Always returns 200 (no enumeration).
/auth/local/resetPOST{token, new_password}Set password with a valid single-use token.

Configuring Google OAuth

Google Sign-In lets human users log into the FA dashboard with their Google account instead of pasting a raw API key. This is optional — API-key auth always works regardless.

Prerequisites

  1. A Google Cloud project with the OAuth 2.0 API enabled.
  2. An OAuth 2.0 Client ID of type "Web application".

Step-by-step

  1. Go to Google Cloud ConsoleAPIs & Services → Credentials.
  2. Click "Create Credentials" → "OAuth 2.0 Client ID". Choose "Web application".
  3. Under "Authorized redirect URIs", add your redirect URI:
    • Development: http://localhost:3100/auth/google/callback
    • Production: https://your-fa-domain.com/auth/google/callback
  4. Click Create. Note the Client ID and Client Secret.

Environment variables

Add these three variables to your .env (or system environment — never commit secrets):

env
# Google OAuth (for human sign-in to the dashboard)
GOOGLE_CLIENT_ID=123456789-xxxx.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=GOCSPX-xxxxxxxxxxxxxxxxxxxxx
GOOGLE_REDIRECT_URI=https://your-fa-domain.com/auth/google/callback

# Session secret — signs the browser session cookie (generate a strong random value)
SESSION_SECRET=<at-least-32-random-bytes-hex-or-base64>

SESSION_SECRET must be set for Google sign-in (or API-key-to-session conversion) to work. If it is absent, Bearer-key auth still works but the "Sign in with Google" button returns a configuration error. Generate a strong value with:

bash
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"

How it works

  • FA implements OAuth 2.0 authorization code + PKCE (RFC 7636) with state for CSRF protection on the handshake.
  • The returned id_token is verified locally (signature against Google's JWKS, aud, iss, exp, email_verified). No secret is ever logged.
  • Sign-in is invite-only: the verified Google email must match a pre-created FA user (POST /api/users). Unknown emails are rejected with a clear message. No auto-provisioning for self-hosted.
  • On success, FA issues an httpOnly + SameSite=Lax (+ Secure in production) signed session cookie valid for 7 days. State-changing requests via the session cookie require an X-CSRF-Token header (the dashboard handles this automatically).

Redirect URI in production

Set GOOGLE_REDIRECT_URI to the exact URI registered in Google Cloud Console, including the scheme and path. If your FA instance is behind a reverse proxy (Nginx, Cloudflare Tunnel), use the public-facing HTTPS URL.

User provisioning

Before a Google user can sign in, an admin must create their FA user account with a matching email:

bash
curl -s -X POST https://your-fa-domain.com/api/users \
  -H "Authorization: Bearer $ADMIN_KEY" \
  -H "Content-Type: application/json" \
  -d '{"email": "alice@company.com", "name": "Alice", "role": "admin"}'

Alice can then sign in with Google using alice@company.com.


SSO / OIDC — Enterprise Identity (Generic IdP)

FA supports a bring-your-own-IdP SSO option using standards-based OpenID Connect (OIDC). Any OIDC-compliant issuer works — Okta, Azure AD / Entra, Auth0, Keycloak, PingFederate, Google Workspace (via generic OIDC), or any OpenID Connect–compliant provider. This is distinct from the Google-specific integration: the generic OIDC path uses OIDC Discovery (/.well-known/openid-configuration) and is configured entirely by env vars.

Requirements

  • SESSION_SECRET must be set (same secret used by Google and local-password login).
  • An OAuth 2.0 / OIDC application registered at your IdP with the authorization code flow enabled.
  • The IdP must issue an id_token with email and email_verified claims (standard OIDC scope openid email profile).

Step-by-step (generic)

  1. Create an OIDC application (called "Web application", "Regular Web App", or similar) in your IdP's admin console.
  2. Set the Allowed Redirect URI / Callback URL to:
    • Development: http://localhost:3100/auth/oidc/callback
    • Production: https://your-fa-domain.com/auth/oidc/callback
  3. Note the Client ID and Client Secret issued by the IdP.
  4. Note the Issuer URL (e.g. https://login.microsoftonline.com/<tenant-id>/v2.0 for Entra ID, or https://your-org.okta.com for Okta). FA appends /.well-known/openid-configuration to this URL for discovery.

Environment variables

env
# Generic OIDC/SSO (enterprise sign-in — spec 059 inc 2)
OIDC_ISSUER=https://login.microsoftonline.com/<tenant-id>/v2.0
OIDC_CLIENT_ID=<your-client-id>
OIDC_CLIENT_SECRET=<your-client-secret>          # secret — never commit to source control
OIDC_REDIRECT_URI=https://your-fa-domain.com/auth/oidc/callback

# Optional:
OIDC_SCOPES=openid email profile                 # default — change only if your IdP needs extra scopes
OIDC_ALLOW_SIGNUP=false                          # default false (least-privilege) — set to true to auto-provision accounts

OIDC_CLIENT_SECRET is a secret. Inject it via an environment variable, a secret manager (Vault, Doppler, AWS SSM, K8s Secret), or a .env file that is never committed. FA never logs or returns it via any API.

IdP-specific notes

IdPIssuer URL patternNotes
Oktahttps://your-org.okta.com or https://your-org.okta.com/oauth2/defaultUse the Authorization Server issuer, not just the org domain.
Azure AD / Entra IDhttps://login.microsoftonline.com/<tenant-id>/v2.0Enable email claim via "Token configuration" in Azure Portal.
Auth0https://your-tenant.auth0.comEnable OIDC-conformant mode; add email to the ID token claims.
Keycloakhttps://your-keycloak/realms/<realm>The realm's discovery URL is https://your-keycloak/realms/<realm>/.well-known/openid-configuration.
Google Workspace (generic OIDC)https://accounts.google.comWorks in addition to the FA-native Google OAuth path.

How it works

  • FA performs OIDC Discovery at startup-per-login (with a 1-hour TTL cache): GET <OIDC_ISSUER>/.well-known/openid-configuration → fetches authorization_endpoint, token_endpoint, and jwks_uri.
  • The id_token is verified locally (RSA-SHA256 signature against the IdP's JWKS, plus iss, aud, exp, nonce, and email_verified). No secret is ever sent to a verification service.
  • Nonce is used for anti-replay (state cookie + signed nonce in the token).
  • OIDC is enabled iff OIDC_ISSUER, OIDC_CLIENT_ID, OIDC_CLIENT_SECRET, and OIDC_REDIRECT_URI are all set.
  • Sign-in is invite-only by default (OIDC_ALLOW_SIGNUP=false): the IdP-verified email must match a pre-created FA user account. Unknown emails are rejected with a clear message directing the user to ask an admin. Set OIDC_ALLOW_SIGNUP=true to let any IdP-authenticated user self-provision an FA account with the least-privilege approver role.

User provisioning

By default (OIDC_ALLOW_SIGNUP=false), an admin must create each FA user account before they can sign in via SSO:

bash
curl -s -X POST https://your-fa-domain.com/api/users \
  -H "Authorization: Bearer $ADMIN_KEY" \
  -H "Content-Type: application/json" \
  -d '{"email": "alice@company.com", "name": "Alice", "role": "approver"}'

Then Alice can sign in with SSO using alice@company.com. The IdP-verified email is the link key.

With OIDC_ALLOW_SIGNUP=true, first-time SSO users are auto-provisioned with role approver. Admins can promote them afterwards via PATCH /api/users/:id.

Identity linking

On a successful SSO login, FA records an oidc entry in the identities table (provider='oidc', provider_subject=<sub>). On subsequent logins with the same sub, the identity row is found directly without an email lookup — making email changes at the IdP safe after first login.


Users & Roles

Feature Agent has a built-in user system with two roles:

  • Admin — Can manage projects, users, and all features via the dashboard or API
  • Approver — Linked to specific projects; can approve features for those projects

Creating Your First Admin User

After starting the server, create an admin user via the API (using the legacy ADMIN_API_KEY or dev mode):

bash
curl -s -X POST http://localhost:3100/api/users \
  -H "Content-Type: application/json" \
  -d '{"email": "admin@company.com", "name": "Admin", "role": "admin"}' | jq .

Save the returned api_key — use it to log into the dashboard and for all admin API calls going forward.

Creating Approvers

bash
# Create the approver
APPROVER=$(curl -s -X POST http://localhost:3100/api/users \
  -H "Authorization: Bearer $ADMIN_KEY" \
  -H "Content-Type: application/json" \
  -d '{"email": "po@company.com", "name": "PO Smith", "role": "approver"}')

APPROVER_ID=$(echo $APPROVER | jq -r .id)

# Link them to a project
curl -s -X POST http://localhost:3100/api/users/$APPROVER_ID/projects \
  -H "Authorization: Bearer $ADMIN_KEY" \
  -H "Content-Type: application/json" \
  -d "{\"project_id\": \"$PROJECT_ID\"}"

The approver can now log into the dashboard with their API key and approve features for their linked projects.

Auth Model

Key TypeWho Uses ItWhat It Can Do
Project API key (fa_...)External systems (CI, apps)Submit features, list features, poke for updates
Admin user key (fa_...)Human adminsEverything — project CRUD, user CRUD, approve any feature
Approver user key (fa_...)Product owners / approversApprove features for linked projects, view scoped features
Legacy ADMIN_API_KEY env varBackwards compatibilitySame as admin user key

You can manage all of this from the dashboard's Users tab (admin only), or via the API.


Protected Projects

Projects can be marked as protected to prevent accidental deletion:

bash
# Set on creation
curl -X POST http://localhost:3100/api/projects \
  -H "Content-Type: application/json" \
  -d '{"name": "my-app", "repo_url": "...", "autonomy_mode": "po_approval", "protected": true}'

# Or update an existing project
curl -X PATCH http://localhost:3100/api/projects/:id \
  -H "Content-Type: application/json" \
  -d '{"protected": true}'

Protected projects return 403 on delete attempts. The dashboard shows a "protected" badge instead of a delete button.


Self-Managed Development

Feature Agent is enrolled as its own project. This means you can submit feature requests for Feature Agent itself, and the agent will implement them on the feature-agent-self branch with po_approval mode.

The featureagent project is protected and configured with:

  • Autonomy mode: po_approval — features require approval before implementation
  • Target branch: feature-agent-self
  • Auto PR: enabled
  • Notifications: Telegram

To submit a feature for Feature Agent:

bash
curl -X POST http://localhost:3100/api/features \
  -H "Authorization: Bearer $FEATUREAGENT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Add dark mode to dashboard",
    "description": "Add a light/dark mode toggle to the admin dashboard..."
  }'

The feature will appear as awaiting_approval in the dashboard. After approval, the agent clones the featureagent repo, creates a feature branch, implements the change with tests, and opens a draft PR.


Diagnostic Tool

Verify your environment is correctly configured for headless Claude Code operation:

bash
npm run diagnose
npm run diagnose -- --prompt "Create a hello.txt file"

The diagnostic spawns Claude Code the same way the agent does and reports success/failure with detailed error messages for common issues (auth, permissions, timeout).


Production Deployment

Environment Variables

env
# Server
PORT=3100
FA_SECRET_MODE=file                       # 'file' (default) | 'managed' — see "Secrets management" below
ADMIN_API_KEY=a-strong-random-key-here    # Legacy admin key (optional — user-based admin auth is preferred)

# Storage
DATABASE_PATH=/var/featureagent/data/featureagent.db
WORKSPACE_DIR=/var/featureagent/workspaces

# Backup (VACUUM INTO snapshots — see docs/OPERATIONS.md §7 for the restore runbook)
BACKUP_DIR=./backups                  # Directory for snapshot files (created automatically)
BACKUP_RETENTION=14                   # Keep N most-recent snapshots; older ones pruned per backup
BACKUP_INTERVAL_MS=0                  # 0 = disabled (default). Set e.g. 3600000 for hourly backups

# Agent
AGENT_POLL_INTERVAL_MS=30000
AGENT_MAX_CONCURRENCY=3
AGENT_MODEL=sonnet
AGENT_MAX_TURNS=100
AGENT_MAX_RETRIES=3
AGENT_ALLOWED_TOOLS=                       # Comma-separated allowlist (empty = all tools)
AGENT_PERMISSIONS=dangerously-skip-permissions
CLAUDE_CLI_PATH=/usr/local/bin/claude

# Runtime (sandbox) — see docs/design/engine-runtime.md
RUNTIME=local                             # 'local' (host) or 'docker' (isolated container)
FA_RUNTIME_IMAGE=fa-runtime:latest        # Image for RUNTIME=docker (needs git + node + claude)
                                          # Curated images: fa-runtime (Node/TS), fa-runtime-python
                                          # (Python), fa-runtime-browser (Chromium + Playwright).
                                          # Build: docker build -t <tag> containers/<dir>
                                          # Select per-project/feature via the runtime_image API
                                          # field. See docs/USER_GUIDE.md §"Governed browser tooling"
                                          # for the full governed-browse + verify_command + verify_gate recipe.
FA_RUNTIME_NETWORK=none                   # 'none' | 'bridge' | <docker network>

# CORS
CORS_ALLOWED_ORIGINS=                      # Comma-separated global origins (projects can set their own)

# Auth
ANTHROPIC_API_KEY=sk-ant-...

# Integrations
TWILIO_ACCOUNT_SID=AC...
TWILIO_AUTH_TOKEN=...
TWILIO_WHATSAPP_FROM=whatsapp:+1...
GITHUB_TOKEN=ghp_...
PR_CHECK_INTERVAL_MS=600000                # Per-feature throttle for PR merge/review polling (default 10 min)

# Webhooks
WEBHOOK_MAX_RETRIES=3
WEBHOOK_TIMEOUT_MS=10000

# Fleet Attention Queue SLA thresholds (spec 057)
# How long a human-gated feature can wait before it is flagged as a breach in the
# Fleet Overview attention queue (/fleet.html) and the GET /api/features/admin/attention endpoint.
FLEET_APPROVAL_SLA_HOURS=24        # awaiting_approval → operator must approve within N hours
FLEET_CLARIFICATION_SLA_HOURS=24   # clarification_needed → operator must answer within N hours
FLEET_MERGE_SLA_HOURS=48           # implemented+PR → operator must merge within N hours

# Fleet Attention Alerts — push SLA breaches to project notification channels (spec 057 phase 2)
# Opt-in; off by default. A project opts in by configuring notification channels (existing mechanism).
FLEET_ATTENTION_ALERTS_ENABLED=false      # Set to 'true' to enable breach push-notifications
FLEET_ATTENTION_SWEEP_INTERVAL_MS=900000  # How often to check for new breaches (default 15 min)

Running with systemd

ini
# /etc/systemd/system/featureagent.service
[Unit]
Description=Feature Agent
After=network.target

[Service]
Type=simple
User=featureagent
WorkingDirectory=/opt/featureagent
ExecStart=/usr/bin/node dist/index.js
EnvironmentFile=/opt/featureagent/.env
Restart=always
RestartSec=10

[Install]
WantedBy=multi-user.target
bash
sudo systemctl enable featureagent
sudo systemctl start featureagent

Deploy with Docker Compose (one command)

The fastest path from a fresh clone to a running, persistent FA with per-run agent sandboxing is Docker Compose. The compose file keeps RUNTIME=docker as the default so the safe path (isolated per-run containers) is also the easy path — you never need to opt out of safety to get started.

Prerequisites: Docker 20.10+ and Docker Compose v2 (docker compose version).

bash
# 1. Copy the example env file and fill in your values
cp .env.example .env
#    Required: ANTHROPIC_API_KEY, GITHUB_TOKEN, ADMIN_API_KEY
#    Set RUNTIME=docker (already the compose default — do not set RUNTIME=local here)

# 2. Start FA in the background
docker compose up -d

# 3. Open the dashboard
open http://localhost:3100

FA is now running with:

  • Persistent statedata/ (SQLite DB) and workspaces/ stored in named Docker volumes; they survive docker compose restart and image rebuilds.
  • Per-run agent sandboxing — FA launches each agent run inside a fresh container via RUNTIME=docker; the untrusted agent never touches the host.
  • Auto-restart — the service restarts automatically on crash or host reboot.

Docker socket trust posture. The compose file mounts /var/run/docker.sock into the FA container so FA (the trusted Governor/orchestrator) can spawn per-run sandbox containers on the host daemon. This grants the FA process the ability to control Docker on the host, which is effectively host-root access. This is acceptable only because FA is the Governor, not the untrusted agent — keep RUNTIME=docker (the default) so the agents themselves remain sandboxed. Never set RUNTIME=local in a compose deployment; that removes the per-run isolation and defeats the purpose of the socket mount.

oauth auth mode (optional). If you prefer Claude OAuth credentials over an API key, uncomment the FA_AGENT_OAUTH_DIR volume line in docker-compose.yml and set FA_AGENT_AUTH=oauth in .env. FA will mount your local ~/.claude creds directory read-only into each agent container.

Useful operations:

bash
docker compose logs -f fa          # Tail live FA logs
docker compose restart fa          # Restart without rebuilding
docker compose up -d --build       # Rebuild image after a code change
docker compose down                # Stop (volumes are preserved)
docker compose down -v             # Stop AND delete state volumes (destructive!)

See docs/OPERATIONS.md for the full operations runbook (state, backup, and the relationship to the systemd path).

Secrets management (FA_SECRET_MODE)

FA supports two modes for the admin API key:

ModeBehaviour
file (default)FA self-provisions ADMIN_API_KEY at startup if absent and writes it to .env. Back-compat; existing installs unaffected.
managedFA reads ADMIN_API_KEY from the environment only. If the key is absent at startup, FA fails closed with an actionable error and writes nothing to disk. Use this with a secret manager (Vault, K8s Secrets, Doppler) that injects env vars at launch.

How to use managed mode:

  1. Set FA_SECRET_MODE=managed in your deployment config (systemd EnvironmentFile, K8s envFrom, etc.).
  2. Inject ADMIN_API_KEY=<your-key> via your secret manager before FA starts.
  3. FA reads the injected value on startup — no .env write, no self-provisioning.

Admin key rotation (see OPERATIONS.md for the full runbook):

bash
# Rotate via the API (admin auth required):
curl -X POST https://your-fa-host/api/admin/rotate-admin-key \
  -H "Authorization: Bearer <current-admin-key>"
# → { "admin_key": "fa_<new-64-hex>" }

In file mode, FA rewrites the .env file immediately and the new key is active in-process. In managed mode, FA updates in-process only — update your secret manager and redeploy to persist.

Security Checklist

  • [ ] Create admin users and use their API keys instead of the legacy ADMIN_API_KEY
  • [ ] Set ADMIN_API_KEY as a fallback to protect project management endpoints
  • [ ] Use fine-grained GitHub tokens with minimal repo access
  • [ ] Run in an isolated environment when using dangerously-skip-permissions
  • [ ] Restrict network egress to required endpoints only
  • [ ] Keep WORKSPACE_DIR on a separate volume
  • [ ] Regularly rotate API keys and tokens
  • [ ] Monitor agent activity via the dashboard and webhooks

Troubleshooting

Agent not processing features

  1. Check agent status: curl http://localhost:3100/api/status
  2. Ensure Claude Code is authenticated: claude --print "hello"
  3. Check logs for errors
  4. Verify features are in queued status (not stuck in analyzing or clarification_needed)

Claude Code permission errors

If features fail with permission errors:

  • Use AGENT_PERMISSIONS=dangerously-skip-permissions in isolated environments
  • Or configure Claude Code's .claude/settings.json to auto-approve tools

Webhook delivery failures

  • Check your endpoint is reachable from the Feature Agent server
  • Verify HTTPS certificates are valid
  • Increase WEBHOOK_TIMEOUT_MS for slow endpoints
  • Check logs for retry messages

WhatsApp messages not sending

  • Verify Twilio credentials in .env
  • Ensure recipients have joined the Twilio sandbox (for testing)
  • Check phone numbers are in whatsapp:+15551234567 format
  • Verify notifications_enabled is true on the project

Database issues

bash
# Reset the database (dev only!)
npm run db:reset

The SQLite database auto-creates on first run. If corrupted, delete the .db file and restart.

Released under the MIT License.