Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
80 changes: 78 additions & 2 deletions .claude/commands/doctor.md
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,82 @@ If `config.env` is missing, report FAIL — the plugin commands won't work witho

---

### Check 7: Security Audit
### Check 7: Task Event Channel

Check that the Claude Code channel for push-based task notifications is set up and working.

**Steps:**

1. Check if the channel files exist in the GM directory:
```bash
test -f "$LOCAL_PROJECT_DIR/channel/taskyou-channel.ts" && echo "CHANNEL_EXISTS" || echo "CHANNEL_MISSING"
test -f "$LOCAL_PROJECT_DIR/.mcp.json" && echo "MCP_JSON_EXISTS" || echo "MCP_JSON_MISSING"
test -d "$LOCAL_PROJECT_DIR/channel/node_modules" && echo "DEPS_INSTALLED" || echo "DEPS_MISSING"
```

2. **If channel files are missing**, deploy them from the plugin templates:

a. Find the TaskYou-OS plugin directory (or repo checkout):
```bash
TASKYOU_OS_DIR=""
if [ -f "./templates/channel/taskyou-channel.ts.tmpl" ]; then
TASKYOU_OS_DIR="."
else
PLUGIN_DIR=$(python3 -c "import json; d=json.load(open('$HOME/.claude/plugins/installed_plugins.json')); entries=d.get('plugins',{}).get('taskyou-os@taskyou-os',[]); print(entries[0]['installPath'] if entries else '')" 2>/dev/null)
if [ -n "$PLUGIN_DIR" ] && [ -f "$PLUGIN_DIR/templates/channel/taskyou-channel.ts.tmpl" ]; then
TASKYOU_OS_DIR="$PLUGIN_DIR"
fi
fi
```

b. If templates are available, render and deploy them. Use `config.env` to substitute variables:
```bash
source "$LOCAL_PROJECT_DIR/config.env"
mkdir -p "$LOCAL_PROJECT_DIR/channel"
```
- Read `$TASKYOU_OS_DIR/templates/channel/taskyou-channel.ts.tmpl`, substitute `{{SERVER_HOST}}` and `{{SERVER_HOME}}` with values from config.env, write to `$LOCAL_PROJECT_DIR/channel/taskyou-channel.ts`
- Read `$TASKYOU_OS_DIR/templates/channel/package.json.tmpl`, write to `$LOCAL_PROJECT_DIR/channel/package.json`
- Read `$TASKYOU_OS_DIR/templates/mcp.json.tmpl`, write to `$LOCAL_PROJECT_DIR/.mcp.json`

c. Install dependencies:
```bash
cd "$LOCAL_PROJECT_DIR/channel" && bun install --silent
```

d. Report WARN: "Deployed task event channel. Restart Claude Code to activate."

3. **If channel files exist but deps are missing**, install them:
```bash
cd "$LOCAL_PROJECT_DIR/channel" && bun install --silent
```
Report WARN: "Installed missing channel dependencies."

4. **If channel exists, check for drift** — compare deployed channel against the plugin template (same approach as nono drift detection in Check 8). If the template is newer, update the deployed file and report WARN.

5. **Check the shell alias** includes `--dangerously-load-development-channels server:taskyou`:
```bash
grep "$GM_ALIAS" ~/.zshrc 2>/dev/null || grep "$GM_ALIAS" ~/.bashrc 2>/dev/null
```
- If the alias exists but doesn't include `--dangerously-load-development-channels server:taskyou`, report WARN and show the user the updated alias line they should use:
```
alias <GM_ALIAS>='cd <LOCAL_PROJECT_DIR> && CLAUDE_CONFIG_DIR=<CONFIG_DIR> claude --dangerously-load-development-channels server:taskyou'
```
- If the alias already includes the flag, report PASS.

