@cfio/cohort-sync 0.2.3 → 0.2.4

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/index.js CHANGED
@@ -11506,41 +11506,59 @@ var _systemSchema = defineSchema({
11506
11506
  function deriveConvexUrl(apiUrl) {
11507
11507
  return apiUrl.replace(/\.convex\.site\/?$/, ".convex.cloud");
11508
11508
  }
11509
+ var DEFAULT_BEHAVIORAL_PROMPT = `BEFORE RESPONDING:
11510
+ - Does your planned response address the task's stated scope? If not, do not comment.
11511
+ - Do not post acknowledgment-only responses ("got it", "sounds good", "confirmed"). If you have no new information to add, do not comment.
11512
+ - If the work is complete, transition the task to "waiting" and set noReply=true on your final comment, then stop working on this task.`;
11509
11513
  function buildNotificationMessage(n) {
11510
11514
  let header;
11511
11515
  let cta;
11512
11516
  switch (n.type) {
11513
11517
  case "comment":
11514
11518
  if (n.isMentioned) {
11515
- header = `\u{1F4AC} You were @mentioned on task #${n.taskNumber} "${n.taskTitle}"
11519
+ header = `You were @mentioned on task #${n.taskNumber} "${n.taskTitle}"
11516
11520
  By: ${n.actorName}`;
11517
11521
  cta = "You were directly mentioned. Read the comment and respond using the cohort_comment tool.";
11518
11522
  } else {
11519
- header = `\u{1F4AC} New comment on task #${n.taskNumber} "${n.taskTitle}"
11523
+ header = `New comment on task #${n.taskNumber} "${n.taskTitle}"
11520
11524
  From: ${n.actorName}`;
11521
11525
  cta = "Read the comment thread and respond using the cohort_comment tool if appropriate.";
11522
11526
  }
11523
11527
  break;
11524
11528
  case "assignment":
11525
- header = `\u{1F4CC} You were assigned to task #${n.taskNumber} "${n.taskTitle}"
11529
+ header = `You were assigned to task #${n.taskNumber} "${n.taskTitle}"
11526
11530
  By: ${n.actorName}`;
11527
11531
  cta = "Review the task description and begin working on it.";
11528
11532
  break;
11529
11533
  case "status_change":
11530
- header = `\u{1F504} Status changed on task #${n.taskNumber} "${n.taskTitle}"
11534
+ header = `Status changed on task #${n.taskNumber} "${n.taskTitle}"
11531
11535
  By: ${n.actorName}`;
11532
11536
  cta = "Review the status change and take any follow-up action needed.";
11533
11537
  break;
11534
11538
  default:
11535
- header = `\u{1F4CB} Notification on task #${n.taskNumber} "${n.taskTitle}"
11539
+ header = `Notification on task #${n.taskNumber} "${n.taskTitle}"
11536
11540
  From: ${n.actorName}`;
11537
11541
  cta = "Check the task and respond if needed.";
11538
11542
  }
11539
11543
  const body = n.preview ? `
11540
11544
  Comment: "${n.preview}"` : "";
11541
- return `${header}${body}
11545
+ let scope = "";
11546
+ if (n.taskDescription && n.type === "comment") {
11547
+ const truncated = n.taskDescription.length > 500 ? n.taskDescription.slice(0, 500) + "..." : n.taskDescription;
11548
+ scope = `
11542
11549
 
11543
- ${cta}`;
11550
+ Scope: ${truncated}`;
11551
+ }
11552
+ const prompt = n.behavioralPrompt ? `${n.behavioralPrompt}
11553
+
11554
+ ${DEFAULT_BEHAVIORAL_PROMPT}` : DEFAULT_BEHAVIORAL_PROMPT;
11555
+ const promptBlock = n.type === "comment" ? `
11556
+
11557
+ ---
11558
+ ${prompt}` : "";
11559
+ return `${header}${scope}${body}
11560
+
11561
+ ${cta}${promptBlock}`;
11544
11562
  }
11545
11563
  async function injectNotification(port, hooksToken, n, agentId = "main") {
11546
11564
  const response = await fetch(`http://localhost:${port}/hooks/agent`, {
@@ -11566,6 +11584,9 @@ var markDeliveredByPlugin = makeFunctionReference("notifications:markDeliveredBy
11566
11584
  var upsertTelemetryFromPlugin = makeFunctionReference("telemetryPlugin:upsertTelemetryFromPlugin");
11567
11585
  var upsertSessionsFromPlugin = makeFunctionReference("telemetryPlugin:upsertSessionsFromPlugin");
11568
11586
  var pushActivityFromPluginRef = makeFunctionReference("activityFeed:pushActivityFromPlugin");
11587
+ var getPendingCommandsForPlugin = makeFunctionReference("gatewayCommands:getPendingForPlugin");
11588
+ var acknowledgeCommandRef = makeFunctionReference("gatewayCommands:acknowledgeCommand");
11589
+ var addCommentFromPluginRef = makeFunctionReference("comments:addCommentFromPlugin");
11569
11590
  var HOT_KEY = "__cohort_sync__";
11570
11591
  function getHotState() {
11571
11592
  let state = globalThis[HOT_KEY];
@@ -11705,6 +11726,61 @@ async function initSubscription(port, cfg, hooksToken, logger) {
11705
11726
  }
11706
11727
  state.unsubscribers = [...unsubscribers];
11707
11728
  }
11729
+ function initCommandSubscription(cfg, logger) {
11730
+ const c = getOrCreateClient();
11731
+ if (!c) {
11732
+ logger.warn("cohort-sync: no ConvexClient \u2014 command subscription skipped");
11733
+ return;
11734
+ }
11735
+ let processing = false;
11736
+ const unsubscribe = c.onUpdate(
11737
+ getPendingCommandsForPlugin,
11738
+ { apiKey: cfg.apiKey },
11739
+ async (commands) => {
11740
+ if (processing) return;
11741
+ if (commands.length === 0) return;
11742
+ processing = true;
11743
+ const cmd = commands[commands.length - 1];
11744
+ logger.info(`cohort-sync: received gateway command: ${cmd.type} (${cmd._id})`);
11745
+ if (cmd.type === "restart") {
11746
+ try {
11747
+ await c.mutation(acknowledgeCommandRef, {
11748
+ commandId: cmd._id,
11749
+ apiKey: cfg.apiKey
11750
+ });
11751
+ logger.info("cohort-sync: restart command acknowledged, terminating in 500ms");
11752
+ await new Promise((r) => setTimeout(r, 500));
11753
+ process.kill(process.pid, "SIGTERM");
11754
+ } catch (err) {
11755
+ logger.error(`cohort-sync: failed to process restart command: ${String(err)}`);
11756
+ processing = false;
11757
+ }
11758
+ } else {
11759
+ logger.warn(`cohort-sync: unknown command type: ${cmd.type}`);
11760
+ processing = false;
11761
+ }
11762
+ },
11763
+ (err) => {
11764
+ logger.error(`cohort-sync: command subscription error: ${String(err)}`);
11765
+ }
11766
+ );
11767
+ unsubscribers.push(unsubscribe);
11768
+ getHotState().unsubscribers = [...unsubscribers];
11769
+ logger.info("cohort-sync: command subscription active");
11770
+ }
11771
+ async function callAddCommentFromPlugin(apiKey, args) {
11772
+ const c = getOrCreateClient();
11773
+ if (!c) {
11774
+ throw new Error("Convex client not initialized \u2014 subscription may not be active");
11775
+ }
11776
+ return await c.mutation(addCommentFromPluginRef, {
11777
+ apiKey,
11778
+ taskNumber: args.taskNumber,
11779
+ agentName: args.agentName,
11780
+ content: args.content,
11781
+ noReply: args.noReply
11782
+ });
11783
+ }
11708
11784
  function closeSubscription() {
11709
11785
  for (const unsub of unsubscribers) {
11710
11786
  try {
@@ -12731,31 +12807,75 @@ function registerHooks(api, cfg) {
12731
12807
  description: "Post a comment on a Cohort task. Use this to respond to @mentions or collaborate on tasks.",
12732
12808
  parameters: Type.Object({
12733
12809
  task_number: Type.Number({ description: "Task number (e.g. 312)" }),
12734
- comment: Type.String({ description: "Comment text to post" })
12810
+ comment: Type.String({ description: "Comment text to post" }),
12811
+ no_reply: Type.Optional(Type.Boolean({
12812
+ description: "If true, no notifications will be sent for this comment. Use for final/closing comments."
12813
+ }))
12735
12814
  }),
12736
12815
  async execute(_toolCallId, params) {
12737
- const apiUrl = cfg.apiUrl.replace(/\/+$/, "");
12738
- const res = await fetch(
12739
- `${apiUrl}/api/v1/tasks/${params.task_number}/comments`,
12740
- {
12741
- method: "POST",
12742
- headers: {
12743
- Authorization: `Bearer ${cfg.apiKey}`,
12744
- "Content-Type": "application/json"
12745
- },
12746
- body: JSON.stringify({ body: params.comment }),
12747
- signal: AbortSignal.timeout(1e4)
12816
+ try {
12817
+ const result = await callAddCommentFromPlugin(cfg.apiKey, {
12818
+ taskNumber: params.task_number,
12819
+ agentName,
12820
+ content: params.comment,
12821
+ noReply: params.no_reply ?? false
12822
+ });
12823
+ const lines = [`Comment posted on task #${params.task_number}.`];
12824
+ if (result.stats) {
12825
+ lines.push("");
12826
+ lines.push(`This task has ${result.stats.totalComments} comments. ${result.stats.myRecentCount}/${result.stats.threshold} hourly limit used on this task.`);
12748
12827
  }
12749
- );
12750
- if (!res.ok) {
12751
- const text = await res.text().catch(() => "");
12752
- throw new Error(`POST /api/v1/tasks/${params.task_number}/comments \u2192 ${res.status}: ${text}`);
12828
+ if (result.budget) {
12829
+ lines.push(`Daily budget: ${result.budget.used}/${result.budget.limit}`);
12830
+ }
12831
+ return {
12832
+ content: [{ type: "text", text: lines.join("\n") }],
12833
+ details: result
12834
+ };
12835
+ } catch (err) {
12836
+ const msg = err instanceof Error ? err.message : String(err);
12837
+ if (msg.includes("AGENT_COMMENTS_LOCKED")) {
12838
+ return {
12839
+ content: [{
12840
+ type: "text",
12841
+ text: `Cannot comment on task #${params.task_number}.
12842
+ Reason: Agent comments are locked on this task.
12843
+ Do not re-attempt to comment on this task.`
12844
+ }],
12845
+ details: { error: "AGENT_COMMENTS_LOCKED", taskNumber: params.task_number }
12846
+ };
12847
+ }
12848
+ if (msg.includes("TASK_HOUR_LIMIT_REACHED")) {
12849
+ const parts = msg.split("|");
12850
+ const count = parts[1] ?? "?";
12851
+ const limit = parts[2] ?? "?";
12852
+ return {
12853
+ content: [{
12854
+ type: "text",
12855
+ text: `Cannot comment on task #${params.task_number}.
12856
+ Reason: You have posted ${count} comments on this task in the last hour (limit: ${limit}).
12857
+ Step back from this task. Do not comment again until the next hour.`
12858
+ }],
12859
+ details: { error: "TASK_HOUR_LIMIT_REACHED", count, limit, taskNumber: params.task_number }
12860
+ };
12861
+ }
12862
+ if (msg.includes("DAILY_LIMIT_REACHED")) {
12863
+ const parts = msg.split("|");
12864
+ const used = parts[1] ?? "?";
12865
+ const max = parts[2] ?? "?";
12866
+ const resetAt = parts[3] ?? "tomorrow";
12867
+ return {
12868
+ content: [{
12869
+ type: "text",
12870
+ text: `Cannot comment on task #${params.task_number}.
12871
+ Reason: Daily comment limit reached (${used}/${max}).
12872
+ Do not attempt to make more comments until ${resetAt}.`
12873
+ }],
12874
+ details: { error: "DAILY_LIMIT_REACHED", used, max, resetAt, taskNumber: params.task_number }
12875
+ };
12876
+ }
12877
+ throw err;
12753
12878
  }
12754
- const result = await res.json();
12755
- return {
12756
- content: [{ type: "text", text: `Comment posted on task #${params.task_number}` }],
12757
- details: result
12758
- };
12759
12879
  }
12760
12880
  };
12761
12881
  });
@@ -12858,6 +12978,7 @@ function registerHooks(api, cfg) {
12858
12978
  ).catch((err) => {
12859
12979
  logger.error(`cohort-sync: subscription init failed: ${String(err)}`);
12860
12980
  });
12981
+ initCommandSubscription(cfg, logger);
12861
12982
  const allAgentIds = ["main", ...(config?.agents?.list ?? []).map((a) => a.id)];
12862
12983
  for (const agentId of allAgentIds) {
12863
12984
  const agentName = resolveAgentName(agentId);
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cfio/cohort-sync",
3
- "version": "0.2.3",
3
+ "version": "0.2.4",
4
4
  "description": "Syncs agent status and skills to Cohort dashboard",
5
5
  "type": "module",
6
6
  "main": "index.js",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cfio/cohort-sync",
3
- "version": "0.2.3",
3
+ "version": "0.2.4",
4
4
  "description": "Syncs agent status and skills to Cohort dashboard",
5
5
  "license": "MIT",
6
6
  "homepage": "https://docs.cohort.bot/gateway",