@aman_asmuei/aman-agent 0.27.0 → 0.28.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/dist/index.js CHANGED
@@ -1,3 +1,337 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropNames = Object.getOwnPropertyNames;
3
+ var __esm = (fn, res) => function __init() {
4
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
5
+ };
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+
11
+ // src/user-model.ts
12
+ var user_model_exports = {};
13
+ __export(user_model_exports, {
14
+ aggregateSession: () => aggregateSession,
15
+ computeProfile: () => computeProfile,
16
+ createEmptyModel: () => createEmptyModel,
17
+ defaultModelPath: () => defaultModelPath,
18
+ feedForward: () => feedForward,
19
+ loadUserModel: () => loadUserModel,
20
+ predictBurnout: () => predictBurnout,
21
+ saveUserModel: () => saveUserModel
22
+ });
23
+ import fs13 from "fs/promises";
24
+ import path13 from "path";
25
+ import os12 from "os";
26
+ function defaultModelPath() {
27
+ return path13.join(os12.homedir(), ".acore", "user-model.json");
28
+ }
29
+ function createEmptyModel() {
30
+ const now = (/* @__PURE__ */ new Date()).toISOString();
31
+ return {
32
+ version: 1,
33
+ sessions: [],
34
+ profile: emptyProfile(),
35
+ createdAt: now,
36
+ updatedAt: now
37
+ };
38
+ }
39
+ function emptyProfile() {
40
+ return {
41
+ trustScore: 0.5,
42
+ trustTrajectory: "stable",
43
+ totalSessions: 0,
44
+ preferredTimePeriod: "afternoon",
45
+ energyDistribution: {},
46
+ avgSessionMinutes: 0,
47
+ baselineFrustration: 0,
48
+ baselineExcitement: 0,
49
+ sentimentTrend: "stable",
50
+ frustrationCorrelations: { toolErrors: 0, longSessions: 0, lateNight: 0 },
51
+ avgTurnsPerSession: 0,
52
+ engagementTrend: "stable",
53
+ nudgeStats: {}
54
+ };
55
+ }
56
+ async function loadUserModel(filePath) {
57
+ const fp = filePath ?? defaultModelPath();
58
+ try {
59
+ const raw = await fs13.readFile(fp, "utf-8");
60
+ const parsed = JSON.parse(raw);
61
+ if (parsed?.version !== 1) return null;
62
+ return parsed;
63
+ } catch {
64
+ return null;
65
+ }
66
+ }
67
+ async function saveUserModel(model, filePath) {
68
+ const fp = filePath ?? defaultModelPath();
69
+ const dir = path13.dirname(fp);
70
+ await fs13.mkdir(dir, { recursive: true });
71
+ const tmp = fp + `.tmp-${Date.now()}`;
72
+ await fs13.writeFile(tmp, JSON.stringify(model, null, 2), "utf-8");
73
+ await fs13.rename(tmp, fp);
74
+ }
75
+ function aggregateSession(model, snapshot) {
76
+ const sessions = [...model.sessions, snapshot];
77
+ while (sessions.length > MAX_SESSIONS) {
78
+ sessions.shift();
79
+ }
80
+ const totalSessions = model.profile.totalSessions + 1;
81
+ const profile = computeProfile(sessions, totalSessions);
82
+ return {
83
+ ...model,
84
+ sessions,
85
+ profile,
86
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
87
+ };
88
+ }
89
+ function computeProfile(sessions, totalSessions) {
90
+ if (sessions.length === 0) return { ...emptyProfile(), totalSessions };
91
+ const n = sessions.length;
92
+ let trustScore = 0.5;
93
+ for (const s of sessions) {
94
+ trustScore = TRUST_ALPHA * ratingSignal(s) + (1 - TRUST_ALPHA) * trustScore;
95
+ }
96
+ const trustTrajectory = computeTrustTrajectory(sessions);
97
+ const baselineFrustration = avg(sessions.map((s) => s.avgFrustration));
98
+ const baselineExcitement = avg(sessions.map((s) => s.avgExcitement));
99
+ const sentimentTrend = computeSentimentTrend(sessions);
100
+ const energyDistribution = {};
101
+ for (const s of sessions) {
102
+ energyDistribution[s.timePeriod] = (energyDistribution[s.timePeriod] || 0) + 1;
103
+ }
104
+ const preferredTimePeriod = Object.entries(energyDistribution).sort(
105
+ (a, b) => b[1] - a[1]
106
+ )[0]?.[0] ?? "afternoon";
107
+ const avgSessionMinutes = avg(sessions.map((s) => s.durationMinutes));
108
+ const avgTurnsPerSession = avg(sessions.map((s) => s.turnCount));
109
+ const engagementTrend = computeLinearTrend(sessions.map((s) => s.turnCount));
110
+ const frustrationCorrelations = n >= MIN_SESSIONS_FOR_CORRELATIONS ? {
111
+ toolErrors: pearsonR(
112
+ sessions.map((s) => s.avgFrustration),
113
+ sessions.map((s) => s.toolErrors)
114
+ ),
115
+ longSessions: pearsonR(
116
+ sessions.map((s) => s.avgFrustration),
117
+ sessions.map((s) => s.durationMinutes)
118
+ ),
119
+ lateNight: pearsonR(
120
+ sessions.map((s) => s.avgFrustration),
121
+ sessions.map((s) => s.timePeriod === "late-night" || s.timePeriod === "night" ? 1 : 0)
122
+ )
123
+ } : { toolErrors: 0, longSessions: 0, lateNight: 0 };
124
+ const nudgeStats = {};
125
+ for (const s of sessions) {
126
+ const ratingVal = ratingToNumber(s.rating);
127
+ for (const nudge of s.wellbeingNudges) {
128
+ if (!nudgeStats[nudge]) nudgeStats[nudge] = { fired: 0, sessionRatingAfter: 0 };
129
+ nudgeStats[nudge].fired++;
130
+ nudgeStats[nudge].sessionRatingAfter += ratingVal;
131
+ }
132
+ }
133
+ for (const key of Object.keys(nudgeStats)) {
134
+ if (nudgeStats[key].fired > 0) {
135
+ nudgeStats[key].sessionRatingAfter /= nudgeStats[key].fired;
136
+ }
137
+ }
138
+ return {
139
+ trustScore,
140
+ trustTrajectory,
141
+ totalSessions,
142
+ preferredTimePeriod,
143
+ energyDistribution,
144
+ avgSessionMinutes,
145
+ baselineFrustration,
146
+ baselineExcitement,
147
+ sentimentTrend,
148
+ frustrationCorrelations,
149
+ avgTurnsPerSession,
150
+ engagementTrend,
151
+ nudgeStats
152
+ };
153
+ }
154
+ function feedForward(model) {
155
+ if (model.profile.totalSessions < MIN_SESSIONS_FOR_FEED_FORWARD) return null;
156
+ const p4 = model.profile;
157
+ const overrides = {
158
+ compactGreeting: false,
159
+ frustrationNudgeThreshold: 0.6,
160
+ defaultToPersonalMode: false
161
+ };
162
+ const nightSessions = (p4.energyDistribution["late-night"] || 0) + (p4.energyDistribution["night"] || 0);
163
+ const totalInWindow = model.sessions.length;
164
+ if (totalInWindow > 0 && nightSessions / totalInWindow >= 0.7 && p4.baselineFrustration < 0.3) {
165
+ overrides.energyOverride = "steady";
166
+ }
167
+ if (p4.trustScore > 0.8) {
168
+ overrides.compactGreeting = true;
169
+ }
170
+ if (p4.frustrationCorrelations.toolErrors > 0.4) {
171
+ overrides.frustrationNudgeThreshold = 0.4;
172
+ }
173
+ if (p4.sentimentTrend === "worsening") {
174
+ overrides.defaultToPersonalMode = true;
175
+ }
176
+ return overrides;
177
+ }
178
+ function predictBurnout(sessions, currentSession) {
179
+ const recent = sessions.slice(-7);
180
+ if (recent.length < 3) {
181
+ return { risk: 0, factors: [] };
182
+ }
183
+ const factors = [];
184
+ let risk = 0;
185
+ const mid = Math.floor(recent.length / 2);
186
+ const firstHalf = recent.slice(0, mid);
187
+ const secondHalf = recent.slice(mid);
188
+ const avgFrustFirst = avg(firstHalf.map((s) => s.avgFrustration));
189
+ const avgFrustSecond = avg(secondHalf.map((s) => s.avgFrustration));
190
+ if (avgFrustSecond > avgFrustFirst + 0.1 && avgFrustSecond > 0.4) {
191
+ risk += 0.25;
192
+ factors.push("rising frustration trend");
193
+ }
194
+ const ratings = recent.filter((s) => s.rating).map((s) => ratingSignal(s));
195
+ if (ratings.length >= 3) {
196
+ const lastThree = ratings.slice(-3);
197
+ const avgLast3 = avg(lastThree);
198
+ if (avgLast3 < 0.5) {
199
+ risk += 0.2;
200
+ factors.push("low recent ratings");
201
+ }
202
+ }
203
+ const avgMins = avg(recent.map((s) => s.durationMinutes));
204
+ if (avgMins > 90) {
205
+ risk += 0.15;
206
+ factors.push("consistently long sessions");
207
+ }
208
+ const lateNightCount = recent.filter((s) => s.timePeriod === "late-night" || s.timePeriod === "night").length;
209
+ if (lateNightCount / recent.length > 0.5) {
210
+ risk += 0.15;
211
+ factors.push("frequent late-night sessions");
212
+ }
213
+ const avgBlockers = avg(recent.map((s) => s.blockers));
214
+ if (avgBlockers > 1) {
215
+ risk += 0.15;
216
+ factors.push("frequent blockers");
217
+ }
218
+ if (currentSession) {
219
+ if (currentSession.minutes > 120 && currentSession.frustration > 0.5) {
220
+ risk += 0.1;
221
+ factors.push("current session: long + frustrated");
222
+ }
223
+ }
224
+ risk = clamp(risk, 0, 1);
225
+ let recommendation;
226
+ if (risk > 0.7) {
227
+ recommendation = "Consider taking a longer break. You've been pushing hard \u2014 rest is productive too.";
228
+ } else if (risk > 0.5) {
229
+ recommendation = "Watch for signs of fatigue. A change of pace or shorter sessions might help.";
230
+ }
231
+ return { risk, factors, recommendation };
232
+ }
233
+ function clamp(val, min, max) {
234
+ return Math.max(min, Math.min(max, val));
235
+ }
236
+ function avg(values) {
237
+ if (values.length === 0) return 0;
238
+ return values.reduce((sum, v) => sum + v, 0) / values.length;
239
+ }
240
+ function ratingSignal(session) {
241
+ if (session.rating === "great") return 1;
242
+ if (session.rating === "good") return 0.75;
243
+ if (session.rating === "okay") return 0.5;
244
+ if (session.rating === "frustrating") return 0.25;
245
+ let implicit = 1;
246
+ implicit -= session.avgFrustration * 0.4;
247
+ implicit -= session.toolErrors > 3 ? 0.2 : 0;
248
+ implicit -= session.blockers > 2 ? 0.2 : 0;
249
+ implicit += session.milestones > 0 ? 0.1 : 0;
250
+ return clamp(implicit, 0, 1);
251
+ }
252
+ function ratingToNumber(rating) {
253
+ if (rating === "great") return 1;
254
+ if (rating === "good") return 0.75;
255
+ if (rating === "okay") return 0.5;
256
+ if (rating === "frustrating") return 0.25;
257
+ return 0.5;
258
+ }
259
+ function pearsonR(x, y) {
260
+ const n = x.length;
261
+ if (n < 3) return 0;
262
+ const mx = avg(x);
263
+ const my = avg(y);
264
+ let num = 0;
265
+ let dx2 = 0;
266
+ let dy2 = 0;
267
+ for (let i = 0; i < n; i++) {
268
+ const dx = x[i] - mx;
269
+ const dy = y[i] - my;
270
+ num += dx * dy;
271
+ dx2 += dx * dx;
272
+ dy2 += dy * dy;
273
+ }
274
+ const denom = Math.sqrt(dx2 * dy2);
275
+ if (denom === 0) return 0;
276
+ return num / denom;
277
+ }
278
+ function computeTrustTrajectory(sessions) {
279
+ if (sessions.length < 10) return "stable";
280
+ const recent5 = sessions.slice(-5).map(ratingSignal);
281
+ const prev5 = sessions.slice(-10, -5).map(ratingSignal);
282
+ const recentAvg = avg(recent5);
283
+ const prevAvg = avg(prev5);
284
+ const delta = recentAvg - prevAvg;
285
+ if (delta > 0.1) return "ascending";
286
+ if (delta < -0.1) return "declining";
287
+ return "stable";
288
+ }
289
+ function computeSentimentTrend(sessions) {
290
+ if (sessions.length < 5) return "stable";
291
+ const frustrations = sessions.slice(-10).map((s) => s.avgFrustration);
292
+ const slope = linearSlope(frustrations);
293
+ if (slope > 0.02) return "worsening";
294
+ if (slope < -0.02) return "improving";
295
+ return "stable";
296
+ }
297
+ function computeLinearTrend(values) {
298
+ if (values.length < 5) return "stable";
299
+ const recent = values.slice(-10);
300
+ const slope = linearSlope(recent);
301
+ const mean = avg(recent);
302
+ const relativeSlope = mean > 0 ? slope / mean : slope;
303
+ if (relativeSlope > 0.03) return "increasing";
304
+ if (relativeSlope < -0.03) return "decreasing";
305
+ return "stable";
306
+ }
307
+ function linearSlope(values) {
308
+ const n = values.length;
309
+ if (n < 2) return 0;
310
+ let sumX = 0;
311
+ let sumY = 0;
312
+ let sumXY = 0;
313
+ let sumX2 = 0;
314
+ for (let i = 0; i < n; i++) {
315
+ sumX += i;
316
+ sumY += values[i];
317
+ sumXY += i * values[i];
318
+ sumX2 += i * i;
319
+ }
320
+ const denom = n * sumX2 - sumX * sumX;
321
+ if (denom === 0) return 0;
322
+ return (n * sumXY - sumX * sumY) / denom;
323
+ }
324
+ var MAX_SESSIONS, TRUST_ALPHA, MIN_SESSIONS_FOR_FEED_FORWARD, MIN_SESSIONS_FOR_CORRELATIONS;
325
+ var init_user_model = __esm({
326
+ "src/user-model.ts"() {
327
+ "use strict";
328
+ MAX_SESSIONS = 30;
329
+ TRUST_ALPHA = 0.3;
330
+ MIN_SESSIONS_FOR_FEED_FORWARD = 5;
331
+ MIN_SESSIONS_FOR_CORRELATIONS = 10;
332
+ }
333
+ });
334
+
1
335
  // src/index.ts