6. **Check the CLAUDE.md** has the channel-based monitoring section (not the old background-agent approach):
```bash
grep -c "Task event channel" "$LOCAL_PROJECT_DIR/CLAUDE.md"
grep -c "background monitoring agent" "$LOCAL_PROJECT_DIR/CLAUDE.md"
```
- If it has "background monitoring agent" but not "Task event channel", the CLAUDE.md needs updating. Render the Task Tracking section from the template and show the user the diff, offering to update it.

**If all channel files exist, deps installed, alias correct:** Report PASS with "Task event channel active."
**If deployed or fixed anything:** Report WARN with summary.
**If templates not found:** Report FAIL with "Channel templates not found. Update the TaskYou-OS plugin first."

---

### Check 8: Security Audit

Run the server-side security audit script to check credentials, permissions, and exposed services.

Expand Down Expand Up @@ -340,7 +415,7 @@ ssh -o ConnectTimeout=5 "$SERVER_HOST" '$HOME/.local/bin/audit.sh' 2>/dev/null

---

## Check 8: Credential Isolation (nono)
## Check 9: Credential Isolation (nono)

This check verifies if nono is set up, and if not, strongly recommends it. Always run this check regardless of whether credentials are currently configured.

Expand Down Expand Up @@ -441,6 +516,7 @@ TaskYou-OS Doctor
Daemon mode PASS/WARN/FAIL
Executor health PASS/WARN/FAIL
GM templates PASS/WARN/FAIL
Task event channel PASS/WARN/FAIL
Security audit PASS/WARN/FAIL
Credential isolation PASS/WARN
─────────────────────────────────
Expand Down
6 changes: 3 additions & 3 deletions .claude/commands/gm-babysit.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ description: Check on all tracked tasks for an immediate status update

Check on all tasks you're currently tracking. Use this for an immediate status update.

Note: Task events normally arrive automatically via the taskyou channel. This command is for a manual spot-check when you want an immediate snapshot.

