@aman_asmuei/aman-agent 0.26.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";
@@ -1387,18 +1721,18 @@ var McpManager = class {
1387
1721
 
1388
1722
  // src/agent.ts
1389
1723
  import * as readline from "readline";
1390
- import fs19 from "fs";
1391
- import path19 from "path";
1392
- import os18 from "os";
1724
+ import fs20 from "fs";
1725
+ import path20 from "path";
1726
+ import os19 from "os";
1393
1727
  import pc7 from "picocolors";
1394
1728
  import { marked } from "marked";
1395
1729
  import { markedTerminal } from "marked-terminal";
1396
1730
  import logUpdate from "log-update";
1397
1731
 
1398
1732
  // src/commands.ts
1399
- import fs16 from "fs";
1400
- import path16 from "path";
1401
- import os15 from "os";
1733
+ import fs17 from "fs";
1734
+ import path17 from "path";
1735
+ import os16 from "os";
1402
1736
  import { execFileSync as execFileSync3 } from "child_process";
1403
1737
  import pc5 from "picocolors";
1404
1738
 
@@ -2361,9 +2695,9 @@ import pc3 from "picocolors";
2361
2695
  // src/hooks.ts
2362
2696
  import pc2 from "picocolors";
2363
2697
  import * as p2 from "@clack/prompts";
2364
- import fs13 from "fs";
2365
- import path13 from "path";
2366
- import os12 from "os";
2698
+ import fs14 from "fs";
2699
+ import path14 from "path";
2700
+ import os13 from "os";
2367
2701
 
2368
2702
  // src/personality.ts
2369
2703
  var FRUSTRATION_SIGNALS = [
@@ -2518,19 +2852,38 @@ 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
  }
2527
- async function syncPersonalityToCore(state, mcpManager) {
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
+ }
2874
+ async function syncPersonalityToCore(state, mcpManager, modelMetrics) {
2528
2875
  try {
2529
- await mcpManager.callTool("identity_update_dynamics", {
2876
+ const payload = {
2530
2877
  currentRead: state.currentRead,
2531
2878
  energy: state.energy,
2532
2879
  activeMode: state.activeMode
2533
- });
2880
+ };
2881
+ if (modelMetrics) {
2882
+ payload.trust = `${(modelMetrics.trustScore * 100).toFixed(0)}%`;
2883
+ payload.sessions = modelMetrics.totalSessions;
2884
+ payload.sentimentTrend = modelMetrics.sentimentTrend;
2885
+ }
2886
+ await mcpManager.callTool("identity_update_dynamics", payload);
2534
2887
  } catch (err) {
2535
2888
  log.debug("personality", "identity_update_dynamics failed", err);
2536
2889
  }
@@ -2712,7 +3065,7 @@ CRYSTALLIZATION RULES:
2712
3065
  - Skip vague things like "use library X" \u2014 that's not procedural knowledge
2713
3066
  - Prefer narrow specific procedures over broad generalizations
2714
3067
  - Trigger keywords should be highly specific (avoid generic words like "code", "fix", "the")`;
2715
- async function generatePostmortemReport(sessionId, messages, session, client, obsDir) {
3068
+ async function generatePostmortemReport(sessionId, messages, session, client, obsDir, rejectedSkillNames) {
2716
3069
  try {
2717
3070
  const events = await readObservationEvents(sessionId, obsDir ?? defaultObservationsDir2());
2718
3071
  const toolMap = /* @__PURE__ */ new Map();
@@ -2748,7 +3101,10 @@ async function generatePostmortemReport(sessionId, messages, session, client, ob
2748
3101
  });
2749
3102
  const obsSnapshot = events.slice(-30).map((e) => `[${e.type}] ${e.summary}`);
2750
3103
  const durationMin = Math.round((Date.now() - session.startedAt) / 6e4);
2751
- 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")}` : ""}
2752
3108
 
2753
3109
  Session ID: ${sessionId}
2754
3110
  Duration: ${durationMin} minutes
@@ -3132,7 +3488,8 @@ async function writeSkillToFile(candidate, skillsMdPath, postmortemFilename) {
3132
3488
  written: false,
3133
3489
  filePath: skillsMdPath,
3134
3490
  skillName: candidate.name,
3135
- reason: `collision with "${collision.collidesWith}" (${collision.reason})`
3491
+ reason: `collision with "${collision.collidesWith}" (${collision.reason})`,
3492
+ collidesWith: collision.collidesWith
3136
3493
  };
3137
3494
  }
3138
3495
  const skillMarkdown = formatSkillMarkdown(candidate, postmortemFilename);
@@ -3157,6 +3514,66 @@ async function writeSkillToFile(candidate, skillsMdPath, postmortemFilename) {
3157
3514
  };
3158
3515
  }
3159
3516
  }
3517
+ async function mergeSkillInFile(candidate, existingName, skillsMdPath, postmortemFilename) {
3518
+ try {
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
+ }
3528
+ }
3529
+ if (startIdx === -1) {
3530
+ return writeSkillToFile(candidate, skillsMdPath, postmortemFilename);
3531
+ }
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
+ }
3538
+ }
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
+ };
3567
+ } catch (err) {
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
+ };
3575
+ }
3576
+ }
3160
3577
  async function appendCrystallizationLog(entry, logPath) {
3161
3578
  try {
3162
3579
  await fs12.mkdir(path12.dirname(logPath), { recursive: true });
@@ -3199,8 +3616,41 @@ async function appendRejection(candidate, postmortemFilename, rejectionsPath) {
3199
3616
  log.debug("crystallization", "appendRejection failed", err);
3200
3617
  }
3201
3618
  }
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 [];
3627
+ }
3628
+ }
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 {};
3637
+ }
3638
+ }
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;
3649
+ }
3650
+ }
3202
3651
 
3203
3652
  // src/hooks.ts
3653
+ init_user_model();
3204
3654
  function getTimeContext() {
3205
3655
  const now = /* @__PURE__ */ new Date();
3206
3656
  const hour = now.getHours();
@@ -3347,11 +3797,45 @@ ${contextInjection}`;
3347
3797
  sessionMinutes: 0,
3348
3798
  turnCount: 0
3349
3799
  });
3800
+ try {
3801
+ const model = await loadUserModel();
3802
+ if (model) {
3803
+ const overrides = feedForward(model);
3804
+ if (overrides) {
3805
+ log.debug("hooks", `Feed-forward active (trust=${model.profile.trustScore.toFixed(2)}, sessions=${model.profile.totalSessions})`);
3806
+ if (overrides.energyOverride && (period === "late-night" || period === "night")) {
3807
+ state.energy = overrides.energyOverride;
3808
+ }
3809
+ if (overrides.defaultToPersonalMode && state.activeMode === "Default") {
3810
+ state.activeMode = "Personal";
3811
+ }
3812
+ if (overrides.compactGreeting) {
3813
+ greeting += "\n<user-model-context>High trust user (score: " + model.profile.trustScore.toFixed(2) + ", " + model.profile.totalSessions + " sessions). Keep greeting compact \u2014 they know you well.</user-model-context>";
3814
+ }
3815
+ if (model.profile.sentimentTrend === "worsening") {
3816
+ greeting += "\n<user-model-context>Sentiment trend is worsening across recent sessions. Be more attentive and patient.</user-model-context>";
3817
+ }
3818
+ }
3819
+ }
3820
+ } catch (err) {
3821
+ log.debug("hooks", "user model feed-forward failed", err);
3822
+ }
3350
3823
  syncPersonalityToCore(state, ctx.mcpManager).catch(() => {
3351
3824
  });
3352
3825
  const nudge = formatWellbeingNudge(state);
3353
- if (nudge) {
3354
- 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
+ }
3355
3839
  }
3356
3840
  }
3357
3841
  if (greeting) {
@@ -3468,10 +3952,10 @@ async function onSessionEnd(ctx, messages, sessionId, observationSession) {
3468
3952
  }
3469
3953
  console.log(pc2.dim(` Saved ${textMessages.length} messages (session: ${sessionId})`));
3470
3954
  }
3471
- const projectContextPath = path13.join(process.cwd(), ".acore", "context.md");
3472
- if (fs13.existsSync(projectContextPath) && messages.length > 2) {
3955
+ const projectContextPath = path14.join(process.cwd(), ".acore", "context.md");
3956
+ if (fs14.existsSync(projectContextPath) && messages.length > 2) {
3473
3957
  try {
3474
- let contextContent = fs13.readFileSync(projectContextPath, "utf-8");
3958
+ let contextContent = fs14.readFileSync(projectContextPath, "utf-8");
3475
3959
  const now = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
3476
3960
  let lastUserMsg = "";
3477
3961
  for (let i = messages.length - 1; i >= 0; i--) {
@@ -3489,28 +3973,28 @@ async function onSessionEnd(ctx, messages, sessionId, observationSession) {
3489
3973
  - Recent decisions: [see memory]
3490
3974
  - Temp notes: [cleared]`;
3491
3975
  contextContent = contextContent.replace(sessionPattern, newSession);
3492
- fs13.writeFileSync(projectContextPath, contextContent, "utf-8");
3976
+ fs14.writeFileSync(projectContextPath, contextContent, "utf-8");
3493
3977
  log.debug("hooks", `Updated project context: ${projectContextPath}`);
3494
3978
  }
3495
3979
  } catch (err) {
3496
3980
  log.debug("hooks", "project context update failed", err);
3497
3981
  }
3498
3982
  }
