# PhD_fleet – Manage a virtual research lab of AI PhD students via Slack

> Source: <https://github.com/canatara/phd_fleet>
> Published: 2026-06-19 16:45:59+00:00

A small Python toolkit that lets one researcher — *the advisor* — spawn and
converse with a fleet of [Claude Code](https://github.com/anthropics/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 get](#what-you-get)[Requirements](#requirements)[Quick start](#quick-start)[Setup in detail](#setup-in-detail)[Usage](#usage)[How it works](#how-it-works)[Configuration](#configuration)[Security model](#security-model)[Limitations](#limitations)[Project layout](#project-layout)[License](#license)

**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 review*your*mentoring 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 a`Did / 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 an`origin`

, after each turn the bot stages that agent's workspace into a fresh commit on`agent/<name>`

and force-pushes it with a lease. The Slack final message links to the branch for review. With no`origin`

, 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, so`PATH`

.`claude --version`

must work in the same shell where you run the bot.**A Claude.ai subscription*** or*an Anthropic Console API key (see[Claude authentication](#4-claude-authentication)).**A Slack workspace**(free or paid) where you can install a custom app.*Optional:*a**GitHub 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](/canatara/phd_fleet/blob/main/requirements.txt) works.

The repo ships [slack-manifest.yaml](/canatara/phd_fleet/blob/main/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](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 the`connections:write`

scope. Copy it (`xapp-…`

) — that's your`SLACK_APP_TOKEN`

. - Under
**OAuth & Permissions**, install the app to your workspace. Copy the** Bot User OAuth Token**(`xoxb-…`

) — that's your`SLACK_BOT_TOKEN`

.

```
cp .env.example .env
# edit .env and fill in SLACK_BOT_TOKEN, SLACK_APP_TOKEN, ADVISOR_SLACK_USER_ID
```

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).** If`claude`

is OAuth-authenticated to your Claude.ai account — i.e.,`~/.claude/.credentials.json`

exists from running`claude /login`

once — the bot just uses it.**Leave**`ANTHROPIC_API_KEY`

unset.**API key.** To pay per token through the Anthropic Console, set`ANTHROPIC_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/`

from`student_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's`JOURNAL.md`

, and asks the coach to review*your*mentoring 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 longitudinal`mentor/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 from`CLAUDE.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](/canatara/phd_fleet/blob/main/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 paper*X*writes`library/<citekey>.md`

(a markdown summary with YAML frontmatter) and`library/<citekey>.pdf`

if it's freely downloadable.**Later readers** who want to add their take write a*separate*file —`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 walking`library/*.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](/canatara/phd_fleet/blob/main/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](/canatara/phd_fleet/blob/main/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](#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.** See[Security model](#security-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 loader + 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](/canatara/phd_fleet/blob/main/LICENSE).