2
336
  import { Command } from "commander";
3
337
  import * as p3 from "@clack/prompts";
@@ -2518,12 +2852,25 @@ The user seems tired. Keep responses concise and to the point. If they mention w
2518
2852
  </wellbeing>`,
2519
2853
  "break-long-session": `<wellbeing>
2520
2854
  This session has been running for over 2 hours. If there's a natural moment, a brief mention that a short break might help maintain focus is fine. Once is enough.
2855
+ </wellbeing>`,
2856
+ "burnout-warning": `<wellbeing>
2857
+ Recent patterns suggest the user may be approaching burnout \u2014 rising frustration, declining satisfaction, long or late sessions. Be extra mindful: keep responses concise, celebrate small wins, gently suggest breaks or scope reduction. Don't mention burnout directly unless they bring it up.
2521
2858
  </wellbeing>`
2522
2859
  };
2523
2860
  function formatWellbeingNudge(state) {
2524
2861
  if (!state.wellbeingNudge) return null;
2525
2862
  return WELLBEING_NUDGES[state.wellbeingNudge] || null;
2526
2863
  }
2864
+ function shouldFireNudge(nudgeType, profile) {
2865
+ if (!profile) return true;
2866
+ const stats = profile.nudgeStats[nudgeType];
2867
+ if (!stats || stats.fired < 5) return true;
2868
+ if (stats.sessionRatingAfter < 0.4) {
2869
+ log.debug("personality", `suppressing nudge "${nudgeType}" \u2014 low avg rating ${stats.sessionRatingAfter.toFixed(2)} after ${stats.fired} fires`);
2870
+ return false;
2871
+ }
2872
+ return true;
2873
+ }
2527
2874
  async function syncPersonalityToCore(state, mcpManager, modelMetrics) {
2528
2875
  try {
2529
2876
  const payload = {
@@ -2718,7 +3065,7 @@ CRYSTALLIZATION RULES:
2718
3065
  - Skip vague things like "use library X" \u2014 that's not procedural knowledge
2719
3066
  - Prefer narrow specific procedures over broad generalizations
2720
3067
  - Trigger keywords should be highly specific (avoid generic words like "code", "fix", "the")`;
