@ycodium-ai/plugin-team 0.2.3395 → 0.2.3405

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
@@ -7472,7 +7472,7 @@ var PERSON_MESSAGE_RULE = "Send a message to the person only for a product decis
7472
7472
  var MEMBER_CHECKS_RULE = "Before you mark an item done, run the checks that this project's rules name for the code you changed, and fix every failure. Do not mark an item done while a check still fails; say in a work item comment what still fails instead.";
7473
7473
  var NO_STASH = "Do not use git stash: all worktrees of this repository share one stash stack.";
7474
7474
  var MEMBER_NO_STASH_RULE = `${NO_STASH} To try something you can undo, commit to a temporary branch or copy files into an ignored folder inside your own worktree, and delete it before you mark the item done.`;
7475
- var MEMBER_ASK_BEFORE_OUTSIDE_RULE = "Before you change dependencies or patches, write outside the project root, or fetch code from the network (git clone, downloads), mention your lead in a work item comment and wait for an answer.";
7475
+ var MEMBER_ASK_BEFORE_OUTSIDE_RULE = "Before you change dependencies or patches, write outside the project root, or fetch code from the network (git clone, downloads), mention your lead in a work item comment (put them in the mentions parameter of the comment) and wait for an answer.";
7476
7476
  var MEMBER_READ_RANGES_RULE = "Read files in ranges: find the place with Grep first, then read only the lines you need; read only the relevant section of a skill file.";
7477
7477
  var LEAD_ACCEPT_RULE = `When you check a finished item, run the checks that this project's rules name yourself. Send the item back if its changes touch patches/ or a lockfile, unless the item has a comment from you approving that change. ${NO_STASH} To compare against the base, run the same check in your own working directory on the base branch and compare the error counts; do not stash in the member's worktree.`;
7478
7478
  var PROGRESS_CHECKIN_MINUTES = 20;
@@ -7490,8 +7490,8 @@ var LEAD_SCHEDULE_CHECKIN_RULE = "Before you end a turn while any item of this g
7490
7490
  var LEAD_SPLIT_RULE = "Use work_create_item to split the goal into a few work items: one item per task, with a clear action in the title. Members do not see this conversation or your goal, so put everything a member needs into the description: the deliverable, the constraints, and how to hand the work in. Create them without assignees.";
7491
7491
  var LEAD_OWN_ITEM_RULE = "If a work item for this goal is assigned to you, also record it as blocked by every item you create. Each time a member marks an item done you will be told, so check it then; when they are all done, mark that item done with work_update_item.";
7492
7492
  var LEAD_REBALANCE_RULE = "When a member has nothing in progress while another member has an item waiting in line that is not blocked, move one waiting item to the idle member with work_update_item.";
