wayfind 2.0.19 → 2.0.20

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.
@@ -7,34 +7,39 @@ Copy and paste the block below into a new Claude Code session. That's it.
7
7
  ```
8
8
  Please set up Wayfind for my Claude Code environment.
9
9
 
10
- Run these commands:
10
+ Step 1 — Install the CLI:
11
11
  npm install -g wayfind
12
12
  wayfind init
13
13
 
14
- Once it completes:
14
+ Step 2 — Install the Claude Code plugin:
15
+ /plugin marketplace add usewayfind/wayfind
16
+ /plugin install wayfind@usewayfind
17
+
18
+ Once both steps complete:
15
19
  1. Tell me what was installed and what still needs to be configured
16
- 2. Run /init-memory to initialize memory for the current repo
20
+ 2. Run /wayfind:init-memory to initialize memory for the current repo
17
21
  3. Ask me what my preferences are (communication style, tool preferences, commit
18
22
  conventions, anything I want Claude to always know) so we can fill in
19
23
  global-state.md together
20
24
  4. Ask me if I want to set up backup (see below) — I will need to provide a
21
25
  private GitHub repo URL before you can proceed with that step
22
26
  5. Run `wayfind whoami --setup` so I can provide my Slack user ID (used for @mentions and DMs)
23
- 6. Ask me if I want to set up team context (/init-team) for shared journals,
27
+ 6. Ask me if I want to set up team context (/wayfind:init-team) for shared journals,
24
28
  digests, and product state
25
- 7. Let me know about opt-in telemetry (set TEAM_CONTEXT_TELEMETRY=true to enable)
29
+ 7. Let me know that anonymous usage telemetry is enabled by default (set TEAM_CONTEXT_TELEMETRY=false to opt out)
26
30
  ```
27
31
 
28
32
  ---
29
33
 
30
34
  ## What happens
31
35
 
32
- The install command will:
33
- - Create `~/.claude/memory/` and `~/.claude/memory/journal/`
34
- - Install `~/.claude/global-state.md` (your persistent index)
35
- - Install `~/.claude/hooks/check-global-state.sh` (warns when state is stale)
36
- - Install slash commands: `/init-memory`, `/init-team`, `/journal`, `/doctor`
37
- - Register the hook in `~/.claude/settings.json`
36
+ The CLI install creates:
37
+ - `~/.claude/memory/` and `~/.claude/memory/journal/`
38
+ - `~/.claude/global-state.md` (your persistent index)
39
+
40
+ The plugin provides:
41
+ - SessionStart and Stop hooks (context loading, decision extraction)
42
+ - Slash commands: `/wayfind:init-memory`, `/wayfind:init-team`, `/wayfind:journal`, `/wayfind:doctor`, `/wayfind:standup`
38
43
 
39
44
  After the paste, Claude will walk you through filling in your preferences. From
40
45
  then on, every session in every repo will start with full context of where you
@@ -72,7 +77,7 @@ After that, your memory is backed up silently on every session — no manual ste
72
77
 
73
78
  ## Setting up team context (optional)
74
79
 
75
- Once you have the basics working, run `/init-team` in a Claude Code session to set up:
80
+ Once you have the basics working, run `/wayfind:init-team` in a Claude Code session to set up:
76
81
  - A shared team context repo for journals and digests
77
82
  - Slack integration for weekly digest posts
78
83
  - Notion integration for browseable product state and digest archives
@@ -87,7 +92,7 @@ visibility across product, engineering, and strategy.
87
92
 
88
93
  In any repo you work in, run:
89
94
  ```
90
- /init-memory
95
+ /wayfind:init-memory
91
96
  ```
92
97
 
93
98
  Claude will create `.claude/team-state.md` (shared) and `.claude/personal-state.md`
@@ -102,6 +107,8 @@ npm install -g wayfind
102
107
  wayfind init-cursor
103
108
  ```
104
109
 
110
+ (The Claude Code plugin is not available for Cursor — use the CLI-only setup.)
111
+
105
112
  ---
106
113
 
107
114
  ## Source
@@ -110,11 +117,11 @@ https://github.com/usewayfind/wayfind
110
117
 
111
118
  To install a specific version:
112
119
  ```
113
- npm install -g wayfind@1.1.0
120
+ npm install -g wayfind@2.0.15
114
121
  wayfind init
115
122
  ```
116
123
 
117
124
  Or via the shell installer with a pinned version:
118
125
  ```
