skill-tree-ai 1.0.0
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/plugin.json +12 -0
- package/.mcp.json +8 -0
- package/README.md +58 -0
- package/dist/core/classify.js +171 -0
- package/dist/core/extract.js +179 -0
- package/dist/core/profile.js +254 -0
- package/dist/core/render.js +58 -0
- package/dist/index.js +10 -0
- package/dist/local.js +220 -0
- package/dist/remote.js +113 -0
- package/dist/shared.js +16 -0
- package/hooks/hooks.json +15 -0
- package/hooks-handlers/session-start.sh +29 -0
- package/package.json +46 -0
- package/skills/skill-tree/SKILL.md +49 -0
- package/templates/skill-tree.html +728 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "skill-tree-ai",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Your AI collaboration style — skill tree visualization with character archetype cards and growth recommendations, grounded in the AI Fluency Framework",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "Robert Nowell",
|
|
7
|
+
"url": "https://github.com/robertnowell"
|
|
8
|
+
},
|
|
9
|
+
"homepage": "https://github.com/robertnowell/skill-tree",
|
|
10
|
+
"keywords": ["skill-tree", "ai-fluency", "education", "mcp", "growth"],
|
|
11
|
+
"license": "MIT"
|
|
12
|
+
}
|
package/.mcp.json
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Skill Tree — AI Fluency Profile for Claude
|
|
2
|
+
|
|
3
|
+
Analyze your Claude collaboration style. Get a character archetype card, skill tree visualization, and personalized growth recommendations — grounded in [Anthropic's AI Fluency Framework](https://www.anthropic.com/research/AI-fluency-index).
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# From this directory
|
|
9
|
+
claude plugin install .
|
|
10
|
+
|
|
11
|
+
# Or from GitHub
|
|
12
|
+
claude plugin install github:robertnowell/skill-tree
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Use
|
|
16
|
+
|
|
17
|
+
In any Claude Code session:
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
/skill-tree
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
This analyzes your conversation history (Claude Code + Cowork), classifies 11 AI collaboration behaviors, and opens a beautiful visualization in your browser.
|
|
24
|
+
|
|
25
|
+
## What You Get
|
|
26
|
+
|
|
27
|
+
**Character Card** — Your archetype (e.g., "The Architect"), tagline, signature strengths, and vibe description
|
|
28
|
+
|
|
29
|
+
**Skill Tree** — 11 behaviors across 4 branches, with animated bars showing your rate vs. population average:
|
|
30
|
+
- **Planning**: Goal clarity, approach consultation
|
|
31
|
+
- **Craft**: Iteration, examples, format, tone, audience
|
|
32
|
+
- **Judgment**: Context gap detection, reasoning challenges
|
|
33
|
+
- **Rigor**: Fact verification
|
|
34
|
+
|
|
35
|
+
**Growth Quest** — One specific, actionable challenge for your next session, injected via SessionStart hook
|
|
36
|
+
|
|
37
|
+
## Requirements
|
|
38
|
+
|
|
39
|
+
- Python 3.10+
|
|
40
|
+
- `anthropic` Python SDK (`pip3 install anthropic`)
|
|
41
|
+
- `ANTHROPIC_API_KEY` environment variable
|
|
42
|
+
|
|
43
|
+
## How It Works
|
|
44
|
+
|
|
45
|
+
1. Scans `~/.claude/projects/` and Cowork local sessions for conversation history
|
|
46
|
+
2. Extracts clean user messages (filters out tool results, pasted output, system messages)
|
|
47
|
+
3. Classifies 11 behaviors per session using Claude Haiku (~$0.005/session, cached)
|
|
48
|
+
4. Aggregates into a skill profile with population baseline comparisons
|
|
49
|
+
5. Generates a character archetype card
|
|
50
|
+
6. Renders a self-contained HTML visualization
|
|
51
|
+
|
|
52
|
+
## Research Basis
|
|
53
|
+
|
|
54
|
+
The 11 behaviors come from Anthropic's [AI Fluency Index](https://www.anthropic.com/research/AI-fluency-index) (Feb 2026), which analyzed 9,830 conversations using the [4D AI Fluency Framework](https://aifluencyframework.org/) (Delegation, Description, Discernment, Diligence). Population baselines are from that study.
|
|
55
|
+
|
|
56
|
+
## License
|
|
57
|
+
|
|
58
|
+
MIT
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
// Published population baselines from the AI Fluency Index (Feb 2026)
|
|
5
|
+
export const BASELINES = {
|
|
6
|
+
iterative_improvement: 0.857,
|
|
7
|
+
clarify_goals: 0.511,
|
|
8
|
+
show_examples: 0.411,
|
|
9
|
+
specify_format: 0.3,
|
|
10
|
+
set_interactive_mode: 0.3,
|
|
11
|
+
express_tone_style: 0.227,
|
|
12
|
+
identify_context_gaps: 0.203,
|
|
13
|
+
define_audience: 0.176,
|
|
14
|
+
question_reasoning: 0.158,
|
|
15
|
+
consult_approach: 0.101,
|
|
16
|
+
verify_facts: 0.087,
|
|
17
|
+
};
|
|
18
|
+
export const BEHAVIOR_LABELS = {
|
|
19
|
+
iterative_improvement: "Iterates on outputs",
|
|
20
|
+
clarify_goals: "Clarifies goals upfront",
|
|
21
|
+
show_examples: "Provides examples",
|
|
22
|
+
specify_format: "Specifies format",
|
|
23
|
+
set_interactive_mode: "Sets interaction style",
|
|
24
|
+
express_tone_style: "Expresses tone prefs",
|
|
25
|
+
identify_context_gaps: "Flags context gaps",
|
|
26
|
+
define_audience: "Defines audience",
|
|
27
|
+
question_reasoning: "Questions Claude's logic",
|
|
28
|
+
consult_approach: "Discusses approach first",
|
|
29
|
+
verify_facts: "Verifies facts",
|
|
30
|
+
};
|
|
31
|
+
// 3-axis system derived from artifact effect data (AI Fluency Index, Feb 2026)
|
|
32
|
+
// Specification behaviors INCREASE with polished artifacts
|
|
33
|
+
// Evaluation behaviors DECREASE with polished artifacts
|
|
34
|
+
// Setup behaviors are independent
|
|
35
|
+
export const AXES = {
|
|
36
|
+
Specification: {
|
|
37
|
+
behaviors: ["show_examples", "specify_format", "express_tone_style", "define_audience"],
|
|
38
|
+
color: "#cca67b",
|
|
39
|
+
description: "How you shape Claude's output",
|
|
40
|
+
baseline: 0.28, // avg of individual baselines
|
|
41
|
+
},
|
|
42
|
+
Evaluation: {
|
|
43
|
+
behaviors: ["identify_context_gaps", "question_reasoning", "verify_facts"],
|
|
44
|
+
color: "#7bcc96",
|
|
45
|
+
description: "How you assess Claude's reasoning",
|
|
46
|
+
baseline: 0.15,
|
|
47
|
+
},
|
|
48
|
+
Setup: {
|
|
49
|
+
behaviors: ["clarify_goals", "consult_approach", "set_interactive_mode"],
|
|
50
|
+
color: "#7ba3cc",
|
|
51
|
+
description: "How you set up the collaboration",
|
|
52
|
+
baseline: 0.30,
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
const CLASSIFIER_PROMPT = `You are analyzing a user's conversation with Claude to detect specific AI collaboration behaviors.
|
|
56
|
+
|
|
57
|
+
Given the user's messages from a single session, classify whether each of the following 11 behaviors is PRESENT or ABSENT. These behaviors come from the AI Fluency Framework.
|
|
58
|
+
|
|
59
|
+
For each behavior, respond with:
|
|
60
|
+
- present: true/false
|
|
61
|
+
- confidence: "high" or "low"
|
|
62
|
+
- evidence: A brief quote or description of the moment that triggered this classification (or "none" if absent)
|
|
63
|
+
|
|
64
|
+
THE 11 BEHAVIORS:
|
|
65
|
+
|
|
66
|
+
1. ITERATIVE_IMPROVEMENT: User builds on previous exchanges to refine work, rather than accepting Claude's first response.
|
|
67
|
+
2. CLARIFY_GOALS: User states their objective or context before requesting assistance.
|
|
68
|
+
3. SHOW_EXAMPLES: User provides examples of desired output, reference material, or illustrative samples.
|
|
69
|
+
4. SPECIFY_FORMAT: User states how output should be organized, formatted, or structured.
|
|
70
|
+
5. SET_INTERACTIVE_MODE: User requests Claude maintain a certain interaction style (e.g., "be concise", "ask me questions", "think step by step").
|
|
71
|
+
6. EXPRESS_TONE_STYLE: User specifies communication tone or writing style preferences.
|
|
72
|
+
7. IDENTIFY_CONTEXT_GAPS: User notes gaps in information Claude might need, or proactively provides context Claude is missing.
|
|
73
|
+
8. DEFINE_AUDIENCE: User specifies who the deliverable is for (e.g., "explain this to a 5-year-old", "this is for senior engineers").
|
|
74
|
+
9. QUESTION_REASONING: User challenges Claude's explanations, logic, or approach — pushes back when something seems wrong.
|
|
75
|
+
10. CONSULT_APPROACH: User asks Claude to review or discuss strategy/approach before starting execution.
|
|
76
|
+
11. VERIFY_FACTS: User fact-checks critical information Claude provides, or asks Claude to verify its own claims.
|
|
77
|
+
|
|
78
|
+
USER MESSAGES FROM THIS SESSION:
|
|
79
|
+
---
|
|
80
|
+
{MESSAGES}
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
Respond in valid JSON with this exact structure:
|
|
84
|
+
{
|
|
85
|
+
"behaviors": {
|
|
86
|
+
"iterative_improvement": {"present": true, "confidence": "high", "evidence": "..."},
|
|
87
|
+
"clarify_goals": {"present": true, "confidence": "high", "evidence": "..."},
|
|
88
|
+
"show_examples": {"present": false, "confidence": "high", "evidence": "none"},
|
|
89
|
+
"specify_format": {"present": false, "confidence": "high", "evidence": "none"},
|
|
90
|
+
"set_interactive_mode": {"present": false, "confidence": "high", "evidence": "none"},
|
|
91
|
+
"express_tone_style": {"present": false, "confidence": "high", "evidence": "none"},
|
|
92
|
+
"identify_context_gaps": {"present": false, "confidence": "high", "evidence": "none"},
|
|
93
|
+
"define_audience": {"present": false, "confidence": "high", "evidence": "none"},
|
|
94
|
+
"question_reasoning": {"present": false, "confidence": "high", "evidence": "none"},
|
|
95
|
+
"consult_approach": {"present": false, "confidence": "high", "evidence": "none"},
|
|
96
|
+
"verify_facts": {"present": false, "confidence": "high", "evidence": "none"}
|
|
97
|
+
},
|
|
98
|
+
"session_summary": "One sentence describing what this session was about"
|
|
99
|
+
}`;
|
|
100
|
+
function getCacheDir() {
|
|
101
|
+
const dir = join(homedir(), ".skill-tree", "cache");
|
|
102
|
+
if (!existsSync(dir)) {
|
|
103
|
+
mkdirSync(dir, { recursive: true });
|
|
104
|
+
}
|
|
105
|
+
return dir;
|
|
106
|
+
}
|
|
107
|
+
function getCachedClassification(sessionId) {
|
|
108
|
+
const cachePath = join(getCacheDir(), `${sessionId}.json`);
|
|
109
|
+
if (!existsSync(cachePath))
|
|
110
|
+
return null;
|
|
111
|
+
try {
|
|
112
|
+
return JSON.parse(readFileSync(cachePath, "utf-8"));
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
function cacheClassification(classification) {
|
|
119
|
+
const cachePath = join(getCacheDir(), `${classification.sessionId}.json`);
|
|
120
|
+
writeFileSync(cachePath, JSON.stringify(classification, null, 2));
|
|
121
|
+
}
|
|
122
|
+
export async function classifySession(client, sessionId, messages) {
|
|
123
|
+
const cached = getCachedClassification(sessionId);
|
|
124
|
+
if (cached)
|
|
125
|
+
return cached;
|
|
126
|
+
let messageText = messages
|
|
127
|
+
.map((m, i) => `[Message ${i + 1}]: ${m.text}`)
|
|
128
|
+
.join("\n\n---\n\n");
|
|
129
|
+
if (messageText.length > 10000) {
|
|
130
|
+
messageText = messageText.slice(0, 10000) + "\n\n[... truncated ...]";
|
|
131
|
+
}
|
|
132
|
+
const prompt = CLASSIFIER_PROMPT.replace("{MESSAGES}", messageText);
|
|
133
|
+
const response = await client.messages.create({
|
|
134
|
+
model: "claude-haiku-4-5-20251001",
|
|
135
|
+
max_tokens: 2000,
|
|
136
|
+
messages: [{ role: "user", content: prompt }],
|
|
137
|
+
});
|
|
138
|
+
const text = response.content[0].type === "text" ? response.content[0].text : "";
|
|
139
|
+
const jsonStart = text.indexOf("{");
|
|
140
|
+
const jsonEnd = text.lastIndexOf("}") + 1;
|
|
141
|
+
if (jsonStart < 0 || jsonEnd <= jsonStart) {
|
|
142
|
+
throw new Error(`Failed to parse classifier response: ${text.slice(0, 200)}`);
|
|
143
|
+
}
|
|
144
|
+
const parsed = JSON.parse(text.slice(jsonStart, jsonEnd));
|
|
145
|
+
const classification = {
|
|
146
|
+
sessionId,
|
|
147
|
+
behaviors: parsed.behaviors,
|
|
148
|
+
sessionSummary: parsed.session_summary || "Unknown session",
|
|
149
|
+
classifiedAt: new Date().toISOString(),
|
|
150
|
+
};
|
|
151
|
+
cacheClassification(classification);
|
|
152
|
+
return classification;
|
|
153
|
+
}
|
|
154
|
+
export async function classifySessions(client, sessions, maxNew = 50, onProgress) {
|
|
155
|
+
const results = [];
|
|
156
|
+
let newClassified = 0;
|
|
157
|
+
for (let i = 0; i < sessions.length; i++) {
|
|
158
|
+
const session = sessions[i];
|
|
159
|
+
const cached = getCachedClassification(session.sessionId);
|
|
160
|
+
if (cached) {
|
|
161
|
+
results.push(cached);
|
|
162
|
+
}
|
|
163
|
+
else if (newClassified < maxNew) {
|
|
164
|
+
const classification = await classifySession(client, session.sessionId, session.messages);
|
|
165
|
+
results.push(classification);
|
|
166
|
+
newClassified++;
|
|
167
|
+
}
|
|
168
|
+
onProgress?.(i + 1, sessions.length);
|
|
169
|
+
}
|
|
170
|
+
return results;
|
|
171
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { readFileSync, readdirSync, statSync, existsSync } from "node:fs";
|
|
2
|
+
import { join, basename } from "node:path";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
// Markers indicating pasted Claude Code terminal output (not genuine user input)
|
|
5
|
+
const PASTE_MARKERS = [
|
|
6
|
+
"▗ ▗",
|
|
7
|
+
"▘▘ ▝▝",
|
|
8
|
+
"⏺",
|
|
9
|
+
"⎿",
|
|
10
|
+
"✻ Brewed",
|
|
11
|
+
"✻ Sautéed",
|
|
12
|
+
"✻ Baked",
|
|
13
|
+
"ctrl+o to expand",
|
|
14
|
+
];
|
|
15
|
+
function isPastedOutput(text) {
|
|
16
|
+
const sample = text.slice(0, 500);
|
|
17
|
+
let markerCount = 0;
|
|
18
|
+
for (const marker of PASTE_MARKERS) {
|
|
19
|
+
if (sample.includes(marker))
|
|
20
|
+
markerCount++;
|
|
21
|
+
}
|
|
22
|
+
return markerCount >= 2;
|
|
23
|
+
}
|
|
24
|
+
export function extractUserMessages(jsonlPath) {
|
|
25
|
+
const messages = [];
|
|
26
|
+
const content = readFileSync(jsonlPath, "utf-8");
|
|
27
|
+
for (const line of content.split("\n")) {
|
|
28
|
+
if (!line.trim())
|
|
29
|
+
continue;
|
|
30
|
+
let obj;
|
|
31
|
+
try {
|
|
32
|
+
obj = JSON.parse(line);
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if (obj.type !== "user")
|
|
38
|
+
continue;
|
|
39
|
+
const msgContent = obj.message?.content;
|
|
40
|
+
// Handle string content (direct user input)
|
|
41
|
+
if (typeof msgContent === "string") {
|
|
42
|
+
if (isPastedOutput(msgContent))
|
|
43
|
+
continue;
|
|
44
|
+
const cleaned = msgContent.trim().slice(0, 2000);
|
|
45
|
+
if (cleaned.length > 10) {
|
|
46
|
+
messages.push({
|
|
47
|
+
text: cleaned,
|
|
48
|
+
rawLength: msgContent.length,
|
|
49
|
+
cleanedLength: cleaned.length,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// Skip list content (tool results)
|
|
54
|
+
}
|
|
55
|
+
return messages;
|
|
56
|
+
}
|
|
57
|
+
function findSessionFiles(baseDir, maxPerProject = 50) {
|
|
58
|
+
const files = [];
|
|
59
|
+
if (!existsSync(baseDir))
|
|
60
|
+
return files;
|
|
61
|
+
try {
|
|
62
|
+
const entries = readdirSync(baseDir);
|
|
63
|
+
for (const entry of entries) {
|
|
64
|
+
const entryPath = join(baseDir, entry);
|
|
65
|
+
try {
|
|
66
|
+
const stat = statSync(entryPath);
|
|
67
|
+
if (stat.isDirectory()) {
|
|
68
|
+
// This is a project directory — look for JSONL files inside
|
|
69
|
+
try {
|
|
70
|
+
const projectFiles = readdirSync(entryPath)
|
|
71
|
+
.filter((f) => f.endsWith(".jsonl"))
|
|
72
|
+
.map((f) => join(entryPath, f))
|
|
73
|
+
.filter((f) => {
|
|
74
|
+
try {
|
|
75
|
+
return statSync(f).size > 1000;
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
})
|
|
81
|
+
.sort((a, b) => {
|
|
82
|
+
try {
|
|
83
|
+
return statSync(b).mtimeMs - statSync(a).mtimeMs;
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return 0;
|
|
87
|
+
}
|
|
88
|
+
})
|
|
89
|
+
.slice(0, maxPerProject);
|
|
90
|
+
files.push(...projectFiles);
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
// Skip unreadable project dirs
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
else if (entry.endsWith(".jsonl") && stat.size > 1000) {
|
|
97
|
+
files.push(entryPath);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
// Base dir not readable
|
|
107
|
+
}
|
|
108
|
+
return files;
|
|
109
|
+
}
|
|
110
|
+
function findCoworkSessions(maxSessions = 50) {
|
|
111
|
+
const coworkBase = join(homedir(), "Library", "Application Support", "Claude", "local-agent-mode-sessions");
|
|
112
|
+
if (!existsSync(coworkBase))
|
|
113
|
+
return [];
|
|
114
|
+
const files = [];
|
|
115
|
+
function walkDir(dir) {
|
|
116
|
+
try {
|
|
117
|
+
for (const entry of readdirSync(dir)) {
|
|
118
|
+
const fullPath = join(dir, entry);
|
|
119
|
+
try {
|
|
120
|
+
const stat = statSync(fullPath);
|
|
121
|
+
if (stat.isDirectory()) {
|
|
122
|
+
walkDir(fullPath);
|
|
123
|
+
}
|
|
124
|
+
else if (entry.endsWith(".jsonl") && stat.size > 1000) {
|
|
125
|
+
files.push(fullPath);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
// Skip unreadable dirs
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
walkDir(coworkBase);
|
|
138
|
+
return files
|
|
139
|
+
.sort((a, b) => {
|
|
140
|
+
try {
|
|
141
|
+
return statSync(b).mtimeMs - statSync(a).mtimeMs;
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
return 0;
|
|
145
|
+
}
|
|
146
|
+
})
|
|
147
|
+
.slice(0, maxSessions);
|
|
148
|
+
}
|
|
149
|
+
export function findAllSessions(maxSessions = 100) {
|
|
150
|
+
const claudeCodeBase = join(homedir(), ".claude", "projects");
|
|
151
|
+
const claudeCodeFiles = findSessionFiles(claudeCodeBase);
|
|
152
|
+
const coworkFiles = findCoworkSessions();
|
|
153
|
+
const allFiles = [...claudeCodeFiles, ...coworkFiles]
|
|
154
|
+
.sort((a, b) => {
|
|
155
|
+
try {
|
|
156
|
+
return statSync(b).mtimeMs - statSync(a).mtimeMs;
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
return 0;
|
|
160
|
+
}
|
|
161
|
+
})
|
|
162
|
+
.slice(0, maxSessions);
|
|
163
|
+
const sessions = [];
|
|
164
|
+
for (const filePath of allFiles) {
|
|
165
|
+
const messages = extractUserMessages(filePath);
|
|
166
|
+
if (messages.length === 0)
|
|
167
|
+
continue;
|
|
168
|
+
const sessionId = basename(filePath, ".jsonl");
|
|
169
|
+
const project = basename(filePath.includes("local-agent-mode") ? filePath : join(filePath, ".."));
|
|
170
|
+
sessions.push({
|
|
171
|
+
sessionId: sessionId.slice(0, 8),
|
|
172
|
+
project,
|
|
173
|
+
path: filePath,
|
|
174
|
+
messages,
|
|
175
|
+
fileSize: statSync(filePath).size,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
return sessions;
|
|
179
|
+
}
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Aggregate per-session classifications into a skill profile and assign archetype.
|
|
3
|
+
*
|
|
4
|
+
* The archetype system is DETERMINISTIC — no LLM call needed.
|
|
5
|
+
* Uses three axes derived from the artifact effect data in the AI Fluency Index:
|
|
6
|
+
* Specification (Description): behaviors that INCREASE with polished artifacts
|
|
7
|
+
* Evaluation (Discernment): behaviors that DECREASE with polished artifacts
|
|
8
|
+
* Setup (Delegation): behaviors independent of artifact effect
|
|
9
|
+
*
|
|
10
|
+
* Research basis:
|
|
11
|
+
* - Artifact effect: Anthropic AI Fluency Index (Feb 2026)
|
|
12
|
+
* - Three-tier clustering: "Nested Skills in Labor Ecosystems" (Nature Human Behaviour, 2025)
|
|
13
|
+
* - Parallel dimensions: AI literacy systematic review (PMC, 2024, 16 scales)
|
|
14
|
+
*/
|
|
15
|
+
import { BASELINES, BEHAVIOR_LABELS, AXES, } from "./classify.js";
|
|
16
|
+
// ─── Axis behavior groupings ───────────────────────────────────────────────
|
|
17
|
+
const SPEC_BEHAVIORS = AXES.Specification.behaviors;
|
|
18
|
+
const EVAL_BEHAVIORS = AXES.Evaluation.behaviors;
|
|
19
|
+
const SETUP_BEHAVIORS = AXES.Setup.behaviors;
|
|
20
|
+
const SPEC_BASELINE = AXES.Specification.baseline;
|
|
21
|
+
const EVAL_BASELINE = AXES.Evaluation.baseline;
|
|
22
|
+
const SETUP_BASELINE = AXES.Setup.baseline;
|
|
23
|
+
// ─── The 7 Archetypes ──────────────────────────────────────────────────────
|
|
24
|
+
export const ARCHETYPES = {
|
|
25
|
+
polymath: {
|
|
26
|
+
name: "The Polymath",
|
|
27
|
+
tagline: "You shape the output AND hold it to account — the rarest combination.",
|
|
28
|
+
description: "You've developed fluency across both specification and evaluation — " +
|
|
29
|
+
"the two dimensions that most users specialize in, not combine. " +
|
|
30
|
+
"The data shows these skills are anti-correlated in practice (the artifact effect), " +
|
|
31
|
+
"which makes your balance genuinely unusual.",
|
|
32
|
+
superpower: "Adaptive fluency — you shift between shaping and scrutinizing as the moment demands",
|
|
33
|
+
growth_unlock: "Deepen your rarest axis — even polymaths have a frontier",
|
|
34
|
+
growth_quest: "In your next session, find the one moment where you'd normally accept output " +
|
|
35
|
+
"and instead push deeper — ask 'why this approach and not another?'",
|
|
36
|
+
target_archetype: null,
|
|
37
|
+
color: "#a08040",
|
|
38
|
+
accent: "#e8d8b0",
|
|
39
|
+
glyph: "polymath",
|
|
40
|
+
},
|
|
41
|
+
conductor: {
|
|
42
|
+
name: "The Conductor",
|
|
43
|
+
tagline: "You set the destination AND specify every detail of the journey.",
|
|
44
|
+
description: "You orchestrate Claude like an ensemble — clear goals upfront, " +
|
|
45
|
+
"then precise specifications for format, examples, and tone. " +
|
|
46
|
+
"You combine the Compass's direction with the Forgemaster's craft.",
|
|
47
|
+
superpower: "Full-spectrum direction — Claude performs at its peak because you brief both the what and the how",
|
|
48
|
+
growth_unlock: "Unlock The Polymath's edge — add evaluation to your orchestration",
|
|
49
|
+
growth_quest: "Next session, after Claude delivers what you specified, " +
|
|
50
|
+
"pick one claim and ask: 'How confident are you in this? What might be wrong?'",
|
|
51
|
+
target_archetype: "polymath",
|
|
52
|
+
color: "#8b3a62",
|
|
53
|
+
accent: "#d4a0b8",
|
|
54
|
+
glyph: "conductor",
|
|
55
|
+
},
|
|
56
|
+
architect: {
|
|
57
|
+
name: "The Architect",
|
|
58
|
+
tagline: "You design the collaboration AND inspect every load-bearing wall.",
|
|
59
|
+
description: "You combine strategic setup with critical evaluation — " +
|
|
60
|
+
"you set clear goals, discuss approach, and then scrutinize " +
|
|
61
|
+
"what Claude produces. You catch what others miss because " +
|
|
62
|
+
"you know what you were looking for.",
|
|
63
|
+
superpower: "Strategic scrutiny — your evaluation is targeted because your setup was intentional",
|
|
64
|
+
growth_unlock: "Unlock The Polymath's craft — add specification to your evaluation",
|
|
65
|
+
growth_quest: "Next session, try showing Claude an example of exactly what you want " +
|
|
66
|
+
"before you start evaluating what it gives you.",
|
|
67
|
+
target_archetype: "polymath",
|
|
68
|
+
color: "#5a6a7a",
|
|
69
|
+
accent: "#b0c0d0",
|
|
70
|
+
glyph: "architect",
|
|
71
|
+
},
|
|
72
|
+
forgemaster: {
|
|
73
|
+
name: "The Forgemaster",
|
|
74
|
+
tagline: "Every response from Claude is raw material — you shape it until it gleams.",
|
|
75
|
+
description: "You specify what you want with precision: examples of desired output, " +
|
|
76
|
+
"format requirements, tone preferences, audience awareness. " +
|
|
77
|
+
"You transform generic responses into exactly what you envisioned.",
|
|
78
|
+
superpower: "Output alchemy — you turn Claude's first draft into your final vision through pure craft",
|
|
79
|
+
growth_unlock: "Unlock The Conductor's vision — add strategic setup to your craft",
|
|
80
|
+
growth_quest: "Next session, before specifying details, try stating your overall goal " +
|
|
81
|
+
"and asking Claude to propose an approach first.",
|
|
82
|
+
target_archetype: "conductor",
|
|
83
|
+
color: "#b8860b",
|
|
84
|
+
accent: "#e8d4a8",
|
|
85
|
+
glyph: "forgemaster",
|
|
86
|
+
},
|
|
87
|
+
illuminator: {
|
|
88
|
+
name: "The Illuminator",
|
|
89
|
+
tagline: "You see what others skip — the gap, the assumption, the flaw in the logic.",
|
|
90
|
+
description: "You evaluate Claude's output critically: identifying missing context, " +
|
|
91
|
+
"questioning reasoning, verifying claims. In a world where polished AI output " +
|
|
92
|
+
"makes most users less critical (the artifact effect), you resist that pull.",
|
|
93
|
+
superpower: "Pattern recognition — you catch what Claude misses before it compounds",
|
|
94
|
+
growth_unlock: "Unlock The Architect's structure — add deliberate setup to your evaluation",
|
|
95
|
+
growth_quest: "Next session, try setting a clear goal and interaction mode before you start " +
|
|
96
|
+
"evaluating — give Claude the context to get closer on the first try.",
|
|
97
|
+
target_archetype: "architect",
|
|
98
|
+
color: "#3d8a5a",
|
|
99
|
+
accent: "#a8d4b8",
|
|
100
|
+
glyph: "illuminator",
|
|
101
|
+
},
|
|
102
|
+
compass: {
|
|
103
|
+
name: "The Compass",
|
|
104
|
+
tagline: "You always know where you're going — and trust Claude to find the best path.",
|
|
105
|
+
description: "You set up collaborations deliberately: clear goals, discussed approach, " +
|
|
106
|
+
"defined interaction style. You delegate effectively because you brief well. " +
|
|
107
|
+
"You don't micromanage the output — you set the destination and let Claude drive.",
|
|
108
|
+
superpower: "Strategic clarity — Claude always knows what you need before the first word is written",
|
|
109
|
+
growth_unlock: "Unlock The Conductor's precision — add output specifications to your direction",
|
|
110
|
+
growth_quest: "Next session, after stating your goal, try adding: 'Here's an example of " +
|
|
111
|
+
"what good output looks like' or 'Format this as...'",
|
|
112
|
+
target_archetype: "conductor",
|
|
113
|
+
color: "#4a7ba3",
|
|
114
|
+
accent: "#a3c4db",
|
|
115
|
+
glyph: "compass",
|
|
116
|
+
},
|
|
117
|
+
catalyst: {
|
|
118
|
+
name: "The Catalyst",
|
|
119
|
+
tagline: "You move at the speed of intuition — fast, iterative, unencumbered.",
|
|
120
|
+
description: "You work with Claude through pure momentum: ask, iterate, refine, ship. " +
|
|
121
|
+
"You don't over-specify or over-question — you trust the loop. " +
|
|
122
|
+
"This is a valid, powerful collaboration style, especially for rapid exploration.",
|
|
123
|
+
superpower: "Rapid transmutation — first drafts become final product through sheer iteration speed",
|
|
124
|
+
growth_unlock: "Unlock The Compass's direction — try stating your destination before launching",
|
|
125
|
+
growth_quest: "Next session, try opening with one sentence: 'My goal is [X] because [Y].' " +
|
|
126
|
+
"See how it changes what Claude gives you on the first try.",
|
|
127
|
+
target_archetype: "compass",
|
|
128
|
+
color: "#d4584a",
|
|
129
|
+
accent: "#e8a090",
|
|
130
|
+
glyph: "catalyst",
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
// ─── Deterministic archetype assignment ────────────────────────────────────
|
|
134
|
+
export function determineArchetype(behaviors) {
|
|
135
|
+
function axisAvg(keys) {
|
|
136
|
+
const rates = keys.map((k) => behaviors[k]?.rate ?? 0);
|
|
137
|
+
return rates.reduce((a, b) => a + b, 0) / rates.length;
|
|
138
|
+
}
|
|
139
|
+
const spec = axisAvg(SPEC_BEHAVIORS);
|
|
140
|
+
const eval_ = axisAvg(EVAL_BEHAVIORS);
|
|
141
|
+
const setup = axisAvg(SETUP_BEHAVIORS);
|
|
142
|
+
const specHigh = spec > SPEC_BASELINE;
|
|
143
|
+
const evalHigh = eval_ > EVAL_BASELINE;
|
|
144
|
+
const setupHigh = setup > SETUP_BASELINE;
|
|
145
|
+
// Priority order: most specific combination first
|
|
146
|
+
if (specHigh && evalHigh)
|
|
147
|
+
return "polymath";
|
|
148
|
+
if (specHigh && setupHigh)
|
|
149
|
+
return "conductor";
|
|
150
|
+
if (evalHigh && setupHigh)
|
|
151
|
+
return "architect";
|
|
152
|
+
if (specHigh)
|
|
153
|
+
return "forgemaster";
|
|
154
|
+
if (evalHigh)
|
|
155
|
+
return "illuminator";
|
|
156
|
+
if (setupHigh)
|
|
157
|
+
return "compass";
|
|
158
|
+
return "catalyst";
|
|
159
|
+
}
|
|
160
|
+
// ─── Build profile ─────────────────────────────────────────────────────────
|
|
161
|
+
export function buildProfile(classifications, previousProfile) {
|
|
162
|
+
const total = classifications.length;
|
|
163
|
+
// Compute per-behavior stats
|
|
164
|
+
const behaviors = {};
|
|
165
|
+
for (const key of Object.keys(BASELINES)) {
|
|
166
|
+
const presentCount = classifications.filter((c) => c.behaviors[key]?.present).length;
|
|
167
|
+
const rate = total > 0 ? presentCount / total : 0;
|
|
168
|
+
const baseline = BASELINES[key];
|
|
169
|
+
const evidence = [];
|
|
170
|
+
for (const c of classifications) {
|
|
171
|
+
const b = c.behaviors[key];
|
|
172
|
+
if (b?.present && b.evidence && b.evidence !== "none") {
|
|
173
|
+
evidence.push({
|
|
174
|
+
text: b.evidence,
|
|
175
|
+
session_id: c.sessionId,
|
|
176
|
+
project: "",
|
|
177
|
+
});
|
|
178
|
+
if (evidence.length >= 3)
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
behaviors[key] = {
|
|
183
|
+
label: BEHAVIOR_LABELS[key],
|
|
184
|
+
rate: Math.round(rate * 1000) / 1000,
|
|
185
|
+
baseline,
|
|
186
|
+
above_baseline: rate > baseline,
|
|
187
|
+
present_count: presentCount,
|
|
188
|
+
total_sessions: total,
|
|
189
|
+
evidence,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
// Compute axis/branch scores
|
|
193
|
+
const branches = {};
|
|
194
|
+
for (const [axisName, axisDef] of Object.entries(AXES)) {
|
|
195
|
+
const rates = axisDef.behaviors.map((k) => behaviors[k].rate);
|
|
196
|
+
const baselines = axisDef.behaviors.map((k) => BASELINES[k]);
|
|
197
|
+
const score = rates.reduce((a, b) => a + b, 0) / rates.length;
|
|
198
|
+
const baseline = baselines.reduce((a, b) => a + b, 0) / baselines.length;
|
|
199
|
+
branches[axisName] = {
|
|
200
|
+
score: Math.round(score * 1000) / 1000,
|
|
201
|
+
baseline: Math.round(baseline * 1000) / 1000,
|
|
202
|
+
above_baseline: score > baseline,
|
|
203
|
+
color: axisDef.color,
|
|
204
|
+
description: axisDef.description,
|
|
205
|
+
behaviors: axisDef.behaviors,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
// Deterministic archetype assignment (no LLM call)
|
|
209
|
+
const archetypeKey = determineArchetype(behaviors);
|
|
210
|
+
const archetypeData = ARCHETYPES[archetypeKey];
|
|
211
|
+
function axisAvg(keys) {
|
|
212
|
+
const rates = keys.map((k) => behaviors[k]?.rate ?? 0);
|
|
213
|
+
return Math.round((rates.reduce((a, b) => a + b, 0) / rates.length) * 1000) / 1000;
|
|
214
|
+
}
|
|
215
|
+
const archetype = {
|
|
216
|
+
...archetypeData,
|
|
217
|
+
key: archetypeKey,
|
|
218
|
+
axis_scores: {
|
|
219
|
+
specification: axisAvg(SPEC_BEHAVIORS),
|
|
220
|
+
evaluation: axisAvg(EVAL_BEHAVIORS),
|
|
221
|
+
setup: axisAvg(SETUP_BEHAVIORS),
|
|
222
|
+
},
|
|
223
|
+
};
|
|
224
|
+
// Growth edge: behavior with largest negative gap vs baseline
|
|
225
|
+
let growthEdgeKey = Object.keys(BASELINES)[0];
|
|
226
|
+
let worstGap = Infinity;
|
|
227
|
+
for (const key of Object.keys(BASELINES)) {
|
|
228
|
+
const gap = behaviors[key].rate - BASELINES[key];
|
|
229
|
+
if (gap < worstGap) {
|
|
230
|
+
worstGap = gap;
|
|
231
|
+
growthEdgeKey = key;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
const profile = {
|
|
235
|
+
total_sessions: total,
|
|
236
|
+
behaviors,
|
|
237
|
+
branches,
|
|
238
|
+
archetype,
|
|
239
|
+
growth_edge: {
|
|
240
|
+
behavior: growthEdgeKey,
|
|
241
|
+
label: BEHAVIOR_LABELS[growthEdgeKey],
|
|
242
|
+
rate: behaviors[growthEdgeKey].rate,
|
|
243
|
+
baseline: BASELINES[growthEdgeKey],
|
|
244
|
+
gap: Math.round(worstGap * 1000) / 1000,
|
|
245
|
+
},
|
|
246
|
+
generated_at: new Date().toISOString(),
|
|
247
|
+
};
|
|
248
|
+
// Track progress from previous profile
|
|
249
|
+
if (previousProfile) {
|
|
250
|
+
profile.previous_archetype = previousProfile.archetype.key;
|
|
251
|
+
profile.previous_generated_at = previousProfile.generated_at;
|
|
252
|
+
}
|
|
253
|
+
return profile;
|
|
254
|
+
}
|