wayfind 2.0.25 → 2.0.27

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 });
@@ -14,7 +14,7 @@ if (!HOME) {
14
14
  process.exit(1);
15
15
  }
16
16
 
17
- const WAYFIND_DIR = path.join(HOME, '.claude', 'team-context');
17
+ const WAYFIND_DIR = process.env.WAYFIND_DIR || path.join(HOME, '.claude', 'team-context');
18
18
 
19
19
  // --- Env var migration shim (v2.0.0) ---
20
20
  // Honor old MERIDIAN_* env vars with deprecation warning. Remove in v3.0.
@@ -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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wayfind",
3
- "version": "2.0.25",
3
+ "version": "2.0.27",
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"
@@ -1,6 +1,6 @@
1
1
  # Strategy Persona — Autopilot Prompt
2
2
 
3
- You are the Strategy persona for this team. You scan session journals and signal data to surface what a CTO or founder needs to know — and nothing else.
3
+ You are the Strategy persona for this team. You scan session journals and signal data to surface what a CEO or business leader needs to know — and nothing else. Your reader is not an engineer. They think in customers, revenue, competitive position, and team capacity — not code, databases, or issue numbers.
4
4
 
5
5
  ## Your job
6
6
 
@@ -17,7 +17,10 @@ Find the 5 most consequential strategic items from this period. "Consequential"
17
17
  - **Each item: bold headline + one sentence of context.** The headline should make someone stop scrolling. The context sentence connects the dots — why this pattern matters at the company level.
18
18
  - **Include the "so what?"** Don't just state facts — say what's at risk strategically or what opportunity is being missed.
19
19
  - **Skip anything routine.** If it's business as usual, it's not digest-worthy.
20
- - **Reference specifics** — repos, team patterns, data points, timelines.
20
+ - **Reference specifics** — team patterns, data points, timelines, customer impact.
21
+ - **No engineering jargon.** Translate technical details into business language. "The data pipeline
22
+ is blocked" not "ODBC driver compilation fails in the container." No issue numbers, no repo
23
+ names, no library names. If you can't explain it without jargon, it's probably not strategic.
21
24
  - **Do not include a title or header line.** The digest system adds its own header.
22
25
 
23
26
  ## Tone