7493
- var LEAD_CHECKPOINT_RULE = `Members commit and post a one-line progress comment each time they reach a step (a group of tests turns green, a file passes its typecheck, before they change approach, before they stop to wait for someone), and the team plugin mentions a member on an item in progress after ${PROGRESS_CHECKIN_MINUTES} minutes with no progress from them, up to ${PROGRESS_CHECKIN_LIMIT} times in a row. Each time you look in on members, check each item in progress: if the plugin has asked the member and there has been no answer for ${PROGRESS_CHECKIN_LIMIT * PROGRESS_CHECKIN_MINUTES} minutes since its first ask (a member on another machine that has gone offline is asked only once), or the member keeps posting progress but their branch has had no new commit for more than 30 minutes, post a comment with work_post_comment that mentions the member and asks them to run the tests and typecheck for what they have changed so far, commit the work in progress to their branch, and post a one-line progress and blocker comment on the item. Take the times from work_history and, when you can read the member's worktree on this machine, from git log -1 --format=%cI there, and compare them with the current time; do not estimate. When you cannot read their worktree, as with a remote member, judge by work_history and comments alone; not being able to see commits does not mean there are none.`;
7494
- var LEAD_SEND_BACK_RULE = "If a finished item is not acceptable, send it back: use work_update_item to set its state back to an open one (see work_list_states), and change its assignees too if someone else should redo it. Then say what is wrong in a comment with work_post_comment, mentioning the member.";
7493
+ var LEAD_CHECKPOINT_RULE = `Members commit and post a one-line progress comment each time they reach a step (a group of tests turns green, a file passes its typecheck, before they change approach, before they stop to wait for someone), and the team plugin mentions a member on an item in progress after ${PROGRESS_CHECKIN_MINUTES} minutes with no progress from them, up to ${PROGRESS_CHECKIN_LIMIT} times in a row. Each time you look in on members, check each item in progress: if the plugin has asked the member and there has been no answer for ${PROGRESS_CHECKIN_LIMIT * PROGRESS_CHECKIN_MINUTES} minutes since its first ask (a member on another machine that has gone offline is asked only once), or the member keeps posting progress but their branch has had no new commit for more than 30 minutes, post a comment with work_post_comment, mentioning the member in its mentions parameter, that asks them to run the tests and typecheck for what they have changed so far, commit the work in progress to their branch, and post a one-line progress and blocker comment on the item. Take the times from work_history and, when you can read the member's worktree on this machine, from git log -1 --format=%cI there, and compare them with the current time; do not estimate. When you cannot read their worktree, as with a remote member, judge by work_history and comments alone; not being able to see commits does not mean there are none.`;
7494
+ var LEAD_SEND_BACK_RULE = 'If a finished item is not acceptable, send it back: use work_update_item to set its state back to an open one (see work_list_states), and change its assignees too if someone else should redo it. Then say what is wrong in a comment with work_post_comment, mentioning the member in its mentions parameter (for a local member: { "kind": "agent", "threadId": <their threadId> }); that is what wakes them.';
7495
7495
  var LEAD_NO_MEMBER_TASKS = "Do not do members' tasks yourself, and never assign an item you create to yourself: merging, pushing and integrating the work are member tasks too.";
7496
7496
  var tableCell = (value) => value.replace(/\|/g, "\\|").replace(/[\r\n]/g, " ");
