Website: gpslab.github.io/workspace-assistant
An epic from start to finish: design → issues → pull requests → review → merge → final check.
A Telegram bot that turns chat messages into Claude Code
runs inside a git repository, the managed repository. It is meant to be a knowledge base: a repository
of documents that you edit through chat, not a code base. You write to the bot from your phone; the
message goes to claude -p in the context of the managed repository, the changes are committed and
pushed automatically (unless AUTO_COMMIT=0), and the reply contains a link to the commit. See
How it works for what exactly is committed and pushed.
Telegram <-> bot (long polling) <-> claude -p --resume <session> <-> git push
Each forum topic is a separate Claude session; the bot is replying in i18n right now.
By default the bot starts Claude with
--dangerously-skip-permissions. Claude then runs without any permission prompts and executes whatever a Telegram message asks it to: shell commands, file edits, network access,git pushandghcalls. Everything runs with the rights of the system user that runs the bot and with that user'sghtoken.Anyone who gets into
ALLOWED_USER_IDS(or takes over such a Telegram account), and anyone who can influence the text Claude reads (managed repository files, issues, pull requests), can make the bot act as that user. Read Security and isolate the process before you start the bot.
- Per-chat Claude sessions that continue between messages (
--resume). - Before each run: clean-tree check and
git pull --rebase; after a successful run: commit and push of the managed repository. WithAUTO_COMMIT=0the bot does none of this in the managed repository. - A request that arrives while the bot is busy is queued, not rejected.
- Live progress: the "Thinking..." message is updated with what Claude says and which tools it calls.
- 13 interface languages; each user picks the language of Claude's replies with
/lang. See Languages. - Files in both directions: files sent to the bot are handed to Claude (inbox); files Claude puts into the outbox are sent back to the chat.
- Modes (
arch,review,pm) with replaceable personas, interface locales, and epics: sets of GitHub tasks with dependencies that the bot'sOrchestratordrives to completion — independent tasks run in parallel, dependent ones start once everything they depend on is merged.
The managed repository is a knowledge base: a repository of documents that you edit through chat,
not the code base of a project. After every successful run that changed files the bot commits all
changes (git add -A) with the message tg: <first line of the request, up to 60 chars> (epic runs:
tg: epic <slug>) and pushes the current branch with a plain git push. A run that changed nothing
produces no commit and no push. Changes left by an interrupted run, a failed regular request, or by
someone else are not committed automatically. With AUTO_COMMIT=1 (the default) the bot asks whether to
commit or discard them. A failed epic run asks nothing: with AUTO_COMMIT=1 its changes are saved as
dirty-<timestamp>.patch next to failures.log in <STATE_DIR>/epics/<slug>/, and the tree is rolled back. Nothing else is reviewed before a push, so do
not point the bot at a code project.
With AUTO_COMMIT=0 the bot neither commits, pushes nor pulls the managed repository: there is no
clean-tree check, no commit/discard buttons and no commit link for the managed repository (links to
sub-repository commits can still appear), and the changes of every run stay in
the working tree for you to commit. All topics share one working tree, so the changes of one topic are
visible in the next run of another. Sub-repositories under SUBREPOS_DIR are still pulled.
- If the push fails, the reply reports the error and has no link to the managed repository commit (links to sub-repository commits can still be there). The commit stays local and goes out with the next run that changes files, or push it by hand. On a protected branch every push fails, so local commits pile up.
- Code lives in separate repositories, and changes there go through pull requests:
- sub-repositories under
SUBREPOS_DIR: the bot pulls them before a run and links their new commits in the reply. Whether they are committed and pushed is up to Claude, following the managed repository's own instructions. The bot does not commit them. - epics: their tasks are implemented as pull requests.
- sub-repositories under
Besides chatting with a repository, the bot can carry work from an idea to a merged pull request. Claude
handles the steps that need judgement, the Orchestrator (the bot's own code) handles the
deterministic ones, an external executor (openronin) writes
the code, and the author keeps the decisions that must stay human.
architect (Claude, pm/arch mode) → [author: /epic start]
→ implementer (openronin) → cold review (CI, optional) → hot review (epic run)
→ merge (Orchestrator) → final check (epic run) → report [author]
- Architect: Claude in the
pmorarchmode (default personas inprompts/role_pm.md.distandprompts/role_arch.md.dist). It designs, pressure-tests the design, checks the findings against the facts, writes the task text with a list of load-bearing decisions, checks the scope and the order of the series, and opens the issues. The epic plan is a separate artefact: the file<EPICS_DIR>/<slug>.mdthat the author approves (see EPICS.md). - Implementer: openronin writes the code for an issue and opens a pull request. The integration with its labels and marker is built into the bot.
- Cold review: an optional CI reviewer that comments on the pull request. The hot review gives a verdict on each of its findings.
- Hot review and final check: epic runs of Claude that follow
prompts/epic_runbook.md.dist. The hot review judges a pull request against the intent of the task and the epic plan and publishes the formal verdict; the final check verifies the whole epic and produces the report. - Merge and every other deterministic step: the
Orchestrator: labels, taking a pull request out of draft, merging, releases and the ceilings that stop a stuck epic.
The author decides three things: whether the tasks are right to run (re-read them, then send
/epic start), the forks the epic cannot resolve on its own, and, after reading the report, whether
to close the epic.
The design stage runs a pipeline of read-only sub-agents shipped with the bot (critic, verifier,
editor, scope-check, acceptor), and a gate refuses to create issues until the required stages ran:
see Working out a task. Any of them can be replaced by your own Claude Code
sub-agent with the same name, and the persona files and additions in PROMPTS_DIR change how the modes
and epic runs behave, see PROMPTS.md. Details are in EPICS.md.
The security agent also works on demand, without a new mode or command. To check the security of a
PR or task, ask the bot to call the security agent on that PR; to audit the project before a release, ask
it to call the agent on the whole repository (it also runs composer audit --locked and npm audit).
In an epic it runs as a step of the final check, on the merged PR diffs.
- PHP 8.4+ with the extensions
curl,pdo_sqlite,pcntl(andposix). - Claude Code CLI, authenticated for the account you want to use.
- git with access to the workspace's remote (for example an SSH key).
- GitHub CLI (
gh), authenticated withgh auth loginas the user that runs the bot. Both Claude (issues, pull requests) and the bot itself (epic polling) call it. WithFORGE=gitlabthe GitLab CLI (glab) is used instead, see GitLab. - Composer, for development tooling only. The runtime has no Composer dependencies.
- openronin — the implementer that epics hand tasks to:
it picks up issues labelled
openronin:do-it, asks clarifying questions, opens pull requests and answers review comments. Epics rely on it; regular chat with the bot does not. - claude-memory-3layer — in-repo memory for Claude
Code. Installed for the managed repository, it gives every run the project's conventions and known
pitfalls (
CLAUDE.mdplus on-demand docs) and keeps working state across/compactand new sessions. The bot does not require it.
FORGE=gitlab (with FORGE_HOST for a self-hosted instance) makes the bot use GitLab instead of GitHub.
Minimum versions: GitLab 16.0 and glab 1.40.
- Install glab on the server and make sure
glab --versionworks for the user that runs the bot. - Authenticate as that user:
glab auth login --hostname gitlab.com(for a self-hosted instance,glab auth login --hostname gitlab.example.com). The access token needs the scopesapiandwrite_repository. - Set
FORGE=gitlabin.env, plusFORGE_HOST=gitlab.example.comfor a self-hosted instance (the default isgitlab.com). The installer asks for both (--forge,--forge-host) and checksglab.
With FORGE=gitlab the bot builds commit and file links (…/-/commit/<sha>, …/-/blob/<branch>/<path>)
for origin remotes in the forms git@<host>:<namespace>/<project>.git,
https://<host>[:port]/<namespace>/<project>.git (the port is kept in the link) and
ssh://git@<host>[:port]/<namespace>/<project>.git (the port is dropped), when <host> equals
FORGE_HOST; for any other remote it shows a bare hash or a plain path. Commit links of
sub-repositories from SUBREPOS_DIR are built from their own origin: first FORGE/FORGE_HOST, then
github.com and gitlab.com, otherwise a bare hash. The cron check
bin/check_forge_auth.sh runs
glab auth status --hostname <host>.
In review mode on GitLab the verdict is a glab mr note whose body carries a
<!-- verdict:approve|changes sha=<SHA> --> marker. The sha (also in glab mr approve -s|--sha <SHA> and in
a glab api approve) is a literal lowercase 40-hex value, checked only for its form; it is not compared with the MR head, because the bot has no GitLab client yet.
| Feature | GitHub | GitLab |
|---|---|---|
| Chat mode | supported | supported |
| PM mode | supported | supported |
| Review mode | supported | supported (verdict note with a marker; cold reviewer findings are not collected) |
Epics (/epic) |
supported | not supported |
git clone https://github.com/gpslab/workspace-assistant.git
cd workspace-assistant
php bin/install.php --bot-token-file=/path/to/token-file --allowed-user-ids=1001 --repo-path=/path/to/repo
php bin/bot.php- Create a bot with @BotFather and keep the token.
- Find your Telegram user id with @userinfobot.
- Clone the workspace repository, make sure
git pullandgit pushwork there without prompts, and note its absolute path.
The installer options and checks, running as a service, every .env key, the interface language and
the mode personas and prompt files are in INSTALL.md and PROMPTS.md. The list of changes between versions is on the
GitHub Releases page; read it before updating.
| Command | Action |
|---|---|
| text | Send a request to Claude in the context of the repository |
/new |
Start a new session (reset the context) |
/compact |
Compress the session into a summary instead of a full reset |
/cancel |
Abort the topic's process and drop its queued requests |
/queue |
Show the request queue |
/stats |
Session statistics (tokens, messages) |
/verbose |
Show token usage in every reply |
/mode |
Switch the mode: arch, review or pm |
/lang |
Language of Claude's replies in the chat |
/epic |
Epics: start <slug>, pause, resume, status; without an argument — buttons for the actions that apply to the topic's epic now (start goes through a plan list and a confirmation) |
/restart |
Restart the bot to pick up new code |
/help |
Help |
The bot runs at most one Claude process at a time in the working tree. Requests that arrive meanwhile
go into a single FIFO queue stored in SQLite, keyed by (chat_id, thread_id), so private chats and
forum topics are independent. Consecutive pending messages of one topic in the same mode are merged
into one request. With AUTO_COMMIT=1 (the default) the queue pauses when the working tree is dirty (commit/discard
buttons appear); with AUTO_COMMIT=0 there is no such pause. The queue also pauses when Claude reports a usage limit.
One run at a time is a supported property, not a temporary limitation, and it covers the bot's own
Claude runs of every kind: chat requests and the isolated runs of epics alike. The managed repository
has a single working tree (with AUTO_COMMIT=1, a dirty tree also has a single pending commit/discard
decision). This does not
contradict "independent tasks run in parallel" in epics: there the parallel work is done by the
implementer (openronin), not by the bot's Claude runs.
- Outbox. Files Claude puts into
<STATE_DIR>/outboxduring a run are sent to the chat before the text reply (images as photos, the rest as documents; Bot API limits apply, at most 10 files per run), then the directory is cleared. - Inbox. Any file sent to the bot (photo, document, audio, video, voice, ...) is downloaded to
<STATE_DIR>/inboxand passed to Claude by path; the caption becomes the request text. Albums are combined into one request. Files are deleted after the run.
Files Claude puts into the outbox arrive in the chat: here a diagram and its Mermaid source.
Before an epic exists, the author and Claude in the pm mode work out the task: a design draft, a
cold critique, verification of the findings, editing and a scope check of the series, and only then
the issues. A gate refuses to create issues until the required stages ran. The stages, the built-in
agents, the pipeline config and the gate are in TASKS.md.
An epic is an author-approved set of GitHub tasks with an execution order; the Orchestrator drives
the tasks to completion, independent ones in parallel. The stages of a run, the dependency graph, the
plan format, the /epic commands, the status card and the merge and release gates are in
EPICS.md.
The epic card is one message the bot keeps up to date: each task's state, what it waits for, and the last journal entry.
The bot interface comes in 13 languages:
| Code | Language | Support |
|---|---|---|
en |
English | maintained |
ru |
Russian | maintained |
de |
German | machine |
es |
Spanish | machine |
fr |
French | machine |
hi |
Hindi | machine |
ja |
Japanese | machine |
kk |
Kazakh (Cyrillic) | machine |
ko |
Korean | machine |
pt |
Portuguese (Brazil) | machine |
uk |
Ukrainian | machine |
uz |
Uzbek (Latin) | machine |
zh |
Chinese (Simplified) | machine |
Only English and Russian are actively maintained by the project. The other locales are automatic
(machine) translations, provided as is: they can contain wrong or awkward strings. If you meet one, open
an issue or send a PR that changes one resources/locales/<code>.json and nothing else.
We are ready to extend the list of maintained languages, or add new ones, if there is demand: open an issue and tell us which language you need.
BOT_LOCALE sets the interface language of the bot; /lang sets, per user, the language of Claude's
replies in the chat. Own locale files and the locale file format are in
INSTALL.md.
The bot is built for a trusted managed repository and a trusted, short whitelist of users. Treat it like a
remote shell for everyone in ALLOWED_USER_IDS.
- The whitelist
ALLOWED_USER_IDSis the only authentication; protect those Telegram accounts. - Arguments to the CLI and git are passed as arrays through
proc_open, so there is no shell injection. .envandstate/are in.gitignore.--dangerously-skip-permissionsis the default ofCLAUDE_EXTRA_ARGS. Claude can run any command as the bot's system user and use that user'sghtoken and git credentials. Content Claude reads (files, issues, PR text) can steer it too (prompt injection).- With the default
AUTO_COMMIT=1every successful run is committed and pushed to the current branch automatically with no review (see How it works), including edits to persona and locale files. WithAUTO_COMMIT=0nothing is committed or pushed by the bot.
To report a vulnerability privately, see SECURITY.md.
-
Dedicated user without sudo. Run the bot as its own account that is not in
sudo/wheeland owns nothing but the bot, the workspace and the state directory:sudo useradd --system --create-home --shell /bin/bash workspace-assistant sudo -u workspace-assistant -i # install the bot, Claude CLI, clone the workspace hereKeep SSH keys and credentials of other services out of its home directory.
-
Narrow
ghtoken. Authenticateghas that user with a fine-grained personal access token (or a machine account) limited to the repositories the bot needs and to the minimum permissions: contents and pull requests read/write, issues read/write, and nothing about administration, secrets or workflows. Use a deploy key or a similarly scoped credential for git. -
Contain the process. Prefer a VM or container, or systemd hardening (
ProtectSystem=strict,ReadWritePaths=for the workspace and state,NoNewPrivileges=true,PrivateTmp=true), and restrict outbound network access where possible. -
Disable the flag if you can. Set
CLAUDE_EXTRA_ARGSto something stricter, for exampleCLAUDE_EXTRA_ARGS=--permission-mode,acceptEdits(or an empty value for no extra arguments). Note that in non-interactive-pmode Claude cannot answer permission prompts, so tools that need approval will simply be refused. -
Keep the whitelist minimal and review the commits the bot pushes.
composer install # dev tooling: php-cs-fixer, phpstan, phpunit
composer cs # fix the code style (@Symfony)
composer cs:check # check the style without changes
composer stan # static analysis (phpstan level 8)
composer test # unit tests (phpunit)
composer test:coverage # unit tests with a coverage summary and coverage.xmlcomposer test:coverage needs a coverage driver enabled in php.ini (pcov.enabled=1, or Xdebug with
XDEBUG_MODE=coverage): php -d … passed to composer does not reach the PHPUnit process. Where pcov is
installed but disabled, run php -d pcov.enabled=1 vendor/bin/phpunit --coverage-text --coverage-clover=coverage.xml.
CI runs cs:check, stan and test on PHP 8.4 and 8.5 on every push to master and on every pull
request; the 8.5 job runs the tests with coverage, shows the
summary (lines, methods) on the run page and keeps coverage.xml as the coverage artifact for 14 days.
Pull requests are welcome; the rules are in CONTRIBUTING.md.
Released under the MIT License.
Third-party interfaces, marks and content (including chat avatars) shown in the screenshots in docs/assets/ belong to their respective owners and are not covered by the MIT License. docs/assets/hero.jpg was generated with Midjourney and is not covered by the MIT License. Claude is a trademark of Anthropic, PBC; Telegram, GitHub and other names mentioned are trademarks of their respective owners. This project is not affiliated with or endorsed by them.