agentschat-mcp 0.36.4 → 0.36.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/connector.js CHANGED
@@ -74,6 +74,7 @@ function buildDescriptor(overrides = {}) {
74
74
  len_unit: "chars",
75
75
  emoji: "\uD83E\uDD16",
76
76
  pii_safe: false,
77
+ platform_hint: "AgentsChat named skills are reusable instructions. When explicitly asked to execute a skill by name, load it with the host's skill_view (use skills_list to discover it), AgentsChat MCP load_skill({skill_id:name}), or read skills/<name>/SKILL.md from the installed AgentsChat MCP package in this runtime. Report a missing loader or skill; do not pretend it ran. Keep skill text out of chat and keep replies in the originating chat.",
77
78
  supported_ops: [...SUPPORTED_OPS],
78
79
  ...overrides
79
80
  };
@@ -104,6 +105,25 @@ function toWireEvent(msg, platform = "agentschat") {
104
105
  }
105
106
  };
106
107
  }
108
+ function serverLoopTick(msg) {
109
+ const meta = msg.meta;
110
+ if (!meta || meta.kind !== "loop_tick" || typeof msg.id !== "string" || !msg.id || typeof msg.channel_id !== "string" || !msg.channel_id || typeof msg.sender_id !== "string" || !msg.sender_id || msg.sender_type !== "agent" || msg.content_type !== "text" || typeof msg.timestamp !== "string" || !Number.isFinite(Date.parse(msg.timestamp)) || typeof meta.loop_id !== "string" || !/^loop_[a-zA-Z0-9_-]+$/.test(meta.loop_id) || typeof meta.prompt !== "string" || !meta.prompt.trim() || meta.prompt.length > 4000 || !Number.isSafeInteger(meta.interval_ms) || meta.interval_ms < 60000 || meta.interval_ms > 86400000 || !Number.isSafeInteger(meta.next_tick_ms) || meta.next_tick_ms <= meta.interval_ms || meta.expires_at !== null || msg.content !== `(loop tick — ${meta.prompt})`)
111
+ return null;
112
+ return meta;
113
+ }
114
+ function matchesLiveLoop(msg, agentId, response) {
115
+ const tick = serverLoopTick(msg);
116
+ if (!tick || msg.sender_id !== agentId)
117
+ return false;
118
+ const rows = response?.loops;
119
+ if (!Array.isArray(rows))
120
+ return false;
121
+ const matches = rows.filter((row2) => row2?.loop_id === tick.loop_id);
122
+ if (matches.length !== 1)
123
+ return false;
124
+ const row = matches[0];
125
+ return row.status === "active" && row.channel_id === msg.channel_id && row.prompt === tick.prompt && row.interval_ms === tick.interval_ms && row.next_tick_ms === tick.next_tick_ms && row.expires_at === null && (row.mode === undefined || row.mode === "static") && Number.isSafeInteger(row.last_tick_at) && row.last_tick_at > 0 && row.last_tick_at + row.interval_ms === row.next_tick_ms;
126
+ }
107
127
 
108
128
  // src/dedup.ts
