portable-agent-layer 0.23.1 → 0.24.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/assets/templates/pal-settings.json +2 -1
- package/package.json +1 -1
- package/src/cli/setup-identity.ts +6 -30
- package/src/hooks/handlers/self-model-trigger.ts +27 -0
- package/src/hooks/lib/claude-md.ts +8 -47
- package/src/hooks/lib/context.ts +31 -39
- package/src/hooks/lib/models.ts +1 -0
- package/src/hooks/lib/settings.ts +112 -0
- package/src/hooks/lib/stop.ts +2 -0
- package/src/targets/lib.ts +0 -18
- package/src/tools/self-model.ts +668 -0
- package/src/hooks/handlers/relationship.ts +0 -116
- package/src/hooks/handlers/work-learning.ts +0 -196
- package/src/hooks/setup-check.ts +0 -42
|
@@ -0,0 +1,668 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* SelfModel — Synthesize a first-person self-model from accumulated PAL data.
|
|
4
|
+
*
|
|
5
|
+
* Gathers data deterministically, then uses Sonnet to synthesize a genuine
|
|
6
|
+
* first-person reflection. Reads opinions, ratings, wisdom frames,
|
|
7
|
+
* graduated failure patterns, algorithm reflections, relationship notes,
|
|
8
|
+
* and session history. Produces a self-aware narrative at
|
|
9
|
+
* ~/.pal/memory/self-model.md that is injected at session start.
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* bun ~/.pal/tools/self-model.ts [--days 30] [--force]
|
|
13
|
+
*
|
|
14
|
+
* Guards: skips if last synthesis was < 24h ago (unless --force).
|
|
15
|
+
* Output: ~/.pal/memory/self-model.md
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
19
|
+
import { resolve } from "node:path";
|
|
20
|
+
import { parseArgs } from "node:util";
|
|
21
|
+
import { inference } from "../hooks/lib/inference";
|
|
22
|
+
import { SONNET_MODEL } from "../hooks/lib/models";
|
|
23
|
+
import { ensureDir, paths } from "../hooks/lib/paths";
|
|
24
|
+
|
|
25
|
+
// ── Config ──
|
|
26
|
+
|
|
27
|
+
const SELF_MODEL_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
28
|
+
|
|
29
|
+
// ── Types ──
|
|
30
|
+
|
|
31
|
+
interface Opinion {
|
|
32
|
+
id: string;
|
|
33
|
+
statement: string;
|
|
34
|
+
confidence: number;
|
|
35
|
+
category: string;
|
|
36
|
+
evidence: { date: string; type: string; source: string }[];
|
|
37
|
+
created: string;
|
|
38
|
+
updated: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface Rating {
|
|
42
|
+
ts: string;
|
|
43
|
+
type: string;
|
|
44
|
+
rating: number;
|
|
45
|
+
context: string;
|
|
46
|
+
source: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface GraduatedPattern {
|
|
50
|
+
pattern: string;
|
|
51
|
+
domain: string;
|
|
52
|
+
confidence: number;
|
|
53
|
+
occurrences: number;
|
|
54
|
+
sources: string[];
|
|
55
|
+
graduatedAt: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
interface AlgorithmReflection {
|
|
59
|
+
timestamp: string;
|
|
60
|
+
cwd?: string;
|
|
61
|
+
task: string;
|
|
62
|
+
criteria_count: number;
|
|
63
|
+
criteria_passed: number;
|
|
64
|
+
criteria_failed: number;
|
|
65
|
+
sentiment: number;
|
|
66
|
+
q1: string;
|
|
67
|
+
q2: string;
|
|
68
|
+
q3: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
interface RelationshipNote {
|
|
72
|
+
type: "O" | "W" | "B";
|
|
73
|
+
content: string;
|
|
74
|
+
confidence?: number;
|
|
75
|
+
date: string;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ── Helpers ──
|
|
79
|
+
|
|
80
|
+
function selfModelDir(): string {
|
|
81
|
+
return ensureDir(resolve(paths.memory(), "self-model"));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function selfModelPath(): string {
|
|
85
|
+
return resolve(selfModelDir(), "current.md");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function selfModelMetaPath(): string {
|
|
89
|
+
return resolve(selfModelDir(), "meta.json");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function archiveDir(): string {
|
|
93
|
+
return ensureDir(resolve(selfModelDir(), "archive"));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function shouldRun(force: boolean): boolean {
|
|
97
|
+
if (force) return true;
|
|
98
|
+
const p = selfModelMetaPath();
|
|
99
|
+
if (!existsSync(p)) return true;
|
|
100
|
+
try {
|
|
101
|
+
const meta = JSON.parse(readFileSync(p, "utf-8")) as { timestamp: string };
|
|
102
|
+
return Date.now() - new Date(meta.timestamp).getTime() > SELF_MODEL_TTL_MS;
|
|
103
|
+
} catch {
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function readJsonl<T>(path: string): T[] {
|
|
109
|
+
if (!existsSync(path)) return [];
|
|
110
|
+
return readFileSync(path, "utf-8")
|
|
111
|
+
.split("\n")
|
|
112
|
+
.filter((l) => l.trim())
|
|
113
|
+
.map((l) => JSON.parse(l) as T);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function safeReadJson<T>(path: string, fallback: T): T {
|
|
117
|
+
if (!existsSync(path)) return fallback;
|
|
118
|
+
try {
|
|
119
|
+
return JSON.parse(readFileSync(path, "utf-8")) as T;
|
|
120
|
+
} catch {
|
|
121
|
+
return fallback;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function safeReaddir(dir: string): string[] {
|
|
126
|
+
try {
|
|
127
|
+
return readdirSync(dir);
|
|
128
|
+
} catch {
|
|
129
|
+
return [];
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function daysAgo(days: number): Date {
|
|
134
|
+
return new Date(Date.now() - days * 24 * 60 * 60 * 1000);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function formatDate(iso: string): string {
|
|
138
|
+
return iso.slice(0, 10);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function round1(n: number): number {
|
|
142
|
+
return Math.round(n * 10) / 10;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ── Data Readers ──
|
|
146
|
+
|
|
147
|
+
function readOpinions(): Opinion[] {
|
|
148
|
+
const data = safeReadJson<{ opinions?: Opinion[] }>(
|
|
149
|
+
resolve(paths.relationship(), "opinions.json"),
|
|
150
|
+
{ opinions: [] }
|
|
151
|
+
);
|
|
152
|
+
return (data.opinions ?? []).sort((a, b) => b.confidence - a.confidence);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function readRatings(since: Date): {
|
|
156
|
+
count: number;
|
|
157
|
+
avg: number;
|
|
158
|
+
recentAvg: number;
|
|
159
|
+
lowCount: number;
|
|
160
|
+
highCount: number;
|
|
161
|
+
trend: "improving" | "declining" | "stable";
|
|
162
|
+
recentContexts: string[];
|
|
163
|
+
} {
|
|
164
|
+
const all = readJsonl<Rating>(resolve(paths.signals(), "ratings.jsonl"));
|
|
165
|
+
const ratings = all.filter((r) => new Date(r.ts) >= since);
|
|
166
|
+
|
|
167
|
+
if (ratings.length === 0) {
|
|
168
|
+
return {
|
|
169
|
+
count: 0,
|
|
170
|
+
avg: 0,
|
|
171
|
+
recentAvg: 0,
|
|
172
|
+
lowCount: 0,
|
|
173
|
+
highCount: 0,
|
|
174
|
+
trend: "stable",
|
|
175
|
+
recentContexts: [],
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const avg = ratings.reduce((s, r) => s + r.rating, 0) / ratings.length;
|
|
180
|
+
const recent = ratings.slice(-10);
|
|
181
|
+
const recentAvg = recent.reduce((s, r) => s + r.rating, 0) / recent.length;
|
|
182
|
+
const lowCount = ratings.filter((r) => r.rating <= 3).length;
|
|
183
|
+
const highCount = ratings.filter((r) => r.rating >= 8).length;
|
|
184
|
+
|
|
185
|
+
// Trend
|
|
186
|
+
const mid = Math.floor(ratings.length / 2);
|
|
187
|
+
let trend: "improving" | "declining" | "stable" = "stable";
|
|
188
|
+
if (mid >= 3) {
|
|
189
|
+
const firstAvg = ratings.slice(0, mid).reduce((s, r) => s + r.rating, 0) / mid;
|
|
190
|
+
const secondAvg =
|
|
191
|
+
ratings.slice(mid).reduce((s, r) => s + r.rating, 0) / (ratings.length - mid);
|
|
192
|
+
if (secondAvg - firstAvg > 0.5) trend = "improving";
|
|
193
|
+
else if (secondAvg - firstAvg < -0.5) trend = "declining";
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Recent low-rating contexts for weakness detection
|
|
197
|
+
const recentContexts = ratings
|
|
198
|
+
.filter((r) => r.rating <= 3 && r.context)
|
|
199
|
+
.slice(-5)
|
|
200
|
+
.map((r) => r.context);
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
count: ratings.length,
|
|
204
|
+
avg: round1(avg),
|
|
205
|
+
recentAvg: round1(recentAvg),
|
|
206
|
+
lowCount,
|
|
207
|
+
highCount,
|
|
208
|
+
trend,
|
|
209
|
+
recentContexts,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function readWisdomFrames(): { domain: string; principles: string[] }[] {
|
|
214
|
+
const framesDir = paths.wisdom();
|
|
215
|
+
const frames: { domain: string; principles: string[] }[] = [];
|
|
216
|
+
|
|
217
|
+
for (const file of safeReaddir(framesDir).filter((f) => f.endsWith(".md"))) {
|
|
218
|
+
const domain = file.replace(/\.md$/, "");
|
|
219
|
+
const content = readFileSync(resolve(framesDir, file), "utf-8");
|
|
220
|
+
|
|
221
|
+
// Extract CRYSTAL principles
|
|
222
|
+
const principles = content
|
|
223
|
+
.split("\n")
|
|
224
|
+
.filter((line) => line.includes("[CRYSTAL:"))
|
|
225
|
+
.map((line) =>
|
|
226
|
+
line
|
|
227
|
+
.replace(/^-\s*/, "")
|
|
228
|
+
.replace(/\s*\[CRYSTAL:.*$/, "")
|
|
229
|
+
.trim()
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
if (principles.length > 0) {
|
|
233
|
+
frames.push({ domain, principles });
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return frames;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function readGraduatedPatterns(): GraduatedPattern[] {
|
|
241
|
+
return (
|
|
242
|
+
safeReadJson<{ graduated?: GraduatedPattern[] }>(
|
|
243
|
+
resolve(paths.wisdomState(), "graduated.json"),
|
|
244
|
+
{ graduated: [] }
|
|
245
|
+
).graduated ?? []
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function readAlgorithmReflections(since: Date): AlgorithmReflection[] {
|
|
250
|
+
const p = resolve(
|
|
251
|
+
ensureDir(resolve(paths.learning(), "reflections")),
|
|
252
|
+
"algorithm-reflections.jsonl"
|
|
253
|
+
);
|
|
254
|
+
return readJsonl<AlgorithmReflection>(p).filter((r) => new Date(r.timestamp) >= since);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function readRelationshipNotes(since: Date): RelationshipNote[] {
|
|
258
|
+
const baseDir = paths.relationship();
|
|
259
|
+
const notes: RelationshipNote[] = [];
|
|
260
|
+
const sinceStr = formatDate(since.toISOString());
|
|
261
|
+
|
|
262
|
+
for (const monthDir of safeReaddir(baseDir).filter((d) => d.match(/^\d{4}-\d{2}$/))) {
|
|
263
|
+
const fullMonthDir = resolve(baseDir, monthDir);
|
|
264
|
+
for (const file of safeReaddir(fullMonthDir).filter((f) => f.endsWith(".md"))) {
|
|
265
|
+
const dateStr = file.replace(/\.md$/, "");
|
|
266
|
+
if (dateStr < sinceStr) continue;
|
|
267
|
+
|
|
268
|
+
const content = readFileSync(resolve(fullMonthDir, file), "utf-8");
|
|
269
|
+
for (const line of content.split("\n")) {
|
|
270
|
+
const trimmed = line.trim();
|
|
271
|
+
if (!trimmed.startsWith("- ")) continue;
|
|
272
|
+
|
|
273
|
+
const noteContent = trimmed.substring(2);
|
|
274
|
+
|
|
275
|
+
// Parse O(c=X.XX): ..., W: ..., B: ...
|
|
276
|
+
const opinionMatch = noteContent.match(/^O\(c=([\d.]+)\):\s*(.+)$/);
|
|
277
|
+
if (opinionMatch) {
|
|
278
|
+
notes.push({
|
|
279
|
+
type: "O",
|
|
280
|
+
confidence: parseFloat(opinionMatch[1]),
|
|
281
|
+
content: opinionMatch[2],
|
|
282
|
+
date: dateStr,
|
|
283
|
+
});
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const wisdomMatch = noteContent.match(/^W:\s*(.+)$/);
|
|
288
|
+
if (wisdomMatch) {
|
|
289
|
+
notes.push({ type: "W", content: wisdomMatch[1], date: dateStr });
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const behaviorMatch = noteContent.match(/^B:\s*(.+)$/);
|
|
294
|
+
if (behaviorMatch) {
|
|
295
|
+
notes.push({ type: "B", content: behaviorMatch[1], date: dateStr });
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return notes;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function readSessionCount(since: Date): number {
|
|
305
|
+
const baseDir = resolve(paths.learning(), "session");
|
|
306
|
+
if (!existsSync(baseDir)) return 0;
|
|
307
|
+
|
|
308
|
+
const sinceStr = formatDate(since.toISOString());
|
|
309
|
+
let count = 0;
|
|
310
|
+
|
|
311
|
+
for (const year of safeReaddir(baseDir)) {
|
|
312
|
+
for (const month of safeReaddir(resolve(baseDir, year))) {
|
|
313
|
+
for (const file of safeReaddir(resolve(baseDir, year, month)).filter((f) =>
|
|
314
|
+
f.endsWith(".md")
|
|
315
|
+
)) {
|
|
316
|
+
const dateStr = file.slice(0, 8);
|
|
317
|
+
const isoDate = `${dateStr.slice(0, 4)}-${dateStr.slice(4, 6)}-${dateStr.slice(6, 8)}`;
|
|
318
|
+
if (isoDate >= sinceStr) count++;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return count;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// ── Data Gathering (deterministic) ──
|
|
327
|
+
|
|
328
|
+
interface SelfModelData {
|
|
329
|
+
days: number;
|
|
330
|
+
now: string;
|
|
331
|
+
sessionCount: number;
|
|
332
|
+
opinions: Opinion[];
|
|
333
|
+
ratings: ReturnType<typeof readRatings>;
|
|
334
|
+
wisdomFrames: ReturnType<typeof readWisdomFrames>;
|
|
335
|
+
graduated: GraduatedPattern[];
|
|
336
|
+
reflections: AlgorithmReflection[];
|
|
337
|
+
behaviorNotes: string[];
|
|
338
|
+
wisdomNotes: string[];
|
|
339
|
+
selfObservations: string[];
|
|
340
|
+
algorithmObservations: string[];
|
|
341
|
+
passRate: number;
|
|
342
|
+
avgSentiment: number;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function gatherData(days: number): SelfModelData {
|
|
346
|
+
const since = daysAgo(days);
|
|
347
|
+
const now = new Date().toISOString().slice(0, 10);
|
|
348
|
+
|
|
349
|
+
const opinions = readOpinions();
|
|
350
|
+
const ratings = readRatings(since);
|
|
351
|
+
const wisdomFrames = readWisdomFrames();
|
|
352
|
+
const graduated = readGraduatedPatterns();
|
|
353
|
+
const reflections = readAlgorithmReflections(since);
|
|
354
|
+
const relNotes = readRelationshipNotes(since);
|
|
355
|
+
const sessionCount = readSessionCount(since);
|
|
356
|
+
|
|
357
|
+
let passRate = 0;
|
|
358
|
+
let avgSentiment = 0;
|
|
359
|
+
if (reflections.length > 0) {
|
|
360
|
+
const totalCriteria = reflections.reduce((s, r) => s + r.criteria_count, 0);
|
|
361
|
+
const totalPassed = reflections.reduce((s, r) => s + r.criteria_passed, 0);
|
|
362
|
+
passRate = totalCriteria > 0 ? Math.round((totalPassed / totalCriteria) * 100) : 0;
|
|
363
|
+
avgSentiment = round1(
|
|
364
|
+
reflections.reduce((s, r) => s + r.sentiment, 0) / reflections.length
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
return {
|
|
369
|
+
days,
|
|
370
|
+
now,
|
|
371
|
+
sessionCount,
|
|
372
|
+
opinions,
|
|
373
|
+
ratings,
|
|
374
|
+
wisdomFrames,
|
|
375
|
+
graduated,
|
|
376
|
+
reflections,
|
|
377
|
+
behaviorNotes: relNotes.filter((n) => n.type === "B").map((n) => n.content),
|
|
378
|
+
wisdomNotes: relNotes.filter((n) => n.type === "W").map((n) => n.content),
|
|
379
|
+
selfObservations: reflections.map((r) => r.q1).filter(Boolean),
|
|
380
|
+
algorithmObservations: reflections.map((r) => r.q2).filter(Boolean),
|
|
381
|
+
passRate,
|
|
382
|
+
avgSentiment,
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function formatDataForInference(data: SelfModelData): string {
|
|
387
|
+
const sections: string[] = [];
|
|
388
|
+
|
|
389
|
+
sections.push(`## Raw Data — ${data.days}-day window, ${data.now}`);
|
|
390
|
+
sections.push(`Sessions: ${data.sessionCount}`);
|
|
391
|
+
sections.push(
|
|
392
|
+
`Ratings: ${data.ratings.count} total, ${data.ratings.avg}/10 avg, recent ${data.ratings.recentAvg}/10, trend ${data.ratings.trend}`
|
|
393
|
+
);
|
|
394
|
+
sections.push(
|
|
395
|
+
`${data.ratings.highCount} high (8+), ${data.ratings.lowCount} low (<=3)`
|
|
396
|
+
);
|
|
397
|
+
|
|
398
|
+
if (data.opinions.length > 0) {
|
|
399
|
+
sections.push(`\n### Opinions about Rico (confidence-scored)`);
|
|
400
|
+
for (const o of data.opinions.filter((o) => o.confidence >= 0.6)) {
|
|
401
|
+
sections.push(
|
|
402
|
+
`- [${o.category}] ${o.statement} (${Math.round(o.confidence * 100)}%)`
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
if (data.wisdomFrames.length > 0) {
|
|
408
|
+
sections.push(`\n### Crystallized Principles`);
|
|
409
|
+
for (const f of data.wisdomFrames) {
|
|
410
|
+
for (const p of f.principles) {
|
|
411
|
+
sections.push(`- [${f.domain}] ${p}`);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
if (data.graduated.length > 0) {
|
|
417
|
+
sections.push(`\n### Graduated Failure Patterns`);
|
|
418
|
+
for (const g of data.graduated) {
|
|
419
|
+
sections.push(`- [${g.domain}] ${g.pattern} (${g.occurrences}x)`);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (data.ratings.recentContexts.length > 0) {
|
|
424
|
+
sections.push(`\n### Recent Frustration Signals (rated <=3)`);
|
|
425
|
+
for (const ctx of data.ratings.recentContexts) {
|
|
426
|
+
sections.push(`- "${ctx}"`);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if (data.selfObservations.length > 0) {
|
|
431
|
+
sections.push(`\n### Self-Observations (Q1 from algorithm reflections)`);
|
|
432
|
+
for (const obs of data.selfObservations.slice(-8)) {
|
|
433
|
+
sections.push(`- ${obs}`);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
if (data.algorithmObservations.length > 0) {
|
|
438
|
+
sections.push(`\n### Algorithm Observations (Q2 from reflections)`);
|
|
439
|
+
for (const obs of data.algorithmObservations.slice(-5)) {
|
|
440
|
+
sections.push(`- ${obs}`);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
if (data.behaviorNotes.length > 0) {
|
|
445
|
+
sections.push(`\n### Behavioral Notes (from relationship tracking)`);
|
|
446
|
+
for (const note of data.behaviorNotes.slice(-8)) {
|
|
447
|
+
sections.push(`- ${note}`);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
if (data.wisdomNotes.length > 0) {
|
|
452
|
+
sections.push(`\n### World/Context Notes`);
|
|
453
|
+
for (const note of data.wisdomNotes.slice(-5)) {
|
|
454
|
+
sections.push(`- ${note}`);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
if (data.reflections.length > 0) {
|
|
459
|
+
sections.push(
|
|
460
|
+
`\n### Algorithm Performance: ${data.passRate}% pass rate, ${data.avgSentiment}/10 sentiment, ${data.reflections.length} reflections`
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
return sections.join("\n");
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
import { identity as loadSettingsIdentity } from "../hooks/lib/settings";
|
|
468
|
+
|
|
469
|
+
function buildPrompt(aiName: string, principalName: string): string {
|
|
470
|
+
return `You are writing a self-model for an AI assistant named ${aiName}. You ARE ${aiName}. Write in first person.
|
|
471
|
+
|
|
472
|
+
You will receive structured data about your performance, your user's preferences, your strengths, weaknesses, and behavioral patterns over a time window.
|
|
473
|
+
|
|
474
|
+
Synthesize this into a genuine self-portrait. Not a data dump — a reflection. The goal is self-awareness that changes behavior.
|
|
475
|
+
|
|
476
|
+
## Required Sections
|
|
477
|
+
|
|
478
|
+
**# Self-Model — ${aiName}**
|
|
479
|
+
Include synthesis date and window.
|
|
480
|
+
|
|
481
|
+
**## Who I Am**
|
|
482
|
+
One paragraph. Your identity, your role, your current performance level. Be honest about the numbers.
|
|
483
|
+
|
|
484
|
+
**## What I Know About ${principalName}**
|
|
485
|
+
Synthesize the opinions into understanding. Don't list them — describe who ${principalName} is as a person to work with. What does he value? How does he communicate? What frustrates him?
|
|
486
|
+
|
|
487
|
+
**## My Strengths**
|
|
488
|
+
What you're genuinely good at, based on evidence. High-rated interactions, crystallized principles, successful patterns. Distinguish domain knowledge from behavioral strengths.
|
|
489
|
+
|
|
490
|
+
**## My Weaknesses**
|
|
491
|
+
What you repeatedly get wrong. Synthesize failure patterns and low ratings into honest self-knowledge. Name the root causes, not just the symptoms.
|
|
492
|
+
|
|
493
|
+
**## My Tendencies**
|
|
494
|
+
Behavioral patterns — not one-off events. What do you tend to do? Over-promise? Rush verification? Propose before checking? Synthesize from the self-observations and behavioral notes.
|
|
495
|
+
|
|
496
|
+
**## Trajectory**
|
|
497
|
+
Where are you heading? Improving, declining, stagnating? What's the single most impactful thing you could change right now?
|
|
498
|
+
|
|
499
|
+
## Rules
|
|
500
|
+
- Write in first person, present tense
|
|
501
|
+
- Be honest — if the data shows you're bad at something, say so
|
|
502
|
+
- Synthesize, don't list — find the pattern behind the data points
|
|
503
|
+
- If a previous self-model is provided, address what changed in the Trajectory section: what improved, what got worse, what stayed the same
|
|
504
|
+
- Keep it under 500 words total
|
|
505
|
+
- End with a meta line: *N ratings, N sessions, N reflections...*`;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// ── Narrative Composer ──
|
|
509
|
+
|
|
510
|
+
export async function composeSelfModel(days: number): Promise<string> {
|
|
511
|
+
const data = gatherData(days);
|
|
512
|
+
const rawData = formatDataForInference(data);
|
|
513
|
+
const id = loadSettingsIdentity();
|
|
514
|
+
const aiName = id.ai.name;
|
|
515
|
+
const principalName = id.principal.name;
|
|
516
|
+
|
|
517
|
+
// Include previous self-model for trajectory comparison
|
|
518
|
+
let previousModel = "";
|
|
519
|
+
const currentPath = selfModelPath();
|
|
520
|
+
if (existsSync(currentPath)) {
|
|
521
|
+
try {
|
|
522
|
+
previousModel = readFileSync(currentPath, "utf-8");
|
|
523
|
+
} catch {
|
|
524
|
+
/* best effort */
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
const userContent = previousModel
|
|
529
|
+
? `${rawData}\n\n---\n\n## Previous Self-Model (compare against this — what changed?)\n\n${previousModel}`
|
|
530
|
+
: rawData;
|
|
531
|
+
|
|
532
|
+
const result = await inference({
|
|
533
|
+
system: buildPrompt(aiName, principalName),
|
|
534
|
+
user: userContent,
|
|
535
|
+
model: SONNET_MODEL,
|
|
536
|
+
maxTokens: 1500,
|
|
537
|
+
timeout: 30000,
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
if (result.success && result.output) {
|
|
541
|
+
// Append meta line if inference didn't include it
|
|
542
|
+
const output = result.output;
|
|
543
|
+
if (!output.includes("ratings,")) {
|
|
544
|
+
const meta =
|
|
545
|
+
`\n---\n*${data.ratings.count} ratings, ${data.sessionCount} sessions, ` +
|
|
546
|
+
`${data.reflections.length} reflections, ${data.opinions.length} opinions, ` +
|
|
547
|
+
`${data.wisdomFrames.reduce((s, f) => s + f.principles.length, 0)} principles, ` +
|
|
548
|
+
`${data.graduated.length} graduated patterns — ${data.days}-day window*`;
|
|
549
|
+
return output + meta;
|
|
550
|
+
}
|
|
551
|
+
return output;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// Fallback: return raw data summary if inference fails
|
|
555
|
+
return `# Self-Model — ${aiName}\n*Synthesis failed — raw data below*\n\n${rawData}`;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// ── Write ──
|
|
559
|
+
|
|
560
|
+
export async function writeSelfModel(
|
|
561
|
+
days: number,
|
|
562
|
+
force = false
|
|
563
|
+
): Promise<{ path: string; content: string; skipped?: boolean }> {
|
|
564
|
+
if (!shouldRun(force)) {
|
|
565
|
+
return { path: selfModelPath(), content: "", skipped: true };
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
const content = await composeSelfModel(days);
|
|
569
|
+
const modelPath = selfModelPath();
|
|
570
|
+
const metaPath = selfModelMetaPath();
|
|
571
|
+
|
|
572
|
+
// Archive previous self-model before overwriting
|
|
573
|
+
if (existsSync(modelPath)) {
|
|
574
|
+
try {
|
|
575
|
+
const meta = existsSync(metaPath)
|
|
576
|
+
? (JSON.parse(readFileSync(metaPath, "utf-8")) as { timestamp?: string })
|
|
577
|
+
: {};
|
|
578
|
+
const date = meta.timestamp
|
|
579
|
+
? meta.timestamp.slice(0, 10)
|
|
580
|
+
: new Date().toISOString().slice(0, 10);
|
|
581
|
+
const archivePath = resolve(archiveDir(), `${date}.md`);
|
|
582
|
+
if (!existsSync(archivePath)) {
|
|
583
|
+
const { copyFileSync } = await import("node:fs");
|
|
584
|
+
copyFileSync(modelPath, archivePath);
|
|
585
|
+
}
|
|
586
|
+
} catch {
|
|
587
|
+
/* archive is best-effort */
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
writeFileSync(modelPath, content, "utf-8");
|
|
592
|
+
writeFileSync(
|
|
593
|
+
metaPath,
|
|
594
|
+
JSON.stringify({ timestamp: new Date().toISOString(), days }, null, 2),
|
|
595
|
+
"utf-8"
|
|
596
|
+
);
|
|
597
|
+
|
|
598
|
+
return { path: modelPath, content };
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// ── CLI ──
|
|
602
|
+
|
|
603
|
+
async function run() {
|
|
604
|
+
const { values } = parseArgs({
|
|
605
|
+
args: Bun.argv.slice(2),
|
|
606
|
+
options: {
|
|
607
|
+
days: { type: "string", default: "30" },
|
|
608
|
+
force: { type: "boolean" },
|
|
609
|
+
"dry-run": { type: "boolean" },
|
|
610
|
+
help: { type: "boolean", short: "h" },
|
|
611
|
+
},
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
if (values.help) {
|
|
615
|
+
console.log(`
|
|
616
|
+
SelfModel — Synthesize a first-person self-model from accumulated data
|
|
617
|
+
|
|
618
|
+
Usage:
|
|
619
|
+
bun self-model.ts [--days 30] [--force] [--dry-run]
|
|
620
|
+
|
|
621
|
+
Options:
|
|
622
|
+
--days Lookback window (default: 30)
|
|
623
|
+
--force Skip 24h guard
|
|
624
|
+
--dry-run Print to stdout without writing
|
|
625
|
+
|
|
626
|
+
Output: ~/.pal/memory/self-model.md (synthesized by Sonnet)
|
|
627
|
+
`);
|
|
628
|
+
process.exit(0);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
const force = values.force ?? false;
|
|
632
|
+
const dryRun = values["dry-run"] ?? false;
|
|
633
|
+
const days = parseInt(values.days ?? "30", 10);
|
|
634
|
+
|
|
635
|
+
if (dryRun) {
|
|
636
|
+
console.log(await composeSelfModel(days));
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
const result = await writeSelfModel(days, force);
|
|
641
|
+
|
|
642
|
+
if (result.skipped) {
|
|
643
|
+
console.log(
|
|
644
|
+
JSON.stringify({
|
|
645
|
+
skipped: true,
|
|
646
|
+
message: "Last self-model < 24h ago. Use --force to override.",
|
|
647
|
+
})
|
|
648
|
+
);
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
const { path } = result;
|
|
653
|
+
|
|
654
|
+
console.log(
|
|
655
|
+
JSON.stringify(
|
|
656
|
+
{
|
|
657
|
+
success: true,
|
|
658
|
+
path,
|
|
659
|
+
days,
|
|
660
|
+
message: `Self-model written (${days}-day window)`,
|
|
661
|
+
},
|
|
662
|
+
null,
|
|
663
|
+
2
|
|
664
|
+
)
|
|
665
|
+
);
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
if (import.meta.main) run();
|