wayfind 2.0.24 → 2.0.26

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/bin/digest.js CHANGED
@@ -805,7 +805,7 @@ async function generateDigest(config, personaIds, sinceDate, onProgress) {
805
805
  fs.writeFileSync(combinedFile, combinedContent, 'utf8');
806
806
  files.push(combinedFile);
807
807
 
808
- return { files, personas: personaIds, dateRange };
808
+ return { files, personas: personaIds, dateRange, scores };
809
809
  }
810
810
 
811
811
  /**
@@ -145,9 +145,73 @@ function filterForPersona(signalContent, journalContent, scores, personaId, thre
145
145
  return { signals: filteredSignals, journals: filteredJournals };
146
146
  }
147
147
 
148
+ /**
149
+ * Build per-member mention data for a digest persona.
150
+ * Counts how many items scored 2 (directly relevant) for each member's personas.
151
+ *
152
+ * @param {Array<{id: number, [personaId]: number}>} scores - Scoring results
153
+ * @param {Array<{name: string, slack_user_id?: string, personas?: string[]}>} members - Team members
154
+ * @param {string} digestPersonaId - The persona this digest is for
155
+ * @returns {Array<{name: string, slackId: string, count: number}>} Members to mention, sorted by count desc
156
+ */
157
+ function buildMentions(scores, members, digestPersonaId) {
158
+ if (!scores || !members || members.length === 0) return [];
159
+
160
+ const MENTION_THRESHOLD = 2; // Only mention for directly relevant items
161
+ const mentions = [];
162
+
163
+ for (const member of members) {
164
+ if (!member.slack_user_id) continue;
165
+ const memberPersonas = member.personas || [];
166
+
167
+ // For unified digest: check all of the member's personas
168
+ // For persona-specific digest: only mention if member has that persona
169
+ const relevantPersonas = digestPersonaId === 'unified'
170
+ ? memberPersonas
171
+ : memberPersonas.filter(p => p === digestPersonaId);
172
+
173
+ if (relevantPersonas.length === 0) continue;
174
+
175
+ // Count items that scored >= MENTION_THRESHOLD for any of the member's relevant personas
176
+ let count = 0;
177
+ for (const score of scores) {
178
+ const isRelevant = relevantPersonas.some(p => (score[p] ?? 0) >= MENTION_THRESHOLD);
179
+ if (isRelevant) count++;
180
+ }
181
+
182
+ if (count > 0) {
183
+ mentions.push({
184
+ name: member.name || 'unknown',
185
+ slackId: member.slack_user_id,
186
+ count,
187
+ });
188
+ }
189
+ }
190
+
191
+ return mentions.sort((a, b) => b.count - a.count);
192
+ }
193
+
194
+ /**
195
+ * Format mentions into a Slack mrkdwn message for thread reply.
196
+ * @param {Array<{name: string, slackId: string, count: number}>} mentions
197
+ * @returns {string|null} Formatted message or null if no mentions
198
+ */
199
+ function formatMentionsMessage(mentions) {
200
+ if (!mentions || mentions.length === 0) return null;
201
+
202
+ const lines = mentions.map(m => {
203
+ const items = m.count === 1 ? '1 item' : `${m.count} items`;
204
+ return `<@${m.slackId}> — ${items} directly relevant to you`;
205
+ });
206
+
207
+ return `:bell: *Heads up*\n${lines.join('\n')}`;
208
+ }
209
+
148
210
  module.exports = {
149
211
  scoreItems,
150
212
  filterForPersona,
213
+ buildMentions,
214
+ formatMentionsMessage,
151
215
  DEFAULT_THRESHOLDS,
152
216
  // Exported for testing
153
217
  buildScoringPrompt,
package/bin/slack.js CHANGED
@@ -203,9 +203,11 @@ function postToWebhook(webhookUrl, payload) {
203
203
  * @param {string} header - One-liner header (e.g. ":compass: *Wayfind Digest* (Mar 11–18)")
204
204
  * @param {string} content - Full digest body as mrkdwn
205
205
  * @param {string} personaName - Persona ID
206
+ * @param {Object} [extras] - Optional extras
207
+ * @param {string} [extras.mentionsMessage] - Pre-formatted @mentions message for thread reply
206
208
  * @returns {Promise<{ ok: true, persona: string, ts: string, channel: string }>}
207
209
  */
208
- async function deliverViaBot(botToken, channel, header, content, personaName) {
210
+ async function deliverViaBot(botToken, channel, header, content, personaName, extras) {
209
211
  const { WebClient } = require('@slack/web-api');
210
212
  const client = new WebClient(botToken);
211
213
 
@@ -225,7 +227,22 @@ async function deliverViaBot(botToken, channel, header, content, personaName) {
225
227
  unfurl_links: false,
226
228
  });
227
229
 
228
- // Post feedback prompt as second thread reply
230
+ // Post @mentions as thread reply (before feedback prompt)
231
+ const ext = extras || {};
232
+ if (ext.mentionsMessage) {
233
+ try {
234
+ await client.chat.postMessage({
235
+ channel,
236
+ thread_ts: headerResult.ts,
237
+ text: ext.mentionsMessage,
238
+ unfurl_links: false,
239
+ });
240
+ } catch (err) {
241
+ // Non-fatal — digest was delivered, mentions are optional
242
+ }
243
+ }
244
+
245
+ // Post feedback prompt as thread reply
229
246
  try {
230
247
  await client.chat.postMessage({
231
248
  channel,
@@ -254,6 +271,7 @@ async function deliverViaBot(botToken, channel, header, content, personaName) {
254
271
  * @param {Object} [options] - Optional delivery options
255
272
  * @param {string} [options.botToken] - Slack bot token for chat.postMessage delivery
256
273
  * @param {string} [options.channel] - Slack channel for bot delivery
274
+ * @param {string} [options.mentionsMessage] - Pre-formatted @mentions for thread reply
257
275
  * @returns {Promise<{ ok: true, persona: string, ts?: string, channel?: string }>}
258
276
  */
259
277
  async function deliver(webhookUrl, digestContent, personaName, dateRange, options) {
@@ -283,7 +301,9 @@ async function deliver(webhookUrl, digestContent, personaName, dateRange, option
283
301
  const opts = options || {};
284
302
  if (opts.botToken && opts.channel) {
285
303
  try {
286
- return await deliverViaBot(opts.botToken, opts.channel, header, mrkdwn, personaName);
304
+ return await deliverViaBot(opts.botToken, opts.channel, header, mrkdwn, personaName, {
305
+ mentionsMessage: opts.mentionsMessage,
306
+ });
287
307
  } catch (err) {
288
308
  console.error(`Bot delivery failed for ${personaName}, falling back to webhook: ${err.message}`);
289
309
  }
@@ -308,6 +328,7 @@ async function deliver(webhookUrl, digestContent, personaName, dateRange, option
308
328
  * @param {Object} [options] - Optional delivery options
309
329
  * @param {string} [options.botToken] - Slack bot token for chat.postMessage delivery
310
330
  * @param {string} [options.channel] - Slack channel for bot delivery
331
+ * @param {Object} [options.mentionsByPersona] - Map of personaId → formatted mentions message
311
332
  * @returns {Promise<Array<{ ok: true, persona: string, ts?: string, channel?: string }>>}
312
333
  */
313
334
  async function deliverAll(webhookUrl, digestResult, personaIds, options) {
@@ -336,7 +357,12 @@ async function deliverAll(webhookUrl, digestResult, personaIds, options) {
336
357
  await new Promise((r) => setTimeout(r, 1000));
337
358
  }
338
359
 
339
- const result = await deliver(webhookUrl, content, persona, digestResult.dateRange, options);
360
+ const deliverOpts = { ...(options || {}) };
361
+ // Attach per-persona mentions if available
362
+ if (deliverOpts.mentionsByPersona && deliverOpts.mentionsByPersona[persona]) {
363
+ deliverOpts.mentionsMessage = deliverOpts.mentionsByPersona[persona];
364
+ }
365
+ const result = await deliver(webhookUrl, content, persona, digestResult.dateRange, deliverOpts);
340
366
  results.push(result);
341
367
  } catch (err) {
342
368
  results.push({ ok: false, persona, error: err.message });
@@ -1018,10 +1018,32 @@ async function runDigest(args) {
1018
1018
  process.exit(1);
1019
1019
  }
1020
1020
 
1021
+ // Build per-persona @mentions from intelligence scores + member profiles
1022
+ const mentionsByPersona = {};
1023
+ if (result.scores) {
1024
+ const intelligence = require('./intelligence');
1025
+ const teamContextPath = getTeamContextPath();
1026
+ const membersDir = teamContextPath ? path.join(teamContextPath, 'members') : null;
1027
+ if (membersDir && fs.existsSync(membersDir)) {
1028
+ const memberFiles = fs.readdirSync(membersDir).filter(f => f.endsWith('.json'));
1029
+ const members = memberFiles.map(f => {
1030
+ try { return JSON.parse(fs.readFileSync(path.join(membersDir, f), 'utf8')); }
1031
+ catch { return null; }
1032
+ }).filter(Boolean);
1033
+
1034
+ for (const pid of personaIds) {
1035
+ const mentions = intelligence.buildMentions(result.scores, members, pid);
1036
+ const msg = intelligence.formatMentionsMessage(mentions);
1037
+ if (msg) mentionsByPersona[pid] = msg;
1038
+ }
1039
+ }
1040
+ }
1041
+
1021
1042
  console.log('Delivering to Slack...');
1022
1043
  const deliveryResults = await slack.deliverAll(webhookUrl, result, personaIds, {
1023
1044
  botToken: process.env.SLACK_BOT_TOKEN,
1024
1045
  channel: process.env.SLACK_DIGEST_CHANNEL,
1046
+ mentionsByPersona,
1025
1047
  });
1026
1048
  let failures = 0;
1027
1049
  const dateStr = result.dateRange.to;
@@ -4508,11 +4530,11 @@ const COMMANDS = {
4508
4530
  const skipNpm = args.includes('--skip-npm');
4509
4531
  if (!skipNpm) {
4510
4532
  console.log('Updating wayfind from npm...');
4511
- const npmResult = spawnSync('npm', ['update', '-g', 'wayfind'], {
4533
+ const npmResult = spawnSync('npm', ['install', '-g', 'wayfind@latest'], {
4512
4534
  stdio: 'inherit',
4513
4535
  });
4514
4536
  if (npmResult.error || (npmResult.status && npmResult.status !== 0)) {
4515
- console.error('npm update failed. Try running: npm update -g wayfind');
4537
+ console.error('npm install failed. Try running: npm install -g wayfind@latest');
4516
4538
  console.error('Then re-run: wayfind update --skip-npm');
4517
4539
  process.exit(1);
4518
4540
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wayfind",
3
- "version": "2.0.24",
3
+ "version": "2.0.26",
4
4
  "description": "Team decision trail for AI-assisted development. The connective tissue between product, engineering, and strategy.",
5
5
  "bin": {
6
6
  "wayfind": "./bin/team-context.js"