Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
4996cd9
feat: add optional startup notification after Socket Mode connects
Apr 16, 2026
74cbc9c
Merge pull request #9 from chatbotgang/feat/startup-notify
htkuan Apr 17, 2026
6471136
Feat: add Claude Code worktree mode for per-session filesystem isolation
htkuan Apr 17, 2026
aded32a
Merge pull request #10 from htkuan/feat/claude-worktree-mode
htkuan Apr 17, 2026
bd3c15a
Docs: sync env var tables across CLAUDE.md, README.md, and .env.example
htkuan Apr 17, 2026
dceba9b
Docs: mark ANTHROPIC_API_KEY as optional
htkuan Apr 17, 2026
76768fe
Merge pull request #11 from htkuan/docs/env-vars-sync
htkuan Apr 17, 2026
c516319
Fix: count Slack message length in UTF-8 bytes, not characters
htkuan Apr 24, 2026
8a08635
Feat: add heartbeat platform for proactive agent triggering
htkuan Apr 26, 2026
a848300
Merge pull request #12 from htkuan/worktree-heartbeat
htkuan Apr 26, 2026
41ee898
Feat: heartbeat-flavored system prompt for Claude controller
htkuan Apr 26, 2026
13fff4a
Refactor: platform-built system prompt; agent stays platform-agnostic
htkuan Apr 26, 2026
99779cf
Refactor: ephemeral sessions for one-shot platforms; tighten heartbea…
htkuan Apr 26, 2026
40a921c
Merge pull request #13 from htkuan/feat/heartbeat-system-prompt
htkuan Apr 26, 2026
4465183
Feat: configurable Claude Code effort level (default xhigh)
htkuan Apr 26, 2026
1edcbe3
Merge pull request #14 from htkuan/feat/claude-effort-config
htkuan Apr 26, 2026
2e54183
Fix: Slack adapter dropping main reply when followed by tool calls
htkuan Apr 28, 2026
8832e79
Merge pull request #16 from htkuan/fix/slack-final-message-loses-text
htkuan Apr 28, 2026
1f67f0e
Fix: force-remove worktrees with untracked or modified files on cleanup
Apr 28, 2026
92291d3
Merge pull request #17 from chatbotgang/fix/worktree-cleanup-force-re…
htkuan Apr 29, 2026
5cd0274
Feat: surface bot identity in Slack adapter system prompt
htkuan May 1, 2026
371b7ae
Refactor: drop bot display name from system prompt
htkuan May 1, 2026
5c683b7
Merge pull request #18 from htkuan/feat/slack-bot-identity
htkuan May 1, 2026
3dc6853
Feat: cross-session prompt dedupe at the bridge layer
htkuan May 30, 2026
a1d76bf
Docs: document Bridge architecture and dedupe layer
htkuan May 30, 2026
8496ba6
Fix: release dedupe slot when controller yields is_error=True
htkuan May 30, 2026
0c9fa36
Merge pull request #21 from htkuan/feat/bridge-dedupe
htkuan May 31, 2026
83a70a0
Fix: break Claude stdout loop on result event to free wedged slots
htkuan Jun 2, 2026
a9fc21d
Merge pull request #22 from htkuan/fix/claude-stream-break-on-result
htkuan Jun 4, 2026
9ae8c02
Feat: add Slack channel allow-list gate
htkuan Jun 4, 2026
665377c
Merge pull request #23 from htkuan/feat/slack-channel-allowlist
htkuan Jun 4, 2026
212be82
Feat: surface Claude usage/cost as a Slack footer
htkuan Jun 5, 2026
6f182ef
Merge pull request #24 from htkuan/feat/slack-usage-report
htkuan Jun 5, 2026
7206ec4
ci: automate releases from Conventional Commits
htkuan Jun 5, 2026
8558179
docs: trim one-time bootstrap from releasing guide
htkuan Jun 5, 2026
351aeda
Merge pull request #25 from htkuan/ci/release-automation
htkuan Jun 5, 2026
5e58731
0.2.0
Jun 5, 2026
48be703
fix: add catch-all block_actions handler to prevent unhandled request…
Jun 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 57 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,32 @@ AGENT_BRIDGE_SLACK_BOT_TOKEN=
# Slack App-Level Token for Socket Mode (xapp-...)
AGENT_BRIDGE_SLACK_APP_TOKEN=

