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.
- package/BOOTSTRAP_PROMPT.md +22 -15
- package/bin/content-store.js +5 -0
- package/bin/slack-bot.js +103 -1
- package/bin/slack.js +24 -11
- package/bin/team-context.js +303 -1
- package/bin/telemetry.js +1 -1
- package/doctor.sh +36 -0
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +2 -3
- package/plugin/README.md +18 -1
- package/plugin/skills/init-memory/SKILL.md +8 -36
- package/plugin/skills/session-protocol/SKILL.md +5 -23
package/BOOTSTRAP_PROMPT.md
CHANGED
|
@@ -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
|
-
|
|
10
|
+
Step 1 — Install the CLI:
|
|
11
11
|
npm install -g wayfind
|
|
12
12
|
wayfind init
|
|
13
13
|
|
|
14
|
-
|
|
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
|
|
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
|
|
33
|
-
-
|
|
34
|
-
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
-
|
|
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@
|
|
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=
|
|
126
|
+
WAYFIND_VERSION=v2.0.15 bash <(curl -fsSL https://raw.githubusercontent.com/usewayfind/wayfind/main/install.sh)
|
|
120
127
|
```
|
package/bin/content-store.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
*
|
|
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}
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
228
|
+
// Post feedback prompt as second thread reply
|
|
218
229
|
try {
|
|
219
230
|
await client.chat.postMessage({
|
|
220
231
|
channel,
|
|
221
|
-
thread_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
|
-
|
|
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
|
|
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,
|
|
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
|
}
|
package/bin/team-context.js
CHANGED
|
@@ -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/', '
|
|
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()
|
|
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.
|
|
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,
|
|
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:
|
|
111
|
+
## Step 4: Clean up legacy session protocol from CLAUDE.md
|
|
127
112
|
|
|
128
|
-
Read the repo's `CLAUDE.md
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
## Session State Protocol
|
|
115
|
+
Also remove the legacy "## Session State Protocol (AI Memory Kit)" variant if present.
|
|
133
116
|
|
|
134
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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 —
|
|
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
|
|
8
|
+
## Wayfind — Session Memory Protocol
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
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.
|
|
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
|
|