119
- WAYFIND_VERSION=v1.1.0 bash <(curl -fsSL https://raw.githubusercontent.com/usewayfind/wayfind/main/install.sh)
126
+ WAYFIND_VERSION=v2.0.15 bash <(curl -fsSL https://raw.githubusercontent.com/usewayfind/wayfind/main/install.sh)
120
127
  ```
@@ -604,6 +604,11 @@ function searchText(query, options = {}) {
604
604
  function applyFilters(entry, filters) {
605
605
  if (isRepoExcluded(entry.repo)) return false;
606
606
  if (filters.repo && entry.repo.toLowerCase() !== filters.repo.toLowerCase()) return false;
607
+ if (filters.repos && filters.repos.length > 0) {
608
+ const lower = (entry.repo || '').toLowerCase();
609
+ const matches = filters.repos.some(r => lower === r.toLowerCase() || lower.endsWith('/' + r.split('/').pop().toLowerCase()));
610
+ if (!matches) return false;
611
+ }
607
612
  if (filters.since && entry.date < filters.since) return false;
608
613
  if (filters.until && entry.date > filters.until) return false;
609
614
  if (filters.drifted !== undefined && entry.drifted !== filters.drifted) return false;
package/bin/slack-bot.js CHANGED
@@ -10,6 +10,10 @@ const telemetry = require('./telemetry');
10
10
 
11
11
  // ── Slack connection state (for healthcheck) ────────────────────────────────
12
12
  let slackConnected = false;
13
+
14
+ // ── Feature map (loaded at startup, reloaded on-demand) ──────────────────────
15
+ /** In-memory feature map: { "org/repo": { tags: string[], description: string } } */
16
+ let featureMap = null;
13
17
  let slackLastConnected = null;
14
18
  let slackLastDisconnected = null;
15
19
 
@@ -53,6 +57,62 @@ Rules:
53
57
  - Do not invent information that isn't in the provided context.`;
54
58
  }
55
59
 
60
+ // ── Feature map ──────────────────────────────────────────────────────────────
61
+
62
+ /**
63
+ * Load features.json from the team-context repo into memory.
64
+ * Silently no-ops if the file doesn't exist.
65
+ * @param {Object} config - Bot config (uses team_context_dir or TEAM_CONTEXT_TEAM_CONTEXT_DIR)
66
+ */
67
+ function loadFeatureMap(config) {
68
+ const teamDir = config.team_context_dir || process.env.TEAM_CONTEXT_TEAM_CONTEXT_DIR || '';
69
+ if (!teamDir) return;
70
+ const featuresFile = path.join(teamDir, 'features.json');
71
+ try {
72
+ const raw = fs.readFileSync(featuresFile, 'utf8');
73
+ featureMap = JSON.parse(raw);
74
+ } catch {
75
+ featureMap = null;
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Use Haiku to determine which repos are relevant to a query, based on the feature map.
81
+ * Returns an array of repo slugs (e.g. ["org/api-service", "org/analytics"]).
82
+ * Returns null if routing cannot be determined (map empty, LLM fails, etc.).
83
+ * @param {string} query
84
+ * @param {Object} map - Feature map object
85
+ * @param {Object} llmConfig - LLM configuration
86
+ * @returns {Promise<string[]|null>}
87
+ */
88
+ async function routeQueryToRepos(query, map, llmConfig) {
89
+ if (!map || Object.keys(map).length === 0) return null;
90
+
91
+ const repoList = Object.entries(map).map(([repo, entry]) => {
92
+ const tags = (entry.tags || []).join(', ');
93
+ const desc = entry.description ? ` — ${entry.description}` : '';
94
+ return `${repo}: ${tags}${desc}`;
95
+ }).join('\n');
96
+
97
+ const systemPrompt = `You are a routing assistant. Given a user query and a list of repositories with their feature tags, return a JSON array of repository slugs (e.g. ["org/repo"]) that are most relevant to the query. Return only the repos that are clearly relevant. If no repos match, return an empty array. Return only valid JSON — no explanation.`;
98
+
99
+ const userContent = `Query: ${query}\n\nRepositories:\n${repoList}`;
100
+
101
+ const haikuConfig = {
102
+ ...llmConfig,
103
+ model: process.env.TEAM_CONTEXT_HAIKU_MODEL || 'claude-haiku-4-5-20251001',
104
+ };
105
+
106
+ try {
107
+ const raw = await llm.call(haikuConfig, systemPrompt, userContent);
108
+ const parsed = JSON.parse(raw.trim());
109
+ if (Array.isArray(parsed)) return parsed;
110
+ return null;
111
+ } catch {
112
+ return null;
113
+ }
114
+ }
115
+
56
116
  // ── Helpers ──────────────────────────────────────────────────────────────────
57
117
 
58
118
  function ask(question) {
@@ -504,6 +564,9 @@ async function searchDecisionTrail(query, config) {
504
564
  if (config.journal_dir) {
505
565
  searchOpts.journalDir = config.journal_dir;
506
566
  }
567
+ if (config._repoFilter && config._repoFilter.length > 0) {
568
+ searchOpts.repos = config._repoFilter;
569
+ }
507
570
 
508
571
  // Resolve temporal references to date filters
509
572
  const dateFilters = resolveDateFilters(query);
@@ -984,6 +1047,19 @@ async function handleQuery(query, config, threadHistory) {
984
1047
  return { text: promptResult, results: [], _promptQuery: true };
985
1048
  }
986
1049
 
1050
+ // On-demand feature map reload — user signals they just updated features
1051
+ if (/\b(?:just\s+(?:added|updated|set|ran)\s+(?:features?|wayfind\s+features?))\b/i.test(query) ||
1052
+ /\bwayfind\s+features\s+(?:add|set|describe)\b/i.test(query)) {
1053
+ loadFeatureMap(config);
1054
+ const count = featureMap ? Object.keys(featureMap).length : 0;
1055
+ return {
1056
+ text: count > 0
1057
+ ? `Feature map reloaded. I now know about ${count} repo(s): ${Object.keys(featureMap).join(', ')}`
1058
+ : 'Feature map reloaded, but no repos are configured yet. Run `wayfind features add` in a repo.',
1059
+ results: [],
1060
+ };
1061
+ }
1062
+
987
1063
  const intent = classifyIntent(query);
988
1064
  const llmConfig = config.llm || {};
989
1065
  const contentOpts = {};
@@ -1061,7 +1137,27 @@ async function handleQuery(query, config, threadHistory) {
1061
1137
  (priorDates.until !== priorDates.since ? ` ${priorDates.until}` : '');
1062
1138
  }
1063
1139
  }
1064
- const results = await searchDecisionTrail(searchQuery, config);
1140
+
1141
+ // Feature map routing: if a map exists, ask Haiku which repos are relevant
1142
+ let repoFilter = null;
1143
+ if (featureMap && Object.keys(featureMap).length > 0) {
1144
+ const routedRepos = await routeQueryToRepos(query, featureMap, llmConfig);
1145
+ if (routedRepos !== null) {
1146
+ if (routedRepos.length === 0) {
1147
+ return {
1148
+ text: "I couldn't determine which repositories are relevant to your question. If this seems wrong, run `wayfind features add` in the relevant repo(s) and try again.",
1149
+ results: [],
1150
+ };
1151
+ }
1152
+ repoFilter = routedRepos;
1153
+ }
1154
+ }
1155
+
1156
+ const searchConfig = repoFilter
1157
+ ? { ...config, _repoFilter: repoFilter }
1158
+ : config;
1159
+
1160
+ const results = await searchDecisionTrail(searchQuery, searchConfig);
1065
1161
  const answer = await synthesizeAnswer(query, results, llmConfig, contentOpts, threadHistory);
1066
1162
  const text = formatResponse(answer, results);
1067
1163
  return {
@@ -1141,6 +1237,12 @@ async function start(config) {
1141
1237
  const teamContextDir = config.team_context_dir || process.env.TEAM_CONTEXT_TEAM_CONTEXT_DIR || null;
1142
1238
  const membersDir = teamContextDir ? path.join(teamContextDir, 'members') : null;
1143
1239
 
1240
+ // Load feature map for repo routing
1241
+ loadFeatureMap(config);
1242
+ if (featureMap && Object.keys(featureMap).length > 0) {
1243
+ console.log(`Loaded feature map: ${Object.keys(featureMap).length} repo(s)`);
1244
+ }
1245
+
1144
1246
  // Handle @mentions
1145
1247
  app.event('app_mention', async ({ event, client }) => {
1146
1248
  const channel = event.channel;
package/bin/slack.js CHANGED
@@ -194,39 +194,51 @@ function postToWebhook(webhookUrl, payload) {
194
194
 
195
195
  /**
196
196
  * Deliver a digest to Slack via chat.postMessage (bot token).
197
- * Returns the message ts for reaction tracking.
197
+ * Posts a scannable one-liner header to the channel, then the full digest
198
+ * content as the first thread reply. Reaction tracking keys on the digest
199
+ * reply ts, not the header.
198
200
  *
199
201
  * @param {string} botToken - Slack bot OAuth token (xoxb-...)
200
202
  * @param {string} channel - Slack channel ID or name
201
- * @param {string} content - Formatted mrkdwn content (already converted)
203
+ * @param {string} header - One-liner header (e.g. ":compass: *Wayfind Digest* (Mar 11–18)")
204
+ * @param {string} content - Full digest body as mrkdwn
202
205
  * @param {string} personaName - Persona ID
203
206
  * @returns {Promise<{ ok: true, persona: string, ts: string, channel: string }>}
204
207
  */
205
- async function deliverViaBot(botToken, channel, content, personaName) {
208
+ async function deliverViaBot(botToken, channel, header, content, personaName) {
206
209
  const { WebClient } = require('@slack/web-api');
207
210
  const client = new WebClient(botToken);
208
211
 
209
- const truncated = content.length > 3900 ? content.slice(0, 3900) + '\n\n_...truncated_' : content;
212
+ // Post scannable header to channel
213
+ const headerResult = await client.chat.postMessage({
214
+ channel,
215
+ text: header,
216
+ unfurl_links: false,
217
+ });
210
218
 
211
- const result = await client.chat.postMessage({
219
+ // Post full digest as first thread reply
220
+ const truncated = content.length > 3900 ? content.slice(0, 3900) + '\n\n_...truncated_' : content;
221
+ const digestResult = await client.chat.postMessage({
212
222
  channel,
223
+ thread_ts: headerResult.ts,
213
224
  text: truncated,
214
225
  unfurl_links: false,
215
226
  });
216
227
 
217
- // Post a threaded follow-up asking for feedback
228
+ // Post feedback prompt as second thread reply
218
229
  try {
219
230
  await client.chat.postMessage({
220
231
  channel,
221
- thread_ts: result.ts,
222
- text: '_React to the digest or reply here with feedback — what was useful? What was missing? Your input shapes future digests._',
232
+ thread_ts: headerResult.ts,
233
+ text: '_React to the digest above or reply here with feedback — what was useful? What was missing? Your input shapes future digests._',
223
234
  unfurl_links: false,
224
235
  });
225
236
  } catch (err) {
226
237
  // Non-fatal — digest was delivered, feedback prompt is optional
227
238
  }
228
239
 
229
- return { ok: true, persona: personaName, ts: result.ts, channel: result.channel };
240
+ // Return digest reply ts (not header ts) so reactions land on the content
241
+ return { ok: true, persona: personaName, ts: digestResult.ts, channel: digestResult.channel };
230
242
  }
231
243
 
232
244
  // ── Deliver ─────────────────────────────────────────────────────────────────
@@ -249,7 +261,8 @@ async function deliver(webhookUrl, digestContent, personaName, dateRange, option
249
261
  const label = personaName === 'unified' ? 'Wayfind' : capitalize(personaName);
250
262
  const range = formatDateRange(dateRange);
251
263
  const mrkdwn = markdownToMrkdwn(digestContent);
252
- const formattedText = `${emoji} *${label} Digest* (${range})\n\n${mrkdwn}`;
264
+ const header = `${emoji} *${label} Digest* (${range})`;
265
+ const formattedText = `${header}\n\n${mrkdwn}`;
253
266
 
254
267
  const payload = {
255
268
  text: formattedText,
@@ -270,7 +283,7 @@ async function deliver(webhookUrl, digestContent, personaName, dateRange, option
270
283
  const opts = options || {};
271
284
  if (opts.botToken && opts.channel) {
272
285
  try {
273
- return await deliverViaBot(opts.botToken, opts.channel, formattedText, personaName);
286
+ return await deliverViaBot(opts.botToken, opts.channel, header, mrkdwn, personaName);
274
287
  } catch (err) {
275
288
  console.error(`Bot delivery failed for ${personaName}, falling back to webhook: ${err.message}`);
276
289
  }
@@ -2061,6 +2061,248 @@ function commitAndPushTeamJournals(teamContextPath, copied) {
2061
2061
  }
2062
2062
  }
2063
2063
 
2064
+ // ── Features command ─────────────────────────────────────────────────────────
2065
+
2066
+ /**
2067
+ * Get the repo slug (org/repo) from the git remote origin URL.
2068
+ * Falls back to the directory name if git remote is unavailable.
2069
+ * @returns {string}
2070
+ */
2071
+ function getRepoSlug() {
2072
+ const result = spawnSync('git', ['remote', 'get-url', 'origin'], {
2073
+ cwd: process.cwd(),
2074
+ stdio: 'pipe',
2075
+ });
2076
+ if (result.status === 0) {
2077
+ const url = result.stdout.toString().trim();
2078
+ // Extract org/repo from https or ssh URLs
2079
+ const match = url.match(/[:/]([^/]+\/[^/]+?)(?:\.git)?$/);
2080
+ if (match) return match[1];
2081
+ }
2082
+ return path.basename(process.cwd());
2083
+ }
2084
+
2085
+ /**
2086
+ * Compile features.json in the team-context repo from the local repo's wayfind.json.
2087
+ * Reads existing features.json, merges/updates the entry for this repo, writes back.
2088
+ * @param {string} teamContextPath
2089
+ * @param {string} repoSlug
2090
+ * @param {Object} features - { tags, description }
2091
+ */
2092
+ function updateFeaturesJson(teamContextPath, repoSlug, features) {
2093
+ const featuresFile = path.join(teamContextPath, 'features.json');
2094
+
2095
+ // Pull latest before modifying to reduce conflicts
2096
+ spawnSync('git', ['pull', '--rebase'], { cwd: teamContextPath, stdio: 'pipe', timeout: 30000 });
2097
+
2098
+ const existing = readJSONFile(featuresFile) || {};
2099
+ existing[repoSlug] = {
2100
+ ...(existing[repoSlug] || {}),
2101
+ ...features,
2102
+ updated_at: new Date().toISOString().slice(0, 10),
2103
+ };
2104
+ fs.writeFileSync(featuresFile, JSON.stringify(existing, null, 2) + '\n');
2105
+
2106
+ // Commit and push
2107
+ const gitAdd = spawnSync('git', ['add', 'features.json'], { cwd: teamContextPath, stdio: 'pipe' });
2108
+ if (gitAdd.status !== 0) {
2109
+ console.error('git add features.json failed');
2110
+ return;
2111
+ }
2112
+ const diffIndex = spawnSync('git', ['diff', '--cached', '--quiet'], { cwd: teamContextPath, stdio: 'pipe' });
2113
+ if (diffIndex.status === 0) {
2114
+ console.log(' features.json up to date — nothing to commit.');
2115
+ return;
2116
+ }
2117
+ const msg = `Update features: ${repoSlug}`;
2118
+ const gitCommit = spawnSync('git', ['commit', '-m', msg], { cwd: teamContextPath, stdio: 'pipe' });
2119
+ if (gitCommit.status !== 0) {
2120
+ console.error('git commit features.json failed');
2121
+ return;
2122
+ }
2123
+ const gitPush = spawnSync('git', ['push'], { cwd: teamContextPath, stdio: 'pipe', timeout: 30000 });
2124
+ if (gitPush.status !== 0) {
2125
+ const stderr = (gitPush.stderr || '').toString().trim();
2126
+ if (stderr.includes('fetch first') || stderr.includes('non-fast-forward')) {
2127
+ spawnSync('git', ['pull', '--rebase'], { cwd: teamContextPath, stdio: 'pipe', timeout: 30000 });
2128
+ spawnSync('git', ['push'], { cwd: teamContextPath, stdio: 'pipe', timeout: 30000 });
2129
+ }
2130
+ }
2131
+ console.log(' Synced features.json to team-context repo.');
2132
+ }
2133
+
2134
+ /**
2135
+ * Features command: manage per-repo feature tags for Slack bot routing.
2136
+ * Usage:
2137
+ * wayfind features add <tag1> [tag2] ... — append tags
2138
+ * wayfind features set <tag1> [tag2] ... — replace all tags
2139
+ * wayfind features describe <text> — set description
2140
+ * wayfind features list — list all repos in team features map
2141
+ * wayfind features search <query> — keyword search over features map
2142
+ * wayfind features suggest — suggest tags via LLM
2143
+ */
2144
+ async function runFeatures(args) {
2145
+ const sub = args[0];
2146
+ const rest = args.slice(1);
2147
+
2148
+ if (!sub || sub === 'help') {
2149
+ console.log(`wayfind features — manage feature-to-repo map for Slack bot routing
2150
+
2151
+ Commands:
2152
+ add <tag1> [tag2...] Add tags to this repo (appends to existing)
2153
+ set <tag1> [tag2...] Replace all tags for this repo
2154
+ describe <text> Set a description for this repo
2155
+ list Show all repos in the team features map
2156
+ search <query> Search repos by tag or description keyword
2157
+ suggest Use AI to suggest tags based on repo content`);
2158
+ return;
2159
+ }
2160
+
2161
+ if (sub === 'list') {
2162
+ const teamPath = getTeamContextPath();
2163
+ if (!teamPath) {
2164
+ console.error('No team configured. Run "wayfind context add" first.');
2165
+ process.exit(1);
2166
+ }
2167
+ const featuresFile = path.join(teamPath, 'features.json');
2168
+ const map = readJSONFile(featuresFile);
2169
+ if (!map || Object.keys(map).length === 0) {
2170
+ console.log('No feature map found. Run "wayfind features add" in a repo to get started.');
2171
+ return;
2172
+ }
2173
+ for (const [repo, entry] of Object.entries(map).sort()) {
2174
+ const tags = (entry.tags || []).join(', ') || '—';
2175
+ const desc = entry.description ? ` ${entry.description}` : '';
2176
+ console.log(`${repo}\n tags: ${tags}${desc}\n`);
2177
+ }
2178
+ return;
2179
+ }
2180
+
2181
+ if (sub === 'search') {
2182
+ if (rest.length === 0) {
2183
+ console.error('Usage: wayfind features search <query>');
2184
+ process.exit(1);
2185
+ }
2186
+ const query = rest.join(' ').toLowerCase();
2187
+ const teamPath = getTeamContextPath();
2188
+ if (!teamPath) {
2189
+ console.error('No team configured.');
2190
+ process.exit(1);
2191
+ }
2192
+ const featuresFile = path.join(teamPath, 'features.json');
2193
+ const map = readJSONFile(featuresFile) || {};
2194
+ const matches = Object.entries(map).filter(([repo, entry]) => {
2195
+ const text = [
2196
+ repo,
2197
+ ...(entry.tags || []),
2198
+ entry.description || '',
2199
+ ].join(' ').toLowerCase();
2200
+ return text.includes(query);
2201
+ });
2202
+ if (matches.length === 0) {
2203
+ console.log(`No repos match "${query}".`);
2204
+ return;
2205
+ }
2206
+ for (const [repo, entry] of matches) {
2207
+ const tags = (entry.tags || []).join(', ') || '—';
2208
+ console.log(`${repo} — ${tags}${entry.description ? ' — ' + entry.description : ''}`);
2209
+ }
2210
+ return;
2211
+ }
2212
+
2213
+ if (sub === 'suggest') {
2214
+ const llm = require('./connectors/llm');
2215
+ const repoSlug = getRepoSlug();
2216
+ const repoName = path.basename(process.cwd());
2217
+
2218
+ // Read recent journal entries or README for context
2219
+ let context = `Repository: ${repoSlug}\n`;
2220
+ const readmePath = ['README.md', 'readme.md', 'Readme.md'].find(f => fs.existsSync(path.join(process.cwd(), f)));
2221
+ if (readmePath) {
2222
+ const readme = fs.readFileSync(path.join(process.cwd(), readmePath), 'utf8');
2223
+ context += `README (first 1000 chars):\n${readme.slice(0, 1000)}\n`;
2224
+ }
2225
+ const packagePath = path.join(process.cwd(), 'package.json');
2226
+ if (fs.existsSync(packagePath)) {
2227
+ const pkg = readJSONFile(packagePath);
2228
+ if (pkg && pkg.description) context += `package.json description: ${pkg.description}\n`;
2229
+ if (pkg && pkg.keywords) context += `keywords: ${(pkg.keywords || []).join(', ')}\n`;
2230
+ }
2231
+
2232
+ const llmConfig = {
2233
+ provider: 'anthropic',
2234
+ model: process.env.TEAM_CONTEXT_HAIKU_MODEL || 'claude-haiku-4-5-20251001',
2235
+ api_key_env: 'ANTHROPIC_API_KEY',
2236
+ };
2237
+
2238
+ const systemPrompt = `You suggest concise feature tags for a software repository. Tags describe what features live in this repo, in both technical terms (component names, services) and business/domain language (user-facing features). Return a JSON object with two fields: "tags" (array of 5-15 short lowercase tags) and "description" (one sentence describing the repo's purpose). Return only valid JSON.`;
2239
+
2240
+ console.log(`Analyzing ${repoSlug}...`);
2241
+ try {
2242
+ const raw = await llm.call(llmConfig, systemPrompt, context);
2243
+ const parsed = JSON.parse(raw.trim());
2244
+ console.log(`\nSuggested tags: ${(parsed.tags || []).join(', ')}`);
2245
+ if (parsed.description) console.log(`Description: ${parsed.description}`);
2246
+ console.log(`\nTo apply: wayfind features set ${(parsed.tags || []).join(' ')}`);
2247
+ if (parsed.description) console.log(` wayfind features describe "${parsed.description}"`);
2248
+ } catch (err) {
2249
+ console.error(`Suggestion failed: ${err.message}`);
2250
+ process.exit(1);
2251
+ }
2252
+ return;
2253
+ }
2254
+
2255
+ // add / set / describe — all require writing to .claude/wayfind.json and syncing
2256
+ if (sub === 'add' || sub === 'set' || sub === 'describe') {
2257
+ if (rest.length === 0) {
2258
+ console.error(`Usage: wayfind features ${sub} <value...>`);
2259
+ process.exit(1);
2260
+ }
2261
+
2262
+ const claudeDir = path.join(process.cwd(), '.claude');
2263
+ fs.mkdirSync(claudeDir, { recursive: true });
2264
+ const bindingFile = path.join(claudeDir, 'wayfind.json');
2265
+ const binding = readJSONFile(bindingFile) || {};
2266
+
2267
+ if (sub === 'describe') {
2268
+ binding.features = binding.features || {};
2269
+ binding.features.description = rest.join(' ');
2270
+ } else if (sub === 'set') {
2271
+ binding.features = binding.features || {};
2272
+ binding.features.tags = rest.map(t => t.toLowerCase().replace(/^#/, ''));
2273
+ } else if (sub === 'add') {
2274
+ binding.features = binding.features || {};
2275
+ const newTags = rest.map(t => t.toLowerCase().replace(/^#/, ''));
2276
+ const existing = binding.features.tags || [];
2277
+ binding.features.tags = [...new Set([...existing, ...newTags])];
2278
+ }
2279
+
2280
+ fs.writeFileSync(bindingFile, JSON.stringify(binding, null, 2) + '\n');
2281
+
2282
+ const repoSlug = getRepoSlug();
2283
+ const tags = binding.features.tags || [];
2284
+ const description = binding.features.description || '';
2285
+
2286
+ if (sub === 'describe') {
2287
+ console.log(`Set description for ${repoSlug}: "${description}"`);
2288
+ } else {
2289
+ console.log(`Tags for ${repoSlug}: ${tags.join(', ')}`);
2290
+ }
2291
+
2292
+ // Sync to team-context repo if configured
2293
+ const teamPath = getTeamContextPath();
2294
+ if (teamPath) {
2295
+ updateFeaturesJson(teamPath, repoSlug, { tags, description });
2296
+ } else {
2297
+ console.log(' (No team configured — skipping team-context sync)');
2298
+ }
2299
+ return;
2300
+ }
2301
+
2302
+ console.error(`Unknown subcommand: ${sub}. Run "wayfind features help" for usage.`);
2303
+ process.exit(1);
2304
+ }
2305
+
2064
2306
  // ── Standup command ─────────────────────────────────────────────────────────
2065
2307
 
2066
2308
  /**
@@ -4370,6 +4612,10 @@ const COMMANDS = {
4370
4612
  desc: 'Show cross-project status (or rebuild Active Projects table)',
4371
4613
  run: (args) => runStatus(args),
4372
4614
  },
4615
+ features: {
4616
+ desc: 'Manage feature-to-repo map for Slack bot routing (add, set, describe, list, search, suggest)',
4617
+ run: (args) => runFeatures(args),
4618
+ },
4373
4619
  standup: {
4374
4620
  desc: 'Show a daily standup summary (last session, plan, blockers)',
4375
4621
  run: (args) => runStandup(args),
@@ -4448,7 +4694,7 @@ const COMMANDS = {
4448
4694
  // Files and directories to sync
4449
4695
  const syncItems = [
4450
4696
  'bin/', 'templates/', 'specializations/', 'plugin/', 'tests/', 'simulation/',
4451
- 'backup/', '.github/', 'Dockerfile', 'package.json', 'marketplace.json', 'setup.sh',
4697
+ 'backup/', '.github/', '.claude-plugin/', 'Dockerfile', 'package.json', 'setup.sh',
4452
4698
  'install.sh', 'uninstall.sh', 'doctor.sh', 'journal-summary.sh',
4453
4699
  'BOOTSTRAP_PROMPT.md', '.gitattributes', '.gitignore', 'VERSIONS.md',
4454
4700
  ];
@@ -4720,6 +4966,60 @@ function spawn(cmd, args) {
4720
4966
  process.exit(result.status == null ? 1 : result.status);
4721
4967
  }
4722
4968
 
4969
+ // --- Update notifier --------------------------------------------------------
4970
+ // Checks npm registry in the background, caches the result for 24h,
4971
+ // and prints a one-liner on next run if a newer version is available.
4972
+ // Users can silence with NO_UPDATE_NOTIFIER=1.
4973
+
4974
+ const UPDATE_CHECK_FILE = path.join(WAYFIND_DIR, '.update-check.json');
4975
+ const UPDATE_CHECK_INTERVAL = 24 * 60 * 60 * 1000; // 24 hours
4976
+
4977
+ function checkForUpdateBackground() {
4978
+ if (process.env.NO_UPDATE_NOTIFIER) return;
4979
+ try {
4980
+ if (fs.existsSync(UPDATE_CHECK_FILE)) {
4981
+ const cached = JSON.parse(fs.readFileSync(UPDATE_CHECK_FILE, 'utf8'));
4982
+ if (Date.now() - cached.checkedAt < UPDATE_CHECK_INTERVAL) return;
4983
+ }
4984
+ } catch { /* check anyway */ }
4985
+ // Fire and forget — don't block the CLI
4986
+ const child = spawnChild('npm', ['view', 'wayfind', 'version'], {
4987
+ stdio: ['ignore', 'pipe', 'ignore'],
4988
+ detached: true,
4989
+ env: { ...process.env, NO_UPDATE_NOTIFIER: '1' },
4990
+ });
4991
+ let stdout = '';
4992
+ child.stdout.on('data', (d) => { stdout += d; });
4993
+ child.on('close', () => {
4994
+ const latest = stdout.trim();
4995
+ if (!latest || !/^\d+\.\d+\.\d+/.test(latest)) return;
4996
+ try {
4997
+ fs.mkdirSync(WAYFIND_DIR, { recursive: true });
4998
+ fs.writeFileSync(UPDATE_CHECK_FILE, JSON.stringify({ latest, checkedAt: Date.now() }));
4999
+ } catch { /* best effort */ }
5000
+ });
5001
+ child.unref();
5002
+ }
5003
+
5004
+ function showUpdateNotice() {
5005
+ if (process.env.NO_UPDATE_NOTIFIER) return;
5006
+ try {
5007
+ if (!fs.existsSync(UPDATE_CHECK_FILE)) return;
5008
+ const { latest } = JSON.parse(fs.readFileSync(UPDATE_CHECK_FILE, 'utf8'));
5009
+ const pkg = require(path.join(ROOT, 'package.json'));
5010
+ if (!latest || latest === pkg.version) return;
5011
+ // Simple semver comparison: split, compare numerically
5012
+ const cur = pkg.version.split('.').map(Number);
5013
+ const lat = latest.split('.').map(Number);
5014
+ const isNewer = lat[0] > cur[0] || (lat[0] === cur[0] && lat[1] > cur[1]) ||
5015
+ (lat[0] === cur[0] && lat[1] === cur[1] && lat[2] > cur[2]);
5016
+ if (isNewer) {
5017
+ console.error(`\n\x1b[33m Update available: v${pkg.version} → v${latest}\x1b[0m`);
5018
+ console.error(`\x1b[33m Run \x1b[1mnpm update -g wayfind\x1b[22m to update\x1b[0m\n`);
5019
+ }
5020
+ } catch { /* best effort */ }
5021
+ }
5022
+
4723
5023
  // --- Main ---
4724
5024
 
4725
5025
  const args = process.argv.slice(2);
@@ -4727,9 +5027,11 @@ const command = args[0] || 'help';
4727
5027
  const commandArgs = args.slice(1);
4728
5028
 
4729
5029
  async function main() {
5030
+ checkForUpdateBackground();
4730
5031
  telemetry.capture('command_run', { command }, CLI_USER);
4731
5032
  if (COMMANDS[command]) {
4732
5033
  await COMMANDS[command].run(commandArgs);
5034
+ showUpdateNotice();
4733
5035
  await telemetry.flush();
4734
5036
  } else {
4735
5037
  console.error(`Unknown command: ${command}`);
package/bin/telemetry.js CHANGED
@@ -12,7 +12,7 @@ let enabled = false;
12
12
  function init() {
13
13
  if (client !== null) return; // already initialized (or disabled)
14
14
 
15
- enabled = (process.env.TEAM_CONTEXT_TELEMETRY || '').toLowerCase() === 'true';
15
+ enabled = (process.env.TEAM_CONTEXT_TELEMETRY || '').toLowerCase() !== 'false';
16
16
  if (!enabled) {
17
17
  client = false; // marker: checked but disabled
18
18
  return;
package/doctor.sh CHANGED
@@ -17,9 +17,45 @@ info() { echo " $1"; }
17
17
 
18
18
  ISSUES=0
19
19
 
20
+ is_plugin_installed() {
21
+ # Check if wayfind is installed as a Claude Code plugin
22
+ # Method 1: Check enabledPlugins in settings.json (authoritative — what Claude Code reads)
23
+ local SETTINGS="$HOME/.claude/settings.json"
24
+ if [ -f "$SETTINGS" ] && grep -q '"wayfind@' "$SETTINGS" 2>/dev/null; then
25
+ return 0
26
+ fi
27
+
28
+ # Method 2: Check plugin files on disk
29
+ local PLUGINS_DIR="$HOME/.claude/plugins"
30
+ if [ -d "$PLUGINS_DIR" ]; then
31
+ if find "$PLUGINS_DIR" -path '*/wayfind/plugin/.claude-plugin/plugin.json' -print -quit 2>/dev/null | grep -q .; then
32
+ return 0
33
+ fi
34
+ if find "$PLUGINS_DIR" -name 'plugin.json' -exec grep -l '"name": "wayfind"' {} + 2>/dev/null | grep -q .; then
35
+ return 0
36
+ fi
37
+ fi
38
+ return 1
39
+ }
40
+
20
41
  check_hook_registered() {
21
42
  echo ""
22
43
  echo "Hook registration"
44
+
45
+ # If installed as a plugin, hooks are provided by the plugin — skip legacy checks
46
+ if is_plugin_installed; then
47
+ ok "Installed as Claude Code plugin (hooks provided by plugin)"
48
+ # Warn if old specialization hook files are still present — they cause duplicate execution
49
+ for old_hook in check-global-state.sh session-end.sh; do
50
+ if [ -f "$HOME/.claude/hooks/$old_hook" ]; then
51
+ warn "Orphaned legacy hook: ~/.claude/hooks/$old_hook"
52
+ info "Plugin now handles hooks. Remove with: rm ~/.claude/hooks/$old_hook"
53
+ ISSUES=$((ISSUES + 1))
54
+ fi
55
+ done
56
+ return
57
+ fi
58
+
23
59
  local SETTINGS="$HOME/.claude/settings.json"
24
60
  if [ ! -f "$SETTINGS" ]; then
25
61
  err "settings.json not found — hook is not registered"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wayfind",
3
- "version": "2.0.19",
3
+ "version": "2.0.20",
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
  {
2
2
  "name": "wayfind",
3
- "version": "2.0.14",
3
+ "version": "2.0.20",
4
4
  "description": "Team decision trail for AI-assisted development. Session memory, decision journals, and team digests.",
5
5
  "author": {
6
6
  "name": "Wayfind",
@@ -10,6 +10,5 @@
10
10
  "repository": "https://github.com/usewayfind/wayfind",
11
11
  "license": "Apache-2.0",
12
12
  "keywords": ["team", "context", "memory", "decisions", "digest", "journal", "session"],
13
- "skills": "./skills/",
14
- "hooks": "./hooks/hooks.json"
13
+ "skills": "./skills/"
15
14
  }
package/plugin/README.md CHANGED
@@ -22,7 +22,6 @@ Or from the official Anthropic marketplace (once approved):
22
22
  - **Session memory protocol** — loads team and personal state files at session start, saves at session end
23
23
  - **Slash commands** — `/wayfind:init-memory`, `/wayfind:init-team`, `/wayfind:doctor`, `/wayfind:journal`, `/wayfind:standup`, `/wayfind:review-prs`
24
24
  - **Hooks** — auto-rebuild project index on session start, auto-extract decisions on session end
25
- - **Drift detection** — flags when work diverges from the stated session goal
26
25
 
27
26
  ### Tier 2: Plugin + npm CLI (`npm i -g wayfind`)
28
27
 
@@ -35,6 +34,24 @@ Everything in Tier 1, plus:
35
34
  - **Slack + Notion delivery** — digests post to Slack channels and Notion pages via GitHub Actions
36
35
  - **Bot mode** — `wayfind bot` runs a Slack bot for on-demand queries
37
36
 
37
+ ## Removing legacy session prompts
38
+
39
+ If you installed Wayfind before the plugin existed, you likely have a "Session State Protocol"
40
+ section in your CLAUDE.md files that tells the AI to ask "What's the goal for this session?"
41
+ and flag drift. The plugin now handles session memory without these prompts.
42
+
43
+ To clean up:
44
+
45
+ 1. **Run `/wayfind:init-memory`** in each repo — Step 4 removes the legacy protocol from CLAUDE.md automatically.
46
+
47
+ 2. **Or manually:** delete the `## Session State Protocol` section from these files:
48
+ - `~/CLAUDE.md`
49
+ - `~/.claude/CLAUDE.md`
50
+ - Each repo's `CLAUDE.md`
51
+
52
+ The plugin's hooks and session-protocol skill handle state file loading without
53
+ requiring any CLAUDE.md instructions or interactive prompts.
54
+
38
55
  ## Documentation
39
56
 
40
57
  Full docs: https://github.com/usewayfind/wayfind
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: init-memory
3
- description: Initialize Wayfind for the current repo. Creates .claude/team-state.md (tracked in git) and .claude/personal-state.md (gitignored), ensures correct .gitignore entries, appends session protocol to CLAUDE.md, and registers the repo in the global index. Safe to run multiple times (idempotent).
3
+ description: Initialize Wayfind for the current repo. Creates .claude/team-state.md (tracked in git) and .claude/personal-state.md (gitignored), ensures correct .gitignore entries, cleans up legacy session protocol from CLAUDE.md, and registers the repo in the global index. Safe to run multiple times (idempotent).
4
4
  user-invocable: true
5
5
  ---
6
6
 
@@ -67,21 +67,6 @@ Last updated: [today's date]
67
67
  ## Current Sprint Focus
68
68
  <!-- Team-level "what are we working on right now" -->
69
69
 
70
- ## Elicitation Prompts
71
-
72
- <!-- These prompts guide the AI to capture richer context at decision moments.
73
- The answers aren't for you — they're for your teammates who read the digest. -->
74
-
75
- When a technical or product decision is made without stated reasoning, ask one of:
76
- - "What alternatives did you consider?"
77
- - "What constraint or requirement drove this choice?"
78
- - "What would need to change for you to reverse this decision?"
79
- - "Who else on the team does this affect, and how?"
80
- - "What's the risk if this assumption is wrong?"
81
-
82
- Do not ask if the decision already includes reasoning, tradeoffs, or constraints.
83
- Do not ask more than once per decision. Do not ask during routine implementation.
84
-
85
70
  ## Shared Gotchas
86
71
  <!-- Hard-won lessons. What surprised us. What NOT to do. -->
87
72
  ```
@@ -123,30 +108,17 @@ Check `.gitignore`. Ensure these lines are present:
123
108
 
124
109
  If any of those lines are missing, append them. If `.claude/` (as a directory) is already in `.gitignore`, remove it and replace with the four file-level entries above.
125
110
 
126
- ## Step 4: Append Session Protocol to CLAUDE.md
111
+ ## Step 4: Clean up legacy session protocol from CLAUDE.md
127
112
 
128
- Read the repo's `CLAUDE.md`. If it does NOT already contain "Session State Protocol", append this:
113
+ Read the repo's `CLAUDE.md` (if it exists). If it contains a "## Session State Protocol" section, **remove the entire section** — the plugin's session-protocol skill and hooks handle this now.
129
114
 
130
- ```markdown
131
-
132
- ## Session State Protocol
115
+ Also remove the legacy "## Session State Protocol (AI Memory Kit)" variant if present.
133
116
 
134
- **At session start (REQUIRED):**
135
- 1. Read `~/.claude/global-state.md` — preferences, active projects, memory file manifest
136
- 2. Read `.claude/team-state.md` in this repo — shared team context: architecture decisions, conventions, sprint focus, gotchas
137
- 3. Read `.claude/personal-state.md` in this repo — your personal context: current focus, working notes, opinions
138
- 4. Check the Memory Files table in global-state.md — load any `~/.claude/memory/` files relevant to this session's topic
117
+ If the CLAUDE.md has no other content after removal, leave it with just the repo name heading.
139
118
 
140
- **At session end (when user says stop/done/pause/tomorrow):**
141
- 1. Update `.claude/team-state.md` with shared context: architecture decisions, conventions, gotchas the team should know
142
- 2. Update `.claude/personal-state.md` with personal context: your next steps, working notes, opinions
143
- 3. Do NOT update `~/.claude/global-state.md` — its Active Projects table is rebuilt automatically by `wayfind status`.
144
- 4. If significant new cross-repo context was created (patterns, strategies, decisions), create or update a file in `~/.claude/memory/` and add it to the Memory Files manifest in global-state.md
145
-
146
- **Do NOT use external memory databases or CLI tools for state storage.** Use plain markdown files only.
147
- ```
119
+ Report: "Removed legacy Session State Protocol from CLAUDE.md — the plugin handles this now."
148
120
 
149
- If `CLAUDE.md` doesn't exist, create a minimal one with the repo name as a heading and the block above.
121
+ If no session protocol section was found, skip silently.
150
122
 
151
123
  ## Step 5: Register State Files in Global Index
152
124
 
@@ -166,7 +138,7 @@ Tell the user:
166
138
  - `.claude/team-state.md` — created or already existed (committed to git, shared with team)
167
139
  - `.claude/personal-state.md` — created or already existed (gitignored, personal only)
168
140
  - `.gitignore` — updated or already correct
169
- - `CLAUDE.md` — protocol appended or already present
141
+ - `CLAUDE.md` — legacy session protocol removed (or was already clean)
170
142
  - `global-state.md` — repo registered or already listed
171
143
 
172
144
  **If they haven't set up team context yet**, mention:
@@ -1,15 +1,14 @@
1
1
  ---
2
2
  name: session-protocol
3
- description: Wayfind session memory protocol — behavioral instructions for AI sessions. Elicitation prompts, drift detection, and state file conventions. The plugin's hooks handle automation (loading/saving); this skill defines how the AI should behave.
3
+ description: Wayfind session memory protocol — state file locations and conventions. The plugin's hooks handle automation (loading/saving); this skill tells the AI where state files live.
4
4
  disable-model-invocation: false
5
5
  user-invocable: false
6
6
  ---
7
7
 
8
- ## Wayfind — Session Behavior Protocol
8
+ ## Wayfind — Session Memory Protocol
9
9
 
10
- This skill defines behavioral instructions for AI sessions. Automation (state file loading,
11
- decision extraction, journal sync) is handled by the plugin's hooks this skill covers
12
- the human-judgment parts that hooks can't automate.
10
+ Automation (project index rebuild, decision extraction, journal sync) is handled by
11
+ the plugin's hooks. This skill tells you where state files live so you can load context.
13
12
 
14
13
  ### State File Locations
15
14
 
@@ -29,24 +28,7 @@ the human-judgment parts that hooks can't automate.
29
28
  1. Read `~/.claude/global-state.md`
30
29
  2. Read `.claude/team-state.md` and `.claude/personal-state.md` in the current repo (fall back to `.claude/state.md` for legacy repos)
31
30
  3. Check the Memory Files table — load any `~/.claude/memory/` files whose keywords match this session's topic
32
- 4. Summarize current state, then ask: **"What's the goal for this session? What does success look like?"**
33
-
34
- ### Mid-Session — Drift Detection
35
-
36
- If work drifts from the stated goal, flag it:
37
- > *"Quick check — we set out to [goal]. This feels like [tangent]. Stay the course or pivot?"*
38
-
39
- ### Elicitation Prompts
40
-
41
- When a technical or product decision is made without stated reasoning, ask ONE of:
42
- - "What alternatives did you consider?"
43
- - "What constraint or requirement drove this choice?"
44
- - "What would need to change for you to reverse this decision?"
45
- - "Who else on the team does this affect, and how?"
46
- - "What's the risk if this assumption is wrong?"
47
-
48
- Do not ask if the decision already includes reasoning, tradeoffs, or constraints.
49
- Do not ask more than once per decision. Do not ask during routine implementation.
31
+ 4. Briefly summarize current state for the user
50
32
 
51
33
  ### Rules
52
34