# Optional: Send a Slack message after Socket Mode connects
# AGENT_BRIDGE_SLACK_STARTUP_NOTIFY_CHANNEL=C12345678
# AGENT_BRIDGE_SLACK_STARTUP_NOTIFY_MESSAGE=Bot is online

# Optional: comma-separated channel-name allow-list. Empty/unset = allow every
# channel. When set, only messages from these channels reach the agent; all
# others (including DMs, which have no name) get a fixed reply and are dropped.
# Use names (#hash, whitespace, and case are ignored), not channel IDs.
# AGENT_BRIDGE_SLACK_ALLOW_CHANNELS=ops-alerts,team-eng,incidents

# Optional: reply sent to messages from non-allowed channels (and DMs when the
# allow-list is set). Defaults to a fixed English notice.
# AGENT_BRIDGE_SLACK_CHANNEL_NOT_ALLOWED_MESSAGE=Sorry, I'm not available here.

# Optional: append a usage/cost footer below the final agent reply.
# Unset/false = no footer. The session line is shown only when the bridge has
# tracked the session from its first turn (omitted after restarts / for
# pre-existing sessions). Defaults to false.
# AGENT_BRIDGE_SLACK_USAGE_REPORT_ENABLED=false
# Optional: override the footer format with a {placeholder} template. Unknown
# placeholders render blank. Each token/cost field also has a session_-prefixed
# variant (e.g. {session_cost_usd}). Available: cost_usd, input_tokens,
# output_tokens, cache_read_tokens, cache_creation_tokens, total_tokens,
# num_turns, duration_ms, duration_s, duration_api_ms.
# AGENT_BRIDGE_SLACK_USAGE_REPORT_TEMPLATE=💵 {cost_usd} | {total_tokens} tokens | {duration_s}s

# Anthropic API Key (required for Claude Code CLI)
ANTHROPIC_API_KEY=

Expand All @@ -13,14 +39,44 @@ AGENT_BRIDGE_CLAUDE_WORK_DIR=.
# Claude Code permission mode: acceptEdits, dangerously-skip-permissions, default, plan
AGENT_BRIDGE_CLAUDE_PERMISSION_MODE=acceptEdits

# Claude Code effort level (low, medium, high, xhigh, max). Defaults to xhigh.
# AGENT_BRIDGE_CLAUDE_EFFORT=xhigh

# Path to session store JSON file
AGENT_BRIDGE_SESSION_STORE_PATH=./sessions.json

# Session TTL in hours (sessions inactive longer than this are expired)
AGENT_BRIDGE_SESSION_TTL_HOURS=72

# Timeout for a single Claude invocation in seconds
AGENT_BRIDGE_CLAUDE_TIMEOUT_SECONDS=300
AGENT_BRIDGE_CLAUDE_TIMEOUT_SECONDS=600

# Run each session in its own git worktree (Claude Code's -w flag).
# Requires AGENT_BRIDGE_CLAUDE_WORK_DIR to be a git repo with an 'origin' remote
# whose HEAD resolves (i.e. origin/HEAD is set). Worktrees are created at
# <work_dir>/.claude/worktrees/<session_id>; branches named worktree-<session_id>.
AGENT_BRIDGE_CLAUDE_WORKTREE_ENABLED=false

# Maximum number of sessions processed concurrently
AGENT_BRIDGE_MAX_CONCURRENT_SESSIONS=5

# Cross-session prompt dedupe. When TTL > 0, identical (canonicalized) prompts
# arriving in the same channel-scope within the window collapse to a single
# agent run; subsequent hits get a short-circuit Completion. 0 disables.
# Canonicalization masks URLs / UUIDs / long numbers / timestamps / emails so
# alerts that only differ in those bits still collide.
AGENT_BRIDGE_DEDUPE_TTL_SECONDS=0
AGENT_BRIDGE_DEDUPE_MAX_ENTRIES=512
# Optional SimHash Hamming-distance threshold for fuzzy match. 0 = exact
# canonical match only. Typical range 3-10 when enabled.
AGENT_BRIDGE_DEDUPE_SIMHASH_THRESHOLD=0