2721
- async function generatePostmortemReport(sessionId, messages, session, client, obsDir) {
3068
+ async function generatePostmortemReport(sessionId, messages, session, client, obsDir, rejectedSkillNames) {
2722
3069
  try {
2723
3070
  const events = await readObservationEvents(sessionId, obsDir ?? defaultObservationsDir2());
2724
3071
  const toolMap = /* @__PURE__ */ new Map();
@@ -2754,7 +3101,10 @@ async function generatePostmortemReport(sessionId, messages, session, client, ob
2754
3101
  });
2755
3102
  const obsSnapshot = events.slice(-30).map((e) => `[${e.type}] ${e.summary}`);
2756
3103
  const durationMin = Math.round((Date.now() - session.startedAt) / 6e4);
2757
- const prompt = `${POSTMORTEM_PROMPT}
3104
+ const prompt = `${POSTMORTEM_PROMPT}${rejectedSkillNames && rejectedSkillNames.length > 0 ? `
3105
+
3106
+ PREVIOUSLY REJECTED SKILLS (do NOT suggest these again):
3107
+ ${rejectedSkillNames.map((n) => `- ${n}`).join("\n")}` : ""}
2758
3108
 
2759
3109
  Session ID: ${sessionId}
2760
3110
  Duration: ${durationMin} minutes
@@ -3138,7 +3488,8 @@ async function writeSkillToFile(candidate, skillsMdPath, postmortemFilename) {
3138
3488
  written: false,
3139
3489
  filePath: skillsMdPath,
3140
3490
  skillName: candidate.name,
3141
- reason: `collision with "${collision.collidesWith}" (${collision.reason})`
3491
+ reason: `collision with "${collision.collidesWith}" (${collision.reason})`,
3492
+ collidesWith: collision.collidesWith
3142
3493
  };
3143
3494
  }
3144
3495
  const skillMarkdown = formatSkillMarkdown(candidate, postmortemFilename);
@@ -3163,302 +3514,143 @@ async function writeSkillToFile(candidate, skillsMdPath, postmortemFilename) {
3163
3514
  };
3164
3515
  }
3165
3516
  }
