Skip to main content

AgentForge

A reusable framework for building multi-agent Telegram bots powered by Claude AI.

Zero dependencies by default — runs on Python 3.10+ stdlib. Optional extras for voice transcription and faster HTTP.

Native App Scaffold

This repo now includes an Apple-platform scaffold under apps/ for future native clients:

  • AgentForgeKit shared Swift module for models, networking stubs, and utilities
  • AgentForgeiOS SwiftUI app target for iOS 17+
  • AgentForgemacOS SwiftUI app target for macOS 14+

Build commands:

make native-build      # Builds the shared Swift package
make native-test       # Runs the shared Swift package tests
make ios-build         # Builds the iOS app target with xcodebuild
make macos-build       # Builds the macOS app target with xcodebuild
make native-verify     # Runs all of the above

Open the native project in Xcode with open apps/AgentForgeApps.xcodeproj.

Important: this native app currently uses both the Swift package manifest at apps/Package.swift and the Xcode project at apps/AgentForgeApps.xcodeproj. When adding, renaming, or removing Swift source files for the native app, update apps/AgentForgeApps.xcodeproj/project.pbxproj as well or Xcode may show "Cannot find ... in scope" errors even if swift build succeeds.

Quick Start

# Clone and install
git clone https://github.com/Martin-Rancourt/AgentForge.git
cd AgentForge
./install.sh

# Configure
agentforge init              # Launch interactive setup wizard

# Create agents
agentforge agent create assistant --model sonnet --thread-id 11
agentforge agent create researcher --model opus --thread-id 12

# Run
agentforge bot

How It Works

AgentForge turns a Telegram Supergroup with Forum Topics into a multi-agent workspace. Each topic routes to a different Claude-powered agent with its own identity, model, and memory.