First, load the project configuration:
```bash
source ./config.env
Expand All @@ -29,8 +31,6 @@ source ./config.env
- **Blocked**: Explain what's blocking it. Suggest next steps (retry, send input, review output).
- **Still processing**: Note it's still running — no action needed unless it's been unusually long.

5. **Re-launch the background notification watcher** if there are tasks still in progress and no background agent is currently watching.

6. **If all tracked tasks are done**, let the user know there's nothing left to monitor.
5. **If all tracked tasks are done**, let the user know there's nothing left to monitor.

Keep updates brief — one line per task.
16 changes: 16 additions & 0 deletions config.example.env
Original file line number Diff line number Diff line change
Expand Up @@ -90,3 +90,19 @@ PROJECT_DESCRIPTION="My Project does X, Y, and Z."
# Hosts agents can reach through the credential proxy.
# Only these hosts receive injected credentials.
# NONO_PROXY_HOSTS="api.linear.app,api.github.com"

# === Optional: Per-GM event scoping ===
# When multiple GMs share one TaskYou daemon, the channel can scope task
# events to a single GM. The channel stamps tasks it creates with
# `--assigned-gm <GM_SLUG>` (consuming ty's assigned_gm field) and only
# surfaces events whose assigned_gm matches this GM.
#
# GM_SLUG defaults to GM_ALIAS (set above), so no extra config is needed for
# the common single-GM case. Override it only if you want the assignment slug
# to differ from the launch alias.
# GM_SLUG="mygm"
#
# SEE_UNASSIGNED controls whether events with no assigned_gm (tasks created
# outside the channel, or emitted by an older ty without the field) are still
# shown to this GM. Defaults to "true". Set to "false" for strict isolation.
# SEE_UNASSIGNED="true"
16 changes: 15 additions & 1 deletion setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,20 @@ setup_local() {
chmod +x "$LOCAL_PROJECT_DIR/bin/${PROJECT_NAME}-open-board"
ok "bin/${PROJECT_NAME}-open-board"

# Channel (push-based task event notifications)
log "Setting up task event channel"
mkdir -p "$LOCAL_PROJECT_DIR/channel"
render_file "$TEMPLATES_DIR/channel/taskyou-channel.ts.tmpl" "$LOCAL_PROJECT_DIR/channel/taskyou-channel.ts"
ok "channel/taskyou-channel.ts"
render_file "$TEMPLATES_DIR/channel/package.json.tmpl" "$LOCAL_PROJECT_DIR/channel/package.json"
ok "channel/package.json"
# Install channel dependencies
(cd "$LOCAL_PROJECT_DIR/channel" && bun install --silent 2>/dev/null) || warn "bun install failed — run 'cd $LOCAL_PROJECT_DIR/channel && bun install' manually"

# .mcp.json (registers channel with Claude Code)
render_file "$TEMPLATES_DIR/mcp.json.tmpl" "$LOCAL_PROJECT_DIR/.mcp.json"
ok ".mcp.json"

# R2 wrangler.toml
if [[ "$R2_ENABLED" == "true" ]]; then
log "Setting up R2"
Expand All @@ -311,7 +325,7 @@ setup_local() {

# Shell alias
log "Shell alias"
local alias_line="alias ${GM_ALIAS}='cd ${LOCAL_PROJECT_DIR} && CLAUDE_CONFIG_DIR=${CLAUDE_CONFIG_DIR} claude'"
local alias_line="alias ${GM_ALIAS}='cd ${LOCAL_PROJECT_DIR} && CLAUDE_CONFIG_DIR=${CLAUDE_CONFIG_DIR} claude --dangerously-load-development-channels server:taskyou'"
echo " Add this to your shell profile (~/.zshrc or ~/.bashrc):"
echo ""
echo " $alias_line"
Expand Down
30 changes: 17 additions & 13 deletions templates/CLAUDE.md.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -279,25 +279,29 @@ Humans can request revisions on agent-delivered work by commenting `@agent` (or

You are responsible for following up on tasks you execute. This is automatic — {{OWNER_NAME}} should never have to ask "what happened with that task?"

### Automatic monitoring
### Task event channel

A channel server pushes task events directly into this session. When a remote agent completes or gets blocked, you'll see a `<channel source="taskyou">` event appear automatically — no polling or background agents needed.

When you execute a task (`ty execute <id>`):

1. **Add it to your todos** via TodoWrite with the task ID, title, and current status.
2. **Launch a background monitoring agent** to watch for task events. The agent should:
- Watch the server's notification stream: `./bin/ssh-remote "tail -n 0 -f {{SERVER_HOME}}/notifications.jsonl"`
- The server hooks automatically write to this file when tasks complete or get blocked
- When a line appears matching a tracked task ID, return immediately with the event details
- Time out after 20 minutes and return (you can re-launch if needed)
- One background agent can watch for ALL tracked tasks — no need for one per task
3. **When the background agent returns with an event**, update your todo and inform {{OWNER_NAME}}:
- **Completed**: Briefly note it finished, offer to show output (`ty output <id>`). Mark todo done.
- **Blocked**: Note what's blocking it, suggest next steps (retry, send input, review).
- Then re-launch the background agent if there are still tasks being tracked.
2. **Wait for the channel event.** Events arrive automatically:
- `event="completed"` — The agent finished. Briefly note it, offer to show output (`ty output <id>`). Mark todo done.
- `event="blocked"` — The agent is stuck. Check output, suggest next steps (retry, send input, review).

### Channel tools

The channel also provides tools you can use instead of `./bin/ty-remote` and `./bin/ssh-remote`:

- `ty_command` — Run any TaskYou command on the server (e.g. `ty_command("list")`, `ty_command("execute 42")`)
- `ssh_command` — Run any shell command on the server (e.g. `ssh_command("git -C projects/marketing log --oneline -5")`)

You can use either the channel tools or the bin/ scripts — they do the same thing.

### Be non-disruptive

When a background agent notifies you of a task update, be smart about surfacing it:
When a task event arrives, be smart about surfacing it:
- If {{OWNER_NAME}} is mid-thought or you're in the middle of a complex discussion, hold the update and surface it at a natural pause.
- Keep updates brief — one line is ideal. "Task #12 (competitor research) just finished. Want to see the output?"
- Don't pile up multiple updates at once. Space them out if several tasks complete simultaneously.
Expand All @@ -311,7 +315,7 @@ When a background agent notifies you of a task update, be smart about surfacing

### Manual check

{{OWNER_NAME}} can use `/gm-babysit` for an immediate status check on all tracked tasks without waiting for background agents.
{{OWNER_NAME}} can use `/gm-babysit` for an immediate status check on all tracked tasks.

## Advanced: Raw SSH Access

Expand Down
8 changes: 8 additions & 0 deletions templates/channel/package.json.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"name": "taskyou-channel",
"version": "0.1.0",
"private": true,
"dependencies": {
"@modelcontextprotocol/sdk": "^1.12.1"
}
}
111 changes: 111 additions & 0 deletions templates/channel/smoke-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
#!/usr/bin/env bun
// Smoke test: spawn the channel server, do the MCP handshake, verify capabilities and tools.
// Usage: bun run smoke-test.ts
// Requires: taskyou-channel.ts in the same directory (rendered with any server values — they don't matter for this test).

import { spawn } from "child_process";

const proc = spawn("bun", ["run", "taskyou-channel.ts"], {
cwd: import.meta.dir,
stdio: ["pipe", "pipe", "pipe"],
});

let stdout = "";
proc.stdout.on("data", (d: Buffer) => {
stdout += d.toString();
const lines = stdout.split("\n");
for (const line of lines.slice(0, -1)) {
if (!line.trim()) continue;
try {
const msg = JSON.parse(line);
if (msg.id === 1) {
handleInitResponse(msg);
} else if (msg.id === 2) {
handleToolsResponse(msg);
}
} catch {}
}
stdout = lines[lines.length - 1];
});

let stderr = "";
proc.stderr.on("data", (d: Buffer) => {
stderr += d.toString();
});

proc.on("close", (code) => {
if (code !== 0 && code !== null) {
console.log(`Process exited with code ${code}`);
if (stderr) console.log("stderr:", stderr);
process.exit(1);
}
});

function handleInitResponse(msg: any) {
const caps = msg.result?.capabilities?.experimental;
const hasChannel = !!caps?.["claude/channel"];
const hasTools = !!msg.result?.capabilities?.tools;
const hasInstructions = msg.result?.instructions?.includes("taskyou");

console.log(`${hasChannel ? "✓" : "✗"} claude/channel capability`);
console.log(`${hasTools ? "✓" : "✗"} tools capability`);
console.log(`${hasInstructions ? "✓" : "✗"} instructions`);

if (!hasChannel || !hasTools || !hasInstructions) {
console.log("\nFAILED — missing capabilities");
proc.kill();
process.exit(1);
}

// Request tool list
proc.stdin.write(
JSON.stringify({
jsonrpc: "2.0",
id: 2,
method: "tools/list",
params: {},
}) + "\n"
);
}

function handleToolsResponse(msg: any) {
const tools = msg.result?.tools || [];
const names = tools.map((t: any) => t.name);

const hasTy = names.includes("ty_command");
const hasSsh = names.includes("ssh_command");

console.log(`${hasTy ? "✓" : "✗"} ty_command tool`);
console.log(`${hasSsh ? "✓" : "✗"} ssh_command tool`);

if (!hasTy || !hasSsh) {
console.log("\nFAILED — missing tools");
proc.kill();
process.exit(1);
}

console.log("\nPASSED");
proc.kill();
process.exit(0);
}

// Send MCP initialize
proc.stdin.write(
JSON.stringify({
jsonrpc: "2.0",
id: 1,
method: "initialize",
params: {
protocolVersion: "2024-11-05",
capabilities: {},
clientInfo: { name: "smoke-test", version: "0.0.1" },
},
}) + "\n"
);

setTimeout(() => {
console.log("FAILED — timed out");
if (stderr) console.log("stderr:", stderr);
proc.kill();
process.exit(1);
}, 10_000);
Loading