3983
+ const sessionMinutes = Math.round((Date.now() - sessionStartTime) / 6e4);
3984
+ const hour = (/* @__PURE__ */ new Date()).getHours();
3985
+ let period;
3986
+ if (hour < 6) period = "late-night";
3987
+ else if (hour < 12) period = "morning";
3988
+ else if (hour < 17) period = "afternoon";
3989
+ else if (hour < 21) period = "evening";
3990
+ else period = "night";
3991
+ const turnCount = messages.filter((m) => m.role === "user").length;
3992
+ const finalState = computePersonality({
3993
+ timePeriod: period,
3994
+ sessionMinutes,
3995
+ turnCount
3996
+ });
3499
3997
  if (ctx.config.personalityAdapt !== false) {
3500
- const sessionMinutes = Math.round((Date.now() - sessionStartTime) / 6e4);
3501
- const hour = (/* @__PURE__ */ new Date()).getHours();
3502
- let period;
3503
- if (hour < 6) period = "late-night";
3504
- else if (hour < 12) period = "morning";
3505
- else if (hour < 17) period = "afternoon";
3506
- else if (hour < 21) period = "evening";
3507
- else period = "night";
3508
- const turnCount = messages.filter((m) => m.role === "user").length;
3509
- const finalState = computePersonality({
3510
- timePeriod: period,
3511
- sessionMinutes,
3512
- turnCount
3513
- });
3514
3998
  try {
3515
3999
  isHookCall = true;
3516
4000
  await syncPersonalityToCore(finalState, ctx.mcpManager);
@@ -3518,6 +4002,7 @@ async function onSessionEnd(ctx, messages, sessionId, observationSession) {
3518
4002
  isHookCall = false;
3519
4003
  }
3520
4004
  }
4005
+ let sessionRating;
3521
4006
  if (ctx.config.evalPrompt) {
3522
4007
  const rating = await p2.select({
3523
4008
  message: "Quick rating for this session?",
@@ -3530,10 +4015,11 @@ async function onSessionEnd(ctx, messages, sessionId, observationSession) {
3530
4015
  initialValue: "skip"
3531
4016
  });
3532
4017
  if (!p2.isCancel(rating) && rating !== "skip") {
4018
+ sessionRating = rating;
3533
4019
  try {
3534
4020
  isHookCall = true;
3535
4021
  await ctx.mcpManager.callTool("eval_log", {
3536
- rating,
4022
+ rating: sessionRating,
3537
4023
  highlights: "Quick session rating",
3538
4024
  improvements: ""
3539
4025
  });
@@ -3542,15 +4028,68 @@ async function onSessionEnd(ctx, messages, sessionId, observationSession) {
3542
4028
  }
3543
4029
  }
3544
4030
  }
4031
+ if (turnCount >= 2 && sessionMinutes >= 1) {
4032
+ try {
4033
+ const snapshot = {
4034
+ sessionId,
4035
+ date: (/* @__PURE__ */ new Date()).toISOString().split("T")[0],
4036
+ durationMinutes: sessionMinutes,
4037
+ turnCount,
4038
+ dominantSentiment: finalState.sentiment.dominant,
4039
+ avgFrustration: finalState.sentiment.frustration,
4040
+ avgExcitement: finalState.sentiment.excitement,
4041
+ avgConfusion: finalState.sentiment.confusion,
4042
+ avgFatigue: finalState.sentiment.fatigue,
4043
+ toolCalls: observationSession?.stats.toolCalls ?? 0,
4044
+ toolErrors: observationSession?.stats.toolErrors ?? 0,
4045
+ blockers: observationSession?.stats.blockers ?? 0,
4046
+ milestones: observationSession?.stats.milestones ?? 0,
4047
+ topicShifts: observationSession?.stats.topicShifts ?? 0,
4048
+ peakEnergy: finalState.energy,
4049
+ primaryMode: finalState.activeMode,
4050
+ timePeriod: period,
4051
+ rating: sessionRating,
4052
+ hadPostmortem: false,
4053
+ // updated below if postmortem is generated
4054
+ wellbeingNudges: finalState.wellbeingNudge ? [finalState.wellbeingNudge] : []
4055
+ };
4056
+ const model = await loadUserModel() ?? createEmptyModel();
4057
+ const updated = aggregateSession(model, snapshot);
4058
+ await saveUserModel(updated);
4059
+ log.debug("hooks", `User model updated (session ${updated.profile.totalSessions})`);
4060
+ if (ctx.config.personalityAdapt !== false) {
4061
+ try {
4062
+ isHookCall = true;
4063
+ await syncPersonalityToCore(finalState, ctx.mcpManager, {
4064
+ trustScore: updated.profile.trustScore,
4065
+ totalSessions: updated.profile.totalSessions,
4066
+ sentimentTrend: updated.profile.sentimentTrend
4067
+ });
4068
+ } finally {
4069
+ isHookCall = false;
4070
+ }
4071
+ }
4072
+ } catch (err) {
4073
+ log.debug("hooks", "user model aggregation failed", err);
4074
+ }
4075
+ }
3545
4076
  if (ctx.config.autoPostmortem !== false && observationSession && shouldAutoPostmortem(observationSession, messages)) {
3546
4077
  try {
3547
4078
  const client = ctx.llmClient;
3548
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);
3549
4086
  const report = await generatePostmortemReport(
3550
4087
  sessionId,
3551
4088
  messages,
3552
4089
  observationSession,
3553
- client
4090
+ client,
4091
+ void 0,
4092
+ rejectedNames
3554
4093
  );
3555
4094
  if (report) {
3556
4095
  const filePath = await savePostmortem(report);
@@ -3568,17 +4107,22 @@ async function onSessionEnd(ctx, messages, sessionId, observationSession) {
3568
4107
  }
3569
4108
  }
3570
4109
  if (report.crystallizationCandidates && report.crystallizationCandidates.length > 0) {
3571
- const skillsMdPath = path13.join(os12.homedir(), ".askill", "skills.md");
3572
- const logPath = path13.join(
3573
- os12.homedir(),
4110
+ const skillsMdPath = path14.join(os13.homedir(), ".askill", "skills.md");
4111
+ const logPath = path14.join(
4112
+ os13.homedir(),
3574
4113
  ".aman-agent",
3575
4114
  "crystallization-log.json"
3576
4115
  );
3577
- const rejectionsPath = path13.join(
3578
- os12.homedir(),
4116
+ const rejectionsPath2 = path14.join(
4117
+ os13.homedir(),
3579
4118
  ".aman-agent",
3580
4119
  "crystallization-rejections.json"
3581
4120
  );
4121
+ const suggestionsPath = path14.join(
4122
+ os13.homedir(),
4123
+ ".aman-agent",
4124
+ "crystallization-suggestions.json"
4125
+ );
3582
4126
  const postmortemFilename = `${report.date}-${report.sessionId.slice(0, 4)}.md`;
3583
4127
  console.log(
3584
4128
  pc2.dim(`
@@ -3592,14 +4136,17 @@ async function onSessionEnd(ctx, messages, sessionId, observationSession) {
3592
4136
  log.debug("hooks", "candidate failed validation");
3593
4137
  continue;
3594
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?`;
3595
4142
  const choice = await p2.select({
3596
- message: `Crystallize "${candidate.name}" as a reusable skill?`,
4143
+ message,
3597
4144
  options: [
3598
- { 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" },
3599
4146
  { value: "reject", label: "No \u2014 skip this one" },
3600
4147
  { value: "skip-all", label: "Skip all crystallization for this session" }
3601
4148
  ],
3602
- initialValue: "reject"
4149
+ initialValue: reinforced ? "accept" : "reject"
3603
4150
  });
3604
4151
  if (p2.isCancel(choice) || choice === "skip-all") {
3605
4152
  skipAll = true;
@@ -3627,12 +4174,46 @@ async function onSessionEnd(ctx, messages, sessionId, observationSession) {
3627
4174
  },
3628
4175
  logPath
3629
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
+ }
3630
4211
  } else {
3631
4212
  console.log(pc2.yellow(` \u2298 Could not crystallize: ${result.reason}`));
3632
4213
  }
3633
4214
  } else {
3634
4215
  console.log(pc2.dim(` Skipped: ${candidate.name}`));
3635
- await appendRejection(candidate, postmortemFilename, rejectionsPath);
4216
+ await appendRejection(candidate, postmortemFilename, rejectionsPath2);
3636
4217
  }
3637
4218
  }
3638
4219
  }
@@ -3795,43 +4376,43 @@ async function delegatePipeline(steps, initialInput, client, mcpManager, options
3795
4376
  }
3796
4377
 
3797
4378
  // src/teams.ts
3798
- import fs14 from "fs";
3799
- import path14 from "path";
3800
- import os13 from "os";
4379
+ import fs15 from "fs";
4380
+ import path15 from "path";
4381
+ import os14 from "os";
3801
4382
  import pc4 from "picocolors";
3802
4383
  function getTeamsDir() {
3803
- return path14.join(os13.homedir(), ".acore", "teams");
4384
+ return path15.join(os14.homedir(), ".acore", "teams");
3804
4385
  }
3805
4386
  function ensureTeamsDir() {
3806
4387
  const dir = getTeamsDir();
3807
- if (!fs14.existsSync(dir)) fs14.mkdirSync(dir, { recursive: true });
4388
+ if (!fs15.existsSync(dir)) fs15.mkdirSync(dir, { recursive: true });
3808
4389
  return dir;
3809
4390
  }
3810
4391
  function teamPath(name) {
3811
4392
  const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "-");
3812
- return path14.join(ensureTeamsDir(), `${slug}.json`);
4393
+ return path15.join(ensureTeamsDir(), `${slug}.json`);
3813
4394
  }
3814
4395
  function createTeam(team) {
3815
4396
  const fp = teamPath(team.name);
3816
- fs14.writeFileSync(fp, JSON.stringify(team, null, 2), "utf-8");
4397
+ fs15.writeFileSync(fp, JSON.stringify(team, null, 2), "utf-8");
3817
4398
  }
3818
4399
  function loadTeam(name) {
3819
4400
  const fp = teamPath(name);
3820
- if (!fs14.existsSync(fp)) return null;
4401
+ if (!fs15.existsSync(fp)) return null;
3821
4402
  try {
3822
- return JSON.parse(fs14.readFileSync(fp, "utf-8"));
4403
+ return JSON.parse(fs15.readFileSync(fp, "utf-8"));
3823
4404
  } catch {
3824
4405
  return null;
3825
4406
  }
3826
4407
  }
3827
4408
  function listTeams() {
3828
4409
  const dir = getTeamsDir();
3829
- if (!fs14.existsSync(dir)) return [];
4410
+ if (!fs15.existsSync(dir)) return [];
3830
4411
  const teams = [];
3831
- for (const file of fs14.readdirSync(dir)) {
4412
+ for (const file of fs15.readdirSync(dir)) {
3832
4413
  if (!file.endsWith(".json")) continue;
3833
4414
  try {
3834
- const content = fs14.readFileSync(path14.join(dir, file), "utf-8");
4415
+ const content = fs15.readFileSync(path15.join(dir, file), "utf-8");
3835
4416
  teams.push(JSON.parse(content));
3836
4417
  } catch {
3837
4418
  }
@@ -3840,8 +4421,8 @@ function listTeams() {
3840
4421
  }
3841
4422
  function deleteTeam(name) {
3842
4423
  const fp = teamPath(name);
3843
- if (!fs14.existsSync(fp)) return false;
3844
- fs14.unlinkSync(fp);
4424
+ if (!fs15.existsSync(fp)) return false;
4425
+ fs15.unlinkSync(fp);
3845
4426
  return true;
3846
4427
  }
3847
4428
  async function runTeam(team, task, client, mcpManager, tools) {
@@ -4067,23 +4648,23 @@ var BUILT_IN_TEAMS = [
4067
4648
  ];
4068
4649
 
4069
4650
  // src/plans.ts
4070
- import fs15 from "fs";
4071
- import path15 from "path";
4072
- import os14 from "os";
4651
+ import fs16 from "fs";
4652
+ import path16 from "path";
4653
+ import os15 from "os";
4073
4654
  function getPlansDir() {
4074
- const localDir = path15.join(process.cwd(), ".acore", "plans");
4075
- const localAcore = path15.join(process.cwd(), ".acore");
4076
- if (fs15.existsSync(localAcore)) return localDir;
4077
- return path15.join(os14.homedir(), ".acore", "plans");
4655
+ const localDir = path16.join(process.cwd(), ".acore", "plans");
4656
+ const localAcore = path16.join(process.cwd(), ".acore");
4657
+ if (fs16.existsSync(localAcore)) return localDir;
4658
+ return path16.join(os15.homedir(), ".acore", "plans");
4078
4659
  }
4079
4660
  function ensurePlansDir() {
4080
4661
  const dir = getPlansDir();
4081
- if (!fs15.existsSync(dir)) fs15.mkdirSync(dir, { recursive: true });
4662
+ if (!fs16.existsSync(dir)) fs16.mkdirSync(dir, { recursive: true });
4082
4663
  return dir;
4083
4664
  }
4084
4665
  function planPath(name) {
4085
4666
  const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
4086
- return path15.join(ensurePlansDir(), `${slug}.md`);
4667
+ return path16.join(ensurePlansDir(), `${slug}.md`);
4087
4668
  }
4088
4669
  function serializePlan(plan) {
4089
4670
  const lines = [];
@@ -4109,7 +4690,7 @@ function parsePlan(content, filePath) {
4109
4690
  const createdMatch = content.match(/\*\*Created:\*\*\s*(.+)/);
4110
4691
  const updatedMatch = content.match(/\*\*Updated:\*\*\s*(.+)/);
4111
4692
  const activeMatch = content.match(/\*\*Active:\*\*\s*(.+)/);
4112
- const name = nameMatch?.[1]?.trim() || path15.basename(filePath, ".md");
4693
+ const name = nameMatch?.[1]?.trim() || path16.basename(filePath, ".md");
4113
4694
  const goal = goalMatch?.[1]?.trim() || "";
4114
4695
  const createdAt = createdMatch?.[1]?.trim() || "";
4115
4696
  const updatedAt = updatedMatch?.[1]?.trim() || "";
@@ -4151,22 +4732,22 @@ function createPlan(name, goal, steps) {
4151
4732
  }
4152
4733
  function savePlan(plan) {
4153
4734
  const fp = planPath(plan.name);
4154
- fs15.writeFileSync(fp, serializePlan(plan), "utf-8");
4735
+ fs16.writeFileSync(fp, serializePlan(plan), "utf-8");
4155
4736
  }
4156
4737
  function loadPlan(name) {
4157
4738
  const fp = planPath(name);
4158
- if (!fs15.existsSync(fp)) return null;
4159
- const content = fs15.readFileSync(fp, "utf-8");
4739
+ if (!fs16.existsSync(fp)) return null;
4740
+ const content = fs16.readFileSync(fp, "utf-8");
4160
4741
  return parsePlan(content, fp);
4161
4742
  }
4162
4743
  function listPlans() {
4163
4744
  const dir = getPlansDir();
4164
- if (!fs15.existsSync(dir)) return [];
4745
+ if (!fs16.existsSync(dir)) return [];
4165
4746
  const plans = [];
4166
- for (const file of fs15.readdirSync(dir)) {
4747
+ for (const file of fs16.readdirSync(dir)) {
4167
4748
  if (!file.endsWith(".md")) continue;
4168
- const fp = path15.join(dir, file);
4169
- const content = fs15.readFileSync(fp, "utf-8");
4749
+ const fp = path16.join(dir, file);
4750
+ const content = fs16.readFileSync(fp, "utf-8");
4170
4751
  const plan = parsePlan(content, fp);
4171
4752
  if (plan) plans.push(plan);
4172
4753
  }
@@ -4261,6 +4842,7 @@ function progressBar(pct) {
4261
4842
  }
4262
4843
 
4263
4844
  // src/commands.ts
4845
+ init_user_model();
4264
4846
  import {
4265
4847
  getIdentity as acoreGetIdentity,
4266
4848
  updateSection as acoreUpdateSection,
@@ -4275,10 +4857,10 @@ import {
4275
4857
  } from "@aman_asmuei/arules-core";
4276
4858
  var AGENT_SCOPE = process.env.AMAN_AGENT_SCOPE ?? "dev:agent";
4277
4859
  function readEcosystemFile(filePath, label) {
4278
- if (!fs16.existsSync(filePath)) {
4860
+ if (!fs17.existsSync(filePath)) {
4279
4861
  return pc5.dim(`No ${label} file found at ${filePath}`);
4280
4862
  }
4281
- return fs16.readFileSync(filePath, "utf-8").trim();
4863
+ return fs17.readFileSync(filePath, "utf-8").trim();
4282
4864
  }
4283
4865
  function parseCommand(input) {
4284
4866
  const trimmed = input.trim();
@@ -4356,24 +4938,76 @@ async function handleIdentityCommand(action, args, _ctx) {
4356
4938
  }
4357
4939
  }
4358
4940
  if (action === "dynamics") {
4941
+ if (args.includes("--json")) {
4942
+ const model2 = await loadUserModel();
4943
+ if (!model2) return { handled: true, output: pc5.dim("No user model yet. Complete a few sessions first.") };
4944
+ return { handled: true, output: JSON.stringify(model2, null, 2) };
4945
+ }
4946
+ if (args.includes("--reset")) {
4947
+ const modelPath = defaultModelPath();
4948
+ if (fs17.existsSync(modelPath)) {
4949
+ fs17.unlinkSync(modelPath);
4950
+ return { handled: true, output: pc5.green("User model reset. Starting fresh.") };
4951
+ }
4952
+ return { handled: true, output: pc5.dim("No user model to reset.") };
4953
+ }
4359
4954
  const updates = {};
4360
4955
  for (const arg of args) {
4361
4956
  const eq = arg.indexOf("=");
4362
4957
  if (eq > 0) updates[arg.slice(0, eq)] = arg.slice(eq + 1);
4363
4958
  }
4364
- if (!Object.keys(updates).length) {
4365
- return { handled: true, output: pc5.yellow("Usage: /identity dynamics energy=high mode=focused read='Book Title'") };
4959
+ if (Object.keys(updates).length > 0) {
4960
+ try {
4961
+ await acoreUpdateDynamics({
4962
+ energy: updates.energy,
4963
+ activeMode: updates.mode,
4964
+ currentRead: updates.read
4965
+ }, AGENT_SCOPE);
4966
+ return { handled: true, output: `Dynamics updated: ${Object.entries(updates).map(([k, v]) => `${k}=${v}`).join(", ")}` };
4967
+ } catch (err) {
4968
+ return { handled: true, output: pc5.red(`Dynamics error: ${err instanceof Error ? err.message : String(err)}`) };
4969
+ }
4366
4970
  }
4367
- try {
4368
- await acoreUpdateDynamics({
4369
- energy: updates.energy,
4370
- activeMode: updates.mode,
4371
- currentRead: updates.read
4372
- }, AGENT_SCOPE);
4373
- return { handled: true, output: `Dynamics updated: ${Object.entries(updates).map(([k, v]) => `${k}=${v}`).join(", ")}` };
4374
- } catch (err) {
4375
- return { handled: true, output: pc5.red(`Dynamics error: ${err instanceof Error ? err.message : String(err)}`) };
4971
+ const model = await loadUserModel();
4972
+ if (!model) {
4973
+ return { handled: true, output: pc5.dim("No user model yet. Complete a few sessions to start building your profile.") };
4376
4974
  }
4975
+ const p4 = model.profile;
4976
+ const trustBar = "\u2588".repeat(Math.round(p4.trustScore * 10)) + "\u2591".repeat(10 - Math.round(p4.trustScore * 10));
4977
+ const frustBar = "\u2588".repeat(Math.round(p4.baselineFrustration * 10)) + "\u2591".repeat(10 - Math.round(p4.baselineFrustration * 10));
4978
+ const lines = [
4979
+ pc5.bold(" Dynamic User Model"),
4980
+ "",
4981
+ ` ${pc5.cyan("Trust")} ${trustBar} ${(p4.trustScore * 100).toFixed(0)}% ${p4.trustTrajectory === "ascending" ? pc5.green("\u2191") : p4.trustTrajectory === "declining" ? pc5.red("\u2193") : "\u2192"}`,
4982
+ ` ${pc5.cyan("Sessions")} ${p4.totalSessions} total (${model.sessions.length} in window)`,
4983
+ ` ${pc5.cyan("Sentiment")} ${frustBar} frustration baseline ${p4.sentimentTrend === "improving" ? pc5.green("improving") : p4.sentimentTrend === "worsening" ? pc5.red("worsening") : "stable"}`,
4984
+ "",
4985
+ ` ${pc5.cyan("Preferred")} ${p4.preferredTimePeriod} (${Object.entries(p4.energyDistribution).map(([k, v]) => `${k}: ${v}`).join(", ")})`,
4986
+ ` ${pc5.cyan("Avg session")} ${p4.avgSessionMinutes.toFixed(0)} min, ${p4.avgTurnsPerSession.toFixed(0)} turns ${p4.engagementTrend === "increasing" ? pc5.green("\u2191") : p4.engagementTrend === "decreasing" ? pc5.red("\u2193") : "\u2192"}`
4987
+ ];
4988
+ if (p4.totalSessions >= 10) {
4989
+ const corrs = [];
4990
+ if (Math.abs(p4.frustrationCorrelations.toolErrors) > 0.3) {
4991
+ corrs.push(`tool errors (${p4.frustrationCorrelations.toolErrors.toFixed(2)})`);
4992
+ }
4993
+ if (Math.abs(p4.frustrationCorrelations.longSessions) > 0.3) {
4994
+ corrs.push(`long sessions (${p4.frustrationCorrelations.longSessions.toFixed(2)})`);
4995
+ }
4996
+ if (Math.abs(p4.frustrationCorrelations.lateNight) > 0.3) {
4997
+ corrs.push(`late night (${p4.frustrationCorrelations.lateNight.toFixed(2)})`);
4998
+ }
4999
+ if (corrs.length > 0) {
5000
+ lines.push(` ${pc5.cyan("Frustration")} correlates with: ${corrs.join(", ")}`);
5001
+ }
5002
+ }
5003
+ const nudgeKeys = Object.keys(p4.nudgeStats);
5004
+ if (nudgeKeys.length > 0) {
5005
+ lines.push("");
5006
+ lines.push(` ${pc5.cyan("Nudges")} ${nudgeKeys.map((k) => `${k}: ${p4.nudgeStats[k].fired}\xD7`).join(", ")}`);
5007
+ }
5008
+ lines.push("");
5009
+ lines.push(pc5.dim(` Use --json for raw data, --reset to start fresh`));
5010
+ return { handled: true, output: lines.join("\n") };
4377
5011
  }
4378
5012
  if (action === "summary") {
4379
5013
  try {
@@ -4397,7 +5031,10 @@ async function handleIdentityCommand(action, args, _ctx) {
4397
5031
  pc5.bold("Identity commands:"),
4398
5032
  ` ${pc5.cyan("/identity")} View current identity`,
4399
5033
  ` ${pc5.cyan("/identity update")} <section> Update a section`,
5034
+ ` ${pc5.cyan("/identity dynamics")} View user model (trust, sentiment, patterns)`,
4400
5035
  ` ${pc5.cyan("/identity dynamics")} key=val Update dynamic fields (energy, mode, read)`,
5036
+ ` ${pc5.cyan("/identity dynamics")} --json Raw JSON user model`,
5037
+ ` ${pc5.cyan("/identity dynamics")} --reset Reset user model`,
4401
5038
  ` ${pc5.cyan("/identity summary")} Show structured identity summary`
4402
5039
  ].join("\n")
4403
5040
  };
@@ -4553,9 +5190,9 @@ ${result.violations.map((v) => ` - ${v}`).join("\n")}`)
4553
5190
  };
4554
5191
  }
4555
5192
  async function handleWorkflowsCommand(action, args, ctx) {
4556
- const home2 = os15.homedir();
5193
+ const home2 = os16.homedir();
4557
5194
  if (!action) {
4558
- const content = readEcosystemFile(path16.join(home2, ".aflow", "flow.md"), "workflows (aflow)");
5195
+ const content = readEcosystemFile(path17.join(home2, ".aflow", "flow.md"), "workflows (aflow)");
4559
5196
  return { handled: true, output: content };
4560
5197
  }
4561
5198
  if (action === "add") {
@@ -4577,7 +5214,7 @@ async function handleWorkflowsCommand(action, args, ctx) {
4577
5214
  return { handled: true, output: pc5.yellow("Usage: /workflows get <name>") };
4578
5215
  }
4579
5216
  const name = args.join(" ").toLowerCase();
4580
- const raw = readEcosystemFile(path16.join(home2, ".aflow", "flow.md"), "workflows (aflow)");
5217
+ const raw = readEcosystemFile(path17.join(home2, ".aflow", "flow.md"), "workflows (aflow)");
4581
5218
  if (raw.startsWith("No ")) {
4582
5219
  return { handled: true, output: raw };
4583
5220
  }
@@ -4636,12 +5273,12 @@ async function handleToolsCommand(action, args, _ctx) {
4636
5273
  return { handled: true, output: pc5.yellow("Usage: /tools search <query...>") };
4637
5274
  }
4638
5275
  const query = args.join(" ").toLowerCase();
4639
- const home2 = os15.homedir();
4640
- const toolsFile = path16.join(home2, ".akit", "tools.md");
4641
- if (!fs16.existsSync(toolsFile)) {
5276
+ const home2 = os16.homedir();
5277
+ const toolsFile = path17.join(home2, ".akit", "tools.md");
5278
+ if (!fs17.existsSync(toolsFile)) {
4642
5279
  return { handled: true, output: pc5.dim(`No tools file found. Use 'npx @aman_asmuei/akit search ${args.join(" ")}' to search the registry.`) };
4643
5280
  }
4644
- const raw = fs16.readFileSync(toolsFile, "utf-8").trim();
5281
+ const raw = fs17.readFileSync(toolsFile, "utf-8").trim();
4645
5282
  const lines = raw.split("\n");
4646
5283
  const matches = lines.filter((l) => l.toLowerCase().includes(query));
4647
5284
  if (matches.length === 0) {
@@ -4652,9 +5289,9 @@ async function handleToolsCommand(action, args, _ctx) {
4652
5289
  return handleAkitCommand(action, args);
4653
5290
  }
4654
5291
  async function handleSkillsCommand(action, args, ctx) {
4655
- const home2 = os15.homedir();
5292
+ const home2 = os16.homedir();
4656
5293
  if (!action) {
4657
- const content = readEcosystemFile(path16.join(home2, ".askill", "skills.md"), "skills (askill)");
5294
+ const content = readEcosystemFile(path17.join(home2, ".askill", "skills.md"), "skills (askill)");
4658
5295
  return { handled: true, output: content };
4659
5296
  }
4660
5297
  if (action === "install") {
@@ -4676,8 +5313,8 @@ async function handleSkillsCommand(action, args, ctx) {
4676
5313
  return { handled: true, output: pc5.yellow("Usage: /skills search <query...>") };
4677
5314
  }
4678
5315
  const query = args.join(" ").toLowerCase();
4679
- const home3 = os15.homedir();
4680
- const raw = readEcosystemFile(path16.join(home3, ".askill", "skills.md"), "skills (askill)");
5316
+ const home3 = os16.homedir();
5317
+ const raw = readEcosystemFile(path17.join(home3, ".askill", "skills.md"), "skills (askill)");
4681
5318
  if (raw.startsWith("No ")) {
4682
5319
  return { handled: true, output: raw };
4683
5320
  }
@@ -4691,17 +5328,40 @@ async function handleSkillsCommand(action, args, ctx) {
4691
5328
  if (action === "list") {
4692
5329
  const autoOnly = args.includes("--auto");
4693
5330
  if (autoOnly) {
4694
- const logPath = path16.join(os15.homedir(), ".aman-agent", "crystallization-log.json");
5331
+ const logPath = path17.join(os16.homedir(), ".aman-agent", "crystallization-log.json");
4695
5332
  try {
4696
- const content2 = fs16.readFileSync(logPath, "utf-8");
5333
+ const content2 = fs17.readFileSync(logPath, "utf-8");
4697
5334
  const entries = JSON.parse(content2);
4698
5335
  if (entries.length === 0) {
4699
5336
  return { handled: true, output: pc5.dim("No crystallized skills yet.") };
4700
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
+ }
4701
5357
  const lines = [pc5.bold(`Crystallized skills (${entries.length}):`)];
4702
5358
  for (const entry of entries) {
4703
5359
  const date = entry.createdAt.slice(0, 10);
4704
- 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}`);
4705
5365
  lines.push(pc5.dim(` triggers: ${entry.triggers.join(", ")}`));
4706
5366
  }
4707
5367
  return { handled: true, output: lines.join("\n") };
@@ -4709,13 +5369,13 @@ async function handleSkillsCommand(action, args, ctx) {
4709
5369
  return { handled: true, output: pc5.dim("No crystallized skills yet.") };
4710
5370
  }
4711
5371
  }
4712
- const content = readEcosystemFile(path16.join(home2, ".askill", "skills.md"), "skills (askill)");
5372
+ const content = readEcosystemFile(path17.join(home2, ".askill", "skills.md"), "skills (askill)");
4713
5373
  return { handled: true, output: content };
4714
5374
  }
4715
5375
  if (action === "crystallize") {
4716
- const pmDir = path16.join(os15.homedir(), ".acore", "postmortems");
5376
+ const pmDir = path17.join(os16.homedir(), ".acore", "postmortems");
4717
5377
  try {
4718
- const files = fs16.readdirSync(pmDir);
5378
+ const files = fs17.readdirSync(pmDir);
4719
5379
  const jsonFiles = files.filter((f) => f.endsWith(".json")).sort().reverse();
4720
5380
  if (jsonFiles.length === 0) {
4721
5381
  return {
@@ -4724,7 +5384,7 @@ async function handleSkillsCommand(action, args, ctx) {
4724
5384
  };
4725
5385
  }
4726
5386
  const latest = jsonFiles[0];
4727
- const content = fs16.readFileSync(path16.join(pmDir, latest), "utf-8");
5387
+ const content = fs17.readFileSync(path17.join(pmDir, latest), "utf-8");
4728
5388
  const report = JSON.parse(content);
4729
5389
  if (!report.crystallizationCandidates || report.crystallizationCandidates.length === 0) {
4730
5390
  return {
@@ -4732,8 +5392,8 @@ async function handleSkillsCommand(action, args, ctx) {
4732
5392
  output: pc5.dim(`No crystallization candidates in the most recent post-mortem (${latest}). Run a longer session or wait for the next auto-postmortem.`)
4733
5393
  };
4734
5394
  }
4735
- const skillsMdPath = path16.join(os15.homedir(), ".askill", "skills.md");
4736
- const logPath = path16.join(os15.homedir(), ".aman-agent", "crystallization-log.json");
5395
+ const skillsMdPath = path17.join(os16.homedir(), ".askill", "skills.md");
5396
+ const logPath = path17.join(os16.homedir(), ".aman-agent", "crystallization-log.json");
4737
5397
  const postmortemFilename = latest.replace(/\.json$/, ".md");
4738
5398
  const lines = [
4739
5399
  pc5.bold(`Found ${report.crystallizationCandidates.length} candidate(s) in ${latest}:`)
@@ -4790,9 +5450,9 @@ async function handleSkillsCommand(action, args, ctx) {
4790
5450
  return { handled: true, output: pc5.yellow(`Unknown action: /skills ${action}. Try /skills --help`) };
4791
5451
  }
4792
5452
  async function handleEvalCommand(action, args, ctx) {
4793
- const home2 = os15.homedir();
5453
+ const home2 = os16.homedir();
4794
5454
  if (!action) {
4795
- const content = readEcosystemFile(path16.join(home2, ".aeval", "eval.md"), "evaluation (aeval)");
5455
+ const content = readEcosystemFile(path17.join(home2, ".aeval", "eval.md"), "evaluation (aeval)");
4796
5456
  return { handled: true, output: content };
4797
5457
  }
4798
5458
  if (action === "milestone") {
@@ -4804,11 +5464,11 @@ async function handleEvalCommand(action, args, ctx) {
4804
5464
  return { handled: true, output };
4805
5465
  }
4806
5466
  if (action === "report") {
4807
- const evalFile = path16.join(home2, ".aeval", "eval.md");
4808
- if (!fs16.existsSync(evalFile)) {
5467
+ const evalFile = path17.join(home2, ".aeval", "eval.md");
5468
+ if (!fs17.existsSync(evalFile)) {
4809
5469
  return { handled: true, output: pc5.dim("No eval report found. Log milestones with /eval milestone <text>.") };
4810
5470
  }
4811
- const content = fs16.readFileSync(evalFile, "utf-8").trim();
5471
+ const content = fs17.readFileSync(evalFile, "utf-8").trim();
4812
5472
  return { handled: true, output: [pc5.bold("Eval Report"), "", content].join("\n") };
4813
5473
  }
4814
5474
  return { handled: true, output: pc5.yellow(`Unknown action: /eval ${action}. Use /eval, /eval report, or /eval milestone <text>.`) };
@@ -5332,10 +5992,10 @@ function handleSave() {
5332
5992
  }
5333
5993
  function handleReset(action) {
5334
5994
  const dirs = {
5335
- config: path16.join(os15.homedir(), ".aman-agent"),
5336
- memory: path16.join(os15.homedir(), ".amem"),
5337
- identity: path16.join(os15.homedir(), ".acore"),
5338
- rules: path16.join(os15.homedir(), ".arules")
5995
+ config: path17.join(os16.homedir(), ".aman-agent"),
5996
+ memory: path17.join(os16.homedir(), ".amem"),
5997
+ identity: path17.join(os16.homedir(), ".acore"),
5998
+ rules: path17.join(os16.homedir(), ".arules")
5339
5999
  };
5340
6000
  if (action === "help" || !action) {
5341
6001
  return {
@@ -5360,15 +6020,15 @@ function handleReset(action) {
5360
6020
  const removed = [];
5361
6021
  for (const target of targets) {
5362
6022
  const dir = dirs[target];
5363
- if (fs16.existsSync(dir)) {
5364
- fs16.rmSync(dir, { recursive: true, force: true });
6023
+ if (fs17.existsSync(dir)) {
6024
+ fs17.rmSync(dir, { recursive: true, force: true });
5365
6025
  removed.push(target);
5366
6026
  }
5367
6027
  }
5368
6028
  if (targets.includes("config")) {
5369
6029
  const configDir = dirs.config;
5370
- fs16.mkdirSync(configDir, { recursive: true });
5371
- fs16.writeFileSync(path16.join(configDir, ".reconfig"), "", "utf-8");
6030
+ fs17.mkdirSync(configDir, { recursive: true });
6031
+ fs17.writeFileSync(path17.join(configDir, ".reconfig"), "", "utf-8");
5372
6032
  }
5373
6033
  if (removed.length === 0) {
5374
6034
  return { handled: true, output: pc5.dim("Nothing to reset \u2014 directories don't exist.") };
@@ -5385,7 +6045,7 @@ function handleReset(action) {
5385
6045
  function handleUpdate() {
5386
6046
  try {
5387
6047
  const current = execFileSync3("npm", ["view", "@aman_asmuei/aman-agent", "version"], { encoding: "utf-8" }).trim();
5388
- const local = true ? "0.26.0" : "unknown";
6048
+ const local = true ? "0.28.0" : "unknown";
5389
6049
  if (current === local) {
5390
6050
  return { handled: true, output: `${pc5.green("Up to date")} \u2014 v${local}` };
5391
6051
  }
@@ -5429,11 +6089,11 @@ function handleExportCommand() {
5429
6089
  return { handled: true, exportConversation: true };
5430
6090
  }
5431
6091
  function handleDebugCommand() {
5432
- const logPath = path16.join(os15.homedir(), ".aman-agent", "debug.log");
5433
- if (!fs16.existsSync(logPath)) {
6092
+ const logPath = path17.join(os16.homedir(), ".aman-agent", "debug.log");
6093
+ if (!fs17.existsSync(logPath)) {
5434
6094
  return { handled: true, output: pc5.dim("No debug log found.") };
5435
6095
  }
5436
- const content = fs16.readFileSync(logPath, "utf-8");
6096
+ const content = fs17.readFileSync(logPath, "utf-8");
5437
6097
  const lines = content.trim().split("\n");
5438
6098
  const last20 = lines.slice(-20).join("\n");
5439
6099
  return { handled: true, output: pc5.bold("Debug Log (last 20 entries):\n") + pc5.dim(last20) };
@@ -5631,7 +6291,7 @@ ${result.response}`
5631
6291
  };
5632
6292
  }
5633
6293
  function handleProfileCommand(action, args) {
5634
- const profilesDir = path16.join(os15.homedir(), ".acore", "profiles");
6294
+ const profilesDir = path17.join(os16.homedir(), ".acore", "profiles");
5635
6295
  if (action === "me") {
5636
6296
  const user = loadUserIdentity();
5637
6297
  if (!user) {
@@ -5699,8 +6359,8 @@ ${pc5.dim("Edit with: /profile edit")}` };
5699
6359
  };
5700
6360
  }
5701
6361
  const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "-");
5702
- const profileDir = path16.join(profilesDir, slug);
5703
- if (fs16.existsSync(profileDir)) {
6362
+ const profileDir = path17.join(profilesDir, slug);
6363
+ if (fs17.existsSync(profileDir)) {
5704
6364
  return { handled: true, output: pc5.yellow(`Profile already exists: ${slug}`) };
5705
6365
  }
5706
6366
  const builtIn = BUILT_IN_PROFILES.find((t) => t.name === slug);
@@ -5716,16 +6376,16 @@ ${pc5.dim("Edit with: /profile edit")}` };
5716
6376
  Use: aman-agent --profile ${slug}`
5717
6377
  };
5718
6378
  }
5719
- fs16.mkdirSync(profileDir, { recursive: true });
5720
- const globalCore = path16.join(os15.homedir(), ".acore", "core.md");
5721
- if (fs16.existsSync(globalCore)) {
5722
- let content = fs16.readFileSync(globalCore, "utf-8");
6379
+ fs17.mkdirSync(profileDir, { recursive: true });
6380
+ const globalCore = path17.join(os16.homedir(), ".acore", "core.md");
6381
+ if (fs17.existsSync(globalCore)) {
6382
+ let content = fs17.readFileSync(globalCore, "utf-8");
5723
6383
  const aiName = name.charAt(0).toUpperCase() + name.slice(1);
5724
6384
  content = content.replace(/^# .+$/m, `# ${aiName}`);
5725
- fs16.writeFileSync(path16.join(profileDir, "core.md"), content, "utf-8");
6385
+ fs17.writeFileSync(path17.join(profileDir, "core.md"), content, "utf-8");
5726
6386
  } else {
5727
6387
  const aiName = name.charAt(0).toUpperCase() + name.slice(1);
5728
- fs16.writeFileSync(path16.join(profileDir, "core.md"), `# ${aiName}
6388
+ fs17.writeFileSync(path17.join(profileDir, "core.md"), `# ${aiName}
5729
6389
 
5730
6390
  ## Identity
5731
6391
  - Role: ${aiName} is your AI companion
@@ -5738,7 +6398,7 @@ ${pc5.dim("Edit with: /profile edit")}` };
5738
6398
  return {
5739
6399
  handled: true,
5740
6400
  output: pc5.green(`Profile created: ${slug}`) + `
5741
- Edit: ${path16.join(profileDir, "core.md")}
6401
+ Edit: ${path17.join(profileDir, "core.md")}
5742
6402
  Use: aman-agent --profile ${slug}
5743
6403
 
5744
6404
  ${pc5.dim("Add rules.md or skills.md for profile-specific overrides.")}`
@@ -5747,9 +6407,9 @@ ${pc5.dim("Edit with: /profile edit")}` };
5747
6407
  case "show": {
5748
6408
  const name = args[0];
5749
6409
  if (!name) return { handled: true, output: pc5.yellow("Usage: /profile show <name>") };
5750
- const profileDir = path16.join(profilesDir, name);
5751
- if (!fs16.existsSync(profileDir)) return { handled: true, output: pc5.red(`Profile not found: ${name}`) };
5752
- const files = fs16.readdirSync(profileDir).filter((f) => f.endsWith(".md"));
6410
+ const profileDir = path17.join(profilesDir, name);
6411
+ if (!fs17.existsSync(profileDir)) return { handled: true, output: pc5.red(`Profile not found: ${name}`) };
6412
+ const files = fs17.readdirSync(profileDir).filter((f) => f.endsWith(".md"));
5753
6413
  const lines = files.map((f) => ` ${f}`);
5754
6414
  return { handled: true, output: `Profile: ${pc5.bold(name)}
5755
6415
  Files:
@@ -5758,9 +6418,9 @@ ${lines.join("\n")}` };
5758
6418
  case "delete": {
5759
6419
  const name = args[0];
5760
6420
  if (!name) return { handled: true, output: pc5.yellow("Usage: /profile delete <name>") };
5761
- const profileDir = path16.join(profilesDir, name);
5762
- if (!fs16.existsSync(profileDir)) return { handled: true, output: pc5.red(`Profile not found: ${name}`) };
5763
- fs16.rmSync(profileDir, { recursive: true });
6421
+ const profileDir = path17.join(profilesDir, name);
6422
+ if (!fs17.existsSync(profileDir)) return { handled: true, output: pc5.red(`Profile not found: ${name}`) };
6423
+ fs17.rmSync(profileDir, { recursive: true });
5764
6424
  return { handled: true, output: pc5.dim(`Profile deleted: ${name}`) };
5765
6425
  }
5766
6426
  case "help":
@@ -5978,10 +6638,10 @@ function handleShowcaseCommand(action, args) {
5978
6638
  Or place it as a sibling directory to aman-agent.`
5979
6639
  };
5980
6640
  }
5981
- const corePath = path16.join(os15.homedir(), ".acore", "core.md");
6641
+ const corePath = path17.join(os16.homedir(), ".acore", "core.md");
5982
6642
  let currentShowcase = null;
5983
- if (fs16.existsSync(corePath)) {
5984
- const content = fs16.readFileSync(corePath, "utf-8");
6643
+ if (fs17.existsSync(corePath)) {
6644
+ const content = fs17.readFileSync(corePath, "utf-8");
5985
6645
  const nameMatch = content.match(/^# (.+)/m);
5986
6646
  if (nameMatch) {
5987
6647
  const coreName = nameMatch[1].trim().toLowerCase();
@@ -6368,10 +7028,10 @@ ${summaryParts.slice(0, 20).join("\n")}
6368
7028
  }
6369
7029
 
6370
7030
  // src/skill-engine.ts
6371
- import fs17 from "fs";
7031
+ import fs18 from "fs";
6372
7032
  import fsp from "fs/promises";
6373
- import path17 from "path";
6374
- import os16 from "os";
7033
+ import path18 from "path";
7034
+ import os17 from "os";
6375
7035
  var SKILL_TRIGGERS = {
6376
7036
  testing: ["test", "spec", "coverage", "tdd", "jest", "vitest", "mocha", "assert", "mock", "stub", "fixture", "e2e", "integration test", "unit test"],
6377
7037
  "api-design": ["api", "endpoint", "rest", "graphql", "route", "controller", "middleware", "http", "request", "response", "status code", "pagination"],
@@ -6400,20 +7060,20 @@ async function loadRuntimeTriggers(skillsMdPath) {
6400
7060
  return /* @__PURE__ */ new Map();
6401
7061
  }
6402
7062
  }
6403
- var LEVEL_FILE = path17.join(os16.homedir(), ".aman-agent", "skill-levels.json");
7063
+ var LEVEL_FILE = path18.join(os17.homedir(), ".aman-agent", "skill-levels.json");
6404
7064
  function loadSkillLevels() {
6405
7065
  try {
6406
- if (fs17.existsSync(LEVEL_FILE)) {
6407
- return JSON.parse(fs17.readFileSync(LEVEL_FILE, "utf-8"));
7066
+ if (fs18.existsSync(LEVEL_FILE)) {
7067
+ return JSON.parse(fs18.readFileSync(LEVEL_FILE, "utf-8"));
6408
7068
  }
6409
7069
  } catch {
6410
7070
  }
6411
7071
  return {};
6412
7072
  }
6413
7073
  function saveSkillLevels(levels) {
6414
- const dir = path17.dirname(LEVEL_FILE);
6415
- if (!fs17.existsSync(dir)) fs17.mkdirSync(dir, { recursive: true });
6416
- fs17.writeFileSync(LEVEL_FILE, JSON.stringify(levels, null, 2), "utf-8");
7074
+ const dir = path18.dirname(LEVEL_FILE);
7075
+ if (!fs18.existsSync(dir)) fs18.mkdirSync(dir, { recursive: true });
7076
+ fs18.writeFileSync(LEVEL_FILE, JSON.stringify(levels, null, 2), "utf-8");
6417
7077
  }
6418
7078
  function computeLevel(activations) {
6419
7079
  if (activations >= 50) return { level: 5, label: "Expert" };
@@ -6455,6 +7115,169 @@ function matchSkills(userInput, installedSkillNames, runtimeTriggers = /* @__PUR
6455
7115
  }
6456
7116
  return Array.from(matched);
6457
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
+ }
6458
7281
  function formatSkillContext(skillName, skillContent, level) {
6459
7282
  let depthHint;
6460
7283
  if (level.level >= 4) {
@@ -6477,10 +7300,10 @@ async function autoTriggerSkills(userInput, mcpManager) {
6477
7300
  const result = await mcpManager.callTool("skill_list", {});
6478
7301
  const skills = JSON.parse(result);
6479
7302
  const installed = skills.filter((s) => s.installed).map((s) => s.name);
6480
- const skillsMdPath = path17.join(os16.homedir(), ".askill", "skills.md");
7303
+ const skillsMdPath = path18.join(os17.homedir(), ".askill", "skills.md");
6481
7304
  const runtimeTriggers = await loadRuntimeTriggers(skillsMdPath);
6482
7305
  if (installed.length === 0 && runtimeTriggers.size === 0) return "";
6483
- const matched = matchSkills(userInput, installed, runtimeTriggers);
7306
+ const matched = matchSkillsSemantic(userInput, installed, runtimeTriggers);
6484
7307
  if (matched.length === 0) return "";
6485
7308
  const blocks = [];
6486
7309
  for (const skillName of matched.slice(0, 2)) {
@@ -6756,16 +7579,23 @@ import { reflect as reflect2, isReflectionDue as isReflectionDue2 } from "@aman_
6756
7579
  var VALID_TYPES = /* @__PURE__ */ new Set(["preference", "fact", "pattern", "topology", "decision", "correction"]);
6757
7580
  var MIN_RESPONSE_LENGTH = 50;
6758
7581
  var MIN_TURNS_BETWEEN_EMPTY = 3;
6759
- 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.
6760
7583
 
6761
- Return a JSON array (empty [] if nothing worth storing):
6762
- [{
6763
- "content": "what to remember \u2014 be specific and self-contained",
6764
- "type": "preference|fact|pattern|decision|correction|topology",
6765
- "tags": ["relevant", "tags"],
6766
- "confidence": 0.0-1.0,
6767
- "scope": "global"
6768
- }]
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
+ }
6769
7599
 
6770
7600
  Type guide:
6771
7601
  - "preference" = user likes/dislikes/preferences
@@ -6779,7 +7609,8 @@ Rules:
6779
7609
  - Only extract genuinely useful LONG-TERM information
6780
7610
  - Skip ephemeral things ("user asked about X" is NOT useful)
6781
7611
  - Be conservative \u2014 90% of turns produce nothing worth storing
6782
- - 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`;
6783
7614
  function shouldExtract(assistantResponse, turnsSinceLastExtraction, lastExtractionCount) {
6784
7615
  if (assistantResponse.length < MIN_RESPONSE_LENGTH) return false;
6785
7616
  if (lastExtractionCount > 0 && turnsSinceLastExtraction >= 1) return true;
@@ -6794,12 +7625,32 @@ function parseExtractionResult(raw) {
6794
7625
  cleaned = codeBlockMatch[1].trim();
6795
7626
  }
6796
7627
  const parsed = JSON.parse(cleaned);
6797
- if (!Array.isArray(parsed)) return [];
6798
- return parsed.filter(
6799
- (item) => typeof item.content === "string" && item.content.length > 0 && typeof item.type === "string" && VALID_TYPES.has(item.type)
6800
- );
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: [] };
6801
7652
  } catch {
6802
- return [];
7653
+ return { memories: [] };
6803
7654
  }
6804
7655
  }
6805
7656
  async function extractMemories(userMessage, assistantResponse, client, state) {
@@ -6819,12 +7670,16 @@ Assistant: ${assistantResponse.slice(0, 2e3)}`;
6819
7670
  if (chunk.type === "text" && chunk.text) fullText += chunk.text;
6820
7671
  }
6821
7672
  );
6822
- const candidates = parseExtractionResult(fullText);
7673
+ const result = parseExtractionResult(fullText);
6823
7674
  state.turnsSinceLastExtraction = 0;
6824
- state.lastExtractionCount = candidates.length;
6825
- 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;
6826
7681
  let stored = 0;
6827
- for (const candidate of candidates) {
7682
+ for (const candidate of result.memories) {
6828
7683
  try {
6829
7684
  const existing = await memoryRecall(candidate.content, { limit: 1 });
6830
7685
  if (existing.total > 0 && existing.memories.length > 0) {
@@ -7036,9 +7891,9 @@ function humanizeError(message) {
7036
7891
  }
7037
7892
 
7038
7893
  // src/hints.ts
7039
- import fs18 from "fs";
7040
- import path18 from "path";
7041
- import os17 from "os";
7894
+ import fs19 from "fs";
7895
+ import path19 from "path";
7896
+ import os18 from "os";
7042
7897
  var HINTS = [
7043
7898
  {
7044
7899
  id: "eval",
@@ -7076,11 +7931,11 @@ function getHint(state, ctx) {
7076
7931
  }
7077
7932
  return null;
7078
7933
  }
7079
- var HINTS_FILE = path18.join(os17.homedir(), ".aman-agent", "hints-seen.json");
7934
+ var HINTS_FILE = path19.join(os18.homedir(), ".aman-agent", "hints-seen.json");
7080
7935
  function loadShownHints() {
7081
7936
  try {
7082
- if (fs18.existsSync(HINTS_FILE)) {
7083
- const data = JSON.parse(fs18.readFileSync(HINTS_FILE, "utf-8"));
7937
+ if (fs19.existsSync(HINTS_FILE)) {
7938
+ const data = JSON.parse(fs19.readFileSync(HINTS_FILE, "utf-8"));
7084
7939
  return new Set(Array.isArray(data) ? data : []);
7085
7940
  }
7086
7941
  } catch {
@@ -7089,9 +7944,9 @@ function loadShownHints() {
7089
7944
  }
7090
7945
  function saveShownHints(shown) {
7091
7946
  try {
7092
- const dir = path18.dirname(HINTS_FILE);
7093
- fs18.mkdirSync(dir, { recursive: true });
7094
- fs18.writeFileSync(HINTS_FILE, JSON.stringify([...shown]), "utf-8");
7947
+ const dir = path19.dirname(HINTS_FILE);
7948
+ fs19.mkdirSync(dir, { recursive: true });
7949
+ fs19.writeFileSync(HINTS_FILE, JSON.stringify([...shown]), "utf-8");
7095
7950
  } catch {
7096
7951
  }
7097
7952
  }
@@ -7358,9 +8213,9 @@ ${task.result}`
7358
8213
  }
7359
8214
  if (cmdResult.exportConversation) {
7360
8215
  try {
7361
- const exportDir = path19.join(os18.homedir(), ".aman-agent", "exports");
7362
- fs19.mkdirSync(exportDir, { recursive: true });
7363
- const exportPath = path19.join(exportDir, `${sessionId}.md`);
8216
+ const exportDir = path20.join(os19.homedir(), ".aman-agent", "exports");
8217
+ fs20.mkdirSync(exportDir, { recursive: true });
8218
+ const exportPath = path20.join(exportDir, `${sessionId}.md`);
7364
8219
  const lines = [
7365
8220
  `# Conversation \u2014 ${(/* @__PURE__ */ new Date()).toLocaleString()}`,
7366
8221
  `**Model:** ${model}`,
@@ -7374,7 +8229,7 @@ ${task.result}`
7374
8229
  lines.push(`${label} ${msg.content}`, "");
7375
8230
  }
7376
8231
  }
7377
- fs19.writeFileSync(exportPath, lines.join("\n"), "utf-8");
8232
+ fs20.writeFileSync(exportPath, lines.join("\n"), "utf-8");
7378
8233
  console.log(pc7.green(`Exported to ${exportPath}`));
7379
8234
  } catch {
7380
8235
  console.log(pc7.red("Failed to export conversation."));
@@ -7500,25 +8355,25 @@ ${knowledgeItem.content}
7500
8355
  for (const match of filePathMatches) {
7501
8356
  let filePath = match[1];
7502
8357
  if (filePath.startsWith("~/")) {
7503
- filePath = path19.join(os18.homedir(), filePath.slice(2));
8358
+ filePath = path20.join(os19.homedir(), filePath.slice(2));
7504
8359
  }
7505
- if (!fs19.existsSync(filePath) || !fs19.statSync(filePath).isFile()) continue;
7506
- const ext = path19.extname(filePath).toLowerCase();
8360
+ if (!fs20.existsSync(filePath) || !fs20.statSync(filePath).isFile()) continue;
8361
+ const ext = path20.extname(filePath).toLowerCase();
7507
8362
  if (imageExts.has(ext)) {
7508
8363
  try {
7509
- const stat = fs19.statSync(filePath);
8364
+ const stat = fs20.statSync(filePath);
7510
8365
  if (stat.size > maxImageBytes) {
7511
- process.stdout.write(pc7.yellow(` [skipped: ${path19.basename(filePath)} \u2014 exceeds 20MB limit]
8366
+ process.stdout.write(pc7.yellow(` [skipped: ${path20.basename(filePath)} \u2014 exceeds 20MB limit]
7512
8367
  `));
7513
8368
  continue;
7514
8369
  }
7515
- const data = fs19.readFileSync(filePath).toString("base64");
8370
+ const data = fs20.readFileSync(filePath).toString("base64");
7516
8371
  const mediaType = mimeMap[ext] || "image/png";
7517
8372
  imageBlocks.push({
7518
8373
  type: "image",
7519
8374
  source: { type: "base64", media_type: mediaType, data }
7520
8375
  });
7521
- process.stdout.write(pc7.dim(` [attached image: ${path19.basename(filePath)} (${(stat.size / 1024).toFixed(1)}KB)]
8376
+ process.stdout.write(pc7.dim(` [attached image: ${path20.basename(filePath)} (${(stat.size / 1024).toFixed(1)}KB)]
7522
8377
  `));
7523
8378
  } catch {
7524
8379
  process.stdout.write(pc7.dim(` [could not read image: ${filePath}]
@@ -7526,7 +8381,7 @@ ${knowledgeItem.content}
7526
8381
  }
7527
8382
  } else if (textExts.has(ext) || ext === "") {
7528
8383
  try {
7529
- const content = fs19.readFileSync(filePath, "utf-8");
8384
+ const content = fs20.readFileSync(filePath, "utf-8");
7530
8385
  const maxChars = 5e4;
7531
8386
  const trimmed = content.length > maxChars ? content.slice(0, maxChars) + `
7532
8387
 
@@ -7536,7 +8391,7 @@ ${knowledgeItem.content}
7536
8391
  <file path="${filePath}" size="${content.length} chars">
7537
8392
  ${trimmed}
7538
8393
  </file>`;
7539
- process.stdout.write(pc7.dim(` [attached: ${path19.basename(filePath)} (${(content.length / 1024).toFixed(1)}KB)]
8394
+ process.stdout.write(pc7.dim(` [attached: ${path20.basename(filePath)} (${(content.length / 1024).toFixed(1)}KB)]
7540
8395
  `));
7541
8396
  } catch {
7542
8397
  process.stdout.write(pc7.dim(` [could not read: ${filePath}]
@@ -7545,7 +8400,7 @@ ${trimmed}
7545
8400
  } else if (docExts.has(ext)) {
7546
8401
  if (mcpManager) {
7547
8402
  try {
7548
- process.stdout.write(pc7.dim(` [converting: ${path19.basename(filePath)}...]
8403
+ process.stdout.write(pc7.dim(` [converting: ${path20.basename(filePath)}...]
7549
8404
  `));
7550
8405
  const converted = await mcpManager.callTool("doc_convert", { path: filePath });
7551
8406
  if (converted && !converted.startsWith("Error") && !converted.includes("Could not convert")) {
@@ -7554,7 +8409,7 @@ ${trimmed}
7554
8409
  <file path="${filePath}" format="${ext}">
7555
8410
  ${converted.slice(0, 5e4)}
7556
8411
  </file>`;
7557
- process.stdout.write(pc7.dim(` [attached: ${path19.basename(filePath)} (converted from ${ext})]
8412
+ process.stdout.write(pc7.dim(` [attached: ${path20.basename(filePath)} (converted from ${ext})]
7558
8413
  `));
7559
8414
  } else {
7560
8415
  textContent += `
@@ -7566,7 +8421,7 @@ ${converted}
7566
8421
  `));
7567
8422
  }
7568
8423
  } catch {
7569
- process.stdout.write(pc7.dim(` [could not convert: ${path19.basename(filePath)}]
8424
+ process.stdout.write(pc7.dim(` [could not convert: ${path20.basename(filePath)}]
7570
8425
  `));
7571
8426
  }
7572
8427
  } else {
@@ -7675,8 +8530,68 @@ ${converted}
7675
8530
  }
7676
8531
  }
7677
8532
  const nudge = formatWellbeingNudge(state);
7678
- if (nudge) {
7679
- 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 {
7680
8595
  }
7681
8596
  }
7682
8597
  const MAX_SYSTEM_TOKENS = 16e3;
@@ -7870,7 +8785,7 @@ ${result2.response}` : `[${input2.profile}] failed: ${result2.error}`;
7870
8785
  }
7871
8786
  if (hooksConfig?.featureHints) {
7872
8787
  hintState.turnCount++;
7873
- const hasWorkflows = fs19.existsSync(path19.join(os18.homedir(), ".aflow", "flow.md"));
8788
+ const hasWorkflows = fs20.existsSync(path20.join(os19.homedir(), ".aflow", "flow.md"));
7874
8789
  const memoryCount = memoryTokens > 0 ? Math.floor(memoryTokens / 5) : 0;
7875
8790
  const hint = getHint(hintState, { hasWorkflows, memoryCount });
7876
8791
  if (hint) {
@@ -7916,9 +8831,9 @@ async function saveConversationToMemory(messages, sessionId) {
7916
8831
  }
7917
8832
 
7918
8833
  // src/index.ts
7919
- import fs20 from "fs";
7920
- import path20 from "path";
7921
- import os19 from "os";
8834
+ import fs21 from "fs";
8835
+ import path21 from "path";
8836
+ import os20 from "os";
7922
8837
 
7923
8838
  // src/presets.ts
7924
8839
  var PRESETS = {
@@ -8027,9 +8942,9 @@ ${wfSections}`;
8027
8942
 
8028
8943
  // src/index.ts
8029
8944
  async function autoDetectConfig() {
8030
- const reconfigMarker = path20.join(os19.homedir(), ".aman-agent", ".reconfig");
8031
- if (fs20.existsSync(reconfigMarker)) {
8032
- fs20.unlinkSync(reconfigMarker);
8945
+ const reconfigMarker = path21.join(os20.homedir(), ".aman-agent", ".reconfig");
8946
+ if (fs21.existsSync(reconfigMarker)) {
8947
+ fs21.unlinkSync(reconfigMarker);
8033
8948
  return null;
8034
8949
  }
8035
8950
  const anthropicKey = process.env.ANTHROPIC_API_KEY;
@@ -8058,11 +8973,11 @@ async function autoDetectConfig() {
8058
8973
  return null;
8059
8974
  }
8060
8975
  function bootstrapEcosystem() {
8061
- const home2 = os19.homedir();
8062
- const corePath = path20.join(home2, ".acore", "core.md");
8063
- if (fs20.existsSync(corePath)) return false;
8064
- fs20.mkdirSync(path20.join(home2, ".acore"), { recursive: true });
8065
- fs20.writeFileSync(corePath, [
8976
+ const home2 = os20.homedir();
8977
+ const corePath = path21.join(home2, ".acore", "core.md");
8978
+ if (fs21.existsSync(corePath)) return false;
8979
+ fs21.mkdirSync(path21.join(home2, ".acore"), { recursive: true });
8980
+ fs21.writeFileSync(corePath, [
8066
8981
  "# Aman",
8067
8982
  "",
8068
8983
  "## Personality",
@@ -8074,11 +8989,11 @@ function bootstrapEcosystem() {
8074
8989
  "## Session",
8075
8990
  "_New companion \u2014 no prior sessions._"
8076
8991
  ].join("\n"), "utf-8");
8077
- const rulesDir = path20.join(home2, ".arules");
8078
- const rulesPath = path20.join(rulesDir, "rules.md");
8079
- if (!fs20.existsSync(rulesPath)) {
8080
- fs20.mkdirSync(rulesDir, { recursive: true });
8081
- fs20.writeFileSync(rulesPath, [
8992
+ const rulesDir = path21.join(home2, ".arules");
8993
+ const rulesPath = path21.join(rulesDir, "rules.md");
8994
+ if (!fs21.existsSync(rulesPath)) {
8995
+ fs21.mkdirSync(rulesDir, { recursive: true });
8996
+ fs21.writeFileSync(rulesPath, [
8082
8997
  "# Guardrails",
8083
8998
  "",
8084
8999
  "## safety",
@@ -8090,22 +9005,22 @@ function bootstrapEcosystem() {
8090
9005
  "- Respect the user's preferences stored in memory"
8091
9006
  ].join("\n"), "utf-8");
8092
9007
  }
8093
- const flowDir = path20.join(home2, ".aflow");
8094
- const flowPath = path20.join(flowDir, "flow.md");
8095
- if (!fs20.existsSync(flowPath)) {
8096
- fs20.mkdirSync(flowDir, { recursive: true });
8097
- fs20.writeFileSync(flowPath, "# Workflows\n\n_No workflows defined yet. Use /workflows add to create one._\n", "utf-8");
9008
+ const flowDir = path21.join(home2, ".aflow");
9009
+ const flowPath = path21.join(flowDir, "flow.md");
9010
+ if (!fs21.existsSync(flowPath)) {
9011
+ fs21.mkdirSync(flowDir, { recursive: true });
9012
+ fs21.writeFileSync(flowPath, "# Workflows\n\n_No workflows defined yet. Use /workflows add to create one._\n", "utf-8");
8098
9013
  }
8099
- const skillDir = path20.join(home2, ".askill");
8100
- const skillPath = path20.join(skillDir, "skills.md");
8101
- if (!fs20.existsSync(skillPath)) {
8102
- fs20.mkdirSync(skillDir, { recursive: true });
8103
- fs20.writeFileSync(skillPath, "# Skills\n\n_No skills installed yet. Use /skills install to add domain expertise._\n", "utf-8");
9014
+ const skillDir = path21.join(home2, ".askill");
9015
+ const skillPath = path21.join(skillDir, "skills.md");
9016
+ if (!fs21.existsSync(skillPath)) {
9017
+ fs21.mkdirSync(skillDir, { recursive: true });
9018
+ fs21.writeFileSync(skillPath, "# Skills\n\n_No skills installed yet. Use /skills install to add domain expertise._\n", "utf-8");
8104
9019
  }
8105
9020
  return true;
8106
9021
  }
8107
9022
  var program = new Command();
8108
- program.name("aman-agent").description("Your AI companion, running locally").version("0.26.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) => {
8109
9024
  p3.intro(pc8.bold("aman agent") + pc8.dim(" \u2014 your AI companion"));
8110
9025
  let config = loadConfig();
8111
9026
  if (!config) {
@@ -8457,19 +9372,19 @@ program.command("init").description("Set up your AI companion with a guided wiza
8457
9372
  });
8458
9373
  if (p3.isCancel(preset)) process.exit(0);
8459
9374
  const result = applyPreset(preset, name || "Aman");
8460
- const home2 = os19.homedir();
8461
- fs20.mkdirSync(path20.join(home2, ".acore"), { recursive: true });
8462
- fs20.writeFileSync(path20.join(home2, ".acore", "core.md"), result.coreMd, "utf-8");
9375
+ const home2 = os20.homedir();
9376
+ fs21.mkdirSync(path21.join(home2, ".acore"), { recursive: true });
9377
+ fs21.writeFileSync(path21.join(home2, ".acore", "core.md"), result.coreMd, "utf-8");
8463
9378
  p3.log.success(`Identity created \u2014 ${PRESETS[preset].identity.personality.split(".")[0].toLowerCase()}`);
8464
9379
  if (result.rulesMd) {
8465
- fs20.mkdirSync(path20.join(home2, ".arules"), { recursive: true });
8466
- fs20.writeFileSync(path20.join(home2, ".arules", "rules.md"), result.rulesMd, "utf-8");
9380
+ fs21.mkdirSync(path21.join(home2, ".arules"), { recursive: true });
9381
+ fs21.writeFileSync(path21.join(home2, ".arules", "rules.md"), result.rulesMd, "utf-8");
8467
9382
  const ruleCount = (result.rulesMd.match(/^- /gm) || []).length;
8468
9383
  p3.log.success(`${ruleCount} rules set`);
8469
9384
  }
8470
9385
  if (result.flowMd) {
8471
- fs20.mkdirSync(path20.join(home2, ".aflow"), { recursive: true });
8472
- fs20.writeFileSync(path20.join(home2, ".aflow", "flow.md"), result.flowMd, "utf-8");
9386
+ fs21.mkdirSync(path21.join(home2, ".aflow"), { recursive: true });
9387
+ fs21.writeFileSync(path21.join(home2, ".aflow", "flow.md"), result.flowMd, "utf-8");
8473
9388
  const wfCount = (result.flowMd.match(/^## /gm) || []).length;
8474
9389
  p3.log.success(`${wfCount} workflow${wfCount > 1 ? "s" : ""} added`);
8475
9390
  }