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
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"
68 changes: 52 additions & 16 deletions templates/channel/taskyou-channel.ts.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,14 @@ const SERVER_HOST = "{{SERVER_HOST}}";
const SERVER_HOME = "{{SERVER_HOME}}";
const POLL_INTERVAL_MS = 10_000; // 10 seconds

// Per-GM event scoping. GM_SLUG identifies this GM (defaults to the GM_ALIAS
// chosen at setup time). New tasks created through this channel are stamped
// with --assigned-gm <GM_SLUG>, and emitted notifications are filtered to this
// GM. SEE_UNASSIGNED controls whether events with no assigned_gm (e.g. tasks
// created outside the channel, or older ty versions) are still surfaced here.
const GM_SLUG = process.env.GM_SLUG || process.env.GM_ALIAS || "{{GM_ALIAS}}";
const SEE_UNASSIGNED = (process.env.SEE_UNASSIGNED ?? "true") !== "false";

// Track where we are in the notifications file
let lastLineCount = 0;

Expand Down Expand Up @@ -81,7 +89,15 @@ mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
const { name, arguments: args } = req.params;

if (name === "ty_command") {
const { args: tyArgs } = args as { args: string };
let { args: tyArgs } = args as { args: string };
// Scope new tasks to this GM: inject --assigned-gm on `create` (only),
// unless the caller already specified one. Idempotent.
if (
/^\s*create(\s|$)/.test(tyArgs) &&
!/--assigned-gm(\s|=)/.test(tyArgs)
) {
tyArgs = `${tyArgs} --assigned-gm ${GM_SLUG}`;
}
const result = await runRemote(`ty ${tyArgs}`);
return { content: [{ type: "text", text: result }] };
}
Expand Down Expand Up @@ -147,24 +163,44 @@ async function pollNotifications() {
const trimmed = line.trim();
if (!trimmed) continue;

let event: any;
try {
const event = JSON.parse(trimmed);
await mcp.notification({
method: "notifications/claude/channel",
params: {
content: trimmed,
meta: {
event: event.event || "unknown",
task_id: event.task_id || "",
title: event.title || "",
project: event.project || "",
timestamp: event.timestamp || "",
},
},
});
event = JSON.parse(trimmed);
} catch {
// Skip malformed lines
// Malformed line → treat as unassigned. The cursor still advances
// below (we iterate every line); only the emit is gated here.
if (SEE_UNASSIGNED) {
await mcp.notification({
method: "notifications/claude/channel",
params: { content: trimmed, meta: {} },
});
}
continue;
}

// Per-GM scoping: emit if assigned to this GM, or if unassigned and
// this session opts into seeing unassigned events. A missing/blank
// assigned_gm is treated as unassigned.
const assignedGm = event.assigned_gm;
const isMine = assignedGm === GM_SLUG;
const isUnassigned =
assignedGm === undefined || assignedGm === null || assignedGm === "";
if (!isMine && !(isUnassigned && SEE_UNASSIGNED)) continue;

await mcp.notification({
method: "notifications/claude/channel",
params: {
content: trimmed,
meta: {
event: event.event || "unknown",
task_id: event.task_id || "",
title: event.title || "",
project: event.project || "",
assigned_gm: event.assigned_gm || "",
timestamp: event.timestamp || "",
},
},
});
}
}

Expand Down
3 changes: 2 additions & 1 deletion templates/hooks/task.blocked.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ NOTIFICATIONS_FILE="{{SERVER_HOME}}/notifications.jsonl"
TASK_ID="${TASK_ID:-unknown}"
TASK_TITLE="${TASK_TITLE:-}"
TASK_PROJECT="${TASK_PROJECT:-}"
TASK_ASSIGNED_GM="${TASK_ASSIGNED_GM:-}"
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")

echo "{\"event\":\"blocked\",\"task_id\":\"${TASK_ID}\",\"title\":$(printf '%s' "$TASK_TITLE" | python3 -c 'import sys,json; print(json.dumps(sys.stdin.read()))'),\"project\":\"${TASK_PROJECT}\",\"timestamp\":\"${TIMESTAMP}\"}" >> "$NOTIFICATIONS_FILE"
echo "{\"event\":\"blocked\",\"task_id\":\"${TASK_ID}\",\"title\":$(printf '%s' "$TASK_TITLE" | python3 -c 'import sys,json; print(json.dumps(sys.stdin.read()))'),\"project\":\"${TASK_PROJECT}\",\"assigned_gm\":\"${TASK_ASSIGNED_GM}\",\"timestamp\":\"${TIMESTAMP}\"}" >> "$NOTIFICATIONS_FILE"
3 changes: 2 additions & 1 deletion templates/hooks/task.completed.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ NOTIFICATIONS_FILE="{{SERVER_HOME}}/notifications.jsonl"
TASK_ID="${TASK_ID:-unknown}"
TASK_TITLE="${TASK_TITLE:-}"
TASK_PROJECT="${TASK_PROJECT:-}"
TASK_ASSIGNED_GM="${TASK_ASSIGNED_GM:-}"
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")

echo "{\"event\":\"completed\",\"task_id\":\"${TASK_ID}\",\"title\":$(printf '%s' "$TASK_TITLE" | python3 -c 'import sys,json; print(json.dumps(sys.stdin.read()))'),\"project\":\"${TASK_PROJECT}\",\"timestamp\":\"${TIMESTAMP}\"}" >> "$NOTIFICATIONS_FILE"
echo "{\"event\":\"completed\",\"task_id\":\"${TASK_ID}\",\"title\":$(printf '%s' "$TASK_TITLE" | python3 -c 'import sys,json; print(json.dumps(sys.stdin.read()))'),\"project\":\"${TASK_PROJECT}\",\"assigned_gm\":\"${TASK_ASSIGNED_GM}\",\"timestamp\":\"${TIMESTAMP}\"}" >> "$NOTIFICATIONS_FILE"