Skip to content

Add per-GM event scoping to the channel templates (consume assigned_gm) - #32

Open
kylecarbonneau wants to merge 4 commits into
taskyou:feat/channels-integrationfrom
kylecarbonneau:pr/channel-gm-scoping
Open

Add per-GM event scoping to the channel templates (consume assigned_gm)#32
kylecarbonneau wants to merge 4 commits into
taskyou:feat/channels-integrationfrom
kylecarbonneau:pr/channel-gm-scoping

Conversation

@kylecarbonneau

Copy link
Copy Markdown
Contributor

Makes the TaskYou Claude Code channel multi-GM aware, consuming the new assigned_gm field shipped in bborn/taskyou#561 (the ty binary now sets TASK_ASSIGNED_GM on event hooks and accepts --assigned-gm on ty create).

What

When several GMs share a single TaskYou daemon, the channel now:

  1. Stamps the tasks it creates with --assigned-gm <GM_SLUG> — only on ty create, idempotent (skipped when the caller already passed --assigned-gm).
  2. Filters the events it surfaces to the current GM — emits an event when its assigned_gm matches this GM's slug, or when it's unassigned and the GM opts into seeing unassigned events (SEE_UNASSIGNED, default true).

Files (additive — no deletions):

  • templates/channel/taskyou-channel.ts.tmpl — new GM_SLUG (process.env.GM_SLUG || process.env.GM_ALIAS || {{GM_ALIAS}}) and SEE_UNASSIGNED consts; --assigned-gm auto-injection on ty create (idempotent); notification filter in pollNotifications (the line cursor always advances — only the emit is gated, so nothing is lost from the read position); assigned_gm added to the emitted notification meta.
  • templates/hooks/task.completed.tmpl, templates/hooks/task.blocked.tmpl — emit "assigned_gm": "$TASK_ASSIGNED_GM" in the JSON line.
  • config.example.env — documents GM_SLUG (derives from GM_ALIAS) and SEE_UNASSIGNED.

Why

Channel-side companion to #561: it turns the daemon's per-task GM attribution into actual per-session event routing, so one shared daemon fans tasks/notifications out to the correct GM instead of broadcasting every event to every GM.

Additive on top of #28 (the channels integration) and independent of the local-mode/macOS work (#31) — branched directly off #28 so it's cleanly mergeable on its own (verified zero-conflict merge with #31 in either order). Fully backward-compatible: with no config and an older ty (no assigned_gm), SEE_UNASSIGNED defaults to true, so unassigned events still surface and tasks are stamped with the GM's existing alias. The SSH/remote command path is untouched.

How tested

  • Channel template: rendered with sample setup env (GM_ALIAS=acme); GM_SLUG substitutes to the alias default. bun build clean (transpile + full bundle against the MCP SDK).
  • Hook templates: bash -n clean; rendered task.completed with TASK_ASSIGNED_GM set and a quote-containing title emits valid, correctly-escaped JSON with assigned_gm populated.
  • Confirmed the diff has no file deletions and touches only the four named templates.

bborn and others added 4 commits April 11, 2026 08:51
Replace the pull-based monitoring approach (background agents running
ssh tail -f, 20-minute timeouts, manual /gm-babysit polling) with a
Claude Code channel that pushes task events directly into the GM session.

The channel is an MCP server (taskyou-channel.ts) that polls the server's
notifications.jsonl over SSH and emits events as <channel source="taskyou">
tags. Each GM gets its own channel, baked with that GM's server config at
setup time. Also exposes ty_command and ssh_command tools so the GM can
run server commands through the channel instead of shelling out.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Doctor Check 7 detects missing channel files, deploys them from plugin
templates using config.env, installs bun deps, checks the shell alias
for the channels flag, and verifies CLAUDE.md has the new monitoring
section. Follows the same drift-detection pattern as the nono check.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Spawns the channel server, performs the MCP handshake, and verifies
the claude/channel capability, tools capability, instructions, and
both ty_command and ssh_command tools are registered.

Run: cd channel && bun run smoke-test.ts

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Consume ty's new assigned_gm field (TASK_ASSIGNED_GM env var, shipped in
bborn/taskyou PR #561) so a single TaskYou daemon shared by multiple GMs
can scope task events per GM.

Channel template (templates/channel/taskyou-channel.ts.tmpl):
- Add GM_SLUG (process.env.GM_SLUG || GM_ALIAS, defaulting to the
  setup-time {{GM_ALIAS}}) and SEE_UNASSIGNED (default true) consts.
- Auto-inject `--assigned-gm <GM_SLUG>` on `ty create` only, idempotent
  (skipped if the caller already passed --assigned-gm).
- Filter emitted notifications: emit when assigned_gm === GM_SLUG, or when
  unassigned (missing/blank) and SEE_UNASSIGNED. Malformed lines are treated
  as unassigned. The line cursor always advances; only the emit is gated.
- Surface assigned_gm in the notification meta.

Hook templates (task.completed, task.blocked):
- Emit assigned_gm (from $TASK_ASSIGNED_GM) in the JSON line.

config.example.env:
- Document GM_SLUG (derives from GM_ALIAS) and SEE_UNASSIGNED.