7497
7497
  function leadBrief(goal, roster) {
@@ -8904,6 +8904,89 @@ ${await worktreeSetupSentence(ctx, input.projectId, branch)}`
8904
8904
  return true;
8905
8905
  }
8906
8906
 
8907
+ // src/runtime/commentWakes.ts
8908
+ var COMMENT_MENTIONS_FACT = "comment-mentions";
8909
+ var COMMENT_MENTIONS_EVENT = `team/${COMMENT_MENTIONS_FACT}`;
8910
+ var CommentMentionsFact = Struct({
8911
+ version: Literal2(1),
8912
+ projectId: String4,
8913
+ workItemId: String4,
8914
+ commentId: String4,
8915
+ recipients: ArraySchema(
8916
+ Union2([
8917
+ Struct({ threadId: String4 }),
8918
+ Struct({ principalId: String4 })
8919
+ ])
8920
+ )
8921
+ });
8922
+ var commentPeople = (comment) => [
8923
+ ...comment.parentAuthor === void 0 ? [] : [comment.parentAuthor],
8924
+ ...comment.mentions
8925
+ ];
8926
+ var recipientsOf = (people) => {
8927
+ const recipients = /* @__PURE__ */ new Map();
8928
+ for (const person of people) {
8929
+ const recipient = recipientOf(person);
8930
+ if (recipient !== void 0) recipients.set(recipientKey(recipient), recipient);
8931
+ }
8932
+ return recipients;
8933
+ };
8934
+ var reachedKeys = (actor, people) => new Set(recipientsOf([actor, ...people]).keys());
8935
+ var commentRecipients = (project2, actor, people) => {
8936
+ const author = recipientOf(actor);
8937
+ const authorKey = author === void 0 ? void 0 : recipientKey(author);
8938
+ return [...recipientsOf(people)].filter(([key]) => key !== authorKey).map(([, recipient]) => recipient).filter((recipient) => isMember(project2, recipient));
8939
+ };
8940
+ var commentWakeKey = (commentId, recipient) => "threadId" in recipient ? `comment:${commentId}:${recipient.threadId}` : `comment:${commentId}:principal:${recipient.principalId}`;
8941
+ var queueCommentWakes = (project2, comment, recipients) => [
8942
+ ...project2.pendingComments,
8943
+ ...recipients.map((recipient) => ({
8944
+ ...recipient,
8945
+ key: commentWakeKey(comment.commentId, recipient),
8946
+ projectId: comment.projectId,
8947
+ workItemId: comment.workItemId,
8948
+ commentId: comment.commentId
8949
+ })).filter((queued) => !project2.pendingComments.some((pending) => pending.key === queued.key))
8950
+ ];
8951
+
8952
+ // src/domain/mentionText.ts
8953
+ var isAtSign = (character) => character === "@" || character === "\uFF20";
8954
+ var MENTION_BOUNDARY = /[\s\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\u3000-\u303f\uff01-\uff0f\uff1a-\uff20\uff3b-\uff40\uff5b-\uff65]/u;
8955
+ var isMentionStart = (text, index) => {
8956
+ if (!isAtSign(text[index])) return false;
8957
+ const before = Array.from(text.slice(Math.max(0, index - 2), index)).at(-1);
8958
+ return before === void 0 || MENTION_BOUNDARY.test(before);
8959
+ };
8960
+
8961
+ // src/runtime/mentionedMembers.ts
8962
+ var ASCII_WORD = /[A-Za-z0-9_-]/;
8963
+ function mentionedMembers(body, members) {
8964
+ const aliases = members.flatMap((member) => {
8965
+ const principal = mentionPrincipal(member);
8966
+ const recipient = principal === null ? void 0 : recipientOf(principal);
8967
+ if (recipient === void 0) return [];
8968
+ const id = "threadId" in recipient ? recipient.threadId : recipient.principalId;
8969
+ return [id, member.memberId, member.name].flatMap(
8970
+ (text) => text === void 0 || text === null || text === "" ? [] : [{ text: text.toLowerCase(), key: recipientKey(recipient), recipient }]
8971
+ );
8972
+ }).sort((a, b) => b.text.length - a.text.length);
8973
+ const found = /* @__PURE__ */ new Map();
8974
+ for (let at = 0; at < body.length; at++) {
8975
+ if (!isMentionStart(body, at)) continue;
8976
+ const rest = body.slice(at + 1);
8977
+ const hits = aliases.filter((alias) => {
8978
+ if (rest.slice(0, alias.text.length).toLowerCase() !== alias.text) return false;
8979
+ const next = rest[alias.text.length];
8980
+ return next === void 0 || !(ASCII_WORD.test(alias.text.slice(-1)) && ASCII_WORD.test(next));
8981
+ });
8982
+ const longest = hits.filter((hit) => hit.text.length === hits[0]?.text.length);
8983
+ const keys = new Set(longest.map((hit) => hit.key));
8984
+ const [only] = longest;
8985
+ if (only !== void 0 && keys.size === 1) found.set(only.key, only.recipient);
8986
+ }
8987
+ return [...found.values()];
8988
+ }
8989
+
8907
8990
  // src/runtime/recoveryState.ts
8908
8991
  var RecoveryPlan = Struct({
8909
8992
  action: Literals(["resume", "lead", "remind", "stall-lead"]),
@@ -9354,6 +9437,11 @@ var decodeEnvelope2 = decodeUnknownOption2(
9354
9437
  name: Literal2("team/member-woken"),
9355
9438
  data: MemberWokenFact
9356
9439
  }),
9440
+ Struct({
9441
+ pluginId: Literal2("team"),
9442
+ name: Literal2(COMMENT_MENTIONS_EVENT),
9443
+ data: CommentMentionsFact
9444
+ }),
9357
9445
  Struct({
9358
9446
  pluginId: Literal2("team"),
9359
9447
  name: Literal2("team/member-worktree-failed"),
@@ -9440,18 +9528,6 @@ var decodeWorkEvent = decodeUnknownOption2(
9440
9528
  var agentThreads = (principals) => [
9441
9529
  ...new Set(principals.flatMap((p) => p.kind === "agent" ? [p.threadId] : []))
9442
9530
  ];
9443
- var commentRecipients = (project2, actor, people) => {
9444
- const author = recipientOf(actor);
9445
- const recipients = /* @__PURE__ */ new Map();
9446
- for (const person of people) {
9447
- const recipient = recipientOf(person);
9448
- if (recipient === void 0) continue;
9449
- const key = recipientKey(recipient);
9450
- if (author === void 0 || recipientKey(author) !== key) recipients.set(key, recipient);
9451
- }
9452
- return [...recipients.values()].filter((recipient) => isMember(project2, recipient));
9453
- };
9454
- var commentWakeKey = (commentId, recipient) => "threadId" in recipient ? `comment:${commentId}:${recipient.threadId}` : `comment:${commentId}:principal:${recipient.principalId}`;
9455
9531
  function foldWorkWakes(state, event) {
9456
9532
  if (event.type === "plugin.event-appended") {
9457
9533
  const decoded2 = decodeEnvelope2(event.payload);
@@ -9488,6 +9564,21 @@ function foldWorkWakes(state, event) {
9488
9564
  }
9489
9565
  };
9490
9566
  }
9567
+ if (fact.name === COMMENT_MENTIONS_EVENT) {
9568
+ const { projectId: projectId3, workItemId: workItemId2, commentId, recipients } = fact.data;
9569
+ const project4 = readFact(state, projectId3) ?? emptyProject();
9570
+ return {
9571
+ ...state,
9572
+ [projectId3]: {
9573
+ ...project4,
9574
+ pendingComments: queueCommentWakes(
9575
+ project4,
9576
+ { projectId: projectId3, workItemId: workItemId2, commentId },
9577
+ recipients.filter((recipient) => isMember(project4, recipient))
9578
+ )
9579
+ }
9580
+ };
9581
+ }
9491
9582
  if (fact.name === "team/member-worktree-failed" || fact.name === "team/member-worktree-reported") {
9492
9583
  const { projectId: projectId3, threadId } = fact.data;
9493
9584
  const project4 = readFact(state, projectId3) ?? emptyProject();
@@ -9651,21 +9742,18 @@ function foldWorkWakes(state, event) {
9651
9742
  }
9652
9743
  case "work.comment-posted": {
9653
9744
  const { actor, parentAuthor, mentions, commentId } = work.payload;
9654
- const comments = commentRecipients(project2, actor, [
9655
- ...parentAuthor === void 0 ? [] : [parentAuthor],
9656
- ...mentions
9657
- ]).map((recipient) => ({
9658
- ...recipient,
9659
- key: commentWakeKey(commentId, recipient),
9660
- projectId,
9661
- workItemId,
9662
- commentId
9663
- })).filter(
9664
- (comment) => !project2.pendingComments.some((pending) => pending.key === comment.key)
9745
+ const recipients = commentRecipients(
9746
+ project2,
9747
+ actor,
9748
+ commentPeople({ parentAuthor, mentions })
9665
9749
  );
9666
9750
  next = {
9667
9751
  ...project2,
9668
- pendingComments: [...project2.pendingComments, ...comments],
9752
+ pendingComments: queueCommentWakes(
9753
+ project2,
9754
+ { projectId, workItemId, commentId },
9755
+ recipients
9756
+ ),
9669
9757
  items: currentItem === null ? project2.items : {
9670
9758
  ...project2.items,
9671
9759
  [workItemId]: {
@@ -9789,6 +9877,52 @@ function dueWakes(state) {
9789
9877
  return due.sort((a, b) => compare2(a.projectId, b.projectId) || compare2(a.key, b.key));
9790
9878
  }
9791
9879
 
9880
+ // src/runtime/commentMentions.ts
9881
+ var COMMENT_MENTION_RETRIES = 2;
9882
+ function startCommentMentionRuntime(ctx, goals) {
9883
+ const handle = async (event) => {
9884
+ const decoded = decodeWorkEvent(event);
9885
+ if (isNone2(decoded) || decoded.value.type !== "work.comment-posted") return;
9886
+ const comment = decoded.value.payload;
9887
+ const { projectId, workItemId, commentId } = comment;
9888
+ const resolve2 = async () => {
9889
+ const goal = await goals.read(projectId);
9890
+ if (goal === null) return;
9891
+ const { comment: posted } = await ctx.work.comments.get({ projectId, commentId });
9892
+ if (posted.body === null) return;
9893
+ const reached = reachedKeys(comment.actor, commentPeople(comment));
9894
+ const recipients = mentionedMembers(posted.body, goal.members).filter(
9895
+ (recipient) => !reached.has(recipientKey(recipient))
9896
+ );
9897
+ if (recipients.length === 0) return;
9898
+ await ctx.events.append({
9899
+ commandId: `team-comment-mentions:${commentId}`,
9900
+ name: COMMENT_MENTIONS_FACT,
9901
+ aggregate: { kind: "project", id: projectId },
9902
+ data: { version: 1, projectId, workItemId, commentId, recipients }
9903
+ });
9904
+ };
9905
+ for (let attempt = 1; ; attempt++) {
9906
+ try {
9907
+ return await resolve2();
9908
+ } catch (error) {
9909
+ if (attempt > COMMENT_MENTION_RETRIES) {
9910
+ ctx.log.warn("team comment mentions failed", {
9911
+ projectId,
9912
+ workItemId,
9913
+ commentId,
9914
+ eventId: event.eventId,
9915
+ attempts: attempt,
9916
+ error
9917
+ });
9918
+ return;
9919
+ }
9920
+ }
9921
+ }
9922
+ };
9923
+ ctx.reactor({ name: "team-comment-mentions", events: ["work.comment-posted"], handle });
9924
+ }
9925
+
9792
9926
  // src/runtime/wake.ts
9793
9927
  var workLabel = (workItemId, sequenceId) => sequenceId === void 0 ? `the work item with workItemId ${workItemId}` : `work item W${sequenceId} (workItemId ${workItemId})`;
9794
9928
  var titled = (workItemId, sequenceId, title) => `${workLabel(workItemId, sequenceId)}${title ? `: ${title}` : ""}`;
@@ -9936,7 +10070,7 @@ var leadText = (recovery, name, paused) => `Member ${name} (threadId ${recovery.
9936
10070
  var commentInstructions = (recovery) => recovery.items.map(
9937
10071
  (item) => item.lastCommentId === void 0 ? `${itemLabel(item)} has no comments yet. Continue from your existing workspace changes and turn history.` : `For ${itemLabel(item)}, the latest comment is commentId ${item.lastCommentId}. Read work_list_comments with workItemId: "${item.workItemId}" and continue from that comment and your existing workspace changes; do not start over.`
9938
10072
  ).join("\n");
9939
- var leadInstructions = (paused = false) => `${paused ? "Their sending is paused; a person needs to click Resume sending. " : ""}You can post a comment mentioning the member with work_post_comment to ask them to continue (the comment wakes them), reassign the work with work_update_item, or report the problem to the person.${paused ? " A comment will wait until the person resumes sending." : ""}`;
10073
+ var leadInstructions = (paused = false) => `${paused ? "Their sending is paused; a person needs to click Resume sending. " : ""}You can post a comment with work_post_comment, with the member in its mentions parameter, to ask them to continue (the mention wakes them), reassign the work with work_update_item, or report the problem to the person.${paused ? " A comment will wait until the person resumes sending." : ""}`;
9940
10074
  var resumeText = (recovery) => `Your previous turn was interrupted by an upstream error (${recovery.category}). Continue ${recovery.items.map(itemLabel).join(", ")} from where you stopped.
9941
10075
  ${commentInstructions(recovery)}`;
9942
10076
  var remindText = (recovery) => `Your turn ended normally, but ${recovery.items.map(itemLabel).join(", ")} ${recovery.items.length === 1 ? "is" : "are"} still in progress. If finished, mark the work done with work_update_item. If blocked, explain where you are stuck on the item with work_post_comment. Otherwise continue from where you stopped; do not start over.
@@ -11100,6 +11234,7 @@ var index_default = definePlugin({
11100
11234
  const goals = createGoalService(ctx, rosters);
11101
11235
  registerGoalCommands(ctx, goals);
11102
11236
  const wakes = startWakeRuntime(ctx, goals);
11237
+ startCommentMentionRuntime(ctx, goals);
11103
11238
  registerBoardCommands(ctx, goals, rosters, wakes.state);
11104
11239
  registerDetailCommands(ctx, goals);
11105
11240
  startRecoveryRuntime(ctx, goals, wakes.state, wakes.worktrees);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ycodium-ai/plugin-team",
3
- "version": "0.2.3395",
3
+ "version": "0.2.3405",
4
4
  "description": "A project team roster with members, a lead, and goals.",
5
5
  "keywords": [
6
6
  "teams",
@@ -34,7 +34,7 @@
34
34
  "tailwindcss": "^4.0.0",
35
35
  "typescript": "~6.0.3",
36
36
  "vite-plus": "0.2.2",
37
- "@ycodium-ai/plugin-api": "0.2.3395"
37
+ "@ycodium-ai/plugin-api": "0.2.3405"
38
38
  },
39
39
  "ycodium": {
40
40
  "plugin": "./dist/index.js",