# Heartbeat platform: periodically fires a fixed prompt to the agent.
# When enabled, requires INTERVAL_MINUTES and PROMPT.
AGENT_BRIDGE_HEARTBEAT_ENABLED=false
# AGENT_BRIDGE_HEARTBEAT_INTERVAL_MINUTES=60
# AGENT_BRIDGE_HEARTBEAT_PROMPT=Check your task list and work on the next item.
# AGENT_BRIDGE_HEARTBEAT_STATE_PATH=./heartbeat.json

# Log level: DEBUG / INFO / WARNING / ERROR
AGENT_BRIDGE_LOG_LEVEL=INFO
28 changes: 28 additions & 0 deletions .github/workflows/commitlint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
name: Lint Commits

# Validates that every commit in a PR follows Conventional Commits
# (lowercase types: feat / fix / docs / refactor / chore / ...). This keeps
# the history parseable by python-semantic-release so version bumps are
# computed correctly. Merge commits are ignored by the linter.

on:
pull_request:
branches:
- main

permissions:
contents: read

jobs:
commitlint:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0 # need the full PR commit range to lint each commit

- name: Lint commit messages
uses: wagoid/commitlint-github-action@v6
with:
configFile: commitlint.config.mjs
85 changes: 85 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
name: Release

# Runs on every push to main. python-semantic-release inspects the
# Conventional Commits since the last v* tag, decides the next version,
# bumps pyproject.toml, writes CHANGELOG.md, tags, and creates a GitHub
# Release. If (and only if) a release was cut, the deploy job uploads the
# built distributions to PyPI via Trusted Publishing (OIDC, no secrets).
#
# Pushes made with GITHUB_TOKEN do not re-trigger workflows, so the version
# bump commit PSR pushes back to main will not cause a loop.

on:
push:
branches:
- main
workflow_dispatch:

permissions:
contents: read