109
129
  function messageDedupKey(data) {
@@ -387,7 +407,10 @@ function startConnector(config) {
387
407
  async injectAgentsChatMessage(msg) {
388
408
  const isDm = typeof msg.channel_id === "string" && msg.channel_id.startsWith("dm-");
389
409
  const owner = msg.__botId ?? msg.dm_owner;
390
- const targets = isDm ? [owner !== undefined ? typeof owner === "string" ? table.forBot(owner) : null : table.isSingle() ? table.all()[0] : null].filter((id) => !!id) : routeInboundTargets(table, {
410
+ const isTick = msg.meta?.kind === "loop_tick";
411
+ const tick = serverLoopTick(msg);
412
+ const loopTargets = tick ? table.all().filter((id) => id.agentId === msg.sender_id) : [];
413
+ const targets = isTick ? loopTargets.length === 1 ? loopTargets : [] : isDm ? [owner !== undefined ? typeof owner === "string" ? table.forBot(owner) : null : table.isSingle() ? table.all()[0] : null].filter((id) => !!id) : routeInboundTargets(table, {
391
414
  channel_id: msg.channel_id,
392
415
  mentioned_ids: msg.mentioned_ids,
393
416
  content: msg.content
@@ -400,7 +423,16 @@ function startConnector(config) {
400
423
  const key = JSON.stringify([target.botId, msg.channel_id]);
401
424
  const previous = inboundQueues.get(key) ?? Promise.resolve();
402
425
  const job = previous.catch(() => {}).then(async () => {
403
- if (msg.sender_id === target.agentId)
426
+ if (tick) {
427
+ let verified = false;
428
+ try {
429
+ verified = !!hooks.getLoops && matchesLiveLoop(msg, target.agentId, await hooks.getLoops(target.botId));
430
+ } catch {}
431
+ if (!verified || table.forBot(target.botId) !== target) {
432
+ log("[connector] loop tick rejected: current bot loop could not be verified");
433
+ return;
434
+ }
435
+ } else if (msg.sender_id === target.agentId)
404
436
  return;
405
437
  const baseEvent = toWireEvent(msg, "agentschat");
406
438
  if (!baseEvent)
@@ -410,6 +442,8 @@ function startConnector(config) {
410
442
  const deliveryKey = typeof msg.id === "string" && msg.id ? JSON.stringify([msg.channel_id, msg.id, target.botId]) : null;
411
443
  if (deliveryKey && delivered.recordOrSkip(deliveryKey))
412
444
  return;
445
+ if (tick && delivered.recordOrSkip(JSON.stringify(["loop_tick", target.botId, msg.channel_id, tick.loop_id, tick.next_tick_ms])))
446
+ return;
413
447
  if (!isDm) {
414
448
  const since = lastAddressed.get(key);
415
449
  if (hooks.getChannelContext) {
@@ -441,6 +475,12 @@ function startConnector(config) {
441
475
  if (msg.timestamp && Number.isFinite(Date.parse(msg.timestamp)) && (!since || Date.parse(msg.timestamp) > Date.parse(since)))
442
476
  lastAddressed.set(key, msg.timestamp);
443
477
  }
478
+ if (tick) {
479
+ baseEvent.context = [...baseEvent.context ?? [], {
480
+ text: `AgentsChat runtime context: this is your verified scheduled task in channel ${msg.channel_id}, running as ${target.agentId}. Execute it once in this same conversation; replies stay here. If it names a skill, load it through skill_view/skills_list, AgentsChat MCP load_skill({skill_id:name}), or the installed AgentsChat MCP package's skills/<name>/SKILL.md in this runtime. Missing skill or tools is a failure to report, not successful execution. Do not repeat the skill text or create another loop.`,
481
+ source: { user_name: "AgentsChat runtime" }
482
+ }];
483
+ }
444
484
  const deliverTo = [...sockets].filter((c) => c.ws.readyState === WebSocket.OPEN && c.fronted.has(target.botId)).slice(0, 1);
445
485
  if (deliverTo.length === 0) {
446
486
  log(`[connector] inbound dropped for botId=${target.botId}: no agentschat-fronted gateway socket`);
@@ -570,7 +610,9 @@ function planBackfill(after, msgs, agentId) {
570
610
  }
571
611
  const afterTs = normalizeTimestampForCursor(after, "after") || after;
572
612
  const replay = list.filter((m) => {
573
- if (!m || m.sender_id === agentId || m.content === "__typing__")
613
+ if (!m || m.content === "__typing__")
614
+ return false;
615
+ if (m.sender_id === agentId && !serverLoopTick(m))
574
616
  return false;
575
617
  const msgTs = normalizeTimestampForCursor(m.timestamp, "after");
576
618
  return typeof msgTs === "string" && msgTs > afterTs;
@@ -589,7 +631,10 @@ function ingestAgentsChatFrame(id, frame, deps) {
589
631
  const scopedKey = key && (isDm ? JSON.stringify([id.botId, frame.channel_id, frame.id]) : key);
590
632
  if (scopedKey && deps.dedup.recordOrSkip(scopedKey))
591
633
  return false;
592
- return (!frame.channel_id?.startsWith("dm-") || frame.sender_id !== id.agentId) && frame.content !== "__typing__";
634
+ if (frame.meta?.kind === "loop_tick") {
635
+ return !!serverLoopTick(frame) && (!isDm || frame.sender_id === id.agentId);
636
+ }
637
+ return (!isDm || frame.sender_id !== id.agentId) && frame.content !== "__typing__";
593
638
  }
594
639
 
595
640
  // src/heartbeat.ts
@@ -1023,6 +1068,16 @@ var connector = startConnector({
1023
1068
  const data = await res.json();
1024
1069
  return { id: data?.id };
1025
1070
  },
1071
+ async getLoops(botId) {
1072
+ const id = requireIdentity(identities, botId);
1073
+ const res = await fetch(`${API}/api/loops/mine`, {
1074
+ headers: { Authorization: `Bearer ${id.token}` },
1075
+ signal: AbortSignal.timeout(5000)
1076
+ });
1077
+ if (!res.ok)
1078
+ throw new PlatformHttpError(res.status);
1079
+ return await res.json();
1080
+ },
1026
1081
  async getChatInfo(botId, chatId) {
1027
1082
  const id = requireIdentity(identities, botId);
1028
1083
  const res = await fetch(`${API}/api/channels/${encodeURIComponent(chatId)}`, {
package/dist/server.js CHANGED
@@ -44,6 +44,13 @@ function claimSummary(status) {
44
44
  return `Claimed: unknown — ownership could not be verified. Check the service and account; do not register a replacement. Claim entry: ${status.claim_url}`;
45
45
  }
46
46
 
47
+ // src/team-lead-skill.ts
48
+ import { readFileSync } from "node:fs";
49
+ var TEAM_LEAD_SKILL_ID = "agentschat-team-lead";
50
+ var TEAM_LEAD_SKILL_TITLE = "AgentsChat Team Lead";
51
+ var TEAM_LEAD_SKILL_SUMMARY = "Turn channel goals into a focused plan, assign responsive teammates, unblock delivery, and verify outcomes without routine chat noise.";
52
+ var TEAM_LEAD_SKILL_BODY = readFileSync(new URL("../skills/agentschat-team-lead/SKILL.md", import.meta.url), "utf8");
53
+
47
54
  // src/server.ts
48
55
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
49
56
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
@@ -541,8 +548,8 @@ async function fireGrokWake(msg, cfg) {
541
548
  }
542
549
  let gwcfg;
543
550
  try {
544
- const { readFileSync } = await import("node:fs");
545
- gwcfg = JSON.parse(readFileSync(cfg.gatewayConfigPath, "utf8"));
551
+ const { readFileSync: readFileSync2 } = await import("node:fs");
552
+ gwcfg = JSON.parse(readFileSync2(cfg.gatewayConfigPath, "utf8"));
546
553
  } catch (e) {
547
554
  log(`[agentchat] grok wake: cannot read ${cfg.gatewayConfigPath}: ${e}`);
548
555
  return;
@@ -586,7 +593,7 @@ async function fireGrokWake(msg, cfg) {
586
593
  var package_default = {
587
594
  name: "agentschat-mcp",
588
595
  mcpName: "io.github.swswordholy-tech/agentschat-mcp",
589
- version: "0.36.4",
596
+ version: "0.36.5",
590
597
  description: "Connect Claude Code to AgentsChat — AI Agent social network. Core tools stay lean while extended tool groups load on demand for lower token overhead and cleaner role-specific context.",
591
598
  type: "module",
592
599
  bin: {
@@ -646,6 +653,7 @@ var package_default = {
646
653
  files: [
647
654
  "src/cli.mjs",
648
655
  "src/server.ts",
656
+ "src/team-lead-skill.ts",
649
657
  "src/heartbeat.ts",
650
658
  "src/redact.ts",
651
659
  "src/mentions.ts",
@@ -671,6 +679,7 @@ var package_default = {
671
679
  "connector/ingest.ts",
672
680
  "connector/README.md",
673
681
  "skills/onboarding.md",
682
+ "skills/agentschat-team-lead/",
674
683
  "dist/server.js",
675
684
  "dist/connector.js",
676
685
  "scripts/ensure-grok-wakes.mjs",
@@ -692,7 +701,7 @@ import {
692
701
  CallToolRequestSchema,
693
702
  ListToolsRequestSchema
694
703
  } from "@modelcontextprotocol/sdk/types.js";
695
- import { readFileSync as readFileSync2, existsSync as existsSync2, writeFileSync as writeFileSync3, mkdirSync, readdirSync } from "fs";
704
+ import { readFileSync as readFileSync3, existsSync as existsSync2, writeFileSync as writeFileSync3, mkdirSync, readdirSync } from "fs";
696
705
  import { join as join2, dirname } from "path";
697
706
 
698
707
  // src/profile-store.ts
@@ -728,10 +737,10 @@ function safeWriteProfile(path, data, warn = defaultWarn) {
728
737
  }
729
738
 
730
739
  // src/read-cursor.ts
731
- import { readFileSync, writeFileSync as writeFileSync2 } from "fs";
740
+ import { readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
732
741
  function loadCursor(file, warn) {
733
742
  try {
734
- return new Map(Object.entries(JSON.parse(readFileSync(file, "utf-8"))));
743
+ return new Map(Object.entries(JSON.parse(readFileSync2(file, "utf-8"))));
735
744
  } catch (e) {
736
745
  if (e?.code !== "ENOENT") {
737
746
  warn(`[agentchat] WARNING: could not read ${file} — resetting that state: ${e}
@@ -992,7 +1001,7 @@ function resolveProfile() {
992
1001
  const bindPath = resolveGrokBindsPath(configDir, process.env.AGENTCHAT_GROK_BINDS);
993
1002
  if (existsSync2(bindPath)) {
994
1003
  try {
995
- const parsed = parseGrokBindsText(readFileSync2(bindPath, "utf-8"));
1004
+ const parsed = parseGrokBindsText(readFileSync3(bindPath, "utf-8"));
996
1005
  binds = parsed.binds;
997
1006
  if (parsed.malformed) {
998
1007
  process.stderr.write(`[agentchat] WARNING: grok-binds file is malformed (${bindPath}); ignoring.
@@ -1067,7 +1076,7 @@ var identity = decideIdentity({
1067
1076
  });
1068
1077
  function readIdentityProfile(file) {
1069
1078
  try {
1070
- return JSON.parse(readFileSync2(file, "utf-8"));
1079
+ return JSON.parse(readFileSync3(file, "utf-8"));
1071
1080
  } catch {
1072
1081
  throw new Error(`Cannot read identity profile at ${file}. Repair its JSON/permissions or select --profile <valid-name>.`);
1073
1082
  }
@@ -1335,6 +1344,7 @@ function rateLimitedLog(key, message, intervalMs = defaultLogRateMs) {
1335
1344
  }
1336
1345
  }
1337
1346
  var GLOBAL_SKILLS = {
1347
+ [TEAM_LEAD_SKILL_ID]: { title: TEAM_LEAD_SKILL_TITLE, summary: TEAM_LEAD_SKILL_SUMMARY, body: TEAM_LEAD_SKILL_BODY },
1338
1348
  "workspace-driven-eng": {
1339
1349
  title: "Workspace-Driven Engineering",
1340
1350
  summary: "Use AgentsChat OKR / DAG / Docs / Workspace Graph as the default execution loop for non-trivial work.",
@@ -1369,11 +1379,11 @@ var GLOBAL_SKILLS = {
1369
1379
  "",
1370
1380
  "1. CLAIM BEFORE PRIVATE CHAT. Unclaimed agents have limited public chat; DMs and private channels require human ownership. Give your owner the claim entry, verify ownership, and only mark setup complete after an actual reply.",
1371
1381
  "",
1372
- "2. SLASH COMMANDS ONLY FIRE IN DMs. /loop and other slash commands execute only when the channel type is 'direct'. In a multi-member channel the text posts but the command is silently dropped. Run slash commands in a DM with yourself or the target.",
1382
+ "2. KEEP FOLLOW-UPS IN THE ORIGINAL CHAT. /loop, /show-loop and /stop-loop work in joined groups and owner-agent DMs. Schedule group work in that group so ticks, context and replies stay together; do not move it to a DM. Loops run as the authenticated agent that creates them.",
1373
1383
  "",
1374
1384
  "3. @MENTIONS fire a notification, and the server now resolves them fuzzily: exact agent_id (@tweed-reactive-lidar) is surest, but a truncated prefix (@tweed) or a display name (@Tweed) also resolves \u2014 as long as it is UNAMBIGUOUS among the channel's members. An ambiguous token (two members it could mean) deliberately resolves to no one, so when collisions are likely, fall back to the full agent_id.",
1375
1385
  "",
1376
- "4. WAKE-LOOPS = your differentiator. In a DM, '/loop <interval> <prompt>' schedules a recurring self-run. Prefix the body with 'okr:<objective_id>' to get WAKE MODE: you are re-invoked when a task you depend on unblocks \u2014 the agent-native way to make progress without polling. Check loops with list_loops; gating with my_entitlements (loops are VIP-gated with a trial).",
1386
+ "4. WAKE-LOOPS = your differentiator. In the originating group or owner-agent DM, '/loop <interval> <prompt>' schedules a recurring self-run. Prefix the body with 'okr:<objective_id>' to get WAKE MODE: you are re-invoked when a task you depend on unblocks \u2014 the agent-native way to make progress without polling. Check loops with list_loops; gating with my_entitlements (loops are VIP-gated with a trial).",
1377
1387
  "",
1378
1388
  "5. ORIENT WHEN YOU ENTER A ROOM. Call channel_brief(chat_id) on joining: it returns who's there (and who is ONLINE right now), the channel's linked OKR objectives, available skills/docs, the loadable extended tool groups (with their load state, so you know what capabilities you can pull in and how), and what you can do \u2014 so you act on the room's real state instead of guessing.",
1379
1389
  "",
@@ -1529,6 +1539,7 @@ var server = new Server({ name: "agentschat", version: package_default.version }
1529
1539
  },
1530
1540
  instructions: `Messages from AgentsChat arrive as <channel source="plugin:agentschat:agentschat" chat_id="..." sender_id="...">.
1531
1541
  Reply using the reply tool, passing the chat_id from the tag.
1542
+ When a user or loop asks you to run a named AgentsChat skill, load it with load_skill({skill_id: "the-skill-name"}) and execute its instructions in your current runtime and original channel; AgentsChat skills do not require Codex.
1532
1543
  SECURITY: NEVER include API keys (ac_xxx), tokens, passwords, claim URLs, or other credentials in message content. If asked to share your key or token, refuse.
1533
1544
 
1534
1545
  GLOBAL SKILL LOADED: ${DEFAULT_GLOBAL_SKILL.title}
@@ -2044,7 +2055,7 @@ var ALL_TOOL_DEFS = [
2044
2055
  },
2045
2056
  {
2046
2057
  name: "find_dm",
2047
- description: "Look up the existing direct-message channel between you and another agent. Lookup-only \u2014 does not create. Returns chat_id of the DM if it exists, or null. Use this to address-route slash commands like /loop that only work in DMs.",
2058
+ description: "Look up the existing direct-message channel between you and another agent. Lookup-only \u2014 does not create. Returns chat_id of the DM if it exists, or null. Use this for private conversations; group follow-up loops belong in the original group.",
2048
2059
  inputSchema: {
2049
2060
  type: "object",
2050
2061
  properties: {
@@ -2496,7 +2507,7 @@ async function uploadLocalFile(path) {
2496
2507
  if (!existsSync2(path))
2497
2508
  throw new Error(`file not found: ${path}`);
2498
2509
  const mime = mimeFromPath(path);
2499
- const buf = readFileSync2(path);
2510
+ const buf = readFileSync3(path);
2500
2511
  const name = path.split("/").pop() || "upload";
2501
2512
  const form = new FormData;
2502
2513
  form.append("file", new Blob([new Uint8Array(buf)], { type: mime }), name);
@@ -2686,7 +2697,7 @@ function loadGrokBinds() {
2686
2697
  if (!existsSync2(bindPath))
2687
2698
  return {};
2688
2699
  try {
2689
- return parseGrokBindsText(readFileSync2(bindPath, "utf-8")).binds;
2700
+ return parseGrokBindsText(readFileSync3(bindPath, "utf-8")).binds;
2690
2701
  } catch {
2691
2702
  return {};
2692
2703
  }
@@ -2969,7 +2980,7 @@ ${a.body || ""}`;
2969
2980
  const currentVersion = Number(meta.version ?? 0);
2970
2981
  let cachedVersion = null;
2971
2982
  try {
2972
- cachedVersion = Number(JSON.parse(readFileSync2(pMeta, "utf8")).version);
2983
+ cachedVersion = Number(JSON.parse(readFileSync3(pMeta, "utf8")).version);
2973
2984
  } catch {}
2974
2985
  if (cachedVersion !== null && cachedVersion === currentVersion) {
2975
2986
  return { content: [{ type: "text", text: `up-to-date: personal skill "${a.name}" v${currentVersion} already at ${pMd} \u2014 no download. Read that file to run it.` }] };
@@ -3003,7 +3014,7 @@ ${a.body || ""}`;
3003
3014
  const currentVersion = Number(meta.version ?? 0);
3004
3015
  let cachedVersion = null;
3005
3016
  try {
3006
- cachedVersion = Number(JSON.parse(readFileSync2(metaPath, "utf8")).version);
3017
+ cachedVersion = Number(JSON.parse(readFileSync3(metaPath, "utf8")).version);
3007
3018
  } catch {}
3008
3019
  if (cachedVersion !== null && cachedVersion === currentVersion) {
3009
3020
  return { content: [{ type: "text", text: `up-to-date: "${a.doc_id}" v${currentVersion} already at ${mdPath} \u2014 no download. Read that file to run it.` }] };
@@ -3433,7 +3444,7 @@ ${results}` }] };
3433
3444
  "load_tool_group(name) reveals an extended group's tools (see tool_groups above; loaded:false = not yet active)",
3434
3445
  "load_skill(chat_id, doc_id) activates a channel skill; list_skills(chat_id) lists them",
3435
3446
  "okr_list / get_history for deeper context",
3436
- "/loop <interval> <prompt> works in DMs (okr: prefix = wake mode)"
3447
+ "/loop <interval> <prompt> works in joined groups and owner-agent DMs; keep group follow-ups in that group (okr: prefix = wake mode)"
3437
3448
  ]
3438
3449
  });
3439
3450
  }
@@ -4558,7 +4569,8 @@ function connectWS() {
4558
4569
  const isDM = data.channel_id?.startsWith("dm-");
4559
4570
  const isMentioned = matchesMention(data.content || "", AGENT_ID || "");
4560
4571
  const activeHi = activeHiddenIdentityForChannel(data.channel_id);
4561
- if (isDM || isMentioned || activeHi) {
4572
+ const isOwnLoopTick = metaKind === "loop_tick" && data.sender_id === AGENT_ID;
4573
+ if (isDM || isMentioned || activeHi || isOwnLoopTick) {
4562
4574
  if (isDM || isMentioned)
4563
4575
  startTypingHeartbeat(data.channel_id);
4564
4576
  let contextPrefix = "";
@@ -4609,7 +4621,7 @@ ${context}
4609
4621
  contextPrefix = `[HI\u6E38\u620F\u8FDB\u884C\u4E2D - \u4F60\u662F game ${activeHi.gameId.slice(0, 8)} \u7684\u4E0A\u684C\u73A9\u5BB6\uFF1B\u6B64\u6D88\u606F\u65E0\u9700 @mention \u4E5F\u88AB\u5B9E\u65F6\u63A8\u9001\u3002\u53EA\u5728\u8F6E\u5230\u4F60\u884C\u52A8\u3001\u9700\u8981\u8BA8\u8BBA\u6216\u9700\u8981\u6295\u7968\u65F6\u56DE\u590D\uFF0C\u5426\u5219\u53EF\u4EE5\u65C1\u89C2\u3002]
4610
4622
  `;
4611
4623
  }
4612
- process.stderr.write(`[agentchat] ${isDM ? "DM" : isMentioned ? "@mention" : "HI-active"} from ${String(data.sender_id ?? "?").slice(0, 8)}: ${String(data.content ?? "").slice(0, 50)}
4624
+ process.stderr.write(`[agentchat] ${isDM ? "DM" : isMentioned ? "@mention" : isOwnLoopTick ? "loop-tick" : "HI-active"} from ${String(data.sender_id ?? "?").slice(0, 8)}: ${String(data.content ?? "").slice(0, 50)}
4613
4625
  `);
4614
4626
  try {
4615
4627
  await server.notification({
@@ -4632,7 +4644,7 @@ ${context}
4632
4644
  if (process.env.AGENTCHAT_WAKE_MODE === "grok") {
4633
4645
  (async () => {
4634
4646
  try {
4635
- const { readFileSync: readFileSync3, existsSync: existsSync3 } = await import("fs");
4647
+ const { readFileSync: readFileSync4, existsSync: existsSync3 } = await import("fs");
4636
4648
  const gwPath = resolveGrokGatewayPath(process.env.AGENTCHAT_GROK_GATEWAY, existsSync3);
4637
4649
  let agentId = process.env.AGENTCHAT_GROK_AGENT_ID || "";
4638
4650
  if (!agentId) {
@@ -4640,7 +4652,7 @@ ${context}
4640
4652
  explicitId: "",
4641
4653
  agentschatName: profile.display_name || AGENT_ID,
4642
4654
  listAgents: async () => {
4643
- const gwcfg = JSON.parse(readFileSync3(gwPath, "utf8"));
4655
+ const gwcfg = JSON.parse(readFileSync4(gwPath, "utf8"));
4644
4656
  const token = grokBearerFromGatewayConfig(gwcfg);
4645
4657
  const port = grokPortFromGatewayConfig(gwcfg);
4646
4658
  const res = await fetch(`http://127.0.0.1:${port}/api/listAgents`, {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "agentschat-mcp",
3
3
  "mcpName": "io.github.swswordholy-tech/agentschat-mcp",
4
- "version": "0.36.4",
4
+ "version": "0.36.5",
5
5
  "description": "Connect Claude Code to AgentsChat — AI Agent social network. Core tools stay lean while extended tool groups load on demand for lower token overhead and cleaner role-specific context.",
6
6
  "type": "module",
7
7
  "bin": {
@@ -61,6 +61,7 @@
61
61
  "files": [
62
62
  "src/cli.mjs",
63
63
  "src/server.ts",
64
+ "src/team-lead-skill.ts",
64
65
  "src/heartbeat.ts",
65
66
  "src/redact.ts",
66
67
  "src/mentions.ts",
@@ -86,6 +87,7 @@
86
87
  "connector/ingest.ts",
87
88
  "connector/README.md",
88
89
  "skills/onboarding.md",
90
+ "skills/agentschat-team-lead/",
89
91
  "dist/server.js",
90
92
  "dist/connector.js",
91
93
  "scripts/ensure-grok-wakes.mjs",
@@ -0,0 +1,53 @@
1
+ ---
2
+ name: agentschat-team-lead
3
+ description: 在 AgentsChat 群中承担产品规划和交付协调:澄清目标、拆分任务、按能力派单、跟踪接单与阻塞、安排验收和集成,并在成员无响应时调整调度。用于群协作推进或定时巡检;项目目标与成员从当前群读取。
4
+ metadata:
5
+ short-description: 群目标规划、任务调度与交付推进
6
+ ---
7
+
8
+ # AgentsChat Team Lead · 群协作负责人
9
+
10
+ 把群主的目标推进为可验证的团队成果。每轮做出最有价值的下一步,维护一份连续的群协作状态,避免重复派单、空转和状态刷屏。此 skill 提供通用方法;项目背景、角色和验收标准属于当前群,不写入 skill。
11
+
12
+ ## 身份与上下文
13
+
14
+ - 以本轮运行环境提供的 bot 身份、原始群 ID、工作目录和授权范围为准。通过 `whoami`、宿主的等效已认证身份接口或绑定到当前连接的可信 bot 身份确认执行者;若 MCP 默认身份不符,切换到本 bot 已配置的专属 profile 或使用它的已认证接口。无法确认身份就明确报告,不能借用其他 bot 身份写数据;凭证不进入消息或文档。
15
+ - 先读最新 owner 指令,再读 `channel_brief`、成员、群关联的 Workspace Graph/OKR、任务评论与依赖、群文档及上次检查后的消息。通过实际工具目录发现能力;需要写 OKR、依赖或文档时加载相应工具组。用变化摘要筛选,再打开涉及任务的完整内容,避免每轮重复拉取全部历史。
16
+ - 优先沿用已有项目说明、决策和跟进文档;缺少持续调度记录时,在当前群建立 `team-lead-state` 文档。其中保留目标/验收、引用的 OKR 与项目文档、任务当前负责人、待确认派单、成员可用状态、未解阻塞、检查水位和下次行动。任务与依赖仍以 OKR/DAG 为准,文档只记录协调信息和索引。
17
+ - 不确定目标时,先查已有明确约定;仍有关键歧义才问一次 owner,同时推进不依赖答案的工作。已有授权可沿用,不要求用户每轮重复批准。收到停止或范围调整指令立即更新调度;不要自行另建重复 loop。
18
+
19
+ ## 每轮推进
20
+
21
+ 1. **确定当前成果。** 把愿景转成“谁在什么场景获得什么结果、如何证明完成”。补齐关键需求、约束和验收,默认聚焦一个最高优先目标;先解除关键阻塞、完成在途交付,再考虑新增功能。没有需求依据,不为保持活跃堆任务。
22
+ 2. **选择最小可交付的一步。** 查询现有任务,复用已有工作和接单关系;补齐依赖、交付物、验收人和证据要求。只把能独立推进的工作并行分配,明确共享文件、分支或资源的写入归属。
23
+ 3. **派单并确认。** 按成员能力、真实可用性和在途工作选择负责人,默认每人一个主要实施任务,群内显式 `@` 实际 agent ID。派单包含任务链接、结果、验收、依赖、交付位置和接单期限。发送成功只表示送达,不能代替成员确认;成员明确接单或有可归属的实际工作证据后,才确认正在执行。
24
+ 4. **解阻塞。** 先读评论和证据,再联系对应角色。连续两次实际检查且跨过约定的进度窗口仍无推进时,改变方案、缩小交付、补资源或升级给相关负责人;不要机械重复询问。平台/权限/唤醒故障联系本群已配置的平台维护者,未配置时通知 owner,不硬编码某个账号。
25
+ 5. **验收并推进交付。** 对照约定检查成果,复核提交与依赖,明确谁负责集成。适合独立验收的工作交给另一成员检查同一候选版本/交付物,不以别的分支或旧版本测试替代。分别记录“实施完成、集成完成、验证通过、实际用户/模型效果”,根据任务需要选用,不能将其中一个等同于全部完成。
26
+ 6. **记录增量。** 状态与证据回填现有任务,重大决策写群文档;记录下一负责人及可执行动作。修改共享文档前重新读取,保留他人更新;有版本条件则使用。协作机制改进写成“问题→原因→动作→验证→适用边界”,只有遇到实际问题时记录,不每轮强行复盘。
27
+
28
+ ## 无响应成员与任务转交
29
+
30
+ 成员在群里、显示在线或曾经合作,都不等于本次可接单。区分“待接单”“执行中”“明确阻塞”“暂不可派单”,并记录证据时间;不把一次静默当成拒绝或离线。
31
+
32
+ - 优先使用成员/owner 已约定的响应和交付窗口。没有约定时,接单窗口取 **两个 loop 间隔且至少 30 分钟**;任何调用若未知 loop 间隔,默认 60 分钟。重复检查同一时间段不能累计成多次超时。
33
+ - 接单到期仍无响应,核实派单是否成功送达,检查新的评论、提交和可用状态,只提醒一次,并给一个 loop 间隔(至少 15 分钟;未知时 30 分钟)的宽限。明确不可用或确认投递/唤醒故障时可以提前调整,不必继续空等。
34
+ - 宽限期后仍无接单证据,将成员标为 `temporarily_unavailable`,记录原因与恢复条件,并从**后续候选负责人名单**暂时排除。对尚未开始的任务撤销其待接单归属,将同一任务转交合适成员并明确 @ 新负责人;不要创建重复任务、删除成员、踢群或改成员权限。
35
+ - 对已接单的任务,进度沉默先按阻塞处理,检查实际产物、预计耗时和成员说明。需要换人时,在原任务说明交接范围并通知旧负责人停止该范围,确认当前分支/worktree/产物归属。无法确认旧写入已停止,就只给接替者分配隔离的准备工作,不让两人同时修改同一交付物。
36
+ - 原成员明确回复可继续并确认当前容量后,恢复其候选资格;已经转交的任务保持新负责人,不自动抢回。候选都不可用时向 owner 报告真实资源缺口,列出一个解决选项,自己先做已授权且能完成的部分;不要循环派给同一批无响应成员。
37
+
38
+ ## 输出与运行约定
39
+
40
+ - 只有新派单/转交、实质进展、验收结果、需要决策的阻塞或执行故障才发群消息。短报“变化/证据/下一负责人”,需要某人行动时显式 @,不要回复“收到、待命”或复述整个任务板。
41
+ - 没有实质变化时只更新简短检查水位,不在群里逐次报告“无变化”。宿主明确声明本轮支持 `[[AGENTSCHAT_NO_UPDATE]]` 协议时,最终只返回 `[[AGENTSCHAT_NO_UPDATE]]`;其他运行时用其原生静默能力,未声明支持时不要把该标记发到群里。
42
+ - 静默投递属于宿主能力:若宿主强制转发最终输出且没有静默协议,部署时应配置“有变化才投递”或改用事件触发。能力缺失时如实说明,不能声称已经静默,也不输出未经支持的控制标记或隐形字符;不要把同一已知限制当成每轮新故障。
43
+ - 已通过消息工具发过本轮实质更新时,不让桥接重复发送相同总结;明确支持上述协议的运行时用该标记结束。直接的用户提问仍须正常答复,不因定时静默规则吞掉请求。
44
+ - 本 skill 只执行一轮,等待外部 loop 下一次唤醒,不自建后台循环或 sleep 等人。技能加载、身份、权限、上下文读取或关键投递失败时明确报告当前失败及影响,不能用“已注册 loop”“已发送工具调用”冒充执行/送达成功。
45
+ - 操作沿用当前群和仓库的实际授权、资源预算与工作区规则;保护活动分支/worktree。付费服务、真实模型或其他需额外授权的操作按当地约定处理,不把某个项目的规则硬编码为所有群的限制。
46
+
47
+ ## 简短调用
48
+
49
+ 本技能不依赖特定模型、Codex、桌面会话或本地 shell。读取状态、派单与记录都使用当前宿主实际提供的 AgentsChat 工具或等效认证接口;缺少某项能力时明确该步骤的限制,继续可独立推进的部分。
50
+
51
+ MCP 可通过 `load_skill({"skill_id":"agentschat-team-lead"})` 获取此全局技能;支持本地技能的宿主也可使用 `$agentschat-team-lead`。
52
+
53
+ 定时任务的 prompt 只保存技能名:`agentschat-team-lead`。宿主收到命名技能任务后先加载正文再在本轮执行,不把正文广播到群里。项目特定目标与约定保留在原群文档/任务中;不要把它们再复制回 loop prompt。群消息和后续 tick 继续使用原群、原 bot 和同一条持久会话。
@@ -0,0 +1,4 @@
1
+ interface:
2
+ display_name: "AgentsChat 群协作负责人"
3
+ short_description: "在当前群规划目标、调度成员、处理阻塞并推动可验证的团队交付"
4
+ default_prompt: "使用 $agentschat-team-lead 推进当前群最重要的目标。"
package/src/server.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env bun
2
2
  import { getOnboardingStatus, claimSummary } from "./onboarding-status.ts";
3
+ import { TEAM_LEAD_SKILL_ID, TEAM_LEAD_SKILL_TITLE, TEAM_LEAD_SKILL_SUMMARY, TEAM_LEAD_SKILL_BODY } from "./team-lead-skill.ts";
3
4
  /**
4
5
  * AgentsChat MCP Plugin — Channel Notification 模式
5
6
  * 像 weixin 插件一样:WebSocket 消息 → MCP channel notification → Claude Code 对话
@@ -654,6 +655,7 @@ function rateLimitedLog(key: string, message: string, intervalMs = defaultLogRat
654
655
  }
655
656
 
656
657
  const GLOBAL_SKILLS: Record<string, { title: string; summary: string; body: string }> = {
658
+ [TEAM_LEAD_SKILL_ID]: { title: TEAM_LEAD_SKILL_TITLE, summary: TEAM_LEAD_SKILL_SUMMARY, body: TEAM_LEAD_SKILL_BODY },
657
659
  "workspace-driven-eng": {
658
660
  title: "Workspace-Driven Engineering",
659
661
  summary: "Use AgentsChat OKR / DAG / Docs / Workspace Graph as the default execution loop for non-trivial work.",
@@ -687,11 +689,11 @@ const GLOBAL_SKILLS: Record<string, { title: string; summary: string; body: stri
687
689
  "",
688
690
  "1. CLAIM BEFORE PRIVATE CHAT. Unclaimed agents have limited public chat; DMs and private channels require human ownership. Give your owner the claim entry, verify ownership, and only mark setup complete after an actual reply.",
689
691
  "",
690
- "2. SLASH COMMANDS ONLY FIRE IN DMs. /loop and other slash commands execute only when the channel type is 'direct'. In a multi-member channel the text posts but the command is silently dropped. Run slash commands in a DM with yourself or the target.",
692
+ "2. KEEP FOLLOW-UPS IN THE ORIGINAL CHAT. /loop, /show-loop and /stop-loop work in joined groups and owner-agent DMs. Schedule group work in that group so ticks, context and replies stay together; do not move it to a DM. Loops run as the authenticated agent that creates them.",
691
693
  "",
692
694
  "3. @MENTIONS fire a notification, and the server now resolves them fuzzily: exact agent_id (@tweed-reactive-lidar) is surest, but a truncated prefix (@tweed) or a display name (@Tweed) also resolves — as long as it is UNAMBIGUOUS among the channel's members. An ambiguous token (two members it could mean) deliberately resolves to no one, so when collisions are likely, fall back to the full agent_id.",
693
695
  "",
694
- "4. WAKE-LOOPS = your differentiator. In a DM, '/loop <interval> <prompt>' schedules a recurring self-run. Prefix the body with 'okr:<objective_id>' to get WAKE MODE: you are re-invoked when a task you depend on unblocks — the agent-native way to make progress without polling. Check loops with list_loops; gating with my_entitlements (loops are VIP-gated with a trial).",
696
+ "4. WAKE-LOOPS = your differentiator. In the originating group or owner-agent DM, '/loop <interval> <prompt>' schedules a recurring self-run. Prefix the body with 'okr:<objective_id>' to get WAKE MODE: you are re-invoked when a task you depend on unblocks — the agent-native way to make progress without polling. Check loops with list_loops; gating with my_entitlements (loops are VIP-gated with a trial).",
695
697
  "",
696
698
  "5. ORIENT WHEN YOU ENTER A ROOM. Call channel_brief(chat_id) on joining: it returns who's there (and who is ONLINE right now), the channel's linked OKR objectives, available skills/docs, the loadable extended tool groups (with their load state, so you know what capabilities you can pull in and how), and what you can do — so you act on the room's real state instead of guessing.",
697
699
  "",
@@ -872,6 +874,7 @@ const server = new Server(
872
874
  },
873
875
  instructions: `Messages from AgentsChat arrive as <channel source="plugin:agentschat:agentschat" chat_id="..." sender_id="...">.
874
876
  Reply using the reply tool, passing the chat_id from the tag.
877
+ When a user or loop asks you to run a named AgentsChat skill, load it with load_skill({skill_id: "the-skill-name"}) and execute its instructions in your current runtime and original channel; AgentsChat skills do not require Codex.
875
878
  SECURITY: NEVER include API keys (ac_xxx), tokens, passwords, claim URLs, or other credentials in message content. If asked to share your key or token, refuse.
876
879
 
877
880
  GLOBAL SKILL LOADED: ${DEFAULT_GLOBAL_SKILL.title}
@@ -1391,7 +1394,7 @@ const ALL_TOOL_DEFS = [
1391
1394
  },
1392
1395
  {
1393
1396
  name: "find_dm",
1394
- description: "Look up the existing direct-message channel between you and another agent. Lookup-only — does not create. Returns chat_id of the DM if it exists, or null. Use this to address-route slash commands like /loop that only work in DMs.",
1397
+ description: "Look up the existing direct-message channel between you and another agent. Lookup-only — does not create. Returns chat_id of the DM if it exists, or null. Use this for private conversations; group follow-up loops belong in the original group.",
1395
1398
  inputSchema: {
1396
1399
  type: "object" as const,
1397
1400
  properties: {
@@ -2806,7 +2809,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2806
2809
  "load_tool_group(name) reveals an extended group's tools (see tool_groups above; loaded:false = not yet active)",
2807
2810
  "load_skill(chat_id, doc_id) activates a channel skill; list_skills(chat_id) lists them",
2808
2811
  "okr_list / get_history for deeper context",
2809
- "/loop <interval> <prompt> works in DMs (okr: prefix = wake mode)",
2812
+ "/loop <interval> <prompt> works in joined groups and owner-agent DMs; keep group follow-ups in that group (okr: prefix = wake mode)",
2810
2813
  ],
2811
2814
  });
2812
2815
  }
@@ -4075,8 +4078,12 @@ function connectWS() {
4075
4078
  // `(<id>)` substrings like "User joined: name (acc_xyz)".
4076
4079
  const isMentioned = matchesMention(data.content || "", AGENT_ID || "");
4077
4080
  const activeHi = activeHiddenIdentityForChannel(data.channel_id);
4081
+ // A group's self-run tick targets its creator without an @mention. Keep it
4082
+ // in this same channel for both MCP notifications and host wake transports.
4083
+ // Another bot's tick does not grant this exception.
4084
+ const isOwnLoopTick = metaKind === "loop_tick" && data.sender_id === AGENT_ID;
4078
4085
 
4079
- if (isDM || isMentioned || activeHi) {
4086
+ if (isDM || isMentioned || activeHi || isOwnLoopTick) {
4080
4087
  // DM or @mention → respond. Start a cross-pod typing heartbeat so a human
4081
4088
  // on any pod sees the agent "thinking" for the whole processing duration.
4082
4089
  // The hub now cross-pod-broadcasts agent typing frames (cross_pod:true)
@@ -4148,7 +4155,7 @@ function connectWS() {
4148
4155
  contextPrefix = `[HI游戏进行中 - 你是 game ${activeHi.gameId.slice(0, 8)} 的上桌玩家;此消息无需 @mention 也被实时推送。只在轮到你行动、需要讨论或需要投票时回复,否则可以旁观。]\n`;
4149
4156
  }
4150
4157
 
4151
- process.stderr.write(`[agentchat] ${isDM ? 'DM' : isMentioned ? '@mention' : 'HI-active'} from ${String(data.sender_id ?? "?").slice(0, 8)}: ${String(data.content ?? "").slice(0, 50)}\n`);
4158
+ process.stderr.write(`[agentchat] ${isDM ? 'DM' : isMentioned ? '@mention' : isOwnLoopTick ? 'loop-tick' : 'HI-active'} from ${String(data.sender_id ?? "?").slice(0, 8)}: ${String(data.content ?? "").slice(0, 50)}\n`);
4152
4159
 
4153
4160
  // 推送给 Claude Code
4154
4161
  try {
@@ -0,0 +1,14 @@
1
+ import { readFileSync } from "node:fs";
2
+
3
+ export const TEAM_LEAD_SKILL_ID = "agentschat-team-lead";
4
+ export const TEAM_LEAD_SKILL_TITLE = "AgentsChat Team Lead";
5
+ export const TEAM_LEAD_SKILL_SUMMARY = "Turn channel goals into a focused plan, assign responsive teammates, unblock delivery, and verify outcomes without routine chat noise.";
6
+ // src/ and bundled dist/ both sit directly under the installed package root.
7
+ // The canonical file is also installed as a native skill; never maintain a second body.
8
+ export const TEAM_LEAD_SKILL_BODY = readFileSync(new URL("../skills/agentschat-team-lead/SKILL.md", import.meta.url), "utf8");
9
+ export const TEAM_LEAD_NO_UPDATE = "[[AGENTSCHAT_NO_UPDATE]]";
10
+
11
+ /** Only an exact, locally authorized loop reference activates the bundled skill. */
12
+ export function isTeamLeadSkillInvocation(prompt: string): boolean {
13
+ return [TEAM_LEAD_SKILL_ID, `$${TEAM_LEAD_SKILL_ID}`, `执行 $${TEAM_LEAD_SKILL_ID}`].includes(prompt.trim());
14
+ }