3166
- async function appendCrystallizationLog(entry, logPath) {
3517
+ async function mergeSkillInFile(candidate, existingName, skillsMdPath, postmortemFilename) {
3167
3518
  try {
3168
- await fs12.mkdir(path12.dirname(logPath), { recursive: true });
3169
- let existing = [];
3170
- try {
3171
- const content = await fs12.readFile(logPath, "utf-8");
3172
- existing = JSON.parse(content);
3173
- if (!Array.isArray(existing)) existing = [];
3174
- } catch {
3175
- existing = [];
3519
+ const content = await fs12.readFile(skillsMdPath, "utf-8");
3520
+ const lines = content.split("\n");
3521
+ const heading = toTitleCase(existingName);
3522
+ let startIdx = -1;
3523
+ for (let i = 0; i < lines.length; i++) {
3524
+ if (lines[i].startsWith("# ") && lines[i].slice(2).trim() === heading) {
3525
+ startIdx = i;
3526
+ break;
3527
+ }
3176
3528
  }
3177
- existing.push(entry);
3178
- await fs12.writeFile(logPath, JSON.stringify(existing, null, 2), "utf-8");
3179
- } catch (err) {
3180
- log.debug("crystallization", "appendCrystallizationLog failed", err);
3181
- }
3182
- }
3183
- async function appendRejection(candidate, postmortemFilename, rejectionsPath) {
3184
- try {
3185
- await fs12.mkdir(path12.dirname(rejectionsPath), { recursive: true });
3186
- let existing = [];
3187
- try {
3188
- const content = await fs12.readFile(rejectionsPath, "utf-8");
3189
- existing = JSON.parse(content);
3190
- if (!Array.isArray(existing)) existing = [];
3191
- } catch {
3192
- existing = [];
3529
+ if (startIdx === -1) {
3530
+ return writeSkillToFile(candidate, skillsMdPath, postmortemFilename);
3193
3531
  }
3194
- existing.push({
3195
- name: candidate.name,
3196
- rejectedAt: (/* @__PURE__ */ new Date()).toISOString(),
3197
- fromPostmortem: postmortemFilename,
3198
- triggers: candidate.triggers
3199
- });
3200
- while (existing.length > MAX_REJECTIONS) {
3201
- existing.shift();
3532
+ let endIdx = lines.length;
3533
+ for (let i = startIdx + 1; i < lines.length; i++) {
3534
+ if (lines[i].startsWith("# ") && !lines[i].startsWith("## ")) {
3535
+ endIdx = i;
3536
+ break;
3537
+ }
3202
3538
  }
3203
- await fs12.writeFile(rejectionsPath, JSON.stringify(existing, null, 2), "utf-8");
3539
+ const versionPattern = new RegExp(`^# ${heading.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\.v\\d+`);
3540
+ let maxVersion = 0;
3541
+ for (const line of lines) {
3542
+ if (versionPattern.test(line)) {
3543
+ const vMatch = line.match(/\.v(\d+)/);
3544
+ if (vMatch) maxVersion = Math.max(maxVersion, parseInt(vMatch[1], 10));
3545
+ }
3546
+ }
3547
+ const archiveVersion = maxVersion + 1;
3548
+ const oldBlock = lines.slice(startIdx, endIdx);
3549
+ oldBlock[0] = `# ${heading}.v${archiveVersion}`;
3550
+ const archiveMarker = `<!-- aman-archived version=${archiveVersion} archived-at=${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)} -->`;
3551
+ if (oldBlock.length > 1 && oldBlock[1].includes("aman-auto")) {
3552
+ oldBlock.splice(2, 0, archiveMarker);
3553
+ } else {
3554
+ oldBlock.splice(1, 0, archiveMarker);
3555
+ }
3556
+ const newSkillMarkdown = formatSkillMarkdown(candidate, postmortemFilename);
3557
+ const before = lines.slice(0, startIdx);
3558
+ const after = lines.slice(endIdx);
3559
+ const merged = [...before, ...oldBlock, "", newSkillMarkdown, ...after].join("\n");
3560
+ await fs12.writeFile(skillsMdPath, merged, "utf-8");
3561
+ return {
3562
+ written: true,
3563
+ filePath: skillsMdPath,
3564
+ skillName: candidate.name,
3565
+ reason: `merged with "${existingName}" (archived as .v${archiveVersion})`
3566
+ };
3204
3567
  } catch (err) {
3205
- log.debug("crystallization", "appendRejection failed", err);
3206
- }
3207
- }
3208
-
3209
- // src/user-model.ts
3210
- import fs13 from "fs/promises";
3211
- import path13 from "path";
3212
- import os12 from "os";
3213
- var MAX_SESSIONS = 30;
3214
- var TRUST_ALPHA = 0.3;
3215
- var MIN_SESSIONS_FOR_FEED_FORWARD = 5;
3216
- var MIN_SESSIONS_FOR_CORRELATIONS = 10;
3217
- function defaultModelPath() {
3218
- return path13.join(os12.homedir(), ".acore", "user-model.json");
3219
- }
3220
- function createEmptyModel() {
3221
- const now = (/* @__PURE__ */ new Date()).toISOString();
3222
- return {
3223
- version: 1,
3224
- sessions: [],
3225
- profile: emptyProfile(),
3226
- createdAt: now,
3227
- updatedAt: now
3228
- };
3229
- }
3230
- function emptyProfile() {
3231
- return {
3232
- trustScore: 0.5,
3233
- trustTrajectory: "stable",
3234
- totalSessions: 0,
3235
- preferredTimePeriod: "afternoon",
3236
- energyDistribution: {},
3237
- avgSessionMinutes: 0,
3238
- baselineFrustration: 0,
3239
- baselineExcitement: 0,
3240
- sentimentTrend: "stable",
3241
- frustrationCorrelations: { toolErrors: 0, longSessions: 0, lateNight: 0 },
3242
- avgTurnsPerSession: 0,
3243
- engagementTrend: "stable",
3244
- nudgeStats: {}
3245
- };
3246
- }
3247
- async function loadUserModel(filePath) {
3248
- const fp = filePath ?? defaultModelPath();
3249
- try {
3250
- const raw = await fs13.readFile(fp, "utf-8");
3251
- const parsed = JSON.parse(raw);
3252
- if (parsed?.version !== 1) return null;
3253
- return parsed;
3254
- } catch {
3255
- return null;
3256
- }
3257
- }
3258
- async function saveUserModel(model, filePath) {
3259
- const fp = filePath ?? defaultModelPath();
3260
- const dir = path13.dirname(fp);
3261
- await fs13.mkdir(dir, { recursive: true });
3262
- const tmp = fp + `.tmp-${Date.now()}`;
3263
- await fs13.writeFile(tmp, JSON.stringify(model, null, 2), "utf-8");
3264
- await fs13.rename(tmp, fp);
3265
- }
3266
- function aggregateSession(model, snapshot) {
3267
- const sessions = [...model.sessions, snapshot];
3268
- while (sessions.length > MAX_SESSIONS) {
3269
- sessions.shift();
3568
+ log.warn("crystallization", "mergeSkillInFile failed", err);
3569
+ return {
3570
+ written: false,
3571
+ filePath: skillsMdPath,
3572
+ skillName: candidate.name,
3573
+ reason: err instanceof Error ? err.message : String(err)
3574
+ };
3270
3575
  }
3271
- const totalSessions = model.profile.totalSessions + 1;
3272
- const profile = computeProfile(sessions, totalSessions);
3273
- return {
3274
- ...model,
3275
- sessions,
3276
- profile,
3277
- updatedAt: (/* @__PURE__ */ new Date()).toISOString()
3278
- };
3279
3576
  }
3280
- function computeProfile(sessions, totalSessions) {
3281
- if (sessions.length === 0) return { ...emptyProfile(), totalSessions };
3282
- const n = sessions.length;
3283
- let trustScore = 0.5;
3284
- for (const s of sessions) {
3285
- trustScore = TRUST_ALPHA * ratingSignal(s) + (1 - TRUST_ALPHA) * trustScore;
3286
- }
3287
- const trustTrajectory = computeTrustTrajectory(sessions);
3288
- const baselineFrustration = avg(sessions.map((s) => s.avgFrustration));
3289
- const baselineExcitement = avg(sessions.map((s) => s.avgExcitement));
3290
- const sentimentTrend = computeSentimentTrend(sessions);
3291
- const energyDistribution = {};
3292
- for (const s of sessions) {
3293
- energyDistribution[s.timePeriod] = (energyDistribution[s.timePeriod] || 0) + 1;
3294
- }
3295
- const preferredTimePeriod = Object.entries(energyDistribution).sort(
3296
- (a, b) => b[1] - a[1]
3297
- )[0]?.[0] ?? "afternoon";
3298
- const avgSessionMinutes = avg(sessions.map((s) => s.durationMinutes));
3299
- const avgTurnsPerSession = avg(sessions.map((s) => s.turnCount));
3300
- const engagementTrend = computeLinearTrend(sessions.map((s) => s.turnCount));
3301
- const frustrationCorrelations = n >= MIN_SESSIONS_FOR_CORRELATIONS ? {
3302
- toolErrors: pearsonR(
3303
- sessions.map((s) => s.avgFrustration),
3304
- sessions.map((s) => s.toolErrors)
3305
- ),
3306
- longSessions: pearsonR(
3307
- sessions.map((s) => s.avgFrustration),
3308
- sessions.map((s) => s.durationMinutes)
3309
- ),
3310
- lateNight: pearsonR(
3311
- sessions.map((s) => s.avgFrustration),
3312
- sessions.map((s) => s.timePeriod === "late-night" || s.timePeriod === "night" ? 1 : 0)
3313
- )
3314
- } : { toolErrors: 0, longSessions: 0, lateNight: 0 };
3315
- const nudgeStats = {};
3316
- for (const s of sessions) {
3317
- const ratingVal = ratingToNumber(s.rating);
3318
- for (const nudge of s.wellbeingNudges) {
3319
- if (!nudgeStats[nudge]) nudgeStats[nudge] = { fired: 0, sessionRatingAfter: 0 };
3320
- nudgeStats[nudge].fired++;
3321
- nudgeStats[nudge].sessionRatingAfter += ratingVal;
3577
+ async function appendCrystallizationLog(entry, logPath) {
3578
+ try {
3579
+ await fs12.mkdir(path12.dirname(logPath), { recursive: true });
3580
+ let existing = [];
3581
+ try {
3582
+ const content = await fs12.readFile(logPath, "utf-8");
3583
+ existing = JSON.parse(content);
3584
+ if (!Array.isArray(existing)) existing = [];
3585
+ } catch {
3586
+ existing = [];
3322
3587
  }
3588
+ existing.push(entry);
3589
+ await fs12.writeFile(logPath, JSON.stringify(existing, null, 2), "utf-8");
3590
+ } catch (err) {
3591
+ log.debug("crystallization", "appendCrystallizationLog failed", err);
3323
3592
  }
3324
- for (const key of Object.keys(nudgeStats)) {
3325
- if (nudgeStats[key].fired > 0) {
3326
- nudgeStats[key].sessionRatingAfter /= nudgeStats[key].fired;
3593
+ }
3594
+ async function appendRejection(candidate, postmortemFilename, rejectionsPath) {
3595
+ try {
3596
+ await fs12.mkdir(path12.dirname(rejectionsPath), { recursive: true });
3597
+ let existing = [];
3598
+ try {
3599
+ const content = await fs12.readFile(rejectionsPath, "utf-8");
3600
+ existing = JSON.parse(content);
3601
+ if (!Array.isArray(existing)) existing = [];
3602
+ } catch {
3603
+ existing = [];
3604
+ }
3605
+ existing.push({
3606
+ name: candidate.name,
3607
+ rejectedAt: (/* @__PURE__ */ new Date()).toISOString(),
3608
+ fromPostmortem: postmortemFilename,
3609
+ triggers: candidate.triggers
3610
+ });
3611
+ while (existing.length > MAX_REJECTIONS) {
3612
+ existing.shift();
3327
3613
  }
3614
+ await fs12.writeFile(rejectionsPath, JSON.stringify(existing, null, 2), "utf-8");
3615
+ } catch (err) {
3616
+ log.debug("crystallization", "appendRejection failed", err);
3328
3617
  }
3329
- return {
3330
- trustScore,
3331
- trustTrajectory,
3332
- totalSessions,
3333
- preferredTimePeriod,
3334
- energyDistribution,
3335
- avgSessionMinutes,
3336
- baselineFrustration,
3337
- baselineExcitement,
3338
- sentimentTrend,
3339
- frustrationCorrelations,
3340
- avgTurnsPerSession,
3341
- engagementTrend,
3342
- nudgeStats
3343
- };
3344
3618
  }
3345
- function feedForward(model) {
3346
- if (model.profile.totalSessions < MIN_SESSIONS_FOR_FEED_FORWARD) return null;
3347
- const p4 = model.profile;
3348
- const overrides = {
3349
- compactGreeting: false,
3350
- frustrationNudgeThreshold: 0.6,
3351
- defaultToPersonalMode: false
3352
- };
3353
- const nightSessions = (p4.energyDistribution["late-night"] || 0) + (p4.energyDistribution["night"] || 0);
3354
- const totalInWindow = model.sessions.length;
3355
- if (totalInWindow > 0 && nightSessions / totalInWindow >= 0.7 && p4.baselineFrustration < 0.3) {
3356
- overrides.energyOverride = "steady";
3357
- }
3358
- if (p4.trustScore > 0.8) {
3359
- overrides.compactGreeting = true;
3360
- }
3361
- if (p4.frustrationCorrelations.toolErrors > 0.4) {
3362
- overrides.frustrationNudgeThreshold = 0.4;
3363
- }
3364
- if (p4.sentimentTrend === "worsening") {
3365
- overrides.defaultToPersonalMode = true;
3619
+ async function loadRejectedNames(rejectionsPath) {
3620
+ try {
3621
+ const content = await fs12.readFile(rejectionsPath, "utf-8");
3622
+ const entries = JSON.parse(content);
3623
+ if (!Array.isArray(entries)) return [];
3624
+ return [...new Set(entries.map((e) => e.name))];
3625
+ } catch {
3626
+ return [];
3366
3627
  }
3367
- return overrides;
3368
- }
3369
- function clamp(val, min, max) {
3370
- return Math.max(min, Math.min(max, val));
3371
- }
3372
- function avg(values) {
3373
- if (values.length === 0) return 0;
3374
- return values.reduce((sum, v) => sum + v, 0) / values.length;
3375
- }
3376
- function ratingSignal(session) {
3377
- if (session.rating === "great") return 1;
3378
- if (session.rating === "good") return 0.75;
3379
- if (session.rating === "okay") return 0.5;
3380
- if (session.rating === "frustrating") return 0.25;
3381
- let implicit = 1;
3382
- implicit -= session.avgFrustration * 0.4;
3383
- implicit -= session.toolErrors > 3 ? 0.2 : 0;
3384
- implicit -= session.blockers > 2 ? 0.2 : 0;
3385
- implicit += session.milestones > 0 ? 0.1 : 0;
3386
- return clamp(implicit, 0, 1);
3387
- }
3388
- function ratingToNumber(rating) {
3389
- if (rating === "great") return 1;
3390
- if (rating === "good") return 0.75;
3391
- if (rating === "okay") return 0.5;
3392
- if (rating === "frustrating") return 0.25;
3393
- return 0.5;
3394
3628
  }
3395
- function pearsonR(x, y) {
3396
- const n = x.length;
3397
- if (n < 3) return 0;
3398
- const mx = avg(x);
3399
- const my = avg(y);
3400
- let num = 0;
3401
- let dx2 = 0;
3402
- let dy2 = 0;
3403
- for (let i = 0; i < n; i++) {
3404
- const dx = x[i] - mx;
3405
- const dy = y[i] - my;
3406
- num += dx * dy;
3407
- dx2 += dx * dx;
3408
- dy2 += dy * dy;
3629
+ async function loadSuggestionCounts(suggestionsPath) {
3630
+ try {
3631
+ const content = await fs12.readFile(suggestionsPath, "utf-8");
3632
+ const parsed = JSON.parse(content);
3633
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return {};
3634
+ return parsed;
3635
+ } catch {
3636
+ return {};
3409
3637
  }
3410
- const denom = Math.sqrt(dx2 * dy2);
3411
- if (denom === 0) return 0;
3412
- return num / denom;
3413
- }
3414
- function computeTrustTrajectory(sessions) {
3415
- if (sessions.length < 10) return "stable";
3416
- const recent5 = sessions.slice(-5).map(ratingSignal);
3417
- const prev5 = sessions.slice(-10, -5).map(ratingSignal);
3418
- const recentAvg = avg(recent5);
3419
- const prevAvg = avg(prev5);
3420
- const delta = recentAvg - prevAvg;
3421
- if (delta > 0.1) return "ascending";
3422
- if (delta < -0.1) return "declining";
3423
- return "stable";
3424
- }
3425
- function computeSentimentTrend(sessions) {
3426
- if (sessions.length < 5) return "stable";
3427
- const frustrations = sessions.slice(-10).map((s) => s.avgFrustration);
3428
- const slope = linearSlope(frustrations);
3429
- if (slope > 0.02) return "worsening";
3430
- if (slope < -0.02) return "improving";
3431
- return "stable";
3432
- }
3433
- function computeLinearTrend(values) {
3434
- if (values.length < 5) return "stable";
3435
- const recent = values.slice(-10);
3436
- const slope = linearSlope(recent);
3437
- const mean = avg(recent);
3438
- const relativeSlope = mean > 0 ? slope / mean : slope;
3439
- if (relativeSlope > 0.03) return "increasing";
3440
- if (relativeSlope < -0.03) return "decreasing";
3441
- return "stable";
3442
3638
  }
3443
- function linearSlope(values) {
3444
- const n = values.length;
3445
- if (n < 2) return 0;
3446
- let sumX = 0;
3447
- let sumY = 0;
3448
- let sumXY = 0;
3449
- let sumX2 = 0;
3450
- for (let i = 0; i < n; i++) {
3451
- sumX += i;
3452
- sumY += values[i];
3453
- sumXY += i * values[i];
3454
- sumX2 += i * i;
3639
+ async function incrementSuggestionCount(name, suggestionsPath) {
3640
+ try {
3641
+ await fs12.mkdir(path12.dirname(suggestionsPath), { recursive: true });
3642
+ const counts = await loadSuggestionCounts(suggestionsPath);
3643
+ counts[name] = (counts[name] || 0) + 1;
3644
+ await fs12.writeFile(suggestionsPath, JSON.stringify(counts, null, 2), "utf-8");
3645
+ return counts[name];
3646
+ } catch (err) {
3647
+ log.debug("crystallization", "incrementSuggestionCount failed", err);
3648
+ return 0;
3455
3649
  }
3456
- const denom = n * sumX2 - sumX * sumX;
3457
- if (denom === 0) return 0;
3458
- return (n * sumXY - sumX * sumY) / denom;
3459
3650
  }
3460
3651
 
3461
3652
  // src/hooks.ts
3653
+ init_user_model();
3462
3654
  function getTimeContext() {
3463
3655
  const now = /* @__PURE__ */ new Date();
3464
3656
  const hour = now.getHours();
@@ -3631,8 +3823,19 @@ ${contextInjection}`;
3631
3823
  syncPersonalityToCore(state, ctx.mcpManager).catch(() => {
3632
3824
  });
3633
3825
  const nudge = formatWellbeingNudge(state);
3634
- if (nudge) {
3635
- greeting += "\n" + nudge;
3826
+ if (nudge && state.wellbeingNudge) {
3827
+ let fireNudge = true;
3828
+ try {
3829
+ const model = await loadUserModel();
3830
+ if (model && model.sessions.length >= 5) {
3831
+ const profile = computeProfile(model.sessions, model.sessions.length);
3832
+ fireNudge = shouldFireNudge(state.wellbeingNudge, profile);
3833
+ }
3834
+ } catch {
3835
+ }
3836
+ if (fireNudge) {
3837
+ greeting += "\n" + nudge;
3838
+ }
3636
3839
  }
3637
3840
  }
3638
3841
  if (greeting) {
@@ -3874,11 +4077,19 @@ async function onSessionEnd(ctx, messages, sessionId, observationSession) {
3874
4077
  try {
3875
4078
  const client = ctx.llmClient;
3876
4079
  if (client) {
4080
+ const rejectionsPath = path14.join(
4081
+ os13.homedir(),
4082
+ ".aman-agent",
4083
+ "crystallization-rejections.json"
4084
+ );
4085
+ const rejectedNames = await loadRejectedNames(rejectionsPath);
3877
4086
  const report = await generatePostmortemReport(
3878
4087
  sessionId,
3879
4088
  messages,
3880
4089
  observationSession,
3881
- client
4090
+ client,
4091
+ void 0,
4092
+ rejectedNames
3882
4093
  );
3883
4094
  if (report) {
3884
4095
  const filePath = await savePostmortem(report);
@@ -3902,11 +4113,16 @@ async function onSessionEnd(ctx, messages, sessionId, observationSession) {
3902
4113
  ".aman-agent",
3903
4114
  "crystallization-log.json"
3904
4115
  );
3905
- const rejectionsPath = path14.join(
4116
+ const rejectionsPath2 = path14.join(
3906
4117
  os13.homedir(),
3907
4118
  ".aman-agent",
3908
4119
  "crystallization-rejections.json"
3909
4120
  );
4121
+ const suggestionsPath = path14.join(
4122
+ os13.homedir(),
4123
+ ".aman-agent",
4124
+ "crystallization-suggestions.json"
4125
+ );
3910
4126
  const postmortemFilename = `${report.date}-${report.sessionId.slice(0, 4)}.md`;
3911
4127
  console.log(
3912
4128
  pc2.dim(`
@@ -3920,14 +4136,17 @@ async function onSessionEnd(ctx, messages, sessionId, observationSession) {
3920
4136
  log.debug("hooks", "candidate failed validation");
3921
4137
  continue;
3922
4138
  }
4139
+ const suggestCount = await incrementSuggestionCount(candidate.name, suggestionsPath);
4140
+ const reinforced = suggestCount >= 3;
4141
+ const message = reinforced ? `Crystallize "${candidate.name}"? (suggested ${suggestCount}\xD7 across sessions \u2014 high confidence)` : `Crystallize "${candidate.name}" as a reusable skill?`;
3923
4142
  const choice = await p2.select({
3924
- message: `Crystallize "${candidate.name}" as a reusable skill?`,
4143
+ message,
3925
4144
  options: [
3926
- { value: "accept", label: "Yes \u2014 write to ~/.askill/skills.md" },
4145
+ { value: "accept", label: reinforced ? "Yes \u2014 recommended (seen multiple times)" : "Yes \u2014 write to ~/.askill/skills.md" },
3927
4146
  { value: "reject", label: "No \u2014 skip this one" },
3928
4147
  { value: "skip-all", label: "Skip all crystallization for this session" }
3929
4148
  ],
3930
- initialValue: "reject"
4149
+ initialValue: reinforced ? "accept" : "reject"
3931
4150
  });
3932
4151
  if (p2.isCancel(choice) || choice === "skip-all") {
3933
4152
  skipAll = true;
@@ -3955,12 +4174,46 @@ async function onSessionEnd(ctx, messages, sessionId, observationSession) {
3955
4174
  },
3956
4175
  logPath
3957
4176
  );
4177
+ } else if (result.collidesWith) {
4178
+ const mergeChoice = await p2.select({
4179
+ message: `"${candidate.name}" collides with existing "${result.collidesWith}". Merge?`,
4180
+ options: [
4181
+ { value: "merge", label: `Yes \u2014 replace "${result.collidesWith}" with updated version` },
4182
+ { value: "skip", label: "No \u2014 keep existing" }
4183
+ ],
4184
+ initialValue: "merge"
4185
+ });
4186
+ if (!p2.isCancel(mergeChoice) && mergeChoice === "merge") {
4187
+ const mergeResult = await mergeSkillInFile(
4188
+ candidate,
4189
+ result.collidesWith,
4190
+ skillsMdPath,
4191
+ postmortemFilename
4192
+ );
4193
+ if (mergeResult.written) {
4194
+ console.log(pc2.green(` \u2713 Merged: ${candidate.name} (replaced "${result.collidesWith}")`));
4195
+ await appendCrystallizationLog(
4196
+ {
4197
+ name: candidate.name,
4198
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
4199
+ fromPostmortem: postmortemFilename,
4200
+ confidence: candidate.confidence,
4201
+ triggers: candidate.triggers
4202
+ },
4203
+ logPath
4204
+ );
4205
+ } else {
4206
+ console.log(pc2.yellow(` \u2298 Merge failed: ${mergeResult.reason}`));
4207
+ }
4208
+ } else {
4209
+ console.log(pc2.dim(` Kept existing: ${result.collidesWith}`));
4210
+ }
3958
4211
  } else {
3959
4212
  console.log(pc2.yellow(` \u2298 Could not crystallize: ${result.reason}`));
3960
4213
  }
3961
4214
  } else {
3962
4215
  console.log(pc2.dim(` Skipped: ${candidate.name}`));
3963
- await appendRejection(candidate, postmortemFilename, rejectionsPath);
4216
+ await appendRejection(candidate, postmortemFilename, rejectionsPath2);
3964
4217
  }
3965
4218
  }
3966
4219
  }
@@ -4589,6 +4842,7 @@ function progressBar(pct) {
4589
4842
  }
4590
4843
 
4591
4844
  // src/commands.ts
4845
+ init_user_model();
4592
4846
  import {
4593
4847
  getIdentity as acoreGetIdentity,
4594
4848
  updateSection as acoreUpdateSection,
@@ -5081,10 +5335,33 @@ async function handleSkillsCommand(action, args, ctx) {
5081
5335
  if (entries.length === 0) {
5082
5336
  return { handled: true, output: pc5.dim("No crystallized skills yet.") };
5083
5337
  }
5338
+ const suggestionsPath = path17.join(os16.homedir(), ".aman-agent", "crystallization-suggestions.json");
5339
+ let sugCounts = {};
5340
+ try {
5341
+ const sc = fs17.readFileSync(suggestionsPath, "utf-8");
5342
+ sugCounts = JSON.parse(sc);
5343
+ } catch {
5344
+ }
5345
+ let versionCounts = {};
5346
+ try {
5347
+ const skillsContent = fs17.readFileSync(path17.join(os16.homedir(), ".askill", "skills.md"), "utf-8");
5348
+ const versionRe = /^# (.+)\.v(\d+)$/gm;
5349
+ let vMatch;
5350
+ while ((vMatch = versionRe.exec(skillsContent)) !== null) {
5351
+ const skillHeading = vMatch[1].toLowerCase().replace(/ /g, "-");
5352
+ const ver = parseInt(vMatch[2], 10);
5353
+ versionCounts[skillHeading] = Math.max(versionCounts[skillHeading] || 0, ver);
5354
+ }
5355
+ } catch {
5356
+ }
5084
5357
  const lines = [pc5.bold(`Crystallized skills (${entries.length}):`)];
5085
5358
  for (const entry of entries) {
5086
5359
  const date = entry.createdAt.slice(0, 10);
5087
- lines.push(` ${pc5.cyan(entry.name)} (${date}, conf ${entry.confidence})`);
5360
+ const count = sugCounts[entry.name];
5361
+ const reinforced = count && count >= 3 ? pc5.green(` \u2605 reinforced (${count}\xD7)`) : "";
5362
+ const versions = versionCounts[entry.name];
5363
+ const versionLabel = versions ? pc5.dim(` [v${versions + 1}]`) : "";
5364
+ lines.push(` ${pc5.cyan(entry.name)} (${date}, conf ${entry.confidence})${reinforced}${versionLabel}`);
5088
5365
  lines.push(pc5.dim(` triggers: ${entry.triggers.join(", ")}`));
5089
5366
  }
5090
5367
  return { handled: true, output: lines.join("\n") };
@@ -5768,7 +6045,7 @@ function handleReset(action) {
5768
6045
  function handleUpdate() {
5769
6046
  try {
5770
6047
  const current = execFileSync3("npm", ["view", "@aman_asmuei/aman-agent", "version"], { encoding: "utf-8" }).trim();
5771
- const local = true ? "0.27.0" : "unknown";
6048
+ const local = true ? "0.28.0" : "unknown";
5772
6049
  if (current === local) {
5773
6050
  return { handled: true, output: `${pc5.green("Up to date")} \u2014 v${local}` };
5774
6051
  }
@@ -6838,6 +7115,169 @@ function matchSkills(userInput, installedSkillNames, runtimeTriggers = /* @__PUR
6838
7115
  }
6839
7116
  return Array.from(matched);
6840
7117
  }
7118
+ var SEMANTIC_STOPWORDS = /* @__PURE__ */ new Set([
7119
+ "the",
7120
+ "a",
7121
+ "an",
7122
+ "is",
7123
+ "are",
7124
+ "was",
7125
+ "were",
7126
+ "be",
7127
+ "been",
7128
+ "being",
7129
+ "have",
7130
+ "has",
7131
+ "had",
7132
+ "do",
7133
+ "does",
7134
+ "did",
7135
+ "will",
7136
+ "would",
7137
+ "shall",
7138
+ "should",
7139
+ "may",
7140
+ "might",
7141
+ "must",
7142
+ "can",
7143
+ "could",
7144
+ "to",
7145
+ "of",
7146
+ "in",
7147
+ "for",
7148
+ "on",
7149
+ "with",
7150
+ "at",
7151
+ "by",
7152
+ "from",
7153
+ "as",
7154
+ "into",
7155
+ "through",
7156
+ "during",
7157
+ "before",
7158
+ "after",
7159
+ "above",
7160
+ "below",
7161
+ "between",
7162
+ "and",
7163
+ "but",
7164
+ "or",
7165
+ "nor",
7166
+ "not",
7167
+ "so",
7168
+ "yet",
7169
+ "both",
7170
+ "either",
7171
+ "neither",
7172
+ "each",
7173
+ "every",
7174
+ "all",
7175
+ "any",
7176
+ "few",
7177
+ "more",
7178
+ "most",
7179
+ "other",
7180
+ "some",
7181
+ "such",
7182
+ "no",
7183
+ "only",
7184
+ "own",
7185
+ "same",
7186
+ "than",
7187
+ "too",
7188
+ "very",
7189
+ "just",
7190
+ "because",
7191
+ "if",
7192
+ "when",
7193
+ "while",
7194
+ "how",
7195
+ "what",
7196
+ "which",
7197
+ "who",
7198
+ "whom",
7199
+ "this",
7200
+ "that",
7201
+ "these",
7202
+ "those",
7203
+ "i",
7204
+ "me",
7205
+ "my",
7206
+ "we",
7207
+ "us",
7208
+ "our",
7209
+ "you",
7210
+ "your",
7211
+ "he",
7212
+ "him",
7213
+ "his",
7214
+ "she",
7215
+ "her",
7216
+ "it",
7217
+ "its",
7218
+ "they",
7219
+ "them",
7220
+ "their"
7221
+ ]);
7222
+ function tokenize(text3) {
7223
+ return text3.toLowerCase().replace(/[^a-z0-9\s-]/g, " ").split(/\s+/).filter((w) => w.length > 2 && !SEMANTIC_STOPWORDS.has(w));
7224
+ }
7225
+ function termFrequency(tokens) {
7226
+ const tf = /* @__PURE__ */ new Map();
7227
+ for (const t of tokens) {
7228
+ tf.set(t, (tf.get(t) || 0) + 1);
7229
+ }
7230
+ for (const [k, v] of tf) {
7231
+ tf.set(k, v / tokens.length);
7232
+ }
7233
+ return tf;
7234
+ }
7235
+ function cosineSimilarity2(a, b) {
7236
+ let dot = 0;
7237
+ let normA = 0;
7238
+ let normB = 0;
7239
+ for (const [k, v] of a) {
7240
+ normA += v * v;
7241
+ const bv = b.get(k);
7242
+ if (bv !== void 0) dot += v * bv;
7243
+ }
7244
+ for (const [, v] of b) {
7245
+ normB += v * v;
7246
+ }
7247
+ if (normA === 0 || normB === 0) return 0;
7248
+ return dot / (Math.sqrt(normA) * Math.sqrt(normB));
7249
+ }
7250
+ function semanticSimilarity(userInput, triggers) {
7251
+ const inputTokens = tokenize(userInput);
7252
+ if (inputTokens.length === 0) return 0;
7253
+ const triggerTokens = triggers.flatMap((t) => tokenize(t));
7254
+ if (triggerTokens.length === 0) return 0;
7255
+ const inputTf = termFrequency(inputTokens);
7256
+ const triggerTf = termFrequency(triggerTokens);
7257
+ return cosineSimilarity2(inputTf, triggerTf);
7258
+ }
7259
+ var SEMANTIC_THRESHOLD = 0.15;
7260
+ function matchSkillsSemantic(userInput, installedSkillNames, runtimeTriggers = /* @__PURE__ */ new Map()) {
7261
+ const exact = matchSkills(userInput, installedSkillNames, runtimeTriggers);
7262
+ const matched = new Set(exact);
7263
+ for (const skillName of installedSkillNames) {
7264
+ if (matched.has(skillName)) continue;
7265
+ const triggers = SKILL_TRIGGERS[skillName];
7266
+ if (!triggers) continue;
7267
+ const sim = semanticSimilarity(userInput, triggers);
7268
+ if (sim >= SEMANTIC_THRESHOLD) {
7269
+ matched.add(skillName);
7270
+ }
7271
+ }
7272
+ for (const [skillName, triggers] of runtimeTriggers) {
7273
+ if (matched.has(skillName)) continue;
7274
+ const sim = semanticSimilarity(userInput, triggers);
7275
+ if (sim >= SEMANTIC_THRESHOLD) {
7276
+ matched.add(skillName);
7277
+ }
7278
+ }
7279
+ return Array.from(matched);
7280
+ }
6841
7281
  function formatSkillContext(skillName, skillContent, level) {
6842
7282
  let depthHint;
6843
7283
  if (level.level >= 4) {
@@ -6863,7 +7303,7 @@ async function autoTriggerSkills(userInput, mcpManager) {
6863
7303
  const skillsMdPath = path18.join(os17.homedir(), ".askill", "skills.md");
6864
7304
  const runtimeTriggers = await loadRuntimeTriggers(skillsMdPath);
6865
7305
  if (installed.length === 0 && runtimeTriggers.size === 0) return "";
6866
- const matched = matchSkills(userInput, installed, runtimeTriggers);
7306
+ const matched = matchSkillsSemantic(userInput, installed, runtimeTriggers);
6867
7307
  if (matched.length === 0) return "";
6868
7308
  const blocks = [];
6869
7309
  for (const skillName of matched.slice(0, 2)) {
@@ -7139,16 +7579,23 @@ import { reflect as reflect2, isReflectionDue as isReflectionDue2 } from "@aman_
7139
7579
  var VALID_TYPES = /* @__PURE__ */ new Set(["preference", "fact", "pattern", "topology", "decision", "correction"]);
7140
7580
  var MIN_RESPONSE_LENGTH = 50;
7141
7581
  var MIN_TURNS_BETWEEN_EMPTY = 3;
7142
- var EXTRACTION_PROMPT = `Analyze this conversation turn. Extract any information worth remembering long-term.
7582
+ var EXTRACTION_PROMPT = `Analyze this conversation turn. Extract any information worth remembering long-term, and assess the user's emotional tone.
7143
7583
 
7144
- Return a JSON array (empty [] if nothing worth storing):
7145
- [{
7146
- "content": "what to remember \u2014 be specific and self-contained",
7147
- "type": "preference|fact|pattern|decision|correction|topology",
7148
- "tags": ["relevant", "tags"],
7149
- "confidence": 0.0-1.0,
7150
- "scope": "global"
7151
- }]
7584
+ Return a JSON object with two fields:
7585
+ {
7586
+ "memories": [{
7587
+ "content": "what to remember \u2014 be specific and self-contained",
7588
+ "type": "preference|fact|pattern|decision|correction|topology",
7589
+ "tags": ["relevant", "tags"],
7590
+ "confidence": 0.0-1.0,
7591
+ "scope": "global"
7592
+ }],
7593
+ "sentiment": {
7594
+ "tone": "neutral|positive|frustrated|confused|excited|fatigued",
7595
+ "confidence": 0.0-1.0,
7596
+ "context": "brief reason for sentiment read"
7597
+ }
7598
+ }
7152
7599
 
7153
7600
  Type guide:
7154
7601
  - "preference" = user likes/dislikes/preferences
@@ -7162,7 +7609,8 @@ Rules:
7162
7609
  - Only extract genuinely useful LONG-TERM information
7163
7610
  - Skip ephemeral things ("user asked about X" is NOT useful)
7164
7611
  - Be conservative \u2014 90% of turns produce nothing worth storing
7165
- - Return ONLY the JSON array, no other text`;
7612
+ - Sentiment should reflect the USER's tone, not the assistant's
7613
+ - Return ONLY the JSON object, no other text`;
7166
7614
  function shouldExtract(assistantResponse, turnsSinceLastExtraction, lastExtractionCount) {
7167
7615
  if (assistantResponse.length < MIN_RESPONSE_LENGTH) return false;
7168
7616
  if (lastExtractionCount > 0 && turnsSinceLastExtraction >= 1) return true;
@@ -7177,12 +7625,32 @@ function parseExtractionResult(raw) {
7177
7625
  cleaned = codeBlockMatch[1].trim();
7178
7626
  }
7179
7627
  const parsed = JSON.parse(cleaned);
7180
- if (!Array.isArray(parsed)) return [];
7181
- return parsed.filter(
7182
- (item) => typeof item.content === "string" && item.content.length > 0 && typeof item.type === "string" && VALID_TYPES.has(item.type)
7183
- );
7628
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed) && "memories" in parsed) {
7629
+ const memories = Array.isArray(parsed.memories) ? parsed.memories.filter(
7630
+ (item) => typeof item.content === "string" && item.content.length > 0 && typeof item.type === "string" && VALID_TYPES.has(item.type)
7631
+ ) : [];
7632
+ let sentiment;
7633
+ if (parsed.sentiment && typeof parsed.sentiment === "object") {
7634
+ const s = parsed.sentiment;
7635
+ if (typeof s.tone === "string" && typeof s.confidence === "number") {
7636
+ sentiment = {
7637
+ tone: s.tone,
7638
+ confidence: s.confidence,
7639
+ context: typeof s.context === "string" ? s.context : void 0
7640
+ };
7641
+ }
7642
+ }
7643
+ return { memories, sentiment };
7644
+ }
7645
+ if (Array.isArray(parsed)) {
7646
+ const memories = parsed.filter(
7647
+ (item) => typeof item.content === "string" && item.content.length > 0 && typeof item.type === "string" && VALID_TYPES.has(item.type)
7648
+ );
7649
+ return { memories };
7650
+ }
7651
+ return { memories: [] };
7184
7652
  } catch {
7185
- return [];
7653
+ return { memories: [] };
7186
7654
  }
7187
7655
  }
7188
7656
  async function extractMemories(userMessage, assistantResponse, client, state) {
@@ -7202,12 +7670,16 @@ Assistant: ${assistantResponse.slice(0, 2e3)}`;
7202
7670
  if (chunk.type === "text" && chunk.text) fullText += chunk.text;
7203
7671
  }
7204
7672
  );
7205
- const candidates = parseExtractionResult(fullText);
7673
+ const result = parseExtractionResult(fullText);
7206
7674
  state.turnsSinceLastExtraction = 0;
7207
- state.lastExtractionCount = candidates.length;
7208
- if (candidates.length === 0) return 0;
7675
+ state.lastExtractionCount = result.memories.length;
7676
+ if (result.sentiment) {
7677
+ state.lastLlmSentiment = result.sentiment;
7678
+ log.debug("extractor", `LLM sentiment: ${result.sentiment.tone} (${result.sentiment.confidence})`);
7679
+ }
7680
+ if (result.memories.length === 0) return 0;
7209
7681
  let stored = 0;
7210
- for (const candidate of candidates) {
7682
+ for (const candidate of result.memories) {
7211
7683
  try {
7212
7684
  const existing = await memoryRecall(candidate.content, { limit: 1 });
7213
7685
  if (existing.total > 0 && existing.memories.length > 0) {
@@ -8058,8 +8530,68 @@ ${converted}
8058
8530
  }
8059
8531
  }
8060
8532
  const nudge = formatWellbeingNudge(state);
8061
- if (nudge) {
8062
- augmentedSystemPrompt += "\n" + nudge;
8533
+ if (nudge && state.wellbeingNudge) {
8534
+ let fireNudge = true;
8535
+ try {
8536
+ const { loadUserModel: loadUserModel2, computeProfile: computeProfile2 } = await Promise.resolve().then(() => (init_user_model(), user_model_exports));
8537
+ const model2 = await loadUserModel2();
8538
+ if (model2 && model2.sessions.length >= 5) {
8539
+ const profile = computeProfile2(model2.sessions, model2.sessions.length);
8540
+ fireNudge = shouldFireNudge(state.wellbeingNudge, profile);
8541
+ }
8542
+ } catch {
8543
+ }
8544
+ if (fireNudge) {
8545
+ augmentedSystemPrompt += "\n" + nudge;
8546
+ }
8547
+ }
8548
+ try {
8549
+ const { loadUserModel: loadUserModel2, computeProfile: computeProfile2 } = await Promise.resolve().then(() => (init_user_model(), user_model_exports));
8550
+ const model2 = await loadUserModel2();
8551
+ if (model2 && model2.sessions.length >= 10) {
8552
+ const profile = computeProfile2(model2.sessions, model2.sessions.length);
8553
+ const preemptive = [];
8554
+ const hour2 = (/* @__PURE__ */ new Date()).getHours();
8555
+ const isLate = hour2 >= 21 || hour2 < 6;
8556
+ if (isLate && profile.frustrationCorrelations.lateNight > 0.4) {
8557
+ preemptive.push(
8558
+ "Based on past patterns, late-night sessions tend to increase frustration for this user. Be extra concise, proactive about blockers, and gently suggest wrapping up if frustration rises."
8559
+ );
8560
+ }
8561
+ const sessionMins = Math.round((Date.now() - getSessionStartTime()) / 6e4);
8562
+ if (sessionMins > 60 && profile.frustrationCorrelations.longSessions > 0.4) {
8563
+ preemptive.push(
8564
+ "This session is getting long and past patterns show long sessions correlate with frustration. Proactively suggest natural breakpoints."
8565
+ );
8566
+ }
8567
+ if (preemptive.length > 0) {
8568
+ augmentedSystemPrompt += `
8569
+ <feed-forward-v2>
8570
+ ${preemptive.join("\n")}
8571
+ </feed-forward-v2>`;
8572
+ }
8573
+ }
8574
+ } catch {
8575
+ }
8576
+ try {
8577
+ const { loadUserModel: loadUserModel2, predictBurnout: predictBurnout2 } = await Promise.resolve().then(() => (init_user_model(), user_model_exports));
8578
+ const model2 = await loadUserModel2();
8579
+ if (model2 && model2.sessions.length >= 5) {
8580
+ const sessionMins = Math.round((Date.now() - getSessionStartTime()) / 6e4);
8581
+ const burnout = predictBurnout2(model2.sessions, {
8582
+ minutes: sessionMins,
8583
+ frustration: state.sentiment.frustration,
8584
+ timePeriod: period
8585
+ });
8586
+ if (burnout.risk > 0.7) {
8587
+ const burnoutState = { ...state, wellbeingNudge: "burnout-warning" };
8588
+ const burnoutNudge = formatWellbeingNudge(burnoutState);
8589
+ if (burnoutNudge) {
8590
+ augmentedSystemPrompt += "\n" + burnoutNudge;
8591
+ }
8592
+ }
8593
+ }
8594
+ } catch {
8063
8595
  }
8064
8596
  }
8065
8597
  const MAX_SYSTEM_TOKENS = 16e3;
@@ -8488,7 +9020,7 @@ function bootstrapEcosystem() {
8488
9020
  return true;
8489
9021
  }
8490
9022
  var program = new Command();
8491
- program.name("aman-agent").description("Your AI companion, running locally").version("0.27.0").option("--model <model>", "Override LLM model").option("--budget <tokens>", "Token budget for system prompt (default: 8000)", parseInt).option("--profile <name>", "Use a specific agent profile (e.g., coder, writer, researcher)").action(async (options) => {
9023
+ program.name("aman-agent").description("Your AI companion, running locally").version("0.28.0").option("--model <model>", "Override LLM model").option("--budget <tokens>", "Token budget for system prompt (default: 8000)", parseInt).option("--profile <name>", "Use a specific agent profile (e.g., coder, writer, researcher)").action(async (options) => {
8492
9024
  p3.intro(pc8.bold("aman agent") + pc8.dim(" \u2014 your AI companion"));
8493
9025
  let config = loadConfig();
8494
9026
  if (!config) {