crewly 1.20.324 → 1.20.326

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.
Files changed (76) hide show
  1. package/config/roles/orchestrator/prompt.md +1 -0
  2. package/config/skills/orchestrator/create-channel/SKILL.md +42 -0
  3. package/config/skills/orchestrator/create-channel/execute.sh +62 -0
  4. package/config/skills/orchestrator/create-channel/execute.test.sh +29 -0
  5. package/dist/backend/backend/src/constants.d.ts +4 -0
  6. package/dist/backend/backend/src/constants.d.ts.map +1 -1
  7. package/dist/backend/backend/src/constants.js +4 -0
  8. package/dist/backend/backend/src/constants.js.map +1 -1
  9. package/dist/backend/backend/src/controllers/channels/channels.routes.d.ts +6 -2
  10. package/dist/backend/backend/src/controllers/channels/channels.routes.d.ts.map +1 -1
  11. package/dist/backend/backend/src/controllers/channels/channels.routes.js +37 -6
  12. package/dist/backend/backend/src/controllers/channels/channels.routes.js.map +1 -1
  13. package/dist/backend/backend/src/middleware/caller-identity.middleware.d.ts +23 -0
  14. package/dist/backend/backend/src/middleware/caller-identity.middleware.d.ts.map +1 -1
  15. package/dist/backend/backend/src/middleware/caller-identity.middleware.js +31 -1
  16. package/dist/backend/backend/src/middleware/caller-identity.middleware.js.map +1 -1
  17. package/dist/backend/backend/src/routes/api.routes.d.ts.map +1 -1
  18. package/dist/backend/backend/src/routes/api.routes.js +43 -1
  19. package/dist/backend/backend/src/routes/api.routes.js.map +1 -1
  20. package/dist/backend/backend/src/services/ai/prompt-modules/skills-reference.module.d.ts.map +1 -1
  21. package/dist/backend/backend/src/services/ai/prompt-modules/skills-reference.module.js +1 -1
  22. package/dist/backend/backend/src/services/ai/prompt-modules/skills-reference.module.js.map +1 -1
  23. package/dist/backend/backend/src/services/channels/crewly-channel-compose.service.d.ts +134 -0
  24. package/dist/backend/backend/src/services/channels/crewly-channel-compose.service.d.ts.map +1 -0
  25. package/dist/backend/backend/src/services/channels/crewly-channel-compose.service.js +222 -0
  26. package/dist/backend/backend/src/services/channels/crewly-channel-compose.service.js.map +1 -0
  27. package/dist/backend/backend/src/services/channels/crewly-channel.service.d.ts +3 -0
  28. package/dist/backend/backend/src/services/channels/crewly-channel.service.d.ts.map +1 -1
  29. package/dist/backend/backend/src/services/channels/crewly-channel.service.js +17 -4
  30. package/dist/backend/backend/src/services/channels/crewly-channel.service.js.map +1 -1
  31. package/dist/backend/backend/src/services/slack/slack-directory.service.d.ts +6 -0
  32. package/dist/backend/backend/src/services/slack/slack-directory.service.d.ts.map +1 -1
  33. package/dist/backend/backend/src/services/slack/slack-directory.service.js +2 -0
  34. package/dist/backend/backend/src/services/slack/slack-directory.service.js.map +1 -1
  35. package/dist/backend/backend/src/services/slack/slack-initializer.d.ts.map +1 -1
  36. package/dist/backend/backend/src/services/slack/slack-initializer.js +8 -0
  37. package/dist/backend/backend/src/services/slack/slack-initializer.js.map +1 -1
  38. package/dist/backend/backend/src/services/slack/slack-instance-registry.service.d.ts +13 -0
  39. package/dist/backend/backend/src/services/slack/slack-instance-registry.service.d.ts.map +1 -1
  40. package/dist/backend/backend/src/services/slack/slack-instance-registry.service.js +12 -0
  41. package/dist/backend/backend/src/services/slack/slack-instance-registry.service.js.map +1 -1
  42. package/dist/backend/backend/src/services/slack/slack-team-channel.service.d.ts +23 -0
  43. package/dist/backend/backend/src/services/slack/slack-team-channel.service.d.ts.map +1 -1
  44. package/dist/backend/backend/src/services/slack/slack-team-channel.service.js +100 -3
  45. package/dist/backend/backend/src/services/slack/slack-team-channel.service.js.map +1 -1
  46. package/dist/backend/backend/src/services/slack/slack.service.d.ts +3 -1
  47. package/dist/backend/backend/src/services/slack/slack.service.d.ts.map +1 -1
  48. package/dist/backend/backend/src/services/slack/slack.service.js +24 -0
  49. package/dist/backend/backend/src/services/slack/slack.service.js.map +1 -1
  50. package/dist/backend/backend/src/types/slack.types.d.ts +14 -0
  51. package/dist/backend/backend/src/types/slack.types.d.ts.map +1 -1
  52. package/dist/backend/backend/src/types/slack.types.js.map +1 -1
  53. package/dist/backend/build-info.json +2 -2
  54. package/dist/cli/backend/src/constants.d.ts +4 -0
  55. package/dist/cli/backend/src/constants.d.ts.map +1 -1
  56. package/dist/cli/backend/src/constants.js +4 -0
  57. package/dist/cli/backend/src/constants.js.map +1 -1
  58. package/dist/cli/backend/src/services/ai/prompt-modules/skills-reference.module.d.ts.map +1 -1
  59. package/dist/cli/backend/src/services/ai/prompt-modules/skills-reference.module.js +1 -1
  60. package/dist/cli/backend/src/services/ai/prompt-modules/skills-reference.module.js.map +1 -1
  61. package/dist/cli/backend/src/services/slack/slack-directory.service.d.ts +6 -0
  62. package/dist/cli/backend/src/services/slack/slack-directory.service.d.ts.map +1 -1
  63. package/dist/cli/backend/src/services/slack/slack-directory.service.js +2 -0
  64. package/dist/cli/backend/src/services/slack/slack-directory.service.js.map +1 -1
  65. package/dist/cli/backend/src/services/slack/slack-team-channel.service.d.ts +23 -0
  66. package/dist/cli/backend/src/services/slack/slack-team-channel.service.d.ts.map +1 -1
  67. package/dist/cli/backend/src/services/slack/slack-team-channel.service.js +100 -3
  68. package/dist/cli/backend/src/services/slack/slack-team-channel.service.js.map +1 -1
  69. package/dist/cli/backend/src/services/slack/slack.service.d.ts +3 -1
  70. package/dist/cli/backend/src/services/slack/slack.service.d.ts.map +1 -1
  71. package/dist/cli/backend/src/services/slack/slack.service.js +24 -0
  72. package/dist/cli/backend/src/services/slack/slack.service.js.map +1 -1
  73. package/dist/cli/backend/src/types/slack.types.d.ts +14 -0
  74. package/dist/cli/backend/src/types/slack.types.d.ts.map +1 -1
  75. package/dist/cli/backend/src/types/slack.types.js.map +1 -1
  76. package/package.json +1 -1