Additive only; existing behavior — including the SSH/remote path — is
unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
bborn added a commit that referenced this pull request Jun 9, 2026
notifications.jsonl survives as the push substrate, but the channels
refactor changes three things: copy taskyou-channel.ts (bun/TS) instead
of linear-poll.mjs; reuse assigned_gm (#32 + workflow#561) for per-GM
routing (answers the shared-bot vs per-user question); adopt the
IS_LOCAL/SSH runRemote pattern + OS-aware hooks dir (#31). Adds a
dependencies section and merge order (#28 -> #31/#32 -> Slack).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@bborn
bborn force-pushed the feat/channels-integration branch from 8caee9e to 188555e Compare June 9, 2026 12:35
bborn added a commit that referenced this pull request Jun 9, 2026
notifications.jsonl survives as the push substrate, but the channels
refactor changes three things: copy taskyou-channel.ts (bun/TS) instead
of linear-poll.mjs; reuse assigned_gm (#32 + workflow#561) for per-GM
routing (answers the shared-bot vs per-user question); adopt the
IS_LOCAL/SSH runRemote pattern + OS-aware hooks dir (#31). Adds a
dependencies section and merge order (#28 -> #31/#32 -> Slack).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
bborn added a commit that referenced this pull request Jun 9, 2026
* docs: add Slack module design (manage TaskYou from Slack)

From TaskYou task #3723. Proposes modules/slack/, modeled on
modules/linear/linear-poll.mjs: watch a Slack channel/DM/@mention,
classify intent, drive ty, post replies, and tail notifications.jsonl
to push task.blocked/completed back to the channel. Enabled via
SLACK_ENABLED. Separates the chat-control bridge (recommended) from the
harder hosted/remote-MCP path (deferred to the credential-proxy work).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs: reconcile Slack design with in-flight channels work (#28/#31/#32)

notifications.jsonl survives as the push substrate, but the channels
refactor changes three things: copy taskyou-channel.ts (bun/TS) instead
of linear-poll.mjs; reuse assigned_gm (#32 + workflow#561) for per-GM
routing (answers the shared-bot vs per-user question); adopt the
IS_LOCAL/SSH runRemote pattern + OS-aware hooks dir (#31). Adds a
dependencies section and merge order (#28 -> #31/#32 -> Slack).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs: land Slack design on merged channel work; drop per-GM scoping

#28 (channels) and #31 (local/macOS) are merged to main, so the design
is concrete: a Slack module is taskyou-channel.ts with its two ends
re-pointed — reuse pollNotifications() + runRemote() verbatim; net-new is
just a Slack adapter + LLM classifier. Ship outbound (hook -> Slack
webhook) first, inbound control second. Removes the assigned_gm / per-GM
scoping dependency (out of scope: single bot, single operator).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(slack): implement two-way Slack module (modules/slack)

Builds the Slack integration end to end (outbound + inbound), per the
design in docs/plans/2026-06-08-slack-module-design.md.

modules/slack/slack-bridge.mjs — zero-dependency Node daemon (raw fetch +
global WebSocket, like the Linear poller). Runs on the agents server next
to ty + notifications.jsonl:
  • Outbound: tails notifications.jsonl → chat.postMessage (blocked/
    completed/failed/started). Slack-originated tasks answer in-thread;
    everything else goes to SLACK_NOTIFY_CHANNEL.
  • Inbound: Slack Socket Mode (no public URL) → allowlist check →
    intent classification (Anthropic API, with a keyword-heuristic
    fallback when no key) → ty create/execute/input/list. Replies in-thread.

Also: unit tests for the pure logic (node --test, 11 cases), a
README, the ty-slack systemd service template, and setup.sh wiring
(server + exe.dev install the service; local mode renders files + .env)
plus SLACK_* in config.example.env and the README modules list. The
module is gated behind SLACK_ENABLED=true.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(slack): default classifier to on-box `claude` CLI + runaway guards

Classifier no longer requires a separate ANTHROPIC_API_KEY: it prefers the
already-authenticated on-box `claude` CLI (claude -p — the same primitive the
GM/executors use), falls back to the Anthropic API if a key is set, then a
keyword heuristic. Override via SLACK_CLASSIFIER.

Loop / token-burn hardening (per review):
- claude -p sandboxed per call: --strict-mcp-config --mcp-config {} +
  --disallowedTools … keep it to a single turn (no agentic spiral);
  --max-budget-usd (default $0.05) hard-caps cost; timeout + maxBuffer; no retry.
- Self-loop guards: ignore own/bot/edited messages (bot_id/subtype/BOT_USER_ID).
- Concurrency cap (SLACK_MAX_CONCURRENT, default 3) bounds in-flight
  classifications; excess messages declined, not queued.
- Socket generation guard: only the newest connection's handlers stay live, so
  a reconnect race can't double-deliver events.
- Outbound poller: per-tick cap (SLACK_MAX_NOTIFS_PER_POLL, default 25) with
  byte-accurate gradual drain; only complete lines consumed (partial hook line
  held); lost/corrupt state skips backlog instead of replaying.

Tests: +2 (buildClassifierContext, readNewChunk partial-line/rotation) → 13/13.
Docs: README "Runaway / cost protection", config knobs, design-doc note.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants