wayfind 2.0.13 → 2.0.15
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/.claude-plugin/marketplace.json +14 -0
- package/bin/content-store.js +1 -1
- package/bin/rebuild-status.js +12 -3
- package/bin/team-context.js +454 -2
- package/doctor.sh +42 -0
- package/package.json +3 -1
- package/plugin/.claude-plugin/plugin.json +15 -0
- package/plugin/README.md +40 -0
- package/plugin/hooks/hooks.json +28 -0
- package/plugin/scripts/session-end.sh +55 -0
- package/plugin/scripts/session-start.sh +32 -0
- package/plugin/skills/doctor/SKILL.md +36 -0
- package/plugin/skills/init-memory/SKILL.md +177 -0
- package/plugin/skills/init-team/SKILL.md +198 -0
- package/plugin/skills/journal/SKILL.md +64 -0
- package/plugin/skills/review-prs/SKILL.md +121 -0
- package/plugin/skills/session-protocol/SKILL.md +56 -0
- package/plugin/skills/standup/SKILL.md +28 -0
- package/specializations/claude-code/commands/standup.md +32 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "usewayfind",
|
|
3
|
+
"owner": {
|
|
4
|
+
"name": "Wayfind",
|
|
5
|
+
"url": "https://github.com/usewayfind"
|
|
6
|
+
},
|
|
7
|
+
"plugins": [
|
|
8
|
+
{
|
|
9
|
+
"name": "wayfind",
|
|
10
|
+
"source": "./plugin",
|
|
11
|
+
"description": "Team decision trail — session memory, decision journals, and team digests for AI-assisted development."
|
|
12
|
+
}
|
|
13
|
+
]
|
|
14
|
+
}
|
package/bin/content-store.js
CHANGED
|
@@ -41,7 +41,7 @@ const DATE_FILE_RE = /^(\d{4}-\d{2}-\d{2})(?:-([a-z0-9._-]+))?\.md$/;
|
|
|
41
41
|
|
|
42
42
|
// Team repo allowlist — when set, only entries from matching repos are indexed/queried.
|
|
43
43
|
// Replaces the old TEAM_CONTEXT_EXCLUDE_REPOS blocklist approach.
|
|
44
|
-
// Format: comma-separated, supports org/* wildcards (e.g., "
|
|
44
|
+
// Format: comma-separated, supports org/* wildcards (e.g., "AcmeCorp/*,Frontend/*")
|
|
45
45
|
const INCLUDE_REPOS = (process.env.TEAM_CONTEXT_INCLUDE_REPOS || '')
|
|
46
46
|
.split(',').map(r => r.trim().toLowerCase()).filter(Boolean);
|
|
47
47
|
|
package/bin/rebuild-status.js
CHANGED
|
@@ -24,6 +24,13 @@ const NEXT_HEADERS = [
|
|
|
24
24
|
"What's Left",
|
|
25
25
|
];
|
|
26
26
|
|
|
27
|
+
const BLOCKER_HEADERS = [
|
|
28
|
+
'Blockers',
|
|
29
|
+
'Blocking',
|
|
30
|
+
"What I'm Watching",
|
|
31
|
+
'Open Questions',
|
|
32
|
+
];
|
|
33
|
+
|
|
27
34
|
// ── Scanning ─────────────────────────────────────────────────────────────────
|
|
28
35
|
|
|
29
36
|
/**
|
|
@@ -64,17 +71,18 @@ function scanDir(dir, results, seen, depth) {
|
|
|
64
71
|
const personalState = path.join(claudeDir, 'personal-state.md');
|
|
65
72
|
const plainState = path.join(claudeDir, 'state.md');
|
|
66
73
|
|
|
67
|
-
// Prefer team-state.md, fall back to state.md
|
|
74
|
+
// Prefer team-state.md, fall back to state.md, then personal-state.md
|
|
68
75
|
let stateFile = null;
|
|
69
76
|
if (fs.existsSync(teamState)) stateFile = teamState;
|
|
70
77
|
else if (fs.existsSync(plainState)) stateFile = plainState;
|
|
78
|
+
else if (fs.existsSync(personalState)) stateFile = personalState;
|
|
71
79
|
|
|
72
80
|
if (stateFile && !seen.has(dir)) {
|
|
73
81
|
seen.add(dir);
|
|
74
82
|
results.push({ repoDir: dir, stateFile });
|
|
75
83
|
|
|
76
|
-
// Also note personal-state.md if it exists
|
|
77
|
-
if (fs.existsSync(personalState)) {
|
|
84
|
+
// Also note personal-state.md if it exists and wasn't chosen as primary
|
|
85
|
+
if (stateFile !== personalState && fs.existsSync(personalState)) {
|
|
78
86
|
results[results.length - 1].personalStateFile = personalState;
|
|
79
87
|
}
|
|
80
88
|
}
|
|
@@ -294,4 +302,5 @@ module.exports = {
|
|
|
294
302
|
DEFAULT_ROOTS,
|
|
295
303
|
STATUS_HEADERS,
|
|
296
304
|
NEXT_HEADERS,
|
|
305
|
+
BLOCKER_HEADERS,
|
|
297
306
|
};
|
package/bin/team-context.js
CHANGED
|
@@ -2061,6 +2061,439 @@ function commitAndPushTeamJournals(teamContextPath, copied) {
|
|
|
2061
2061
|
}
|
|
2062
2062
|
}
|
|
2063
2063
|
|
|
2064
|
+
// ── Standup command ─────────────────────────────────────────────────────────
|
|
2065
|
+
|
|
2066
|
+
/**
|
|
2067
|
+
* Find and parse the most recent journal entry.
|
|
2068
|
+
* @param {string} journalDir - Journal directory path
|
|
2069
|
+
* @param {string} [repoFilter] - If set, only match entries whose repo header
|
|
2070
|
+
* matches this name (case-insensitive). Skips non-matching entries.
|
|
2071
|
+
* @returns {{ date: string, repo: string, title: string, what: string } | null}
|
|
2072
|
+
*/
|
|
2073
|
+
function getLastJournalEntry(journalDir, repoFilter) {
|
|
2074
|
+
if (!journalDir || !fs.existsSync(journalDir)) return null;
|
|
2075
|
+
|
|
2076
|
+
const files = fs.readdirSync(journalDir)
|
|
2077
|
+
.filter(f => /^\d{4}-\d{2}-\d{2}/.test(f) && f.endsWith('.md'))
|
|
2078
|
+
.sort()
|
|
2079
|
+
.reverse();
|
|
2080
|
+
|
|
2081
|
+
const filterLower = repoFilter ? repoFilter.toLowerCase() : null;
|
|
2082
|
+
|
|
2083
|
+
for (const file of files) {
|
|
2084
|
+
let content;
|
|
2085
|
+
try {
|
|
2086
|
+
content = fs.readFileSync(path.join(journalDir, file), 'utf8');
|
|
2087
|
+
} catch {
|
|
2088
|
+
continue;
|
|
2089
|
+
}
|
|
2090
|
+
|
|
2091
|
+
const lines = content.split('\n');
|
|
2092
|
+
|
|
2093
|
+
// Find all entry start indices (## Repo — Title or ## Repo)
|
|
2094
|
+
const entryStarts = [];
|
|
2095
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2096
|
+
if (/^## /.test(lines[i])) entryStarts.push(i);
|
|
2097
|
+
}
|
|
2098
|
+
if (entryStarts.length === 0) continue;
|
|
2099
|
+
|
|
2100
|
+
// Walk entries in reverse (most recent first within each file)
|
|
2101
|
+
for (let e = entryStarts.length - 1; e >= 0; e--) {
|
|
2102
|
+
const start = entryStarts[e];
|
|
2103
|
+
const end = e + 1 < entryStarts.length ? entryStarts[e + 1] : lines.length;
|
|
2104
|
+
const entryLines = lines.slice(start, end);
|
|
2105
|
+
|
|
2106
|
+
const headerMatch = entryLines[0].match(/^## (.+?)(?:\s*[—–-]\s*(.+))?$/);
|
|
2107
|
+
const repo = headerMatch ? headerMatch[1].trim() : '';
|
|
2108
|
+
const title = headerMatch && headerMatch[2] ? headerMatch[2].trim() : '';
|
|
2109
|
+
|
|
2110
|
+
// Apply repo filter if set
|
|
2111
|
+
if (filterLower && repo.toLowerCase() !== filterLower) continue;
|
|
2112
|
+
|
|
2113
|
+
// Extract **What:** field
|
|
2114
|
+
let what = '';
|
|
2115
|
+
for (let i = 1; i < entryLines.length; i++) {
|
|
2116
|
+
const match = entryLines[i].match(/^\*\*What:\*\*\s*(.*)$/);
|
|
2117
|
+
if (match) {
|
|
2118
|
+
what = match[1].trim();
|
|
2119
|
+
if (!what) {
|
|
2120
|
+
for (let j = i + 1; j < entryLines.length; j++) {
|
|
2121
|
+
if (!entryLines[j].trim() || /^\*\*/.test(entryLines[j])) break;
|
|
2122
|
+
what += (what ? ' ' : '') + entryLines[j].trim();
|
|
2123
|
+
}
|
|
2124
|
+
}
|
|
2125
|
+
break;
|
|
2126
|
+
}
|
|
2127
|
+
}
|
|
2128
|
+
|
|
2129
|
+
const dateMatch = file.match(/^(\d{4}-\d{2}-\d{2})/);
|
|
2130
|
+
const date = dateMatch ? dateMatch[1] : '';
|
|
2131
|
+
|
|
2132
|
+
if (repo || what) return { date, repo, title, what };
|
|
2133
|
+
}
|
|
2134
|
+
}
|
|
2135
|
+
|
|
2136
|
+
return null;
|
|
2137
|
+
}
|
|
2138
|
+
|
|
2139
|
+
/**
|
|
2140
|
+
* Emit a daily standup summary.
|
|
2141
|
+
* Default: scoped to the current repo. --all scans every repo with state files.
|
|
2142
|
+
* Shows: what was done last (from journal), what's planned, and blockers.
|
|
2143
|
+
* @param {string[]} args - CLI arguments (supports --all)
|
|
2144
|
+
*/
|
|
2145
|
+
function runStandup(args) {
|
|
2146
|
+
const explicitAll = args.includes('--all');
|
|
2147
|
+
const journalDir = contentStore.DEFAULT_JOURNAL_DIR;
|
|
2148
|
+
const cwd = process.cwd();
|
|
2149
|
+
|
|
2150
|
+
// Detect if we're inside a repo (.git + .claude/ state files)
|
|
2151
|
+
// Home dir may have ~/.claude/state.md but isn't a repo
|
|
2152
|
+
const claudeDir = path.join(cwd, '.claude');
|
|
2153
|
+
const hasGit = fs.existsSync(path.join(cwd, '.git'));
|
|
2154
|
+
const inRepo = hasGit && fs.existsSync(claudeDir) && (
|
|
2155
|
+
fs.existsSync(path.join(claudeDir, 'team-state.md')) ||
|
|
2156
|
+
fs.existsSync(path.join(claudeDir, 'state.md')) ||
|
|
2157
|
+
fs.existsSync(path.join(claudeDir, 'personal-state.md'))
|
|
2158
|
+
);
|
|
2159
|
+
|
|
2160
|
+
// If not in a repo, behave like --all
|
|
2161
|
+
const showAll = explicitAll || !inRepo;
|
|
2162
|
+
|
|
2163
|
+
const PLAN_HEADERS = [
|
|
2164
|
+
...rebuildStatus.NEXT_HEADERS,
|
|
2165
|
+
'My Current Focus',
|
|
2166
|
+
'Current Sprint Focus',
|
|
2167
|
+
'Current Focus',
|
|
2168
|
+
];
|
|
2169
|
+
|
|
2170
|
+
// Determine which state files to read
|
|
2171
|
+
let stateEntries;
|
|
2172
|
+
if (showAll) {
|
|
2173
|
+
const envRoots = process.env.AI_MEMORY_SCAN_ROOTS;
|
|
2174
|
+
const roots = envRoots
|
|
2175
|
+
? envRoots.split(':').filter(Boolean)
|
|
2176
|
+
: rebuildStatus.DEFAULT_ROOTS;
|
|
2177
|
+
stateEntries = rebuildStatus.scanStateFiles(roots);
|
|
2178
|
+
} else {
|
|
2179
|
+
// Current repo only — look for .claude/ state files in cwd
|
|
2180
|
+
stateEntries = [];
|
|
2181
|
+
const teamState = path.join(claudeDir, 'team-state.md');
|
|
2182
|
+
const personalState = path.join(claudeDir, 'personal-state.md');
|
|
2183
|
+
const plainState = path.join(claudeDir, 'state.md');
|
|
2184
|
+
|
|
2185
|
+
let stateFile = null;
|
|
2186
|
+
if (fs.existsSync(teamState)) stateFile = teamState;
|
|
2187
|
+
else if (fs.existsSync(plainState)) stateFile = plainState;
|
|
2188
|
+
else if (fs.existsSync(personalState)) stateFile = personalState;
|
|
2189
|
+
|
|
2190
|
+
if (stateFile) {
|
|
2191
|
+
const entry = { repoDir: cwd, stateFile };
|
|
2192
|
+
if (stateFile !== personalState && fs.existsSync(personalState)) {
|
|
2193
|
+
entry.personalStateFile = personalState;
|
|
2194
|
+
}
|
|
2195
|
+
stateEntries.push(entry);
|
|
2196
|
+
}
|
|
2197
|
+
}
|
|
2198
|
+
|
|
2199
|
+
// Gather next steps and blockers from state files
|
|
2200
|
+
const nextItems = [];
|
|
2201
|
+
const blockerItems = [];
|
|
2202
|
+
const seenNext = new Set();
|
|
2203
|
+
const seenBlockers = new Set();
|
|
2204
|
+
|
|
2205
|
+
for (const { stateFile, personalStateFile } of stateEntries) {
|
|
2206
|
+
const filesToCheck = [stateFile, personalStateFile].filter(Boolean);
|
|
2207
|
+
for (const filePath of filesToCheck) {
|
|
2208
|
+
let fileContent;
|
|
2209
|
+
try {
|
|
2210
|
+
fileContent = fs.readFileSync(filePath, 'utf8');
|
|
2211
|
+
} catch {
|
|
2212
|
+
continue;
|
|
2213
|
+
}
|
|
2214
|
+
const lines = fileContent.split('\n').map(l => l.replace(/\r$/, ''));
|
|
2215
|
+
|
|
2216
|
+
const h1 = lines.find(l => /^# /.test(l));
|
|
2217
|
+
const project = h1
|
|
2218
|
+
? h1.replace(/^# /, '').replace(/\s*[—–-].*$/, '').trim()
|
|
2219
|
+
: path.basename(path.dirname(path.dirname(filePath)));
|
|
2220
|
+
|
|
2221
|
+
const next = extractStandupSection(lines, PLAN_HEADERS);
|
|
2222
|
+
if (next && !seenNext.has(next)) {
|
|
2223
|
+
seenNext.add(next);
|
|
2224
|
+
nextItems.push({ project, text: next });
|
|
2225
|
+
}
|
|
2226
|
+
|
|
2227
|
+
const blocker = extractStandupSection(lines, rebuildStatus.BLOCKER_HEADERS);
|
|
2228
|
+
if (blocker && !seenBlockers.has(blocker)) {
|
|
2229
|
+
seenBlockers.add(blocker);
|
|
2230
|
+
blockerItems.push({ project, text: blocker });
|
|
2231
|
+
}
|
|
2232
|
+
}
|
|
2233
|
+
}
|
|
2234
|
+
|
|
2235
|
+
// Filter journal to current repo when scoped, global when --all or home dir
|
|
2236
|
+
const repoName = !showAll ? path.basename(cwd) : null;
|
|
2237
|
+
const lastEntry = getLastJournalEntry(journalDir, repoName);
|
|
2238
|
+
const scope = showAll ? 'all repos' : repoName;
|
|
2239
|
+
|
|
2240
|
+
console.log('');
|
|
2241
|
+
console.log(`━━━ Standup (${scope}) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
|
|
2242
|
+
console.log('');
|
|
2243
|
+
|
|
2244
|
+
// Last session
|
|
2245
|
+
if (lastEntry) {
|
|
2246
|
+
const dateStr = lastEntry.date ? ` (${lastEntry.date})` : '';
|
|
2247
|
+
const repoStr = lastEntry.repo ? ` [${lastEntry.repo}]` : '';
|
|
2248
|
+
console.log(`▶ Last session${dateStr}${repoStr}:`);
|
|
2249
|
+
const summary = lastEntry.what || lastEntry.title || '(no details recorded)';
|
|
2250
|
+
console.log(` ${summary}`);
|
|
2251
|
+
} else {
|
|
2252
|
+
console.log('▶ Last session:');
|
|
2253
|
+
console.log(' (no journal entries found)');
|
|
2254
|
+
}
|
|
2255
|
+
|
|
2256
|
+
// Plan for today
|
|
2257
|
+
console.log('');
|
|
2258
|
+
console.log('▶ Plan for today:');
|
|
2259
|
+
if (nextItems.length > 0) {
|
|
2260
|
+
for (const item of nextItems) {
|
|
2261
|
+
const prefix = showAll ? `${item.project}: ` : '';
|
|
2262
|
+
console.log(` ${prefix}${item.text}`);
|
|
2263
|
+
}
|
|
2264
|
+
} else {
|
|
2265
|
+
console.log(' (no next steps recorded — update your state file to set a goal)');
|
|
2266
|
+
}
|
|
2267
|
+
|
|
2268
|
+
// Blockers
|
|
2269
|
+
console.log('');
|
|
2270
|
+
console.log('▶ Blockers:');
|
|
2271
|
+
if (blockerItems.length > 0) {
|
|
2272
|
+
for (const item of blockerItems) {
|
|
2273
|
+
const prefix = showAll ? `${item.project}: ` : '';
|
|
2274
|
+
console.log(` ${prefix}${item.text}`);
|
|
2275
|
+
}
|
|
2276
|
+
} else {
|
|
2277
|
+
console.log(' None');
|
|
2278
|
+
}
|
|
2279
|
+
|
|
2280
|
+
console.log('');
|
|
2281
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
2282
|
+
if (!showAll && inRepo) {
|
|
2283
|
+
console.log(' Use --all for a cross-repo standup.');
|
|
2284
|
+
}
|
|
2285
|
+
console.log('');
|
|
2286
|
+
}
|
|
2287
|
+
|
|
2288
|
+
/**
|
|
2289
|
+
* Like extractSection but without the 120-char truncation used for status tables.
|
|
2290
|
+
* Returns full paragraph text for standup display.
|
|
2291
|
+
*/
|
|
2292
|
+
function extractStandupSection(lines, headers) {
|
|
2293
|
+
for (const header of headers) {
|
|
2294
|
+
const idx = lines.findIndex(l => {
|
|
2295
|
+
const match = l.match(/^#{2,3}\s+(.+)$/);
|
|
2296
|
+
return match && match[1].trim() === header;
|
|
2297
|
+
});
|
|
2298
|
+
if (idx === -1) continue;
|
|
2299
|
+
|
|
2300
|
+
const para = [];
|
|
2301
|
+
for (let i = idx + 1; i < lines.length; i++) {
|
|
2302
|
+
const line = lines[i];
|
|
2303
|
+
if (/^#{1,3}\s/.test(line)) break;
|
|
2304
|
+
if (line.trim() === '' && para.length > 0) break;
|
|
2305
|
+
if (line.trim() === '') continue;
|
|
2306
|
+
para.push(line.trim());
|
|
2307
|
+
}
|
|
2308
|
+
|
|
2309
|
+
if (para.length === 0) continue;
|
|
2310
|
+
|
|
2311
|
+
let text = para.join(' ');
|
|
2312
|
+
text = text.replace(/\*\*/g, '').replace(/`/g, '');
|
|
2313
|
+
return text;
|
|
2314
|
+
}
|
|
2315
|
+
return '';
|
|
2316
|
+
}
|
|
2317
|
+
|
|
2318
|
+
// ── Update command ─────────────────────────────────────────────────────────
|
|
2319
|
+
|
|
2320
|
+
/**
|
|
2321
|
+
* Re-sync hooks and commands from the installed Wayfind package to ~/.claude/.
|
|
2322
|
+
* Copies hook scripts and slash-command files, overwriting stale copies.
|
|
2323
|
+
*/
|
|
2324
|
+
function runUpdate() {
|
|
2325
|
+
const specDir = path.join(ROOT, 'specializations', 'claude-code');
|
|
2326
|
+
const hooksDir = path.join(HOME, '.claude', 'hooks');
|
|
2327
|
+
const commandsDir = path.join(HOME, '.claude', 'commands');
|
|
2328
|
+
|
|
2329
|
+
// Ensure target dirs exist
|
|
2330
|
+
if (!fs.existsSync(hooksDir)) fs.mkdirSync(hooksDir, { recursive: true });
|
|
2331
|
+
if (!fs.existsSync(commandsDir)) fs.mkdirSync(commandsDir, { recursive: true });
|
|
2332
|
+
|
|
2333
|
+
let updated = 0;
|
|
2334
|
+
let skipped = 0;
|
|
2335
|
+
|
|
2336
|
+
// Hook files to sync
|
|
2337
|
+
const hookFiles = ['check-global-state.sh', 'session-end.sh'];
|
|
2338
|
+
const sourceHooksDir = path.join(specDir, 'hooks');
|
|
2339
|
+
|
|
2340
|
+
for (const file of hookFiles) {
|
|
2341
|
+
const src = path.join(sourceHooksDir, file);
|
|
2342
|
+
const dest = path.join(hooksDir, file);
|
|
2343
|
+
if (!fs.existsSync(src)) continue;
|
|
2344
|
+
|
|
2345
|
+
const srcContent = fs.readFileSync(src, 'utf8');
|
|
2346
|
+
const destContent = fs.existsSync(dest) ? fs.readFileSync(dest, 'utf8') : '';
|
|
2347
|
+
|
|
2348
|
+
if (srcContent === destContent) {
|
|
2349
|
+
skipped++;
|
|
2350
|
+
continue;
|
|
2351
|
+
}
|
|
2352
|
+
|
|
2353
|
+
fs.writeFileSync(dest, srcContent);
|
|
2354
|
+
fs.chmodSync(dest, 0o755);
|
|
2355
|
+
console.log(` Updated: ${file}`);
|
|
2356
|
+
updated++;
|
|
2357
|
+
}
|
|
2358
|
+
|
|
2359
|
+
// Command files to sync
|
|
2360
|
+
const sourceCommandsDir = path.join(specDir, 'commands');
|
|
2361
|
+
if (fs.existsSync(sourceCommandsDir)) {
|
|
2362
|
+
const cmdFiles = fs.readdirSync(sourceCommandsDir).filter(f => f.endsWith('.md'));
|
|
2363
|
+
for (const file of cmdFiles) {
|
|
2364
|
+
const src = path.join(sourceCommandsDir, file);
|
|
2365
|
+
const dest = path.join(commandsDir, file);
|
|
2366
|
+
|
|
2367
|
+
const srcContent = fs.readFileSync(src, 'utf8');
|
|
2368
|
+
const destContent = fs.existsSync(dest) ? fs.readFileSync(dest, 'utf8') : '';
|
|
2369
|
+
|
|
2370
|
+
if (srcContent === destContent) {
|
|
2371
|
+
skipped++;
|
|
2372
|
+
continue;
|
|
2373
|
+
}
|
|
2374
|
+
|
|
2375
|
+
fs.writeFileSync(dest, srcContent);
|
|
2376
|
+
console.log(` Updated: ${file}`);
|
|
2377
|
+
updated++;
|
|
2378
|
+
}
|
|
2379
|
+
}
|
|
2380
|
+
|
|
2381
|
+
// Write version marker
|
|
2382
|
+
const versionFile = path.join(HOME, '.claude', 'team-context', '.wayfind-version');
|
|
2383
|
+
try {
|
|
2384
|
+
const pkg = require(path.join(ROOT, 'package.json'));
|
|
2385
|
+
const versionDir = path.dirname(versionFile);
|
|
2386
|
+
if (!fs.existsSync(versionDir)) fs.mkdirSync(versionDir, { recursive: true });
|
|
2387
|
+
fs.writeFileSync(versionFile, pkg.version);
|
|
2388
|
+
} catch { /* ignore */ }
|
|
2389
|
+
|
|
2390
|
+
if (updated > 0) {
|
|
2391
|
+
console.log(`\n ${updated} file(s) updated, ${skipped} already current.`);
|
|
2392
|
+
} else {
|
|
2393
|
+
console.log(' Everything up to date.');
|
|
2394
|
+
}
|
|
2395
|
+
}
|
|
2396
|
+
|
|
2397
|
+
// ── Migrate to plugin ───────────────────────────────────────────────────────
|
|
2398
|
+
|
|
2399
|
+
function runMigrateToPlugin(args) {
|
|
2400
|
+
const dryRun = args.includes('--dry-run');
|
|
2401
|
+
const settingsPath = path.join(HOME, '.claude', 'settings.json');
|
|
2402
|
+
const hooksDir = path.join(HOME, '.claude', 'hooks');
|
|
2403
|
+
const commandsDir = path.join(HOME, '.claude', 'commands');
|
|
2404
|
+
|
|
2405
|
+
console.log('Wayfind — Migrate to Plugin');
|
|
2406
|
+
console.log('===========================\n');
|
|
2407
|
+
|
|
2408
|
+
if (dryRun) console.log('(dry run — no changes will be made)\n');
|
|
2409
|
+
|
|
2410
|
+
let changes = 0;
|
|
2411
|
+
|
|
2412
|
+
// Step 1: Remove hook entries from settings.json
|
|
2413
|
+
if (fs.existsSync(settingsPath)) {
|
|
2414
|
+
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
2415
|
+
const hooks = settings.hooks || {};
|
|
2416
|
+
let hooksModified = false;
|
|
2417
|
+
|
|
2418
|
+
// Remove SessionStart hooks that reference check-global-state
|
|
2419
|
+
if (hooks.SessionStart) {
|
|
2420
|
+
const before = hooks.SessionStart.length;
|
|
2421
|
+
hooks.SessionStart = hooks.SessionStart.filter(group => {
|
|
2422
|
+
const cmds = (group.hooks || []);
|
|
2423
|
+
return !cmds.some(h => (h.command || '').includes('check-global-state'));
|
|
2424
|
+
});
|
|
2425
|
+
if (hooks.SessionStart.length === 0) delete hooks.SessionStart;
|
|
2426
|
+
if ((hooks.SessionStart || []).length !== before) hooksModified = true;
|
|
2427
|
+
}
|
|
2428
|
+
|
|
2429
|
+
// Remove Stop hooks that reference session-end.sh or wayfind reindex
|
|
2430
|
+
if (hooks.Stop) {
|
|
2431
|
+
const before = hooks.Stop.length;
|
|
2432
|
+
hooks.Stop = hooks.Stop.filter(group => {
|
|
2433
|
+
const cmds = (group.hooks || []);
|
|
2434
|
+
return !cmds.some(h => {
|
|
2435
|
+
const cmd = h.command || '';
|
|
2436
|
+
return cmd.includes('session-end.sh') || cmd.includes('wayfind reindex') || cmd.includes('team-context.js reindex');
|
|
2437
|
+
});
|
|
2438
|
+
});
|
|
2439
|
+
if (hooks.Stop.length === 0) delete hooks.Stop;
|
|
2440
|
+
if ((hooks.Stop || []).length !== before) hooksModified = true;
|
|
2441
|
+
}
|
|
2442
|
+
|
|
2443
|
+
if (hooksModified) {
|
|
2444
|
+
settings.hooks = hooks;
|
|
2445
|
+
if (Object.keys(hooks).length === 0) delete settings.hooks;
|
|
2446
|
+
if (!dryRun) {
|
|
2447
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
|
2448
|
+
}
|
|
2449
|
+
console.log(` ${dryRun ? 'Would remove' : 'Removed'} Wayfind hook entries from ~/.claude/settings.json`);
|
|
2450
|
+
changes++;
|
|
2451
|
+
} else {
|
|
2452
|
+
console.log(' No Wayfind hook entries in settings.json (already clean)');
|
|
2453
|
+
}
|
|
2454
|
+
} else {
|
|
2455
|
+
console.log(' No ~/.claude/settings.json found');
|
|
2456
|
+
}
|
|
2457
|
+
|
|
2458
|
+
// Step 2: Remove old hook scripts
|
|
2459
|
+
const oldHookFiles = ['check-global-state.sh', 'session-end.sh'];
|
|
2460
|
+
for (const file of oldHookFiles) {
|
|
2461
|
+
const hookPath = path.join(hooksDir, file);
|
|
2462
|
+
if (fs.existsSync(hookPath)) {
|
|
2463
|
+
if (!dryRun) fs.unlinkSync(hookPath);
|
|
2464
|
+
console.log(` ${dryRun ? 'Would remove' : 'Removed'} ~/.claude/hooks/${file}`);
|
|
2465
|
+
changes++;
|
|
2466
|
+
}
|
|
2467
|
+
}
|
|
2468
|
+
|
|
2469
|
+
// Step 3: Remove old command files (plugin skills replace these)
|
|
2470
|
+
const oldCommandFiles = ['init-memory.md', 'init-team.md', 'doctor.md', 'journal.md', 'standup.md', 'review-prs.md'];
|
|
2471
|
+
for (const file of oldCommandFiles) {
|
|
2472
|
+
const cmdPath = path.join(commandsDir, file);
|
|
2473
|
+
if (fs.existsSync(cmdPath)) {
|
|
2474
|
+
if (!dryRun) fs.unlinkSync(cmdPath);
|
|
2475
|
+
console.log(` ${dryRun ? 'Would remove' : 'Removed'} ~/.claude/commands/${file}`);
|
|
2476
|
+
changes++;
|
|
2477
|
+
}
|
|
2478
|
+
}
|
|
2479
|
+
|
|
2480
|
+
// Step 4: Summary
|
|
2481
|
+
console.log('');
|
|
2482
|
+
if (changes === 0) {
|
|
2483
|
+
console.log('Nothing to migrate — already clean.');
|
|
2484
|
+
} else if (dryRun) {
|
|
2485
|
+
console.log(`Would make ${changes} change(s). Run without --dry-run to apply.`);
|
|
2486
|
+
} else {
|
|
2487
|
+
console.log(`Done — ${changes} change(s) applied.`);
|
|
2488
|
+
console.log('');
|
|
2489
|
+
console.log('The Wayfind plugin now handles hooks and skills.');
|
|
2490
|
+
console.log('Verify with: /wayfind:doctor');
|
|
2491
|
+
console.log('');
|
|
2492
|
+
console.log('Your old /standup, /doctor, etc. are now /wayfind:standup, /wayfind:doctor, etc.');
|
|
2493
|
+
console.log('The CLI (wayfind digest, wayfind reindex, etc.) still works — only hooks and skills moved.');
|
|
2494
|
+
}
|
|
2495
|
+
}
|
|
2496
|
+
|
|
2064
2497
|
// ── Status command ──────────────────────────────────────────────────────────
|
|
2065
2498
|
|
|
2066
2499
|
function runStatus(args) {
|
|
@@ -3896,6 +4329,14 @@ const COMMANDS = {
|
|
|
3896
4329
|
desc: 'Check if installed version meets team minimum (used by hooks)',
|
|
3897
4330
|
run: () => runCheckVersion(),
|
|
3898
4331
|
},
|
|
4332
|
+
update: {
|
|
4333
|
+
desc: 'Re-sync hooks and commands from the installed Wayfind package',
|
|
4334
|
+
run: () => runUpdate(),
|
|
4335
|
+
},
|
|
4336
|
+
'migrate-to-plugin': {
|
|
4337
|
+
desc: 'Remove old hooks/commands — let the Claude Code plugin handle them',
|
|
4338
|
+
run: (args) => runMigrateToPlugin(args),
|
|
4339
|
+
},
|
|
3899
4340
|
doctor: {
|
|
3900
4341
|
desc: 'Check your Wayfind installation for issues',
|
|
3901
4342
|
run: (args) => {
|
|
@@ -3929,6 +4370,10 @@ const COMMANDS = {
|
|
|
3929
4370
|
desc: 'Show cross-project status (or rebuild Active Projects table)',
|
|
3930
4371
|
run: (args) => runStatus(args),
|
|
3931
4372
|
},
|
|
4373
|
+
standup: {
|
|
4374
|
+
desc: 'Show a daily standup summary (last session, plan, blockers)',
|
|
4375
|
+
run: (args) => runStandup(args),
|
|
4376
|
+
},
|
|
3932
4377
|
signals: {
|
|
3933
4378
|
desc: 'Show configured signal channels and last pull times',
|
|
3934
4379
|
run: () => runSignals(),
|
|
@@ -4002,8 +4447,8 @@ const COMMANDS = {
|
|
|
4002
4447
|
|
|
4003
4448
|
// Files and directories to sync
|
|
4004
4449
|
const syncItems = [
|
|
4005
|
-
'bin/', 'templates/', 'specializations/', 'tests/', 'simulation/',
|
|
4006
|
-
'backup/', '.github/', 'Dockerfile', 'package.json', 'setup.sh',
|
|
4450
|
+
'bin/', 'templates/', 'specializations/', 'plugin/', 'tests/', 'simulation/',
|
|
4451
|
+
'backup/', '.github/', '.claude-plugin/', 'Dockerfile', 'package.json', 'setup.sh',
|
|
4007
4452
|
'install.sh', 'uninstall.sh', 'doctor.sh', 'journal-summary.sh',
|
|
4008
4453
|
'BOOTSTRAP_PROMPT.md', '.gitattributes', '.gitignore', 'VERSIONS.md',
|
|
4009
4454
|
];
|
|
@@ -4237,6 +4682,13 @@ function showHelp() {
|
|
|
4237
4682
|
console.log(' wayfind status --write Rebuild Active Projects in global-state.md');
|
|
4238
4683
|
console.log(' wayfind status --json Machine-readable output');
|
|
4239
4684
|
console.log(' wayfind status --quiet Suppress output (for hooks)');
|
|
4685
|
+
console.log(' wayfind standup Daily standup for current repo (last session, plan, blockers)');
|
|
4686
|
+
console.log(' wayfind standup --all Daily standup across all repos');
|
|
4687
|
+
console.log('');
|
|
4688
|
+
console.log('Maintenance:');
|
|
4689
|
+
console.log(' wayfind update Re-sync hooks and commands from package');
|
|
4690
|
+
console.log(' wayfind migrate-to-plugin Remove old hooks/commands — let the plugin handle them');
|
|
4691
|
+
console.log(' wayfind doctor Check installation health');
|
|
4240
4692
|
console.log('');
|
|
4241
4693
|
console.log('Publishing:');
|
|
4242
4694
|
console.log(' wayfind sync-public Sync code to usewayfind/wayfind (triggers npm + Docker publish)');
|
package/doctor.sh
CHANGED
|
@@ -17,9 +17,33 @@ 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
|
+
# Plugins live under ~/.claude/plugins/ — look for any marketplace that contains our plugin
|
|
23
|
+
local PLUGINS_DIR="$HOME/.claude/plugins"
|
|
24
|
+
if [ -d "$PLUGINS_DIR" ]; then
|
|
25
|
+
# Check marketplaces (e.g. ~/.claude/plugins/marketplaces/usewayfind-wayfind/plugin/)
|
|
26
|
+
if find "$PLUGINS_DIR" -path '*/wayfind/plugin/.claude-plugin/plugin.json' -print -quit 2>/dev/null | grep -q .; then
|
|
27
|
+
return 0
|
|
28
|
+
fi
|
|
29
|
+
# Also check direct plugin installs
|
|
30
|
+
if find "$PLUGINS_DIR" -name 'plugin.json' -exec grep -l '"name": "wayfind"' {} + 2>/dev/null | grep -q .; then
|
|
31
|
+
return 0
|
|
32
|
+
fi
|
|
33
|
+
fi
|
|
34
|
+
return 1
|
|
35
|
+
}
|
|
36
|
+
|
|
20
37
|
check_hook_registered() {
|
|
21
38
|
echo ""
|
|
22
39
|
echo "Hook registration"
|
|
40
|
+
|
|
41
|
+
# If installed as a plugin, hooks are provided by the plugin — skip legacy checks
|
|
42
|
+
if is_plugin_installed; then
|
|
43
|
+
ok "Installed as Claude Code plugin (hooks provided by plugin)"
|
|
44
|
+
return
|
|
45
|
+
fi
|
|
46
|
+
|
|
23
47
|
local SETTINGS="$HOME/.claude/settings.json"
|
|
24
48
|
if [ ! -f "$SETTINGS" ]; then
|
|
25
49
|
err "settings.json not found — hook is not registered"
|
|
@@ -42,6 +66,24 @@ check_hook_registered() {
|
|
|
42
66
|
ISSUES=$((ISSUES + 1))
|
|
43
67
|
fi
|
|
44
68
|
|
|
69
|
+
# Check if installed hooks are stale vs package source
|
|
70
|
+
local SCRIPT_DIR
|
|
71
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
72
|
+
local SOURCE_HOOKS_DIR="$SCRIPT_DIR/specializations/claude-code/hooks"
|
|
73
|
+
local INSTALLED_HOOKS_DIR="$HOME/.claude/hooks"
|
|
74
|
+
for hook_file in check-global-state.sh session-end.sh; do
|
|
75
|
+
local src="$SOURCE_HOOKS_DIR/$hook_file"
|
|
76
|
+
local dest="$INSTALLED_HOOKS_DIR/$hook_file"
|
|
77
|
+
if [ -f "$src" ] && [ -f "$dest" ]; then
|
|
78
|
+
if ! diff -q "$src" "$dest" >/dev/null 2>&1; then
|
|
79
|
+
warn "$hook_file is out of date — run 'wayfind update' to sync"
|
|
80
|
+
ISSUES=$((ISSUES + 1))
|
|
81
|
+
else
|
|
82
|
+
[ "$VERBOSE" = true ] && ok "$hook_file is current"
|
|
83
|
+
fi
|
|
84
|
+
fi
|
|
85
|
+
done
|
|
86
|
+
|
|
45
87
|
# Validate hook structure — correct format: {matcher: str, hooks: [{type, command}]}
|
|
46
88
|
if command -v python3 &>/dev/null; then
|
|
47
89
|
local STRUCT_RESULT
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wayfind",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.15",
|
|
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"
|
|
@@ -27,6 +27,8 @@
|
|
|
27
27
|
},
|
|
28
28
|
"files": [
|
|
29
29
|
"bin/",
|
|
30
|
+
"plugin/",
|
|
31
|
+
".claude-plugin/",
|
|
30
32
|
"setup.sh",
|
|
31
33
|
"install.sh",
|
|
32
34
|
"uninstall.sh",
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "wayfind",
|
|
3
|
+
"version": "2.0.15",
|
|
4
|
+
"description": "Team decision trail for AI-assisted development. Session memory, decision journals, and team digests.",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "Wayfind",
|
|
7
|
+
"url": "https://github.com/usewayfind"
|
|
8
|
+
},
|
|
9
|
+
"homepage": "https://github.com/usewayfind/wayfind",
|
|
10
|
+
"repository": "https://github.com/usewayfind/wayfind",
|
|
11
|
+
"license": "Apache-2.0",
|
|
12
|
+
"keywords": ["team", "context", "memory", "decisions", "digest", "journal", "session"],
|
|
13
|
+
"skills": "./skills/",
|
|
14
|
+
"hooks": "./hooks/hooks.json"
|
|
15
|
+
}
|
package/plugin/README.md
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Wayfind — Claude Code Plugin
|
|
2
|
+
|
|
3
|
+
Team decision trail for AI-assisted development. Session memory, decision journals, and team digests.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
From the GitHub marketplace:
|
|
8
|
+
```
|
|
9
|
+
/plugin marketplace add usewayfind/wayfind
|
|
10
|
+
/plugin install wayfind@usewayfind
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Or from the official Anthropic marketplace (once approved):
|
|
14
|
+
```
|
|
15
|
+
/plugin install wayfind
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## What you get
|
|
19
|
+
|
|
20
|
+
### Tier 1: Plugin only (no npm install)
|
|
21
|
+
|
|
22
|
+
- **Session memory protocol** — loads team and personal state files at session start, saves at session end
|
|
23
|
+
- **Slash commands** — `/wayfind:init-memory`, `/wayfind:init-team`, `/wayfind:doctor`, `/wayfind:journal`, `/wayfind:standup`, `/wayfind:review-prs`
|
|
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
|
+
|
|
27
|
+
### Tier 2: Plugin + npm CLI (`npm i -g wayfind`)
|
|
28
|
+
|
|
29
|
+
Everything in Tier 1, plus:
|
|
30
|
+
|
|
31
|
+
- **Decision indexing** — `wayfind reindex` extracts decisions from conversation transcripts
|
|
32
|
+
- **Journal sync** — `wayfind journal sync` pushes local journals to the team context repo
|
|
33
|
+
- **Weekly digests** — `wayfind digest` generates multi-persona summaries (engineering, product, strategy)
|
|
34
|
+
- **Team coordination** — `wayfind team create/join`, shared context distribution
|
|
35
|
+
- **Slack + Notion delivery** — digests post to Slack channels and Notion pages via GitHub Actions
|
|
36
|
+
- **Bot mode** — `wayfind bot` runs a Slack bot for on-demand queries
|
|
37
|
+
|
|
38
|
+
## Documentation
|
|
39
|
+
|
|
40
|
+
Full docs: https://github.com/usewayfind/wayfind
|