jobs:
release:
runs-on: ubuntu-latest
concurrency:
group: ${{ github.workflow }}-release-${{ github.ref_name }}
cancel-in-progress: false
permissions:
contents: write # push the version bump commit + tag, create the GitHub Release
outputs:
released: ${{ steps.release.outputs.released || 'false' }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0 # PSR needs full history + tags to compute the version
ref: ${{ github.ref_name }}

- name: Pin release branch to the triggering commit
run: git reset --hard ${{ github.sha }}

- name: Semantic version release
id: release
# Pinned to a known-good tag; bump to the latest v10.x as needed.
uses: python-semantic-release/python-semantic-release@v10.5.3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
git_committer_name: "github-actions"
git_committer_email: "actions@users.noreply.github.com"

- name: Attach distributions to the GitHub Release
if: steps.release.outputs.released == 'true'
uses: python-semantic-release/publish-action@v10.5.3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
tag: ${{ steps.release.outputs.tag }}

- name: Upload distributions for the deploy job
if: steps.release.outputs.released == 'true'
uses: actions/upload-artifact@v4
with:
name: distribution-artifacts
path: dist
if-no-files-found: error

deploy:
runs-on: ubuntu-latest
needs: release
if: ${{ needs.release.outputs.released == 'true' }}
environment:
name: pypi
url: https://pypi.org/p/ai-agent-bridge
permissions:
id-token: write # OIDC token for PyPI Trusted Publishing
steps:
- name: Download built distributions
uses: actions/download-artifact@v4
with:
name: distribution-artifacts
path: dist

- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
packages-dir: dist
print-hash: true
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Agent Bridge runtime data
sessions.json
heartbeat.json

# Byte-compiled / optimized / DLL files
__pycache__/
Expand Down
19 changes: 19 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Local guard that rejects non-Conventional-Commit messages at commit time.
# Uses the SAME engine and config as the CI check
# (.github/workflows/commitlint.yml + commitlint.config.mjs), so a message that
# passes locally also passes CI — including the lowercase-type rule (Feat: fails,
# feat: passes). pre-commit manages the Node toolchain itself; no manual install.
#
# One-time setup per clone (writes the git hook into .git/hooks/commit-msg):
# uv sync # installs the pre-commit tool (dev group)
# uv run pre-commit install
# default_install_hook_types makes that install wire the commit-msg stage.
default_install_hook_types: [commit-msg]

repos:
- repo: https://github.com/alessandrojcm/commitlint-pre-commit-hook
rev: v9.25.0
hooks:
- id: commitlint
stages: [commit-msg]
additional_dependencies: ["@commitlint/config-conventional"]
25 changes: 25 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# CHANGELOG

<!-- version list -->

## v0.2.0 (2026-06-05)

### Continuous Integration

- Automate releases from Conventional Commits
([`7206ec4`](https://github.com/htkuan/ai-agent-bridge/commit/7206ec46879e21ab0a41fe1dadb11244efa3c5cc))

### Documentation

- Trim one-time bootstrap from releasing guide
([`8558179`](https://github.com/htkuan/ai-agent-bridge/commit/8558179a8681193aaa27d6b8b6e0327c452814d5))

### Features

- Add optional startup notification after Socket Mode connects
([`4996cd9`](https://github.com/htkuan/ai-agent-bridge/commit/4996cd93abcc505f5277d7d555bf6e71f68dac13))


## v0.1.0 (2026-06-05)

- Initial Release
59 changes: 48 additions & 11 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ Events are defined in `src/agent_bridge/events.py`. Agent-internal events (think

### Protocols

- `AgentController` — `run(session_id, prompt, is_new, context) → AsyncIterator[BridgeEvent]`
- `AgentController` — `run(session_id, prompt, is_new, context, system_prompt) → AsyncIterator[BridgeEvent]`. The platform adapter builds `prompt` (already pre-tagged with sender identity if needed) and `system_prompt` (platform-flavored directives); the agent forwards them as-is. The agent must not interpret platform-specific keys out of `context`.
- `PlatformAdapter` — `start()`, `stop()`

Defined in `src/agent_bridge/protocols.py`. New agents/platforms implement these.
Expand All @@ -54,12 +54,14 @@ Defined in `src/agent_bridge/protocols.py`. New agents/platforms implement these
```
1. User message arrives at Platform Adapter
2. Adapter constructs session_key, acquires per-session lock
3. Bridge.handle_message(session_key, text, context)
→ SessionManager resolves key → (session_id, is_new)
3. Adapter builds `text` (pre-tagged with sender identity) and `system_prompt` (platform directives) — the agent stays platform-agnostic
4. Bridge.handle_message(session_key, text, context, system_prompt, resumable)
→ If `resumable=True`: SessionManager resolves key → (session_id, is_new), persisted on disk
→ If `resumable=False`: bridge mints a fresh ephemeral UUID, SessionManager untouched
→ Semaphore check (reject if capacity full)
→ AgentController.run(session_id, prompt, is_new, context)
4. Agent yields BridgeEvents
5. Adapter renders events as platform-native messages
→ AgentController.run(session_id, prompt, is_new, context, system_prompt)
5. Agent yields BridgeEvents
6. Adapter renders events as platform-native messages
```

## Tech stack
Expand Down Expand Up @@ -136,24 +138,37 @@ src/agent_bridge/
- Async tests run automatically (`asyncio_mode = "auto"`)
- Test naming: `test_{feature}_{scenario}`

### Commits

- Follow [Conventional Commits](https://www.conventionalcommits.org/) with **lowercase** types: `feat:`, `fix:`, `docs:`, `refactor:`, `test:`, `chore:`, `ci:`, `perf:`, `style:`, `build:`, `revert:`. (Not `Feat:`/`Fix:`.)
- Optional scope in parens, imperative subject, no trailing period: `fix(slack): release dedupe slot on error`.
- The type drives the automated release: `feat:` → MINOR, `fix:`/`perf:` → PATCH, `feat!:` or a `BREAKING CHANGE:` footer → breaking (MINOR while in 0.x). Other types cut no release.
- The `commitlint` PR check rejects non-conforming commits. Since PRs merge with merge commits, **each commit on a branch** (not just the PR title) must conform.
- Optional local guard: `uv run pre-commit install` wires a `commit-msg` hook that runs the same commitlint config before each commit (see `docs/releasing.md`).
- Releases are automated from these messages — never hand-edit `[project].version`. See `## Releasing` and `docs/releasing.md`.

### Adding a new platform adapter

1. Create `platforms/{name}/config.py` — config with `from_env()` + `_validate()`
2. Create `platforms/{name}/adapter.py` — implements `PlatformAdapter` protocol
3. Define session key format (e.g. `discord:{guild}:{channel}`)
4. Own per-session locking strategy
5. Consume `BridgeEvent`s from `bridge.handle_message()`
6. Wire up in `__init__.py`
7. Add documentation in `docs/platforms/{name}.md`
5. Build the text the agent receives — pre-tag the prompt with sender identity if your platform has one (e.g. `[name]: text`); pass `None` if it doesn't (proactive triggers)
6. Build the `system_prompt` — platform-flavored directives (chat framing, scheduled-invocation framing, webhook-trigger framing, etc.). The agent forwards it as-is
7. Decide `resumable`: pass `True` (default) if the same `session_key` should be able to resume the same session later (e.g. chat threads); pass `False` for one-shot triggers where every call must be a fresh, untracked session (e.g. heartbeat ticks)
8. Consume `BridgeEvent`s from `bridge.handle_message(session_key, text, context, system_prompt, resumable)`
9. Wire up in `__init__.py`
10. Add documentation in `docs/platforms/{name}.md`

### Adding a new agent

1. Create `agents/{name}/config.py` — config with `from_env()` + `_validate()`
2. Create `agents/{name}/controller.py` — implements `AgentController` protocol
3. Create `agents/{name}/events.py` — parse agent output → `BridgeEvent`s
4. `run()` yields only generic `BridgeEvent`s — agent-internal events stay internal
5. Wire up in `__init__.py`
6. Add documentation in `docs/agents/{name}.md`
5. Treat `system_prompt` and `prompt` as opaque strings built by the platform — do not parse `context` for platform-specific keys
6. Wire up in `__init__.py`
7. Add documentation in `docs/agents/{name}.md`

### Documentation maintenance

Expand All @@ -176,18 +191,40 @@ uv run agent-bridge
uv run pytest tests/ -v
```

## Releasing

Versioning is automated — **do not hand-edit `[project].version`**. On push to `main`,
python-semantic-release reads the [Conventional Commits](https://www.conventionalcommits.org/)
since the last tag (see `### Commits` for the format), bumps the version, tags `vX.Y.Z`,
writes `CHANGELOG.md`, and publishes to PyPI via OIDC. While in 0.x, breaking changes
bump the minor (not 1.0.0). Full process + one-time setup: `docs/releasing.md`.

## Environment variables

All config loads from `.env` via python-dotenv. See `.env.example` for the full list.

| Variable | Required | Default | Component |
|----------|----------|---------|-----------|
| `ANTHROPIC_API_KEY` | No | — | Claude CLI (only if not already authenticated via `claude login`) |
| `AGENT_BRIDGE_SLACK_BOT_TOKEN` | Yes (if using Slack) | — | Slack |
| `AGENT_BRIDGE_SLACK_APP_TOKEN` | Yes (if using Slack) | — | Slack |
| `AGENT_BRIDGE_SLACK_ALLOW_CHANNELS` | No | — (allow all) | Slack (comma-separated channel-name allow-list; non-empty also blocks DMs) |
| `AGENT_BRIDGE_SLACK_CHANNEL_NOT_ALLOWED_MESSAGE` | No | (fixed English notice) | Slack (reply sent to non-allowed channels) |
| `AGENT_BRIDGE_SLACK_USAGE_REPORT_ENABLED` | No | `false` | Slack (append usage/cost footer to the final reply) |
| `AGENT_BRIDGE_SLACK_USAGE_REPORT_TEMPLATE` | No | — (built-in default) | Slack (`{placeholder}` template for the usage footer) |
| `AGENT_BRIDGE_CLAUDE_WORK_DIR` | No | `.` | Claude |
| `AGENT_BRIDGE_CLAUDE_PERMISSION_MODE` | No | `acceptEdits` | Claude |
| `AGENT_BRIDGE_CLAUDE_TIMEOUT_SECONDS` | No | `600` | Claude |
| `AGENT_BRIDGE_CLAUDE_WORKTREE_ENABLED` | No | `false` | Claude |
| `AGENT_BRIDGE_CLAUDE_EFFORT` | No | `xhigh` | Claude (one of `low`, `medium`, `high`, `xhigh`, `max`) |
| `AGENT_BRIDGE_SESSION_STORE_PATH` | No | `./sessions.json` | Bridge |
| `AGENT_BRIDGE_SESSION_TTL_HOURS` | No | `72` | Bridge |
| `AGENT_BRIDGE_MAX_CONCURRENT_SESSIONS` | No | `5` | Bridge |
| `AGENT_BRIDGE_DEDUPE_TTL_SECONDS` | No | `0` | Bridge (cross-session prompt dedupe; 0 disables) |
| `AGENT_BRIDGE_DEDUPE_MAX_ENTRIES` | No | `512` | Bridge (dedupe LRU cap) |
| `AGENT_BRIDGE_DEDUPE_SIMHASH_THRESHOLD` | No | `0` | Bridge (Hamming threshold for fuzzy match; 0 = exact canonical only) |
| `AGENT_BRIDGE_HEARTBEAT_ENABLED` | No | `false` | Heartbeat |
| `AGENT_BRIDGE_HEARTBEAT_INTERVAL_MINUTES` | Yes (if heartbeat enabled) | — | Heartbeat |
| `AGENT_BRIDGE_HEARTBEAT_PROMPT` | Yes (if heartbeat enabled) | — | Heartbeat |
| `AGENT_BRIDGE_HEARTBEAT_STATE_PATH` | No | `./heartbeat.json` | Heartbeat |
| `AGENT_BRIDGE_LOG_LEVEL` | No | `INFO` | Global |
14 changes: 12 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,14 +109,20 @@ All agent output flows through generic events — the shared language between ag

| Variable | Required | Default | Description |
|----------|----------|---------|-------------|
| `AGENT_BRIDGE_SLACK_BOT_TOKEN` | Yes | — | Slack Bot User OAuth Token (`xoxb-...`) |
| `AGENT_BRIDGE_SLACK_APP_TOKEN` | Yes | — | Slack App-Level Token for Socket Mode (`xapp-...`) |
| `ANTHROPIC_API_KEY` | No | — | API key for the Claude Code CLI (skip if already authenticated via `claude login`) |
| `AGENT_BRIDGE_SLACK_BOT_TOKEN` | Yes (if using Slack) | — | Slack Bot User OAuth Token (`xoxb-...`) |
| `AGENT_BRIDGE_SLACK_APP_TOKEN` | Yes (if using Slack) | — | Slack App-Level Token for Socket Mode (`xapp-...`) |
| `AGENT_BRIDGE_CLAUDE_WORK_DIR` | No | `.` | Working directory for Claude Code |
| `AGENT_BRIDGE_CLAUDE_PERMISSION_MODE` | No | `acceptEdits` | Claude permission mode |
| `AGENT_BRIDGE_CLAUDE_TIMEOUT_SECONDS` | No | `600` | Per-invocation timeout (seconds) |
| `AGENT_BRIDGE_CLAUDE_WORKTREE_ENABLED` | No | `false` | Run each session in an isolated git worktree (requires `origin/HEAD`) |
| `AGENT_BRIDGE_SESSION_STORE_PATH` | No | `./sessions.json` | Session mapping file path |
| `AGENT_BRIDGE_SESSION_TTL_HOURS` | No | `72` | Session TTL (hours) |
| `AGENT_BRIDGE_MAX_CONCURRENT_SESSIONS` | No | `5` | Max concurrent agent processes |
| `AGENT_BRIDGE_HEARTBEAT_ENABLED` | No | `false` | Enable the heartbeat platform — fires a fixed prompt on a fixed interval |
| `AGENT_BRIDGE_HEARTBEAT_INTERVAL_MINUTES` | Yes (if heartbeat enabled) | — | Interval between heartbeat ticks (minutes) |
| `AGENT_BRIDGE_HEARTBEAT_PROMPT` | Yes (if heartbeat enabled) | — | Prompt sent on every heartbeat tick |
| `AGENT_BRIDGE_HEARTBEAT_STATE_PATH` | No | `./heartbeat.json` | Last-run timestamp path (used for restart catch-up) |
| `AGENT_BRIDGE_LOG_LEVEL` | No | `INFO` | `DEBUG` / `INFO` / `WARNING` / `ERROR` |

## Extending
Expand All @@ -141,6 +147,10 @@ uv run pytest tests/ -v
AGENT_BRIDGE_LOG_LEVEL=DEBUG uv run agent-bridge
```

Commits follow [Conventional Commits](https://www.conventionalcommits.org/) (lowercase
types: `feat:`, `fix:`, ...) and are enforced on PRs. Merging to `main` cuts a release
automatically — see [docs/releasing.md](docs/releasing.md).

## License

MIT
Loading
Loading