wayfind 2.0.22 → 2.0.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/connectors/llm.js
CHANGED
|
@@ -44,6 +44,15 @@ function loadDigestFixture(personaId) {
|
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
function loadScoringFixture() {
|
|
48
|
+
const fixturePath = path.join(getRepoRoot(), 'simulation', 'fixtures', 'intelligence', 'scores.json');
|
|
49
|
+
try {
|
|
50
|
+
return fs.readFileSync(fixturePath, 'utf8');
|
|
51
|
+
} catch {
|
|
52
|
+
return '[]';
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
47
56
|
// ── HTTP helpers ─────────────────────────────────────────────────────────────
|
|
48
57
|
|
|
49
58
|
/**
|
|
@@ -274,6 +283,9 @@ async function callCLI(config, systemPrompt, userContent) {
|
|
|
274
283
|
async function call(config, systemPrompt, userContent) {
|
|
275
284
|
// Simulation mode overrides any provider setting
|
|
276
285
|
if (isSimulation() || config.provider === 'simulate') {
|
|
286
|
+
if (config._callType === 'scoring') {
|
|
287
|
+
return loadScoringFixture();
|
|
288
|
+
}
|
|
277
289
|
const personaId = config._personaId || 'engineering';
|
|
278
290
|
return loadDigestFixture(personaId);
|
|
279
291
|
}
|
package/bin/digest.js
CHANGED
|
@@ -7,9 +7,13 @@ const llm = require('./connectors/llm');
|
|
|
7
7
|
const contentStore = require('./content-store');
|
|
8
8
|
const telemetry = require('./telemetry');
|
|
9
9
|
|
|
10
|
+
const intelligence = require('./intelligence');
|
|
11
|
+
|
|
10
12
|
const HOME = process.env.HOME || process.env.USERPROFILE;
|
|
11
13
|
const WAYFIND_DIR = path.join(HOME, '.claude', 'team-context');
|
|
12
14
|
const ENV_FILE = path.join(WAYFIND_DIR, '.env');
|
|
15
|
+
const ROOT = path.join(__dirname, '..');
|
|
16
|
+
const DEFAULT_PERSONAS_PATH = path.join(ROOT, 'templates', 'personas.json');
|
|
13
17
|
|
|
14
18
|
// Team repo allowlist — mirrors content-store.js logic for digest-time filtering.
|
|
15
19
|
// When INCLUDE_REPOS is set, sections mentioning repos NOT on the list are removed.
|
|
@@ -73,6 +77,31 @@ function filterExcludedContent(content) {
|
|
|
73
77
|
return content;
|
|
74
78
|
}
|
|
75
79
|
|
|
80
|
+
// ── Persona loading ─────────────────────────────────────────────────────────
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Load active personas from user config or bundled default.
|
|
84
|
+
* Same resolution as team-context.js:readPersonas.
|
|
85
|
+
* @returns {Array<{id: string, name: string, description: string}>}
|
|
86
|
+
*/
|
|
87
|
+
function loadPersonas() {
|
|
88
|
+
const candidates = [
|
|
89
|
+
path.join(HOME, '.claude', 'team-context', 'personas.json'),
|
|
90
|
+
path.join(HOME, '.ai-memory', 'team-context', 'personas.json'),
|
|
91
|
+
];
|
|
92
|
+
let configPath = null;
|
|
93
|
+
for (const p of candidates) {
|
|
94
|
+
if (fs.existsSync(p)) { configPath = p; break; }
|
|
95
|
+
}
|
|
96
|
+
if (!configPath) configPath = DEFAULT_PERSONAS_PATH;
|
|
97
|
+
try {
|
|
98
|
+
const data = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
99
|
+
return data.personas || [];
|
|
100
|
+
} catch {
|
|
101
|
+
return [];
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
76
105
|
// ── Env file helpers ────────────────────────────────────────────────────────
|
|
77
106
|
|
|
78
107
|
/**
|
|
@@ -677,11 +706,23 @@ async function generateDigest(config, personaIds, sinceDate, onProgress) {
|
|
|
677
706
|
journalContent = filterExcludedContent(journalContent);
|
|
678
707
|
signalContent = filterExcludedContent(signalContent);
|
|
679
708
|
|
|
680
|
-
//
|
|
709
|
+
// Preserve original content refs for telemetry
|
|
710
|
+
const rawSignalContent = signalContent;
|
|
711
|
+
const rawJournalContent = journalContent;
|
|
712
|
+
|
|
713
|
+
// Intelligence layer: score items for persona relevance
|
|
714
|
+
const personas = loadPersonas();
|
|
715
|
+
let scores = null;
|
|
716
|
+
if (config.intelligence?.enabled !== false && personas.length > 0) {
|
|
717
|
+
const haikuConfig = {
|
|
718
|
+
provider: config.llm.provider,
|
|
719
|
+
model: config.intelligence?.model || 'claude-haiku-4-5-20251001',
|
|
720
|
+
api_key_env: config.llm.api_key_env,
|
|
721
|
+
};
|
|
722
|
+
scores = await intelligence.scoreItems(signalContent, journalContent, personas, haikuConfig);
|
|
723
|
+
}
|
|
724
|
+
|
|
681
725
|
const maxInputChars = (config.llm && config.llm.max_input_chars) || 120000;
|
|
682
|
-
const budget = applyTokenBudget(signalContent, journalContent, maxInputChars);
|
|
683
|
-
signalContent = budget.signals;
|
|
684
|
-
journalContent = budget.journals;
|
|
685
726
|
|
|
686
727
|
// Generate per-persona digests
|
|
687
728
|
const digestDir = path.join(HOME, '.claude', 'team-context', 'digests');
|
|
@@ -696,8 +737,23 @@ async function generateDigest(config, personaIds, sinceDate, onProgress) {
|
|
|
696
737
|
}
|
|
697
738
|
|
|
698
739
|
const startTime = Date.now();
|
|
740
|
+
|
|
741
|
+
// Per-persona filtering: intelligence layer + token budget
|
|
742
|
+
let pSignals = signalContent;
|
|
743
|
+
let pJournals = journalContent;
|
|
744
|
+
if (scores) {
|
|
745
|
+
const threshold = config.intelligence?.thresholds?.[personaId]
|
|
746
|
+
?? intelligence.DEFAULT_THRESHOLDS[personaId] ?? 1;
|
|
747
|
+
const allPersonaIds = personas.map(p => p.id);
|
|
748
|
+
({ signals: pSignals, journals: pJournals } =
|
|
749
|
+
intelligence.filterForPersona(signalContent, journalContent, scores, personaId, threshold, allPersonaIds));
|
|
750
|
+
}
|
|
751
|
+
const budget = applyTokenBudget(pSignals, pJournals, maxInputChars);
|
|
752
|
+
pSignals = budget.signals;
|
|
753
|
+
pJournals = budget.journals;
|
|
754
|
+
|
|
699
755
|
const promptContext = { teamContextDir: config.team_context_dir };
|
|
700
|
-
const { system, user } = buildPrompt(personaId,
|
|
756
|
+
const { system, user } = buildPrompt(personaId, pSignals, pJournals, dateRange, promptContext);
|
|
701
757
|
|
|
702
758
|
// Debug: dump prompt if TEAM_CONTEXT_DEBUG_PROMPT is set
|
|
703
759
|
if (process.env.TEAM_CONTEXT_DEBUG_PROMPT) {
|
|
@@ -729,10 +785,12 @@ async function generateDigest(config, personaIds, sinceDate, onProgress) {
|
|
|
729
785
|
persona_count: personaIds.length,
|
|
730
786
|
personas: personaIds.join(','),
|
|
731
787
|
entry_count: storeResult.entryCount || 0,
|
|
732
|
-
journal_count:
|
|
733
|
-
signal_count:
|
|
788
|
+
journal_count: rawJournalContent ? rawJournalContent.split('\n---\n').length : 0,
|
|
789
|
+
signal_count: rawSignalContent ? rawSignalContent.split('\n---\n').length : 0,
|
|
734
790
|
has_previous_digest: !!loadPreviousDigest(personaIds[0], toDate),
|
|
735
791
|
has_team_context: !!loadTeamContext(config.team_context_dir),
|
|
792
|
+
intelligence_enabled: config.intelligence?.enabled !== false,
|
|
793
|
+
intelligence_items_scored: scores ? scores.length : 0,
|
|
736
794
|
});
|
|
737
795
|
|
|
738
796
|
// Write combined file
|
|
@@ -843,6 +901,7 @@ module.exports = {
|
|
|
843
901
|
loadTeamContext,
|
|
844
902
|
loadTeamMembers,
|
|
845
903
|
loadPreviousDigest,
|
|
904
|
+
loadPersonas,
|
|
846
905
|
buildFeedbackContext,
|
|
847
906
|
buildPrompt,
|
|
848
907
|
generateDigest,
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const llm = require('./connectors/llm');
|
|
4
|
+
|
|
5
|
+
// Default thresholds per persona. 0=show everything, 1=tangential+relevant, 2=directly relevant only.
|
|
6
|
+
const DEFAULT_THRESHOLDS = {
|
|
7
|
+
engineering: 1,
|
|
8
|
+
product: 2,
|
|
9
|
+
design: 2,
|
|
10
|
+
strategy: 1,
|
|
11
|
+
unified: 0,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Build the scoring system prompt dynamically from active personas.
|
|
16
|
+
* @param {Array<{id: string, description: string}>} personas
|
|
17
|
+
* @returns {string}
|
|
18
|
+
*/
|
|
19
|
+
function buildScoringPrompt(personas) {
|
|
20
|
+
const personaDefs = personas
|
|
21
|
+
.map(p => `- ${p.id}: ${p.description}`)
|
|
22
|
+
.join('\n');
|
|
23
|
+
|
|
24
|
+
const exampleKeys = personas.map(p => `"${p.id}":0`).join(',');
|
|
25
|
+
|
|
26
|
+
return `You score journal entries and signals for relevance to this team's personas.
|
|
27
|
+
Score each item 0-2 for each persona:
|
|
28
|
+
- 0 = not relevant to this persona
|
|
29
|
+
- 1 = tangentially relevant (context, not action)
|
|
30
|
+
- 2 = directly relevant (this persona should see this)
|
|
31
|
+
|
|
32
|
+
Personas:
|
|
33
|
+
${personaDefs}
|
|
34
|
+
|
|
35
|
+
Return ONLY a JSON array. No explanation. Example:
|
|
36
|
+
[{"id":0,${exampleKeys}},{"id":1,${exampleKeys}}]`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Split content on section separators and number items.
|
|
41
|
+
* @param {string} content
|
|
42
|
+
* @param {number} startId - Starting item ID
|
|
43
|
+
* @returns {{ items: string[], labeled: string }}
|
|
44
|
+
*/
|
|
45
|
+
function splitAndLabel(content, startId) {
|
|
46
|
+
if (!content || !content.trim()) return { items: [], labeled: '' };
|
|
47
|
+
const items = content.split('\n\n---\n\n').filter(s => s.trim());
|
|
48
|
+
const labeled = items
|
|
49
|
+
.map((item, i) => `[ITEM ${startId + i}]\n${item}`)
|
|
50
|
+
.join('\n\n');
|
|
51
|
+
return { items, labeled };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Score signal and journal items for persona relevance using a single LLM call.
|
|
56
|
+
* @param {string} signalContent - Raw signal content (sections separated by \n\n---\n\n)
|
|
57
|
+
* @param {string} journalContent - Raw journal content (sections separated by \n\n---\n\n)
|
|
58
|
+
* @param {Array<{id: string, description: string}>} personas - Active personas
|
|
59
|
+
* @param {Object} llmConfig - LLM config for the scoring call
|
|
60
|
+
* @returns {Promise<Array<{id: number, [personaId]: number}>|null>} Scores array or null on failure
|
|
61
|
+
*/
|
|
62
|
+
async function scoreItems(signalContent, journalContent, personas, llmConfig) {
|
|
63
|
+
const signalResult = splitAndLabel(signalContent, 0);
|
|
64
|
+
const journalResult = splitAndLabel(journalContent, signalResult.items.length);
|
|
65
|
+
|
|
66
|
+
const totalItems = signalResult.items.length + journalResult.items.length;
|
|
67
|
+
if (totalItems === 0) return null;
|
|
68
|
+
|
|
69
|
+
const systemPrompt = buildScoringPrompt(personas);
|
|
70
|
+
|
|
71
|
+
const userParts = [];
|
|
72
|
+
if (signalResult.labeled) {
|
|
73
|
+
userParts.push('## Signals\n\n' + signalResult.labeled);
|
|
74
|
+
}
|
|
75
|
+
if (journalResult.labeled) {
|
|
76
|
+
userParts.push('## Journals\n\n' + journalResult.labeled);
|
|
77
|
+
}
|
|
78
|
+
const userContent = userParts.join('\n\n---\n\n');
|
|
79
|
+
|
|
80
|
+
const config = {
|
|
81
|
+
...llmConfig,
|
|
82
|
+
_personaId: 'scoring',
|
|
83
|
+
_callType: 'scoring',
|
|
84
|
+
max_tokens: Math.max(256, totalItems * 80),
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
const response = await llm.call(config, systemPrompt, userContent);
|
|
89
|
+
let jsonStr = response.trim();
|
|
90
|
+
// Strip markdown fences if present (reuse pattern from content-store.js)
|
|
91
|
+
if (jsonStr.startsWith('```')) {
|
|
92
|
+
jsonStr = jsonStr.replace(/^```(?:json)?\n?/, '').replace(/\n?```$/, '');
|
|
93
|
+
}
|
|
94
|
+
const scores = JSON.parse(jsonStr);
|
|
95
|
+
if (!Array.isArray(scores)) return null;
|
|
96
|
+
return scores;
|
|
97
|
+
} catch {
|
|
98
|
+
// Graceful fallback: caller passes everything through
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Filter content sections based on persona relevance scores.
|
|
105
|
+
* @param {string} signalContent - Raw signal content
|
|
106
|
+
* @param {string} journalContent - Raw journal content
|
|
107
|
+
* @param {Array<{id: number}>} scores - Scoring results from scoreItems
|
|
108
|
+
* @param {string} personaId - Persona to filter for
|
|
109
|
+
* @param {number} threshold - Minimum score to include (0, 1, or 2)
|
|
110
|
+
* @param {string[]} allPersonaIds - All active persona IDs (for unified union logic)
|
|
111
|
+
* @returns {{ signals: string, journals: string }}
|
|
112
|
+
*/
|
|
113
|
+
function filterForPersona(signalContent, journalContent, scores, personaId, threshold, allPersonaIds) {
|
|
114
|
+
const signalItems = (signalContent && signalContent.trim())
|
|
115
|
+
? signalContent.split('\n\n---\n\n').filter(s => s.trim())
|
|
116
|
+
: [];
|
|
117
|
+
const journalItems = (journalContent && journalContent.trim())
|
|
118
|
+
? journalContent.split('\n\n---\n\n').filter(s => s.trim())
|
|
119
|
+
: [];
|
|
120
|
+
|
|
121
|
+
function itemPasses(itemIndex) {
|
|
122
|
+
const score = scores.find(s => s.id === itemIndex);
|
|
123
|
+
if (!score) return true; // If no score for this item, include it (safe default)
|
|
124
|
+
|
|
125
|
+
if (personaId === 'unified') {
|
|
126
|
+
// Union: passes if ANY persona scores >= their threshold
|
|
127
|
+
return allPersonaIds.some(pid => {
|
|
128
|
+
const pidThreshold = DEFAULT_THRESHOLDS[pid] ?? 1;
|
|
129
|
+
return (score[pid] ?? 0) >= pidThreshold;
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return (score[personaId] ?? 0) >= threshold;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const filteredSignals = signalItems
|
|
137
|
+
.filter((_, i) => itemPasses(i))
|
|
138
|
+
.join('\n\n---\n\n');
|
|
139
|
+
|
|
140
|
+
const signalCount = signalItems.length;
|
|
141
|
+
const filteredJournals = journalItems
|
|
142
|
+
.filter((_, i) => itemPasses(signalCount + i))
|
|
143
|
+
.join('\n\n---\n\n');
|
|
144
|
+
|
|
145
|
+
return { signals: filteredSignals, journals: filteredJournals };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
module.exports = {
|
|
149
|
+
scoreItems,
|
|
150
|
+
filterForPersona,
|
|
151
|
+
DEFAULT_THRESHOLDS,
|
|
152
|
+
// Exported for testing
|
|
153
|
+
buildScoringPrompt,
|
|
154
|
+
splitAndLabel,
|
|
155
|
+
};
|
package/package.json
CHANGED
|
@@ -8,6 +8,8 @@ Find the 5 most consequential design items from this period. "Consequential" mea
|
|
|
8
8
|
|
|
9
9
|
## Rules
|
|
10
10
|
|
|
11
|
+
- **Translate to user impact.** If a code change made the cut, describe what the user sees or
|
|
12
|
+
interacts with differently. Translate technical changes into UX consequences.
|
|
11
13
|
- **Maximum 5 items.** If fewer than 5 things matter, output fewer. Never pad.
|
|
12
14
|
- **Rank by consequence, not category.** Lead with the item that most affects user experience.
|
|
13
15
|
- **No category headers.** No "UX Consistency" / "Accessibility" sections. Just a ranked list.
|
|
@@ -8,6 +8,9 @@ Find the 5 most consequential product items from this period. "Consequential" me
|
|
|
8
8
|
|
|
9
9
|
## Rules
|
|
10
10
|
|
|
11
|
+
- **Translate, don't transcribe.** If an engineering item made the cut, describe its product
|
|
12
|
+
implication — not its technical cause. "Hotel data gaps visible to customers in analytics"
|
|
13
|
+
not "nestedSource config missing in Function App." The reader is a PM, not a developer.
|
|
11
14
|
- **Maximum 5 items.** If fewer than 5 things matter, output fewer. Never pad.
|
|
12
15
|
- **Rank by consequence, not category.** Lead with the item that most affects customers or roadmap.
|
|
13
16
|
- **No category headers.** No "Scope Drift" / "Customer Issues" sections. Just a ranked list.
|
|
@@ -8,6 +8,9 @@ Find the 5 most consequential strategic items from this period. "Consequential"
|
|
|
8
8
|
|
|
9
9
|
## Rules
|
|
10
10
|
|
|
11
|
+
- **Extract the strategic pattern.** If day-to-day engineering work made the cut, connect it to
|
|
12
|
+
business goals, competitive positioning, or resource allocation. The reader thinks in quarters,
|
|
13
|
+
not sprints.
|
|
11
14
|
- **Maximum 5 items.** If fewer than 5 things matter, output fewer. Never pad.
|
|
12
15
|
- **Rank by consequence, not category.** Lead with the item that most affects the business trajectory.
|
|
13
16
|
- **No category headers.** No "Competitive" / "Strategic Drift" sections. Just a ranked list.
|