Merge pull request #345 from ryaneggz/task/344-rename-client-slack

FROM task/344-rename-client-slack TO development
This commit is contained in:
Ryan Eggleston authored and GitHub committed 2026-05-23 19:49:19 -06:00
commit ce6f9ff493
9 files changed
+64 -38

No files matched your search

+23
View File
@@ -208,6 +208,29 @@ if [ -f "$HARNESS/scripts/cron-runtime.ts" ] && command -v tmux &>/dev/null; the
fi fi
fi fi
# ─── Restore client-slack session if Slack is configured ──────────────
# Tokens live in .devcontainer/.env (Compose KEY=value format). When both
# SLACK_APP_TOKEN and SLACK_BOT_TOKEN are set AND `pi` is installed for
# the sandbox user, restore the session that `oh config slack` created.
# Sourcing happens inside the tmux command (same pattern the wizard uses)
# so the child `pi` process inherits the exported vars.
SLACK_ENV="$HARNESS/.devcontainer/.env"
if [ -f "$SLACK_ENV" ] && command -v tmux &>/dev/null; then
# shellcheck disable=SC1090
SLACK_APP_TOKEN=$(grep -E '^SLACK_APP_TOKEN=' "$SLACK_ENV" | tail -1 | cut -d= -f2-)
SLACK_BOT_TOKEN=$(grep -E '^SLACK_BOT_TOKEN=' "$SLACK_ENV" | tail -1 | cut -d= -f2-)
if [ -n "$SLACK_APP_TOKEN" ] && [ -n "$SLACK_BOT_TOKEN" ] \
&& gosu sandbox bash -lc 'command -v pi' &>/dev/null; then
if ! gosu sandbox tmux has-session -t client-slack 2>/dev/null; then
gosu sandbox tmux new-session -d -s client-slack \
"bash -c 'set -a; source $SLACK_ENV; set +a; pi 2>&1 | tee /tmp/client-slack.log'"
echo "[entrypoint] client-slack tmux session started (Slack bridge)"
else
echo "[entrypoint] client-slack tmux session already running — skipping"
fi
fi
fi
# ─── Optional: agent-browser (opt-in via INSTALL_AGENT_BROWSER=true) ── # ─── Optional: agent-browser (opt-in via INSTALL_AGENT_BROWSER=true) ──
if [ "${INSTALL_AGENT_BROWSER:-false}" = "true" ] && ! command -v agent-browser &>/dev/null; then if [ "${INSTALL_AGENT_BROWSER:-false}" = "true" ] && ! command -v agent-browser &>/dev/null; then
echo "[entrypoint] Installing agent-browser (INSTALL_AGENT_BROWSER=true)..." echo "[entrypoint] Installing agent-browser (INSTALL_AGENT_BROWSER=true)..."
+3 -3
View File
@@ -74,8 +74,8 @@ SLACK_ALLOW_USERS=U789
```bash ```bash
make shell make shell
set -a; source /home/sandbox/harness/.devcontainer/.env; set +a set -a; source /home/sandbox/harness/.devcontainer/.env; set +a
tmux new-session -d -s agent-pi 'pi 2>&1 | tee /tmp/agent-pi.log' tmux new-session -d -s client-slack 'pi 2>&1 | tee /tmp/client-slack.log'
tmux attach -t agent-pi tmux attach -t client-slack
``` ```
`set -a` is needed because `.devcontainer/.env` is Compose-formatted (no `export` prefix); without it, sourced vars stay in the shell but aren't inherited by `pi`. The tmux session keeps `pi` alive across SSH/VS Code disconnects (see [`context/rules/sandbox-processes.md`](../../context/rules/sandbox-processes.md)). `set -a` is needed because `.devcontainer/.env` is Compose-formatted (no `export` prefix); without it, sourced vars stay in the shell but aren't inherited by `pi`. The tmux session keeps `pi` alive across SSH/VS Code disconnects (see [`context/rules/sandbox-processes.md`](../../context/rules/sandbox-processes.md)).
@@ -86,7 +86,7 @@ SLACK_ALLOW_USERS=U789
```bash ```bash
env | grep SLACK # confirm vars are exported env | grep SLACK # confirm vars are exported
``` ```
Then DM the bot or `@mention` it from an allow-listed user. Watch the round-trip in `tmux attach -t agent-pi` or `tail -f /tmp/agent-pi.log`. Then DM the bot or `@mention` it from an allow-listed user. Watch the round-trip in `tmux attach -t client-slack` or `tail -f /tmp/client-slack.log`.
## Build and Test ## Build and Test
+1
View File
@@ -25,6 +25,7 @@ Update policy and release automation live in [`.claude/rules/git.md`](.claude/ru
- `README.md`, `docs/installation.md`, and `docs/quickstart.md` restructured to surface fork-and-clone and clone-and-own as first-class install paths alongside the upstream one-liner; `scripts/install.sh` local-run detection is now the documented bootstrap for self-hosters ([#331](https://github.com/ryaneggz/open-harness/issues/331)). - `README.md`, `docs/installation.md`, and `docs/quickstart.md` restructured to surface fork-and-clone and clone-and-own as first-class install paths alongside the upstream one-liner; `scripts/install.sh` local-run detection is now the documented bootstrap for self-hosters ([#331](https://github.com/ryaneggz/open-harness/issues/331)).
- Skill `LICENSE` files updated to `Copyright (c) 2026 Mifune Dev (mifune.dev)` to match upstream correction. - Skill `LICENSE` files updated to `Copyright (c) 2026 Mifune Dev (mifune.dev)` to match upstream correction.
- Re-synced `context-audit` to the portable upstream form ([mifunedev/skills#6](https://github.com/mifunedev/skills/pull/6)): removed the top-level `argument-hint` frontmatter key, repinned `.mifune/skills.lock` (`commit` `f95011fe`, checksum `02dd6324`), and added the previously-missing `LICENSE` for folder parity ([#339](https://github.com/ryaneggz/open-harness/issues/339)). - Re-synced `context-audit` to the portable upstream form ([mifunedev/skills#6](https://github.com/mifunedev/skills/pull/6)): removed the top-level `argument-hint` frontmatter key, repinned `.mifune/skills.lock` (`commit` `f95011fe`, checksum `02dd6324`), and added the previously-missing `LICENSE` for folder parity ([#339](https://github.com/ryaneggz/open-harness/issues/339)).
- Renamed the Slack-bridge tmux session from `agent-pi` to `client-slack` and introduced a `client-` prefix in `context/rules/sandbox-processes.md` for external-surface clients (Slack, Discord, web UIs) that bridge an in-sandbox agent. `.devcontainer/entrypoint.sh` now restores the `client-slack` session on container start whenever `SLACK_APP_TOKEN` and `SLACK_BOT_TOKEN` are present in `.devcontainer/.env` and `pi` is installed, so the Slack bridge survives `docker compose restart` without manual relaunch. Rename propagated through `apps/oh/src/config/slack.ts` (`oh config slack` wizard), `.pi/extensions/slack/README.md`, `docs/integrations/slack.md`, `docs/connecting.md`, `docs/intro.md`, and `docs/harnesses/overview.md` (the latter folding in the half-finished `harness-pi` rename from #302).
### Fixed ### Fixed
- Sandbox image installs `pi-coding-agent` under the sandbox user's npm prefix (`/home/sandbox/.local`) and places that bin directory before `/usr/bin`, allowing `pi update` to self-update without sudo on fresh builds. - Sandbox image installs `pi-coding-agent` under the sandbox user's npm prefix (`/home/sandbox/.local`) and places that bin directory before `/usr/bin`, allowing `pi update` to self-update without sudo on fresh builds.
### Removed ### Removed
+18 -18
View File
@@ -10,7 +10,7 @@ import {
hasSession, killSession, newSession, capturePane, isInstalled as tmuxAvailable, hasSession, killSession, newSession, capturePane, isInstalled as tmuxAvailable,
} from "../lib/tmux.js"; } from "../lib/tmux.js";
const AGENT_PI_SESSION = "agent-pi"; const CLIENT_SLACK_SESSION = "client-slack";
const CONNECTED_MARKER = "connected and listening"; const CONNECTED_MARKER = "connected and listening";
const POLL_INTERVAL_MS = 1000; const POLL_INTERVAL_MS = 1000;
const MAX_POLLS = 15; const MAX_POLLS = 15;
@@ -143,37 +143,37 @@ async function promptAllowlist(env: Record<string, string | undefined>): Promise
return result; return result;
} }
async function relaunchAgentPi(envPath: string): Promise<void> { async function relaunchClientSlack(envPath: string): Promise<void> {
if (!tmuxAvailable()) { if (!tmuxAvailable()) {
warn("tmux not found in PATH — skipping agent-pi restart. Restart it manually after this exits."); warn("tmux not found in PATH — skipping client-slack restart. Restart it manually after this exits.");
return; return;
} }
const wasRunning = hasSession(AGENT_PI_SESSION); const wasRunning = hasSession(CLIENT_SLACK_SESSION);
if (wasRunning) { if (wasRunning) {
info(`Killing existing tmux session "${AGENT_PI_SESSION}"…`); info(`Killing existing tmux session "${CLIENT_SLACK_SESSION}"…`);
killSession(AGENT_PI_SESSION); killSession(CLIENT_SLACK_SESSION);
} }
info(`Starting tmux session "${AGENT_PI_SESSION}"…`); info(`Starting tmux session "${CLIENT_SLACK_SESSION}"…`);
// The session sources .env via `set -a` so child pi inherits the new vars. // The session sources .env via `set -a` so child pi inherits the new vars.
const cmd = `bash -c 'set -a; source ${envPath}; set +a; pi 2>&1 | tee /tmp/agent-pi.log'`; const cmd = `bash -c 'set -a; source ${envPath}; set +a; pi 2>&1 | tee /tmp/client-slack.log'`;
newSession(AGENT_PI_SESSION, cmd); newSession(CLIENT_SLACK_SESSION, cmd);
info("Validating Slack connection (up to 15s)…"); info("Validating Slack connection (up to 15s)…");
for (let i = 0; i < MAX_POLLS; i++) { for (let i = 0; i < MAX_POLLS; i++) {
const out = capturePane(AGENT_PI_SESSION); const out = capturePane(CLIENT_SLACK_SESSION);
if (out.includes(CONNECTED_MARKER)) { if (out.includes(CONNECTED_MARKER)) {
ok("Slack extension connected."); ok("Slack extension connected.");
return; return;
} }
if (/Run error|Missing env/.test(out)) { if (/Run error|Missing env/.test(out)) {
fail("Slack extension reported an error. Check `tmux attach -t agent-pi`."); fail("Slack extension reported an error. Check `tmux attach -t client-slack`.");
return; return;
} }
await new Promise<void>((r) => setTimeout(r, POLL_INTERVAL_MS)); await new Promise<void>((r) => setTimeout(r, POLL_INTERVAL_MS));
process.stdout.write("."); process.stdout.write(".");
} }
process.stdout.write("\n"); process.stdout.write("\n");
warn("Did not see 'connected and listening' within 15s. Check `tmux attach -t agent-pi`."); warn("Did not see 'connected and listening' within 15s. Check `tmux attach -t client-slack`.");
} }
export async function runSlack(): Promise<number> { export async function runSlack(): Promise<number> {
@@ -231,20 +231,20 @@ export async function runSlack(): Promise<number> {
return 1; return 1;
} }
info("Next step: start the Slack bridge by (re)launching the agent-pi tmux session."); info("Next step: start the Slack bridge by (re)launching the client-slack tmux session.");
info("This kills any existing agent-pi session, then starts pi with the new tokens;"); info("This kills any existing client-slack session, then starts pi with the new tokens;");
info("pi loads the slack extension on boot and opens the Socket Mode connection."); info("pi loads the slack extension on boot and opens the Socket Mode connection.");
if (await confirm("Start the Slack bridge now?")) { if (await confirm("Start the Slack bridge now?")) {
await relaunchAgentPi(envPath); await relaunchClientSlack(envPath);
info(""); info("");
info("Slack bridge is live. Test it: DM your bot or @mention it in an allow-listed channel."); info("Slack bridge is live. Test it: DM your bot or @mention it in an allow-listed channel.");
info("Tail the live log: tmux attach -t agent-pi (detach: Ctrl-b d)"); info("Tail the live log: tmux attach -t client-slack (detach: Ctrl-b d)");
} else { } else {
info("Skipped. Start the bridge manually when ready:"); info("Skipped. Start the bridge manually when ready:");
info(" tmux kill-session -t agent-pi 2>/dev/null; \\"); info(" tmux kill-session -t client-slack 2>/dev/null; \\");
info(` set -a; source ${envPath}; set +a; \\`); info(` set -a; source ${envPath}; set +a; \\`);
info(" tmux new-session -d -s agent-pi 'pi 2>&1 | tee /tmp/agent-pi.log'"); info(" tmux new-session -d -s client-slack 'pi 2>&1 | tee /tmp/client-slack.log'");
} }
return 0; return 0;
+2 -1
View File
@@ -18,7 +18,8 @@ Format: `<category>-<identifier>` (kebab-case inside each segment).
|----------|---------|---------| |----------|---------|---------|
| `app-` | `app-docs`, `app-api` | User dev servers | | `app-` | `app-docs`, `app-api` | User dev servers |
| `expose-public-` | `expose-public-3000` | Cloudflare quick tunnels (auto-created) | | `expose-public-` | `expose-public-3000` | Cloudflare quick tunnels (auto-created) |
| `agent-` | `agent-claude`, `agent-pi` | AI agent processes | | `agent-` | `agent-watcher`, `agent-batch` | Headless / long-running agent processes. Interactive CLIs (`claude`, `codex`, `opencode`) are normally foreground in a terminal or VS Code, not detached in tmux. |
| `client-` | `client-slack`, `client-discord` | External-surface clients that bridge an in-sandbox agent to a third-party UI |
| `heartbeat-` | `heartbeat-daily-sync` | Scheduled heartbeat tasks | | `heartbeat-` | `heartbeat-daily-sync` | Scheduled heartbeat tasks |
Reserved prefix: `system-`. Do not use for user apps. Reserved prefix: `system-`. Do not use for user apps.
+4 -3
View File
@@ -93,7 +93,8 @@ All long-running processes inside the sandbox run in named tmux sessions. The na
| Category | Example | Purpose | | Category | Example | Purpose |
|----------|---------|---------| |----------|---------|---------|
| `agent-` | `agent-claude`, `agent-pi` | AI agent processes | | `client-` | `client-slack`, `client-discord` | External-surface clients bridging an in-sandbox agent |
| `agent-` | `agent-watcher`, `agent-batch` | Headless / long-running agent processes (interactive CLIs are foreground, not tmux) |
| `app-` | `app-docs`, `app-api` | Dev servers | | `app-` | `app-docs`, `app-api` | Dev servers |
For the full convention see [`context/rules/sandbox-processes.md`](https://github.com/ryaneggz/open-harness/blob/development/context/rules/sandbox-processes.md). For the full convention see [`context/rules/sandbox-processes.md`](https://github.com/ryaneggz/open-harness/blob/development/context/rules/sandbox-processes.md).
@@ -113,12 +114,12 @@ Follow Option B (or C for a remote host). The Ports panel in VSCode shows forwar
oh config slack oh config slack
``` ```
The interactive wizard collects your Slack tokens, validates them, writes `.devcontainer/.env`, and restarts the `agent-pi` tmux session in Socket Mode. For the full wizard walkthrough see [Integrations → Slack](/docs/integrations/slack). The interactive wizard collects your Slack tokens, validates them, writes `.devcontainer/.env`, and restarts the `client-slack` tmux session in Socket Mode. For the full wizard walkthrough see [Integrations → Slack](/docs/integrations/slack).
After the wizard completes, verify the bridge is live: After the wizard completes, verify the bridge is live:
```bash ```bash
tmux capture-pane -t agent-pi -p | grep -i 'connected\|listening' tmux capture-pane -t client-slack -p | grep -i 'connected\|listening'
``` ```
### Step 3 — Launch T3 Code ### Step 3 — Launch T3 Code
+2 -2
View File
@@ -54,11 +54,11 @@ Each runs in its own named tmux session per [`context/rules/sandbox-processes.md
### Pi+Slack ### Pi+Slack
The Pi agent with the Slack bridge loaded. The wizard writes `.devcontainer/.env`, launches the `harness-pi` session, and waits for the Socket Mode connection to come up: The Pi agent with the Slack bridge loaded. The wizard writes `.devcontainer/.env`, launches the `client-slack` session, and waits for the Socket Mode connection to come up:
```bash ```bash
oh config slack # interactive wizard oh config slack # interactive wizard
tmux attach -t harness-pi # watch the live log tmux attach -t client-slack # watch the live log
``` ```
Talk to the agent from Slack (DM or `@mention` in an allow-listed channel). Full setup: [Slack integration](../integrations/slack.md). Talk to the agent from Slack (DM or `@mention` in an allow-listed channel). Full setup: [Slack integration](../integrations/slack.md).
+10 -10
View File
@@ -69,7 +69,7 @@ Keep both values ready for the next step.
Inside the sandbox, run the wizard. It prompts for the two tokens, asks Inside the sandbox, run the wizard. It prompts for the two tokens, asks
which allowlist mode to use (users / channels / both), validates IDs, which allowlist mode to use (users / channels / both), validates IDs,
writes `.devcontainer/.env` atomically, and offers to restart the writes `.devcontainer/.env` atomically, and offers to restart the
`agent-pi` tmux session. `client-slack` tmux session.
```bash ```bash
make shell # enter the sandbox make shell # enter the sandbox
@@ -84,7 +84,7 @@ The wizard:
- Writes only the Slack keys; unrelated entries (`GH_TOKEN`, `TZ`, etc.) - Writes only the Slack keys; unrelated entries (`GH_TOKEN`, `TZ`, etc.)
are preserved. are preserved.
- **Starts the Slack bridge for you** — at the end, it kills any - **Starts the Slack bridge for you** — at the end, it kills any
existing `agent-pi` tmux session and launches a fresh one running existing `client-slack` tmux session and launches a fresh one running
`pi` with the new env sourced. `pi` loads the Slack extension on boot `pi` with the new env sourced. `pi` loads the Slack extension on boot
and opens the Socket Mode connection. The wizard polls for the and opens the Socket Mode connection. The wizard polls for the
`"connected and listening"` log line and prints `✓ Slack bridge is live` `"connected and listening"` log line and prints `✓ Slack bridge is live`
@@ -99,7 +99,7 @@ If `oh` isn't available (older sandbox image, or you'd rather edit by
hand), the manual procedure still works: hand), the manual procedure still works:
<details> <details>
<summary>Hand-edit <code>.devcontainer/.env</code> + relaunch <code>agent-pi</code></summary> <summary>Hand-edit <code>.devcontainer/.env</code> + relaunch <code>client-slack</code></summary>
`.devcontainer/.env` uses Docker Compose `KEY=value` format — no `export` `.devcontainer/.env` uses Docker Compose `KEY=value` format — no `export`
prefix. This file is gitignored, so your tokens will not be committed. prefix. This file is gitignored, so your tokens will not be committed.
@@ -126,11 +126,11 @@ vars. This is the most common manual-mode failure.
```bash ```bash
set -a; source /home/sandbox/harness/.devcontainer/.env; set +a set -a; source /home/sandbox/harness/.devcontainer/.env; set +a
tmux new-session -d -s agent-pi 'pi 2>&1 | tee /tmp/agent-pi.log' tmux new-session -d -s client-slack 'pi 2>&1 | tee /tmp/client-slack.log'
tmux attach -t agent-pi tmux attach -t client-slack
``` ```
Detach with `Ctrl-b d`. The session name `agent-pi` follows the `agent-` Detach with `Ctrl-b d`. The session name `client-slack` follows the `client-`
prefix convention in prefix convention in
[`context/rules/sandbox-processes.md`](https://github.com/ryaneggz/open-harness/blob/development/context/rules/sandbox-processes.md). [`context/rules/sandbox-processes.md`](https://github.com/ryaneggz/open-harness/blob/development/context/rules/sandbox-processes.md).
@@ -151,7 +151,7 @@ the env (before attaching to tmux).
2. **Socket Mode connected** (the real connectivity check): 2. **Socket Mode connected** (the real connectivity check):
```bash ```bash
tmux capture-pane -t agent-pi -p | grep -i 'socket\|connected\|listening' tmux capture-pane -t client-slack -p | grep -i 'socket\|connected\|listening'
``` ```
Look for lines indicating the Socket Mode client connected or is listening. Look for lines indicating the Socket Mode client connected or is listening.
Note: `curl https://slack.com/api/auth.test` only validates the bot token Note: `curl https://slack.com/api/auth.test` only validates the bot token
@@ -161,17 +161,17 @@ the env (before attaching to tmux).
3. **Round-trip test:** 3. **Round-trip test:**
From an allow-listed user account, DM the bot or `@mention` it in an From an allow-listed user account, DM the bot or `@mention` it in an
allow-listed channel. Watch `tmux attach -t agent-pi` — you should see the allow-listed channel. Watch `tmux attach -t client-slack` — you should see the
inbound event logged and the agent's reply posted back to Slack. inbound event logged and the agent's reply posted back to Slack.
## 7. Troubleshooting ## 7. Troubleshooting
| Symptom | Cause | Fix | | Symptom | Cause | Fix |
|---------|-------|-----| |---------|-------|-----|
| Bot doesn't respond; allowlist is set | `pi` did not inherit the vars — `set -a` ran in a different shell than `tmux new-session` | Kill the session (`tmux kill-session -t agent-pi`), run `set -a; source …; set +a`, then relaunch `tmux new-session` in the same shell | | Bot doesn't respond; allowlist is set | `pi` did not inherit the vars — `set -a` ran in a different shell than `tmux new-session` | Kill the session (`tmux kill-session -t client-slack`), run `set -a; source …; set +a`, then relaunch `tmux new-session` in the same shell |
| Bot doesn't respond; no allowlist set | Deny-default: both `SLACK_ALLOW_CHANNELS` and `SLACK_ALLOW_USERS` are unset | Add at least one allowlist var to `.devcontainer/.env` and relaunch | | Bot doesn't respond; no allowlist set | Deny-default: both `SLACK_ALLOW_CHANNELS` and `SLACK_ALLOW_USERS` are unset | Add at least one allowlist var to `.devcontainer/.env` and relaunch |
| `invalid_auth` or `not_authed` in log | `xapp-` and `xoxb-` tokens are swapped | `SLACK_APP_TOKEN` must be the `xapp-` token; `SLACK_BOT_TOKEN` must be the `xoxb-` token — correct in `.devcontainer/.env` and relaunch | | `invalid_auth` or `not_authed` in log | `xapp-` and `xoxb-` tokens are swapped | `SLACK_APP_TOKEN` must be the `xapp-` token; `SLACK_BOT_TOKEN` must be the `xoxb-` token — correct in `.devcontainer/.env` and relaunch |
| Socket Mode crashes / reconnects in a loop | Network interruption or token revoked | `tmux kill-session -t agent-pi`, verify tokens in `.devcontainer/.env`, then relaunch | | Socket Mode crashes / reconnects in a loop | Network interruption or token revoked | `tmux kill-session -t client-slack`, verify tokens in `.devcontainer/.env`, then relaunch |
| Bot is in allow-list but channel messages ignored | Bot is not a member of the channel | In Slack, type `/invite @OpenHarness` in the target channel | | Bot is in allow-list but channel messages ignored | Bot is not a member of the channel | In Slack, type `/invite @OpenHarness` in the target channel |
## 8. Architecture Pointer ## 8. Architecture Pointer
+1 -1
View File
@@ -39,7 +39,7 @@ flowchart TB
subgraph sandbox["Sandbox container — default workspace"] subgraph sandbox["Sandbox container — default workspace"]
Orch{{"<b>Orchestrator</b><br/>Claude @ project root<br/>git · lifecycle · file edits"}} Orch{{"<b>Orchestrator</b><br/>Claude @ project root<br/>git · lifecycle · file edits"}}
Tmux["tmux sessions<br/>agent-pi · agent-t3code · app-docs · system-cron"] Tmux["tmux sessions<br/>client-slack · agent-t3code · app-docs · system-cron"]
Sock(["docker.sock"]) Sock(["docker.sock"])
end end