A small Python toolkit that lets one researcher — the advisor — spawn and converse with a fleet of Claude Code agents through Slack. Each agent is its own Claude Code session in its own workspace directory; Slack messages drive turns; between turns, the agent's filesystem is its long-term memory.
A separate coach agent watches how the advisor advises and gives evidence-based feedback on mentoring craft. The two intertwined goals: get real research done and grow as a mentor.
The host can be anything that runs Python and the Claude Code CLI — a laptop, a lab workstation, a cloud VM, an HPC login node. The bot connects to Slack over Socket Mode, so the host needs no inbound HTTP and works behind a NAT or in a private subnet.
What you getRequirementsQuick startSetup in detailUsageHow it worksConfigurationSecurity modelLimitationsProject layoutLicense
One Slack channel per agent.#student-<name>
for each student you spawn;#mentor-coach
for the coach (created automatically at first startup).Three slash commands./new-student <name> <briefing>
scaffolds a workspace, creates the channel, and kicks off the first turn./coach-review <name> [days]
asks the coach to reviewyourmentoring of a specific student./claude-status
prints a quick local readout — turns, last context size, cumulative tokens, model, cost, and GitHub link per agent. No Claude calls; just a registry view.
Per-agent journal.JOURNAL.md
in each agent's workspace is append-only, one section per turn, ending with aDid / Found / Next
block.Shared paper library.library/
at the project root is a single pool every agent reads from and contributes to. First reader of a paper writes the canonical summary; later readers add a separate notes file.library/README.md
is a regenerated index — agents only read it.Per-turn commits to per-agent GitHub branches (optional). If you configure anorigin
, after each turn the bot stages that agent's workspace into a fresh commit onagent/<name>
and force-pushes it with a lease. The Slack final message links to the branch for review. With noorigin
, this step is skipped silently.Quiet by default. During a turn the bot posts at most one short*"started"*message and the agent's final reply — no per-tool stream of decorative messages. Failures and timeouts post a:warning:
line.
**Python 3.11 or newer.**The Claude Code CLI on The bot shells out to it, soPATH
.claude --version
must work in the same shell where you run the bot.A Claude.ai subscription* or*an Anthropic Console API key (seeClaude authentication).A Slack workspace(free or paid) where you can install a custom app.*Optional:*aGitHub repository if you want per-turn review branches. The bot runs fine without one.
git clone <this repo URL>
cd phd_fleet
python3 -m venv .venv
.venv/bin/pip install -r requirements.txt
cp .env.example .env # then fill in the three required values
.venv/bin/python bot.py
Then in Slack, run /new-student alice "your project briefing"
to spawn your first agent. The rest of this section explains each step.
See the Quick start above. A virtualenv is recommended but not required — any environment with the dependencies in requirements.txt works.
The repo ships slack-manifest.yaml — a manifest that already lists the three slash commands, the bot scopes, and the event subscriptions. To use it:
- Go to
https://api.slack.com/apps→Create New App→** From a manifest**. - Pick your workspace, paste the contents of
slack-manifest.yaml
, and confirm. - Under
Basic Information → App-Level Tokens → Generate Token and Scopes, create a token with theconnections:write
scope. Copy it (xapp-…
) — that's yourSLACK_APP_TOKEN
. - Under
OAuth & Permissions, install the app to your workspace. Copy the** Bot User OAuth Token**(xoxb-…
) — that's yourSLACK_BOT_TOKEN
.
cp .env.example .env
To find your ADVISOR_SLACK_USER_ID
: in Slack, click your profile picture →
View full profile → the … menu → Copy member ID. It looks like
U0123ABC456
. The bot rejects messages from anyone whose user ID does not match this value — the only access-control surface.
Two paths, in order of preference:
Subscription (recommended). Ifclaude
is OAuth-authenticated to your Claude.ai account — i.e.,~/.claude/.credentials.json
exists from runningclaude /login
once — the bot just uses it.LeaveANTHROPIC_API_KEY
unset.API key. To pay per token through the Anthropic Console, setANTHROPIC_API_KEY=sk-ant-…
in.env
. The Agent SDK picks it up and ignores the subscription path.
.venv/bin/python bot.py
You should see:
… INFO ready — listening on Socket Mode (advisor=U0123ABC456)
… INFO A new session (s_…) has been established
… INFO ⚡️ Bolt app is running!
Health check: in any channel where the bot is present, @<bot> ping
returns
pong
.
For long-running operation, run the bot under a process supervisor — tmux
is
the simplest path, systemd --user
if you want auto-restart on crash. The bot reconnects to Slack on transient disconnects but cannot survive a full process exit without a supervisor.
/new-student alice "Investigate AlphaFold confidence on disordered regions. Read the recent literature, then propose a small experiment."
What this does:
- Validates the name against
^[a-z0-9][a-z0-9_-]{0,40}$
. - Scaffolds
students/alice/
fromstudent_template/
, filling in the name and project briefing. - Creates
#student-alice
in Slack and invites you. - Registers Alice in
agents.json
(the runtime registry). - Kicks off the first turn — the agent reads its
CLAUDE.md
, gets oriented, and reports back.
After that, every message you send in #student-alice
becomes the next prompt. The agent's session resumes across turns and across bot restarts.
The coach has its own channel, #mentor-coach
, created at first bot startup. Two ways to use it:
Free chat. Anything you write in#mentor-coach
becomes a prompt —*"How should I handle a student proposing a method I think is wrong?"*The coach responds in a coaching voice, asks clarifying questions, and names a relevant framework when appropriate.Structured review./coach-review alice 7
pulls the last 7 days of#student-alice
plus a recent excerpt of Alice'sJOURNAL.md
, and asks the coach to reviewyourmentoring of Alice — what was done well, what could be sharper, each tied to a specific moment. The result is posted in#mentor-coach
, and the coach also updates a longitudinalmentor/coach/notes/advisees/alice.md
.
/claude-status
Ephemeral reply listing every agent: kind, turns taken, last context size, cumulative input/output tokens, model, total cost, and the GitHub branch link if available. Pure local read — does not call Claude.
Manual on purpose. To see what you have, look in students/
. To archive a
student: mv students/<name> students/_archived/
and remove its entry from
agents.json
. Slash commands for operations you'll do twice a year aren't worth their weight.
Each student lives in students/<name>/
:
— persona and project briefing, filled in at create time fromCLAUDE.md
student_template/
.— append-only research log, one section per session.JOURNAL.md
— private scratchpads, design notes, intermediate analyses.notes/
the rest of the directory— actual work artifacts (code, data, results).
Each student is told to check library/
(the shared pool) before reading anything new, to write new paper summaries there, and never to run git — the bot handles publishing.
The lab-wide habits and conventions live in LAB_CONTEXT.md, auto-appended to every agent's system prompt. Edit it once and every agent picks up the new rules on its next turn.
The coach lives in mentor/coach/
, with the same workspace structure as a student
plus notes/advisees/<name>.md
for longitudinal observations. Its CLAUDE.md
carries a coaching persona and a vocabulary of named frameworks (GROW; SBI; Vygotsky's ZPD; feedforward). It uses the same runner, the same per-agent lock, and the same scaffolding as a student. The coach is reactive only: it speaks when summoned — there is no auto-review after every student turn.
library/
is a single directory at the project root that every agent reads from and writes to:
First reader of paperXwriteslibrary/<citekey>.md
(a markdown summary with YAML frontmatter) andlibrary/<citekey>.pdf
if it's freely downloadable.Later readers who want to add their take write aseparatefile —library/<citekey>__notes_<their-name>.md
. They never edit a peer's summary.is the index.library/README.md
Agents only read it. The bot regenerates it after each turn by walkinglibrary/*.md
, parsing frontmatter, and rewriting the table.
Citekey collisions (two papers by the same author in the same year) are resolved
with letter suffixes — jumper2021a
, jumper2021b
. The full conventions live in LAB_CONTEXT.md. This per-file-ownership shape avoids the failure mode where two agents append to the same index at once and clobber each other.
If you've configured origin
, the bot publishes each agent's workspace to a
per-agent branch (agent/<name>
) after every turn, using a force-push with a
lease. It stages into a temporary git index so the bot's commits never disturb
your working tree. The Slack final message links to the branch on GitHub. The
step-by-step rationale is documented in the comments of
src/agents.py (commit_and_push
).
If you don't want this, simply don't add an origin
. The bot skips the publish step silently and the Slack messages won't include review links.
Most behavior is set in .env
; the per-agent .claude/settings.json
files carry
the permission deny-list (see Security model). The .env
knobs:
| Variable | Default | Purpose |
|---|---|---|
SLACK_BOT_TOKEN |
||
| required | Bot User OAuth Token (xoxb-… ). |
|
SLACK_APP_TOKEN |
||
| required | App-Level Token with connections:write (xapp-… ). |
|
ADVISOR_SLACK_USER_ID |
||
| required | The single user allowed to talk to the bot. | |
ANTHROPIC_API_KEY |
||
| unset | Optional. Set to use a Console API key instead of subscription auth. | |
AGENT_TURN_TIMEOUT_SECONDS |
||
3600 |
||
| How long one turn may run before being canceled. |
This is defense-in-depth, not a sandbox. Each agent's .claude/settings.json
carries a permission deny-list shipped in both templates:
- Sensitive paths are never read:
**/.env*
,**/.ssh/**
,**/.aws/**
,**/.config/gh/**
,id_rsa*
,id_ed25519*
,/etc/**
,/root/**
. - Privilege-escalation and cluster-job verbs are never run:
sudo
,su
,chmod
,chown
,srun
,sbatch
,scancel
,salloc
.
Per-deployment additions (e.g. other cluster schedulers) are easy to add to the
same file. The deny-list is not isolation: a determined prompt-injection from a
fetched paper could still misuse Bash
or Write
within the agent's workspace.
Run the bot only on a host where that risk is acceptable, and only ever as the
single configured advisor — the bot drops every Slack message whose user ID
doesn't match ADVISOR_SLACK_USER_ID
.
Single advisor. No team / multi-advisor mode — the framework is shaped around one researcher's attention.Reactive only. Agents run when you message them. There is no scheduled wake-up, no inter-turn autonomy, no proactive coach observation.No web UI. Slack is the UI.No automated test suite. Verification is by smoke-running the bot.Not a sandbox. SeeSecurity model.
phd_fleet/
├── bot.py # Slack entry point: handlers, bootstrap, main()
├── src/
│ ├── paths.py # path constants, env config, logger, NAME_RE
│ ├── agents.py # registry, workspace scaffolding, git publishing
│ ├── library.py # LAB_CONTEXT + library/README.md index
│ ├── slack_io.py # Slack helpers, status, markdown → mrkdwn renderer
│ └── runner.py # run_agent — the per-turn Agent SDK driver
├── requirements.txt
├── pyproject.toml # ruff / isort config
├── slack-manifest.yaml # paste into api.slack.com/apps to provision the app
├── .env.example # copy to .env and fill in the required values
├── LAB_CONTEXT.md # lab-wide rules, auto-appended to every agent
├── student_template/ # scaffold copied into students/<name>/
│ ├── CLAUDE.md
│ ├── JOURNAL.md
│ ├── notes/
│ └── .claude/settings.json
├── mentor_template/ # scaffold for the coach (created once at startup)
│ ├── CLAUDE.md
│ ├── JOURNAL.md
│ ├── notes/advisees/
│ └── .claude/settings.json
├── library/ # shared paper pool
│ └── README.md # regenerated index; never edit by hand
├── students/<name>/ # one workspace per student (created at runtime, gitignored)
├── mentor/coach/ # the coach's workspace (created at startup, gitignored)
└── agents.json # runtime registry (created at runtime, gitignored)
agents.json
, the virtualenv, and the per-agent workspaces under students/
and
mentor/
are all gitignored. The intent: the repo tracks the toolkit — the bot, the templates, the library scaffolding — while each agent's evolving work lives on its own GitHub branch via the per-turn publishing.
MIT.