@@ -405,6 +405,8 @@ export class SlackTeamChannelService {
405
405
  unsubscribeIdentity = null;
406
406
  /** Slack `channel:ts` of messages already routed → the persisted turn. */
407
407
  seenInbound = new Map();
408
+ /** Room messages already handed back to the orchestrator. */
409
+ orcHandedBack = new Set();
408
410
  /** The last room presence Cloud sent per Slack channel — names for a hand-off. */
409
411
  lastRooms = new Map();
410
412
  /** People (not agents) by lower-cased name → Slack user id, for `@Name` in agent replies. */
@@ -1022,13 +1024,26 @@ export class SlackTeamChannelService {
1022
1024
  }
1023
1025
  }
1024
1026
  else {
1025
- const channel = await this.deps.slack.createChannel(derived);
1027
+ // 2026-10-13: a room another machine created (room-join request): link
1028
+ // THAT Slack channel. Asking Slack to create by name could make a
1029
+ // second channel when Slack normalised the name differently.
1030
+ let channel;
1031
+ if (input.existingChannelId) {
1032
+ const found = await this.deps.slack.getChannelInfo(input.existingChannelId);
1033
+ if (!found)
1034
+ throw new Error(`Slack channel ${input.existingChannelId} was not found`);
1035
+ await this.deps.slack.joinChannel(found.id).catch(() => undefined);
1036
+ channel = found;
1037
+ }
1038
+ else {
1039
+ channel = await this.deps.slack.createChannel(derived);
1040
+ }
1026
1041
  // createChannel hands back an existing channel when the name is taken;
1027
1042
  // a team's channel must not be turned into a second room.
1028
1043
  const clash = current.mappings.find((m) => m.slackChannelId === channel.id);
1029
1044
  if (clash)
1030
1045
  throw new Error(`#${channel.name} is already linked to ${isAdhocMapping(clash) ? 'another channel' : 'a team'}`);
1031
- const ownerInvited = await this.inviteOwner(channel.id, channel.name);
1046
+ const ownerInvited = input.existingChannelId ? false : await this.inviteOwner(channel.id, channel.name);
1032
1047
  if (input.purpose.trim()) {
1033
1048
  await this.deps.slack.setChannelPurpose(channel.id, input.purpose.trim()).catch((err) => {
1034
1049
  this.logger.debug('setPurpose failed (non-critical)', { error: err instanceof Error ? err.message : String(err) });
@@ -1458,8 +1473,43 @@ export class SlackTeamChannelService {
1458
1473
  const declinedOnly = declinedAll.length > 0 && resolved.mentions.every((sess) => declinedAll.includes(sess));
1459
1474
  for (const sess of declinedAll)
1460
1475
  resolved.mentions.splice(resolved.mentions.indexOf(sess), 1);
1461
- const agentsMentionedViaCloud = [...new Set([...(message.mentionedAgentSessions ?? []), ...namedElsewhere])];
1476
+ // This machine's orchestrator @'d in a room (2026-10-13, #pro-crewly-marketing:
1477
+ // Ella's "<@orc> Steve approved a new team on your machine…" reached the
1478
+ // Air, Cloud routed it by `agent-author`, and nobody acted). The orc is
1479
+ // deliberately not a room member (leads, rosters and wake-when-all-asleep
1480
+ // rely on that), so the resolver never saw it, and the message counted as
1481
+ // addressed to nobody here. It is either Cloud's mention list (mapped to
1482
+ // the local session name) or the orc's own bot id in the text. The orc is
1483
+ // taken out of the agent lists below: it is neither "elsewhere" nor a
1484
+ // dispatch target of the huddle.
1485
+ const orcSession = CREWLY_CONSTANTS.SESSIONS.ORCHESTRATOR_NAME;
1486
+ const orcBotId = this.deps.identities?.getInstalled(orcSession)?.botUserId;
1487
+ const orcAddressed = !handoffTo &&
1488
+ (!!message.mentionedAgentSessions?.some((sess) => localAgentSession(sess) === orcSession) ||
1489
+ (!!orcBotId && (message.text ?? '').includes(`<@${orcBotId}>`)));
1490
+ const agentsMentionedViaCloud = [...new Set([...(message.mentionedAgentSessions ?? []), ...namedElsewhere])].filter((sess) => localAgentSession(sess) !== orcSession);
1462
1491
  const mentionedElsewhere = agentsMentionedViaCloud.filter((sess) => !isLocal(sess));
1492
+ // Nobody else here is addressed: the orchestrator's own pipeline takes it
1493
+ // (it replies in the same Slack thread). Returning null hands the message
1494
+ // back to the bridge, the way an unmapped channel message is. Members
1495
+ // @'d alongside the orc keep the room path (they answer in the thread).
1496
+ if (orcAddressed && resolved.mentions.length === 0 && declinedAll.length === 0 && mentionedElsewhere.length === 0) {
1497
+ const orcKey = `${message.channelId}:${message.ts}`;
1498
+ // The same message can arrive as two event types; hand it back once.
1499
+ if (this.orcHandedBack.has(orcKey))
1500
+ return { mapping, message: seen ?? {}, mentions: [], dispatch: null, duplicate: true };
1501
+ this.orcHandedBack.add(orcKey);
1502
+ if (this.orcHandedBack.size > SLACK_TEAM_CHANNEL_CONSTANTS.SEEN_INBOUND_MAX) {
1503
+ const oldest = this.orcHandedBack.values().next().value;
1504
+ if (oldest !== undefined)
1505
+ this.orcHandedBack.delete(oldest);
1506
+ }
1507
+ this.logger.info("Room message @'s this machine's orchestrator - handed to it", {
1508
+ slackChannel: `#${mapping.slackChannelName}`,
1509
+ ts: message.ts,
1510
+ });
1511
+ return null;
1512
+ }
1463
1513
  const addressedElsewhereOnly = !handoffTo && resolved.mentions.length === 0 && mentionedElsewhere.length > 0;
1464
1514
  // @-mentions of people: the owner asking a
1465
1515
  // colleague is not a question for the agents (2026-10-01,
@@ -3015,6 +3065,53 @@ export class SlackTeamChannelService {
3015
3065
  this.logger.info('Room message handed to an agent on another machine', { agentSession: hit.session, slackChannel: mapping.slackChannelName });
3016
3066
  return { ok: true, agentSession: hit.session, displayName: hit.name, via: 'cloud' };
3017
3067
  }
3068
+ /**
3069
+ * A room-join request from another machine (via Cloud): link the existing
3070
+ * Slack channel here with the given LOCAL agents as members, so they are in
3071
+ * the room and show up in this machine's heartbeat `rooms`.
3072
+ *
3073
+ * Sessions that are not agents of this machine are rejected and reported,
3074
+ * never silently dropped. A channel that is a team's channel is refused.
3075
+ *
3076
+ * @param input - Slack channel and the sessions Cloud addressed to this machine
3077
+ * @returns Which sessions joined and which were rejected
3078
+ */
3079
+ async joinRoomFromCloud(input) {
3080
+ await this.load();
3081
+ const local = new Set();
3082
+ for (const team of await this.deps.storage.getTeams()) {
3083
+ for (const m of teamChannelMembers(team))
3084
+ local.add(m.sessionName);
3085
+ }
3086
+ const wanted = [...new Set((input.sessions ?? []).map((s) => localAgentSession(String(s))).filter(Boolean))];
3087
+ const joined = wanted.filter((s) => local.has(s));
3088
+ const rejected = wanted.filter((s) => !local.has(s));
3089
+ if (rejected.length > 0) {
3090
+ this.logger.warn('Room join: sessions are not agents of this machine', { slackChannel: input.slackChannelId, rejected });
3091
+ }
3092
+ if (joined.length === 0)
3093
+ return { ok: false, joined, rejected, error: 'no_local_members' };
3094
+ const existing = (this.store?.mappings ?? []).find((m) => m.slackChannelId === input.slackChannelId);
3095
+ if (existing && !isAdhocMapping(existing)) {
3096
+ return { ok: false, joined: [], rejected: wanted, error: 'channel_belongs_to_a_team' };
3097
+ }
3098
+ try {
3099
+ await this.ensureAgentChannel({
3100
+ name: input.slackChannelName,
3101
+ purpose: '',
3102
+ memberSessions: joined,
3103
+ existingChannelId: input.slackChannelId,
3104
+ applyPrefix: false,
3105
+ });
3106
+ }
3107
+ catch (err) {
3108
+ const error = err instanceof Error ? err.message : String(err);
3109
+ this.logger.warn('Room join failed', { slackChannel: input.slackChannelId, error });
3110
+ return { ok: false, joined: [], rejected: wanted, error };
3111
+ }
3112
+ this.logger.info('Joined a room created on another machine', { slackChannel: input.slackChannelId, name: input.slackChannelName, agents: joined });
3113
+ return { ok: true, joined, rejected };
3114
+ }
3018
3115
  /**
3019
3116
  * Every Slack channel this machine maps — team channels and ad-hoc rooms —
3020
3117
  * with the local agents in it. What the delivery audit reads Slack for.