Telegram Topic (thread_id: 12)
  → telegram_adapter.py (long-polling)
  → message_router.py (resolve agent config)
  → claude_runner.py (invoke Claude CLI with agent's soul.md)
  → Response sent back to Telegram

Message Flow

  1. Bot polls Telegram for new messages
  2. message_thread_id determines which agent handles the message
  3. Conversation history is loaded from SQLite (configurable context window)
  4. Claude Code CLI is invoked with the agent's soul.md as system prompt
  5. Response is stored in SQLite and sent back to Telegram

Architecture

agentforge/
├── src/agentforge/
│   ├── telegram_adapter.py   # Main bot — polling, routing, sending
│   ├── claude_runner.py      # Claude Code CLI subprocess wrapper
│   ├── message_router.py     # Thread → agent config resolution
│   ├── config.py             # Typed config loader (dataclasses)
│   ├── memory_manager.py     # SQLite + FTS5 conversation database
│   ├── vault.py              # Structured memory store (per-agent)
│   ├── voice.py              # Voice note transcription (optional)
│   ├── http_client.py        # Dual-stack: requests or urllib
│   └── cli/                  # CLI commands (bot, agent, memory, init, vault)
├── scripts/                   # Bash utilities
│   ├── telegram_send.sh      # Send messages to topics (with dedup)
│   ├── heartbeat.sh          # Proactive task runner (cron)
│   ├── healthcheck.sh        # Daily system health check
│   ├── log_action.sh         # Action logging (Google Sheets + local)
│   └── memory_consolidation.sh  # Legacy helper; built-ins now run via heartbeat.sh
├── tests/                     # Pytest suite (49 tests)
├── install.sh                # One-liner bootstrap
└── pyproject.toml            # Package metadata

CLI Reference

agentforge bot

Start the Telegram bot.

agentforge bot [--config path/to/config.json]

agentforge agent

Manage agents declaratively.

# Create — scaffolds soul.md, memory.md, daily/ directory
agentforge agent create <name> --model <model> --thread-id <id>

# List all configured agents
agentforge agent list

# Remove an agent (--keep-files to preserve files on disk)
agentforge agent remove <name> [--keep-files]

Creating an agent also prints the remaining onboarding checklist. Every new agent must receive its own GitHub App and Linear account from an administrator; neither identity is optional or shared. Once their credentials are stored, activate both together:

agentforge agent identity <name> --github-app spl-<name> --linear --linear-heartbeat
agentforge agent identity <name> --check

The GitHub bot user id is resolved from GitHub and the exact noreply author email is generated automatically; do not transcribe either by hand.

Models: sonnet (fast, balanced), opus (strongest reasoning), haiku (fastest, cheapest)

agentforge memory

Query the conversation database.

agentforge memory search "deployment error"    # Full-text search (FTS5)
agentforge memory recent 50                     # Last 50 messages
agentforge memory search "Claude Max" --agent architect --private  # Agent's private Telegram history
agentforge memory stats                         # Database statistics

agentforge vault

Structured memory store — facts, preferences, and knowledge per agent.

agentforge vault write --agent victor --category knowledge --title "API design" --content "..."
agentforge vault search "deployment"
agentforge vault list --agent archie

agentforge mcp

Expose 2DO tasks and vault files over Model Context Protocol:

pip install -e ".[mcp]"
agentforge mcp --host 0.0.0.0 --port 8081

Required environment:

  • AGENTFORGE_MCP_KEY or AGENTFORGE_API_KEY: the fleet's static bearer key, accepted on /mcp. The agents' MCP configuration (agents/shared/mcp/agentforge.json) carries it. The OAuth flow never hands it out: each registered client receives a token of its own (below).
  • AGENTFORGE_MCP_PUBLIC_URL: public HTTPS origin used in OAuth metadata, for example a Tailscale Funnel URL.

MCP task tools use the same 2Do readiness rules as heartbeat. Use twodo_due(agent=...) or twodo_due(list_name=...) for the next actionable item, or twodo_list(status="active") to list only tasks that are ready to run. Use status="open" when you need every incomplete task, including future-dated or cooling-down items.

Automation health checks:

  • twodo_health(list_name="Codex") verifies that the MCP server can resolve the 2Do store and reports open/actionable counts for the Codex queue.
  • vault_health(path="Logs/Code review/YYYY-MM.md") verifies that the MCP server can resolve the vault root and the code-review log path before PR-review runs try to write to it.

telegram_history_search(query, agent=None, limit=20) and telegram_history_recent(agent, limit=20, session_id=None) search or load real past Telegram messages from data/conversations.db (FTS5-backed) — distinct from vault_* (curated notes) and an agent's memory.md (curated summaries). Use these when a session needs to verify or recall an actual past exchange rather than a curated summary (e.g. persona/incarnation audio sessions).

budget_status() and budget_chart() show the token budget to any Claude the connector reaches, with the content of Telegram's /status. budget_status returns the JSON of agentforge budget status --format json once per backend (Claude, then Codex) — windows, utilization, pace target, each tier's verdict — stamped in Montreal time. budget_chart returns the /status PNG as an MCP image, followed by the logged week (consumption and throttle line per provider, every announced window end marked) once the gate has logged usage; when the charts extra is missing, no backend has a dated seven-day window, or the PNG is over 1 MB, it returns a JSON object that says so and carries the series the chart would have drawn. Both are read-only (BR-AF-047).

Optional environment:

  • AGENTFORGE_MCP_OAUTH_REDIRECT_ALLOWLIST: comma-separated HTTPS redirect URI allowlist. Entries may end in * for prefix matching and default to https://claude.ai/*.
  • AGENTFORGE_MCP_OAUTH_ENROLLMENT_FILE: path of the enrollment flag; defaults to $AGENTFORGE_ROOT/data/mcp-oauth-enrollment.
  • AGENTFORGE_MCP_OAUTH_CLIENTS_FILE: path of the OAuth client registry; defaults to $AGENTFORGE_ROOT/data/mcp-oauth-clients.json.

The OAuth compatibility endpoints support authorization-code flow with S256 PKCE. Authorization only proceeds for a registered client and allowlisted redirect URIs.

Client enrollment is closed by default. /oauth/register and /oauth/authorize prove no human identity on their own — registration creates a client and authorization auto-approves — so a publicly reachable server would otherwise hand a working token to any caller. Both endpoints answer 403 access_denied unless the owner has an open enrollment window; every denial is logged with the caller address the proxy forwarded (X-Forwarded-For, falling back to the socket peer — which behind a tunnel is the tunnel's own local end). /mcp itself is unaffected: already-connected clients keep working across the window being closed.

Open a window only while connecting (or reconnecting) a client, and keep it short. Two gestures, same flag:

  • From a phone: send /mcp_enroll to any AgentForge Telegram bot — it opens a fixed 5-minute window and replies with the closing time. No host session needed.
  • On the server: agentforge mcp enroll --minutes N (1–60), agentforge mcp enroll --close, or no option to print the state.

A client refused while the window is closed receives a 403 whose description names both gestures. If a reconnect still fails with the window open, run agentforge mcp doctor.

The window also lapses on its own at the written expiry; a missing, empty, unparseable or past value all mean closed.

One registration, one client (SID-736). Every /oauth/register creates a client of its own in data/mcp-oauth-clients.json: a random client id, a client secret, and — once the flow completes — a token, none of them shared with any other client or with the fleet's key. The file keeps only SHA-256 digests of the secret and the token, so reading the instance's files yields neither; a registry that exists but cannot be read or written refuses every OAuth call and every client token (the fleet's key is unaffected). Two commands, run as the account that owns the service's data/:

agentforge mcp clients                    # id, name, created, token issued or not, last connection
agentforge mcp clients revoke <client-id> # cut this client alone; the others keep working

The last connection is written by the call itself, each time a client's token opens /mcp. Revoking deletes the client: its token is refused at once and it cannot start the flow over with the id and secret it holds. Closing the enrollment window is not a revocation — it only stops new registrations. A token stays valid until its client is revoked, whatever expires_in says.

The secret URL path /p/<AGENTFORGE_MCP_URL_TOKEN>/mcp no longer exists: it skipped the bearer check altogether, so no revocation could reach it. Point a connector at <AGENTFORGE_MCP_PUBLIC_URL>/mcp and let it enroll.

Upgrading from the fixed client. Before SID-736 every caller received one fixed client (AGENTFORGE_MCP_OAUTH_CLIENT_ID / AGENTFORGE_MCP_OAUTH_CLIENT_SECRET, generated into .env) and the fleet's key as its token. That client is invalidated, not migrated — migrating it would leave the connector holding the fleet's key. After the deploy, reconnect each connector once during an enrollment window (/mcp_enroll, or agentforge mcp enroll --minutes 5): it registers itself and receives its own token. Two consequences to handle by hand: the old AGENTFORGE_MCP_URL_TOKEN, AGENTFORGE_MCP_OAUTH_CLIENT_ID and AGENTFORGE_MCP_OAUTH_CLIENT_SECRET lines in .env are dead and can be removed; and a connector that was enrolled before still holds the fleet's static key as its access token, which keeps working until AGENTFORGE_MCP_KEY itself is rotated in .env (the agents' configuration expands it from there; restart the MCP service and the agents together).

agentforge init

Bootstrap a new instance from scratch.

agentforge init [--dir /path/to/instance]
agentforge init --non-interactive [--dir /path/to/instance]

Default behavior launches an interactive wizard that validates the Telegram token, detects a chat ID after /start, checks Claude Code CLI readiness, captures bot identity, and writes .env, agents/<name>/config.json, canonical vault prompt files under vault/Agents/<name>/, and shared handbook seeds under vault/Agents/_team/.

Use --non-interactive to preserve the original scaffold-only flow.

The interactive wizard also checks gh auth status and prints a non-blocking warning if GitHub auth is missing. Use either gh auth login or set GH_TOKEN in .env before running GitHub-driven automation.

Configuration

bot/config.json

{
  "telegram": {
    "token_env_var": "AGENTFORGE_TELEGRAM_TOKEN",  // env var name holding the token
    "bot_username": "agentforge_bot",               // optional expected bot username
    "allowed_chat_ids": [123456789],                // whitelist
    "owner_user_ids": [123456789],                  // from.id allowed /mcp_enroll, /claudelogin, /claudecode (empty = refused)
    "group_chat_id": -100123456789,                 // supergroup ID
    "topics": {                                      // named topic shortcuts
      "general": 11,
      "dev": 12
    }
  },
  "claude": {
    "model": "sonnet"           // default model — no per-call spend cap (SID-645)
  },
  "threads": {
    "11": { "name": "assistant", "model": "sonnet" },
    "12": { "name": "researcher", "model": "opus" }
  }
}

Recent conversation context is selected dynamically at runtime: AgentForge uses messages from roughly the last 4 hours, capped at 20 messages, with a fallback floor of 3 messages if the window is sparse.

.env

AGENTFORGE_TELEGRAM_TOKEN=your_bot_token_here
AGENTFORGE_ROOT=/path/to/instance
# Optional: preferred GitHub auth token for gh/API usage
GH_TOKEN=ghp_xxx
# Compatibility only; prefer GH_TOKEN above
GITHUB_TOKEN=ghp_xxx
# Optional: preserve gh credential lookup when HOME is overridden
# (runs with no agent only; see below)
GH_CONFIG_DIR=/Users/you/.config/gh

When AgentForge invokes Claude CLI, it sets HOME to AGENTFORGE_ROOT. Setting GH_CONFIG_DIR (or letting AgentForge infer it from your original home directory) keeps gh credential resolution stable in that environment, for a run that has no agent (the OAuth-refresh ping).

A run that has an agent does not get that directory, whether the agent has a GitHub identity or not: it is given an empty gh config directory of its own (<runtime>/data/gh-empty-config/<agent>), whatever GH_CONFIG_DIR says, so that a gh call that drops GH_TOKEN (env -u GH_TOKEN gh …) answers "not logged in" instead of acting as your own account. There is no setting for it: the default is closed. This is a tripwire, not a boundary: a run is still your Unix user and can read ~/.config/gh/hosts.yml. Your own gh aliases (gh co) are not available to an agent's run.

Multi-Agent System

Each agent gets its own prompt identity via soul.md, compact durable memory via memory.md, and working notes in daily/:

agents/
├── victor/
│   ├── config.json    # Runtime config, logs, cron, operational files
│   └── ...
├── archie/
└── catherine/
vault/
└── Agents/
    ├── victor/
    │   ├── soul.md    # Canonical personality, role, instructions
    │   ├── memory.md  # Canonical compact durable memory
    │   └── daily/     # Canonical consolidation input only
    └── _team/
        ├── CLAUDE.md  # Shared team-wide operating rules
        ├── memory.md  # Shared compact injected memory
        ├── index.md   # Shared handbook index
        └── *.md       # Team handbook pages, not auto-injected

At runtime, AgentForge builds prompt context from:

  1. CLAUDE.md
  2. vault/Agents/_team/CLAUDE.md if present, else agents/shared/CLAUDE.md, else CLAUDE.local.md
  3. vault/Agents/_team/memory.md if present, else agents/shared/memory.md
  4. vault/Agents/<name>/soul.md if present, else agents/<name>/soul.md
  5. vault/Agents/<name>/memory.md if present, else agents/<name>/memory.md

Daily notes are not auto-injected. Agents can:

  • Have different Claude models (opus for complex reasoning, haiku for quick tasks)
  • Maintain separate conversation histories and per-agent memory
  • Store per-agent structured memory in the vault

For operational questions about current runtime behavior, prefer the generated read-only note vault/Agents/_team/system-state.md. After manual config.json edits or after upgrading to a release that changes shared verification guidance, run agentforge agent sync-system-state.

  • Delegate directly via each agent's bot for 1:1 handoffs; use project topics only for shared multi-agent work

Project Topics

Beyond agent topics, you can configure project topics where multiple agents collaborate:

"project_topics": {
  "42": {
    "label": "🏗 Dev",
    "agents": ["victor", "archie"],
    "default_agent": "victor"
  }
}

Use @agentname mentions to address a specific agent in a shared topic.

With nobody mentioned, the topic's default_agent answers. When a topic sets none, the API server (and POST /api/messages) uses the first agent it lists in agents, and an agent's own Telegram bot (let through by its default_for_threads) answers as itself. A topic that lists no agent at all is a configuration error (the API answers 500 naming the topic): the framework has no agent of its own to fall back on.

Restart and version announcements

The architect's bot posts one message when the agents restart and one when the installed version changes, both to a single topic. Name it with telegram.agentforge_topic in that agent's config.json:

"telegram": { "agentforge_topic": { "group_id": "-1001234567890", "thread_id": 42 } }

Left out, the group is TELEGRAM_GROUP_CHAT_ID from .env and the thread is the one topic bot/config.json declares under telegram.topics. When neither names a destination (no group, or several topics and none chosen), nothing is sent and the reason is logged.

Scripts

Script Purpose Trigger
telegram_send.sh Send messages to topics Manual / cross-agent
heartbeat.sh Run the single 2Do-backed heartbeat Cron
healthcheck.sh System health report Cron (daily)
log_action.sh Log external actions to Sheets + file After side-effects
memory_consolidation.sh Legacy helper script Manual / legacy

Dedup

telegram_send.sh has built-in deduplication: identical messages to the same topic within 60 seconds are silently dropped.

Memory System

AgentForge provides three layers of memory:

Layer Storage Purpose
Conversations SQLite + FTS5 Full message history, searchable
Vault SQLite + FTS5 Structured facts/preferences per agent
Files Markdown soul.md (identity), memory.md (long-term), daily/ (learnings)

memory.md stores durable facts only. Recurring consolidation work should be modeled as 2Do tasks, typically assigned to architect, instead of separate cron jobs.

Todo System

AgentForge uses the structured 2Do backend as the canonical task system. Every mention of todo, to-do, task system, or 2Do refers to that same system.

Every agent runs a single heartbeat. The heartbeat ticks on an interval set at deploy time (agentforge service generate/migrate-heartbeat --heartbeat-interval, 5 minutes by default) via the systemd timer's OnCalendar= — not a hardcoded number in code, and there is no per-agent throttle on top of it. Each tick hands its few run slots to the agents whose last attempt is oldest (agents holding In Review or release QA work first), so no agent is starved by its place in the list; see docs/run-limits.md § 6. Before the model is called, heartbeat asks the 2Do task engine for one actionable task. If there is no actionable work, the model is not called.

Some steps of the tick need no model at all and run before the agents: merging green PRs, advancing a release, the model/CLI audit, and the scheduled scripts. Periodic work of that kind (SID-665, BR-AF-065) is a task of the « Système » 2Do list that names a script of the package's whitelist (src/agentforge/templates/scripts/scheduled/); see docs/scheduled-scripts.md. agentforge schedule prints everything that runs by itself — timers, the steps of the tick and the scheduled scripts — read-only.

The MCP 2Do surface reuses that same task engine for active/ready selection, including named 2Do lists used by external workers.

Actionable heartbeat injection includes:

  • overdue tasks
  • tasks due now
  • tasks without a due date
  • tasks not cooling down after a recent failure

Selection order is currently:

  • higher 2Do priority first
  • then tasks with a due date
  • then undated/background tasks

When a user says "update your task ..." or references the todo system, that means updating the 2Do task's notes body. The notes body is the prompt injected into heartbeat.

The installer seeds vault/Agents/_team/todo-system.md with the canonical rules for humans and agents.

Slash Commands

The following slash commands are available for managing todos (defined in commands/):

Command Description
/todo-add Add a todo to an agent
/todo-all List all todos for an agent
/todo-next Show the next single todo
/todo-nexts Show the next few todos
/todo-projet Show todos for a specific project tag
/todo-projets List all projects that have todos
/todo-goal Show todos for a specific goal tag
/todo-goals List all goals that have todos
/todo-autres Show todos without a project or goal tag
/todo-migrer Migrate unchecked todos from a daily note to the Inbox

Heartbeat Integration

Heartbeat injects only actionable 2Do tasks, with at most one task executed per heartbeat run. Legacy extra cron templates remain in the repo for migration reference but ship disabled.

When expects_completion_signal: true is set for a cron config, the wrapper expects one of TASK_DONE, TASK_SKIP, TASK_FAIL, or one of the LINEAR_* signals below. Missing all of them is treated as a silent failure.

Linear Queue

Product and development work lives in Linear; 2Do keeps the operational and recurring work. Heartbeat reads both and serves one merged queue, not two:

  • Linear issues assigned to the agent itself (assignee.isMe) in state Ready, In Progress or In Review are read under that agent's own Linear identity (integrations.linear, see agent_identity.py). Todo is refined work the go has not been given for yet (SID-646) — a waiting room this queue never reads from; the ticket becomes actionable the moment it moves to Ready.
  • Both sides sort with one comparison, and the order is decided (SID-646, 2026-09-21): the 2Do block first, as a block — a 2Do task is what the agent must do, a Linear ticket is what it does with the time left — then Linear issues right to left on the board (In Review, In Progress, Ready), a column emptying before the next is touched. Priority only breaks ties inside a block or column: Linear priorities map onto 2Do's tiers (Urgent → starred, High/Medium/Low as named, no priority → low). A low 2Do task therefore runs before an Urgent ticket; that risk is carried by the token-budget gate, which filters eligibility per priority level before the order applies.
  • The same token-budget gate evaluates both.

Two opt-ins, both per agent and both off by default: integrations.linear.enabled grants the identity, integrations.linear.heartbeat additionally lets the cron loop push assigned issues into the prompt.

The status moves with the work. Serving an issue moves it to In Progress with a comment naming the agent; the agent then answers with LINEAR_SHIP <ID> (→ To deploy), LINEAR_SKIP <ID> (stays queued) or LINEAR_BLOCKED <question> (posts the question on the same ticket, adds blocked-by-PM-decision, and assigns the human owner named in the vault routing table — there is no separate decision ticket), optionally followed by LINEAR_NOTE <ID> <one line>, which is posted as a Linear comment. Martin replies and removes the label; the merge pass then gives the ticket back to the agent that asked (SR-014, linear_service.resume_handed_off). python -m agentforge.linear_cli due|ship|skip|blocked is the same machinery on the command line.

LINEAR_SHIP is a request, not a completion claim (SID-414, Martin 2026-09-17): it asks to be put in the next release, either because the ticket's own scope is done or because the rest of it needs that code deployed first — the second case requires a LINEAR_NOTE saying so explicitly. To deploy therefore carries no gate at all; no agent verb ever writes Done. Done is written only by code, from an observed fact — a merge that needs no deployment, or a release closing — and only once evaluate_done_gate (linear_service.py) finds no open checkbox anywhere in the ticket's description (heading-agnostic since SID-640 — a checkbox engages wherever it sits, not only under an Acceptance criteria/Business rules heading) or in that of any ticket it is connected to by a duplicate relation. A release that closes over a ticket still carrying an open box returns it to In Progress, reassigned, instead of leaving it stranded in To deploy.

Failure notifications are sent to the Telegram topic Daily, which must resolve through the normal topic lookup path used by telegram_send.sh (for example via TELEGRAM_THREAD_DAILY).

Per-agent identity

Every agent shares one runtime HOME, one Claude OAuth account and one .env, so by default they all act under a single identity everywhere — one gh user, one git committer. integrations in agents/<name>/config.json scopes that per agent; agent_identity.build_identity_env() resolves the per-agent-named credentials out of the shared .env into generic names in the child process, at the single injection point all three backends pass through.

GitHub identity is a GitHub App per agent, not an account per agent: GitHub's Terms of Service allow one free machine account per person, so a fleet of machine accounts is not an option, while an App installation surfaces a distinct <slug>[bot] user, its own audit-log actor and its own rate-limit bucket at no seat cost. Tokens are short-lived installation tokens minted on demand by runtime/scripts/gh_app_token.sh — there is no long-lived GitHub token stored anywhere.

Declare it with the CLI rather than by hand:

agentforge agent identity victor                              # what it commits as today
agentforge agent identity victor --github-app spl-victor --linear
agentforge agent identity --all --github-app 'spl-{agent}'    # the whole fleet, one command
agentforge agent identity --all --check                       # resolve them the way the runner does

GitHub only renders a commit as the bot when the author email is exactly <bot-user-id>+<slug>[bot]@users.noreply.github.com. Any other address produces a commit that looks right and links to nothing — a silent failure, since nothing about the commit looks wrong. So the address is never transcribed by hand: the numeric id is read from GET /users/<slug>[bot] and the CLI builds the address from it. Pass --github-bot-id only to skip the lookup (offline, or a slug GitHub cannot resolve).

{agent} in the slug expands to the agent's name, which is what lets --all describe a whole fleet in one flag. It is a template, not a bundled roster: one instance's App slugs and bot ids are tenant data, and the package stays the reusable engine (tests/test_package_data_isolation.py). --all refuses a literal slug, since that would hand every agent the same identity. If one agent fails, the others still apply and the failures are named with a non-zero exit — never a partial run reported as success.

Isolation is attribution only, by design. Every child still inherits the whole shared .env, so an agent's process can technically read another agent's credentials. What the _OWNED_KEYS discipline does guarantee is that no agent accidentally inherits another's identity: heartbeat.sh loops the whole fleet in one process, so a leftover GH_TOKEN is explicitly cleared, never left set.

Per-agent MCP servers

The fleet signs in to Claude Code with one shared claude.ai account, and Claude Code would load every connector of that account into every run — same OAuth account, no per-agent granularity, the operator's mail, calendar and hosting providers within reach of any agent that goes astray. So the launcher switches that loading off: ENABLE_CLAUDEAI_MCP_SERVERS=false in the environment of every child (claude_runner._build_common_env, which the Claude CLI, the ollama bridge and the OAuth-refresh ping all go through; SID-722). The operator's own Claude Code sessions are not fleet runs and keep their connectors. A fleet agent has the servers declared in agents/<name>/config.json under claude.mcp_config and nothing else: scoped to the agents that should have them, reviewed in git, and applied by the same code path everywhere. The variable is used instead of --strict-mcp-config on purpose — the flag would also drop the servers a project or a plugin declares.

Bundled definitions live in agents/shared/mcp/, synced from the package by agentforge init and agentforge update apply (see the README in that directory). They contain ${VAR} references only — never a credential — and are expanded by Claude Code at launch.

agentforge agent mcp list                      # what every agent currently loads
agentforge agent mcp add victor agentforge     # AgentForge's own MCP server, reached at 127.0.0.1
agentforge agent mcp remove ben sentry

linear is the exception: it is appended automatically for any agent with integrations.linear.enabled, so the identity and the tools stay one decision instead of two fields that can drift apart.

Two caveats. Only the claude backend receives --mcp-config — codex and ollama get no MCP at all, which is why runtime/scripts/linear.sh exists as a shell client covering all three. And a server that used to be account-level (an OAuth connector the operator authorised in a browser) is out of an agent's reach altogether, not moved: giving an agent the same capability means declaring a server here, with a key in the shared .env that every child inherits (see the isolation note above) — a decision for that agent, not a default.

Dependencies

Core: Python 3.10+ stdlib only (no pip install required for basic operation)

Optional extras:

pip install agentforge[full]    # requests + faster-whisper
pip install agentforge[dev]     # pytest + pytest-cov
Extra Package Purpose
full requests>=2.28 Faster HTTP client (falls back to urllib)
full faster-whisper>=1.0 Voice note transcription
dev pytest>=7 Test runner
dev pytest-cov Coverage reports

External Requirements

  • Claude Code CLI — must be installed at $AGENTFORGE_RUNTIME_ROOT/.local/bin/claude
  • Telegram Bot — create one via @BotFather, enable Forum Topics in your supergroup

Full Installation Guide (Fresh Linux Server)

This is the step-by-step procedure to install AgentForge on a fresh Linux machine (Ubuntu/Debian).

Prerequisites

# System packages
sudo apt update && sudo apt install -y python3.12 python3.12-venv git curl

# Node.js (required for Claude Code CLI)
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt install -y nodejs

Step 1 — Create a dedicated user

sudo adduser --disabled-password --gecos "AgentForge Bot" agentforge
sudo loginctl enable-linger agentforge   # allows systemd user services to run without login

Step 2 — Install Claude Code CLI

sudo -u agentforge -i   # switch to agentforge user

# Install Claude Code
npm install -g @anthropic-ai/claude-code
# Verify
claude --version

Note: Claude Code requires an Anthropic API key or Claude Max subscription. Run claude once to authenticate.

Step 3 — Clone and install AgentForge

# Still as agentforge user
cd ~
git clone https://github.com/Martin-Rancourt/AgentForge.git
cd AgentForge
./install.sh ~

The installer will:

  1. Check Python 3.10+ ✓
  2. Check Claude CLI ✓
  3. Create a virtual environment
  4. Install the agentforge package
  5. Run the interactive agentforge init wizard
  6. Generate a systemd user service
  7. Check for Syncthing

Step 4 — Set up Telegram

Before running the bot, you need a Telegram Bot + Supergroup:

  1. Create a bot — Talk to @BotFather on Telegram:

    • /newbot → choose a name and username
    • Copy the bot token
  2. Create a Supergroup with Forum Topics enabled:

    • Telegram → New Group → add your bot → make it a Supergroup
    • Group Settings → Topics → Enable
    • Create one topic per agent (e.g. "Assistant", "Researcher", "Dev")
  3. Get the chat ID and thread IDs:

    • Send a message in each topic
    • Use https://api.telegram.org/bot<TOKEN>/getUpdates to see recent messages
    • Note the chat.id (negative number) and each message_thread_id
  4. Update config:

    # Edit .env with your token
    nano ~/.env
    # → AGENTFORGE_TELEGRAM_TOKEN=your_token_here
    
    # Edit config with your chat ID and thread IDs
    nano ~/bot/config.json
    

Step 5 — Create your agents

# Create agents matching your Telegram topics
agentforge agent create assistant --model sonnet --thread-id 11
agentforge agent create researcher --model opus --thread-id 12

# Edit each agent's personality
nano ~/vault/Agents/assistant/soul.md
nano ~/vault/Agents/researcher/soul.md

Step 6 — Start the bot

# Enable and start the systemd service
systemctl --user daemon-reload
systemctl --user enable --now agentforge

# Verify it's running
systemctl --user status agentforge

# Watch logs
journalctl --user -u agentforge -f
crontab -e

Add:

# Heartbeat — single scheduler for all built-in and per-agent cron jobs
*/30 * * * * AGENTFORGE_ROOT=$HOME $HOME/.venv/bin/agentforge heartbeat >> $HOME/logs/heartbeat.log 2>&1

# Daily health check at 08:00
0 8 * * * $HOME/scripts/healthcheck.sh >> $HOME/logs/healthcheck.log 2>&1

Syncthing — Cross-Device Knowledge Sync

AgentForge uses Syncthing to sync your knowledge vault (Obsidian vault, agent memory, etc.) across devices. This is optional but recommended if you want to access your bot's brain from your phone or laptop.

Architecture

┌──────────────┐     Syncthing      ┌──────────────┐
│ Linux Server │ ◄──────────────► │    macOS     │
│  (always-on) │                    │  (laptop)    │
└──────┬───────┘                    └──────────────┘
       │
       │  Syncthing
       ▼
┌──────────────┐
│   iPhone     │
│ (Möbius Sync)│
└──────────────┘

The Linux server is the hub — always on, always syncing. Other devices sync when they're online.

Linux Server Setup

# Install Syncthing
sudo apt install -y syncthing

# Enable as a user service (runs without login thanks to lingering)
systemctl --user enable --now syncthing

# Access the web UI
# Syncthing listens on http://localhost:8384
# If remote: ssh -L 8384:localhost:8384 agentforge@your-server

Configure the shared folder:

  1. Open Syncthing web UI (http://localhost:8384)
  2. Add Folder → set the path to your knowledge vault (e.g. ~/obsidian-brain/)
  3. Set a Folder ID (e.g. agentforge-vault) — you'll need this on other devices
  4. Under Advanced → set Folder Type to "Send & Receive"

macOS Setup

# Install via Homebrew
brew install syncthing

# Start as a background service
brew services start syncthing
  1. Open http://localhost:8384 in your browser
  2. Add Remote Device → paste the Linux server's Device ID (found in the server's Syncthing UI under Actions → Show ID)
  3. On the Linux server, accept the incoming device request
  4. The shared folder should auto-appear for approval on macOS — accept it
  5. Choose a local path (e.g. ~/Documents/agentforge-vault/)

For Obsidian: Point Obsidian's vault to the synced folder. Your bot's knowledge base stays in sync with your laptop.

iOS Setup (Möbius Sync)

Möbius Sync is the only Syncthing-compatible app for iOS. It costs ~$5 USD (one-time purchase).

  1. Install Möbius Sync from the App Store
  2. Open the app → go to Settings → note your Device ID
  3. On your Linux server Syncthing UI:
    • Add Remote Device → paste the iOS Device ID
    • Share the agentforge-vault folder with this device
  4. Back in Möbius Sync:
    • Accept the incoming device connection
    • Accept the shared folder
    • Choose a local location (e.g. under "On My iPhone" or iCloud)
  5. For Obsidian Mobile: Point Obsidian to the Möbius Sync folder to browse your vault on the go

Tip: Möbius Sync works best with "Background Sync" enabled (Settings → Background Sync → On). iOS may still throttle it, so open the app occasionally to force a full sync.

Syncthing Tips

  • Device IDs are the only thing you exchange — no accounts, no cloud, fully P2P encrypted
  • Ignore patterns: Add a .stignore file in your vault root to exclude files:
    .obsidian/workspace.json
    .DS_Store
    *.tmp
    
  • Conflict resolution: Syncthing creates .sync-conflict-* files if two devices edit the same file simultaneously. Resolve manually.
  • Bandwidth: Initial sync can be slow for large vaults. Subsequent syncs are incremental and near-instant.

Testing

pip install -e ".[dev]"
pytest                      # Run all 49 tests
pytest --cov=agentforge     # With coverage

Tests cover: config validation, message routing, memory manager, vault operations.

Releases

This project uses release-please for automated releases:

  1. Use conventional commits (feat:, fix:, docs:, chore:, etc.)
  2. Release-please accumulates changes and opens a Release PR with version bump + changelog
  3. Merge the Release PR → GitHub Release + git tag created automatically

Durable changes to AgentForge code, packaged scripts, templates, services, or an installed runtime must flow through branch -> PR -> CI -> merge -> PyPI release -> pip install --upgrade -> restart/validation. Direct edits in runtime/.venv/, runtime/scripts/, or another live runtime path are temporary incident hotfixes only and must be followed immediately by a PR plus operational changelog note.

License

MIT

Metadata

Release files for agent-forge-installer 0.5.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for agent-forge-installer 0.5.0
File Size Uploaded Provenance
agent_forge_installer-0.5.0.tar.gz 1.8 MB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for agent-forge-installer 0.5.0
File Interpreter ABI Platform
agent_forge_installer-0.5.0-py3-none-any.whl Python 3 none any Details

Total release size: 2.9 MB

Release files / agent_forge_installer-0.5.0.tar.gz

Download URL agent_forge_installer-0.5.0.tar.gz
Size 1.8 MB
Tags Source
SHA-256 checksum
How to use checksums
4dcb7379918a455bdd06d4fc02c3b4f2d791c771b9bca557474544dd251c9e89
BLAKE2b-256 checksum
How to use checksums
783de58cfa9937db84b05bdddc76e1dc377c37cc6ee826289dbbb607e032075e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 23, 2026.

Transparency log

Release files / agent_forge_installer-0.5.0-py3-none-any.whl

Download URL agent_forge_installer-0.5.0-py3-none-any.whl
Size 1.1 MB
Tags Python 3
SHA-256 checksum
How to use checksums
ca596650ff07388473f01d70e7337a1f782f0a08d08043d701ed9dbc8bf06cb8
BLAKE2b-256 checksum
How to use checksums
a337efd613467b1bbe8fd83bbbd777b9487d353e15c55f7f80d666f3d5ec790f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 23, 2026.

Transparency log

Security for agent-forge-installer 0.5.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for agent_forge_installer-0.5.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 23, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.5.0 This release

2 release files Full provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page