portable-agent-layer 0.17.0 → 0.18.1
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/ALGORITHM.md +30 -9
- package/assets/templates/settings.claude.json +2 -1
- package/package.json +3 -2
- package/src/hooks/lib/paths.ts +1 -0
- package/src/targets/lib.ts +2 -1
- package/src/tools/agent/analyze.ts +157 -0
- package/src/tools/agent/wisdom-frame.ts +235 -0
- package/src/tools/export.ts +23 -17
- package/src/tools/import.ts +65 -77
- package/src/tools/relationship-reflect.ts +80 -85
- package/src/tools/session-summary.ts +44 -41
- package/src/tools/token-cost.ts +134 -92
- package/src/tools/analyze.ts +0 -152
package/src/tools/token-cost.ts
CHANGED
|
@@ -15,27 +15,9 @@ import { parseArgs } from "node:util";
|
|
|
15
15
|
import { MODEL_PRICING } from "../hooks/lib/models";
|
|
16
16
|
import { palHome } from "../hooks/lib/paths";
|
|
17
17
|
|
|
18
|
-
// ── Args ──
|
|
19
|
-
|
|
20
|
-
const { values: args } = parseArgs({
|
|
21
|
-
options: {
|
|
22
|
-
today: { type: "boolean", default: false },
|
|
23
|
-
week: { type: "boolean", default: false },
|
|
24
|
-
month: { type: "boolean", default: false },
|
|
25
|
-
all: { type: "boolean", default: false },
|
|
26
|
-
project: { type: "string" },
|
|
27
|
-
},
|
|
28
|
-
strict: false,
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
const now = new Date();
|
|
32
|
-
const todayPrefix = now.toISOString().slice(0, 10);
|
|
33
|
-
const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString();
|
|
34
|
-
const monthAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000).toISOString();
|
|
35
|
-
|
|
36
18
|
// ── Types ──
|
|
37
19
|
|
|
38
|
-
interface Bucket {
|
|
20
|
+
export interface Bucket {
|
|
39
21
|
input: number;
|
|
40
22
|
output: number;
|
|
41
23
|
cacheWrite: number;
|
|
@@ -44,14 +26,30 @@ interface Bucket {
|
|
|
44
26
|
calls: number;
|
|
45
27
|
}
|
|
46
28
|
|
|
47
|
-
function emptyBucket(): Bucket {
|
|
29
|
+
export function emptyBucket(): Bucket {
|
|
48
30
|
return { input: 0, output: 0, cacheWrite: 0, cacheRead: 0, cost: 0, calls: 0 };
|
|
49
31
|
}
|
|
50
32
|
|
|
33
|
+
export interface TimeBuckets {
|
|
34
|
+
today: Bucket;
|
|
35
|
+
week: Bucket;
|
|
36
|
+
month: Bucket;
|
|
37
|
+
total: Bucket;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function emptyTimeBuckets(): TimeBuckets {
|
|
41
|
+
return {
|
|
42
|
+
today: emptyBucket(),
|
|
43
|
+
week: emptyBucket(),
|
|
44
|
+
month: emptyBucket(),
|
|
45
|
+
total: emptyBucket(),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ── Helpers ──
|
|
50
|
+
|
|
51
51
|
function findPricing(model: string): (typeof MODEL_PRICING)[string] | null {
|
|
52
|
-
// Exact match first
|
|
53
52
|
if (MODEL_PRICING[model]) return MODEL_PRICING[model];
|
|
54
|
-
// Prefix match (e.g. "claude-sonnet-4-5-20250929" matches "claude-sonnet-4-5")
|
|
55
53
|
for (const key of Object.keys(MODEL_PRICING)) {
|
|
56
54
|
if (model.startsWith(key)) return MODEL_PRICING[key];
|
|
57
55
|
}
|
|
@@ -76,7 +74,7 @@ function costForUsage(
|
|
|
76
74
|
);
|
|
77
75
|
}
|
|
78
76
|
|
|
79
|
-
function addToBucket(
|
|
77
|
+
export function addToBucket(
|
|
80
78
|
bucket: Bucket,
|
|
81
79
|
model: string,
|
|
82
80
|
input: number,
|
|
@@ -120,22 +118,6 @@ function printDetailed(label: string, b: Bucket, labelWidth = 14): void {
|
|
|
120
118
|
|
|
121
119
|
// ── Claude Code transcripts ──
|
|
122
120
|
|
|
123
|
-
interface TimeBuckets {
|
|
124
|
-
today: Bucket;
|
|
125
|
-
week: Bucket;
|
|
126
|
-
month: Bucket;
|
|
127
|
-
total: Bucket;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function emptyTimeBuckets(): TimeBuckets {
|
|
131
|
-
return {
|
|
132
|
-
today: emptyBucket(),
|
|
133
|
-
week: emptyBucket(),
|
|
134
|
-
month: emptyBucket(),
|
|
135
|
-
total: emptyBucket(),
|
|
136
|
-
};
|
|
137
|
-
}
|
|
138
|
-
|
|
139
121
|
function addToTimeBuckets(
|
|
140
122
|
tb: TimeBuckets,
|
|
141
123
|
ts: string,
|
|
@@ -143,7 +125,10 @@ function addToTimeBuckets(
|
|
|
143
125
|
input: number,
|
|
144
126
|
output: number,
|
|
145
127
|
cacheWrite: number,
|
|
146
|
-
cacheRead: number
|
|
128
|
+
cacheRead: number,
|
|
129
|
+
todayPrefix: string,
|
|
130
|
+
weekAgo: string,
|
|
131
|
+
monthAgo: string
|
|
147
132
|
): void {
|
|
148
133
|
addToBucket(tb.total, model, input, output, cacheWrite, cacheRead);
|
|
149
134
|
if (ts >= monthAgo) addToBucket(tb.month, model, input, output, cacheWrite, cacheRead);
|
|
@@ -152,11 +137,16 @@ function addToTimeBuckets(
|
|
|
152
137
|
addToBucket(tb.today, model, input, output, cacheWrite, cacheRead);
|
|
153
138
|
}
|
|
154
139
|
|
|
155
|
-
function readClaudeCode(): {
|
|
140
|
+
export function readClaudeCode(projectFilter?: string): {
|
|
156
141
|
buckets: TimeBuckets;
|
|
157
142
|
byModel: Record<string, Bucket>;
|
|
158
143
|
byProject: Record<string, TimeBuckets>;
|
|
159
144
|
} {
|
|
145
|
+
const now = new Date();
|
|
146
|
+
const todayPrefix = now.toISOString().slice(0, 10);
|
|
147
|
+
const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString();
|
|
148
|
+
const monthAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000).toISOString();
|
|
149
|
+
|
|
160
150
|
const buckets = emptyTimeBuckets();
|
|
161
151
|
const byModel: Record<string, Bucket> = {};
|
|
162
152
|
const byProject: Record<string, TimeBuckets> = {};
|
|
@@ -170,20 +160,17 @@ function readClaudeCode(): {
|
|
|
170
160
|
|
|
171
161
|
for (const projDir of projectDirs) {
|
|
172
162
|
const projPath = resolve(claudeDir, projDir);
|
|
173
|
-
// Project dir is like "-Users-rico-Development-git-myproject" — extract last meaningful segment
|
|
174
163
|
const segments = projDir.replace(/^-/, "").split("-");
|
|
175
164
|
const projName = segments.length > 1 ? segments.slice(-1)[0] : projDir;
|
|
176
165
|
|
|
177
|
-
if (typeof
|
|
166
|
+
if (typeof projectFilter === "string" && !projName.includes(projectFilter)) continue;
|
|
178
167
|
|
|
179
|
-
// Collect all JSONL files: top-level + subagent directories
|
|
180
168
|
const jsonlFiles: string[] = [];
|
|
181
169
|
|
|
182
170
|
for (const entry of readdirSync(projPath, { withFileTypes: true })) {
|
|
183
171
|
if (entry.isFile() && entry.name.endsWith(".jsonl")) {
|
|
184
172
|
jsonlFiles.push(resolve(projPath, entry.name));
|
|
185
173
|
} else if (entry.isDirectory()) {
|
|
186
|
-
// Check for subagent transcripts inside session directories
|
|
187
174
|
const subagentsDir = resolve(projPath, entry.name, "subagents");
|
|
188
175
|
try {
|
|
189
176
|
for (const sub of readdirSync(subagentsDir)) {
|
|
@@ -232,13 +219,35 @@ function readClaudeCode(): {
|
|
|
232
219
|
const cw = usage.cache_creation_input_tokens ?? 0;
|
|
233
220
|
const cr = usage.cache_read_input_tokens ?? 0;
|
|
234
221
|
|
|
235
|
-
addToTimeBuckets(
|
|
222
|
+
addToTimeBuckets(
|
|
223
|
+
buckets,
|
|
224
|
+
ts,
|
|
225
|
+
model,
|
|
226
|
+
input,
|
|
227
|
+
output,
|
|
228
|
+
cw,
|
|
229
|
+
cr,
|
|
230
|
+
todayPrefix,
|
|
231
|
+
weekAgo,
|
|
232
|
+
monthAgo
|
|
233
|
+
);
|
|
236
234
|
|
|
237
235
|
if (!byModel[model]) byModel[model] = emptyBucket();
|
|
238
236
|
addToBucket(byModel[model], model, input, output, cw, cr);
|
|
239
237
|
|
|
240
238
|
if (!byProject[projName]) byProject[projName] = emptyTimeBuckets();
|
|
241
|
-
addToTimeBuckets(
|
|
239
|
+
addToTimeBuckets(
|
|
240
|
+
byProject[projName],
|
|
241
|
+
ts,
|
|
242
|
+
model,
|
|
243
|
+
input,
|
|
244
|
+
output,
|
|
245
|
+
cw,
|
|
246
|
+
cr,
|
|
247
|
+
todayPrefix,
|
|
248
|
+
weekAgo,
|
|
249
|
+
monthAgo
|
|
250
|
+
);
|
|
242
251
|
} catch {
|
|
243
252
|
/* skip */
|
|
244
253
|
}
|
|
@@ -251,7 +260,15 @@ function readClaudeCode(): {
|
|
|
251
260
|
|
|
252
261
|
// ── PAL Haiku inference ──
|
|
253
262
|
|
|
254
|
-
function readPalInference(): {
|
|
263
|
+
export function readPalInference(): {
|
|
264
|
+
buckets: TimeBuckets;
|
|
265
|
+
byCaller: Record<string, Bucket>;
|
|
266
|
+
} {
|
|
267
|
+
const now = new Date();
|
|
268
|
+
const todayPrefix = now.toISOString().slice(0, 10);
|
|
269
|
+
const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString();
|
|
270
|
+
const monthAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000).toISOString();
|
|
271
|
+
|
|
255
272
|
const buckets = emptyTimeBuckets();
|
|
256
273
|
const byCaller: Record<string, Bucket> = {};
|
|
257
274
|
|
|
@@ -270,7 +287,18 @@ function readPalInference(): { buckets: TimeBuckets; byCaller: Record<string, Bu
|
|
|
270
287
|
inputTokens: number;
|
|
271
288
|
outputTokens: number;
|
|
272
289
|
};
|
|
273
|
-
addToTimeBuckets(
|
|
290
|
+
addToTimeBuckets(
|
|
291
|
+
buckets,
|
|
292
|
+
e.ts,
|
|
293
|
+
e.model,
|
|
294
|
+
e.inputTokens,
|
|
295
|
+
e.outputTokens,
|
|
296
|
+
0,
|
|
297
|
+
0,
|
|
298
|
+
todayPrefix,
|
|
299
|
+
weekAgo,
|
|
300
|
+
monthAgo
|
|
301
|
+
);
|
|
274
302
|
if (!byCaller[e.caller]) byCaller[e.caller] = emptyBucket();
|
|
275
303
|
addToBucket(byCaller[e.caller], e.model, e.inputTokens, e.outputTokens, 0, 0);
|
|
276
304
|
} catch {
|
|
@@ -281,54 +309,68 @@ function readPalInference(): { buckets: TimeBuckets; byCaller: Record<string, Bu
|
|
|
281
309
|
return { buckets, byCaller };
|
|
282
310
|
}
|
|
283
311
|
|
|
284
|
-
// ──
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
312
|
+
// ── CLI ──
|
|
313
|
+
|
|
314
|
+
function run() {
|
|
315
|
+
parseArgs({
|
|
316
|
+
options: {
|
|
317
|
+
today: { type: "boolean", default: false },
|
|
318
|
+
week: { type: "boolean", default: false },
|
|
319
|
+
month: { type: "boolean", default: false },
|
|
320
|
+
all: { type: "boolean", default: false },
|
|
321
|
+
project: { type: "string" },
|
|
322
|
+
},
|
|
323
|
+
strict: false,
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
const cc = readClaudeCode();
|
|
327
|
+
const pal = readPalInference();
|
|
328
|
+
|
|
329
|
+
console.log("\n Claude Code Usage\n");
|
|
330
|
+
printRow("Today", cc.buckets.today);
|
|
331
|
+
printRow("7d", cc.buckets.week);
|
|
332
|
+
printRow("30d", cc.buckets.month);
|
|
333
|
+
printRow("Total", cc.buckets.total);
|
|
334
|
+
|
|
335
|
+
if (Object.keys(cc.byModel).length > 0) {
|
|
336
|
+
console.log("\n By Model (all time)\n");
|
|
337
|
+
const sorted = Object.entries(cc.byModel).sort((a, b) => b[1].cost - a[1].cost);
|
|
338
|
+
const modelNames = sorted.map(([m]) => m.replace("claude-", ""));
|
|
339
|
+
const modelWidth = Math.max(14, ...modelNames.map((n) => n.length + 2));
|
|
340
|
+
for (let i = 0; i < sorted.length; i++) {
|
|
341
|
+
printDetailed(modelNames[i], sorted[i][1], modelWidth);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
294
344
|
|
|
295
|
-
if (Object.keys(cc.
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
345
|
+
if (Object.keys(cc.byProject).length > 1) {
|
|
346
|
+
console.log("\n By Project (all time)\n");
|
|
347
|
+
const sorted = Object.entries(cc.byProject).sort(
|
|
348
|
+
(a, b) => b[1].total.cost - a[1].total.cost
|
|
349
|
+
);
|
|
350
|
+
for (const [proj, tb] of sorted) {
|
|
351
|
+
printRow(proj, tb.total);
|
|
352
|
+
}
|
|
302
353
|
}
|
|
303
|
-
}
|
|
304
354
|
|
|
305
|
-
if (
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
(
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
printRow(proj, tb.total);
|
|
355
|
+
if (pal.buckets.total.calls > 0) {
|
|
356
|
+
console.log("\n PAL Inference (Haiku)\n");
|
|
357
|
+
printRow("Today", pal.buckets.today);
|
|
358
|
+
printRow("7d", pal.buckets.week);
|
|
359
|
+
printRow("30d", pal.buckets.month);
|
|
360
|
+
printRow("Total", pal.buckets.total);
|
|
312
361
|
}
|
|
313
|
-
}
|
|
314
362
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
363
|
+
const grand = emptyBucket();
|
|
364
|
+
for (const b of [cc.buckets.total, pal.buckets.total]) {
|
|
365
|
+
grand.input += b.input;
|
|
366
|
+
grand.output += b.output;
|
|
367
|
+
grand.cacheWrite += b.cacheWrite;
|
|
368
|
+
grand.cacheRead += b.cacheRead;
|
|
369
|
+
grand.cost += b.cost;
|
|
370
|
+
grand.calls += b.calls;
|
|
371
|
+
}
|
|
322
372
|
|
|
323
|
-
|
|
324
|
-
const grand = emptyBucket();
|
|
325
|
-
for (const b of [cc.buckets.total, pal.buckets.total]) {
|
|
326
|
-
grand.input += b.input;
|
|
327
|
-
grand.output += b.output;
|
|
328
|
-
grand.cacheWrite += b.cacheWrite;
|
|
329
|
-
grand.cacheRead += b.cacheRead;
|
|
330
|
-
grand.cost += b.cost;
|
|
331
|
-
grand.calls += b.calls;
|
|
373
|
+
console.log(`\n Grand Total: ${fmtCost(grand.cost)}\n`);
|
|
332
374
|
}
|
|
333
375
|
|
|
334
|
-
|
|
376
|
+
if (import.meta.main) run();
|
package/src/tools/analyze.ts
DELETED
|
@@ -1,152 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bun
|
|
2
|
-
/**
|
|
3
|
-
* Unified Learning Analysis — graduation patterns + ratings summary.
|
|
4
|
-
*
|
|
5
|
-
* Reads failures and session learnings, finds recurring patterns,
|
|
6
|
-
* summarizes ratings, and generates recommendations.
|
|
7
|
-
*
|
|
8
|
-
* Usage: bun run tool:analyze
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { parseArgs } from "node:util";
|
|
12
|
-
import { analyze } from "../hooks/lib/graduation";
|
|
13
|
-
|
|
14
|
-
// ── ANSI Colors ──
|
|
15
|
-
|
|
16
|
-
const c = {
|
|
17
|
-
bold: (s: string) => `\x1b[1m${s}\x1b[0m`,
|
|
18
|
-
dim: (s: string) => `\x1b[2m${s}\x1b[0m`,
|
|
19
|
-
cyan: (s: string) => `\x1b[36m${s}\x1b[0m`,
|
|
20
|
-
yellow: (s: string) => `\x1b[33m${s}\x1b[0m`,
|
|
21
|
-
green: (s: string) => `\x1b[32m${s}\x1b[0m`,
|
|
22
|
-
red: (s: string) => `\x1b[31m${s}\x1b[0m`,
|
|
23
|
-
magenta: (s: string) => `\x1b[35m${s}\x1b[0m`,
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
const { values } = parseArgs({
|
|
27
|
-
args: Bun.argv.slice(2),
|
|
28
|
-
options: {
|
|
29
|
-
help: { type: "boolean", short: "h" },
|
|
30
|
-
actionable: { type: "boolean", short: "a" },
|
|
31
|
-
},
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
if (values.help) {
|
|
35
|
-
console.log(`
|
|
36
|
-
PAL Learning Analysis — unified graduation + ratings report
|
|
37
|
-
|
|
38
|
-
Reads all captured failures (rating ≤3) and session learnings,
|
|
39
|
-
groups recurring patterns via Dice similarity on context text,
|
|
40
|
-
and summarizes rating trends.
|
|
41
|
-
|
|
42
|
-
Sections:
|
|
43
|
-
Ratings Overall average, low/high counts
|
|
44
|
-
Graduation Patterns with 3+ occurrences → ready to crystallize
|
|
45
|
-
Emerging Patterns with 2 occurrences → one more to graduate
|
|
46
|
-
|
|
47
|
-
Flags:
|
|
48
|
-
--actionable, -a Generate actionable recommendations via Haiku inference
|
|
49
|
-
|
|
50
|
-
To crystallize a graduated pattern, add it to the target wisdom frame:
|
|
51
|
-
- Your principle here [CRYSTAL: 85%]
|
|
52
|
-
|
|
53
|
-
Usage: bun run tool:analyze [--actionable] [--help]
|
|
54
|
-
`);
|
|
55
|
-
process.exit(0);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const result = await analyze({ actionable: values.actionable });
|
|
59
|
-
|
|
60
|
-
const hasPatterns = result.candidates.length > 0 || result.emerging.length > 0;
|
|
61
|
-
const hasRatings = result.ratings !== null;
|
|
62
|
-
|
|
63
|
-
if (!hasPatterns && !hasRatings) {
|
|
64
|
-
console.log("\n No patterns or ratings data found.\n");
|
|
65
|
-
process.exit(0);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// ── Ratings Summary ──
|
|
69
|
-
|
|
70
|
-
if (result.ratings) {
|
|
71
|
-
const r = result.ratings;
|
|
72
|
-
const avgColor = r.average >= 7 ? c.green : r.average <= 4 ? c.red : c.yellow;
|
|
73
|
-
console.log(
|
|
74
|
-
`\n ${c.bold("Ratings:")} ${avgColor(`${r.average.toFixed(1)}/10`)} avg (${r.total} total)`
|
|
75
|
-
);
|
|
76
|
-
console.log(
|
|
77
|
-
` ${c.red(`Low (≤4): ${r.low.count}`)} | ${c.green(`High (≥7): ${r.high.count}`)}`
|
|
78
|
-
);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// ── Graduation Candidates ──
|
|
82
|
-
|
|
83
|
-
if (result.candidates.length > 0) {
|
|
84
|
-
console.log(
|
|
85
|
-
`\n ${c.bold(c.green(`Graduation Report — ${result.candidates.length} pattern(s) detected`))}\n`
|
|
86
|
-
);
|
|
87
|
-
console.log(` ${c.dim("─────────────────────────────────────────────────")}\n`);
|
|
88
|
-
|
|
89
|
-
for (const candidate of result.candidates) {
|
|
90
|
-
console.log(
|
|
91
|
-
` ${c.cyan(`[${candidate.domain}]`)} ${c.bold(`${candidate.entries.length}x`)} occurrences`
|
|
92
|
-
);
|
|
93
|
-
console.log("");
|
|
94
|
-
|
|
95
|
-
for (const entry of candidate.entries) {
|
|
96
|
-
const sourceType = entry.source.startsWith("failure:") ? "failure" : "learning";
|
|
97
|
-
const tag =
|
|
98
|
-
sourceType === "failure" ? c.red(`[${sourceType}]`) : c.yellow(`[${sourceType}]`);
|
|
99
|
-
console.log(
|
|
100
|
-
` ${c.dim(entry.date || "unknown")} ${tag} ${entry.text.slice(0, 100)}`
|
|
101
|
-
);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
console.log(`\n ${c.dim("Files:")}`);
|
|
105
|
-
for (const entry of candidate.entries) {
|
|
106
|
-
console.log(` ${c.dim(entry.path)}`);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
console.log("");
|
|
110
|
-
console.log(
|
|
111
|
-
` Target frame: ${c.magenta(`memory/wisdom/frames/${candidate.domain}.md`)}`
|
|
112
|
-
);
|
|
113
|
-
console.log(` ${c.dim("─────────────────────────────────────────────────")}\n`);
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// ── Emerging Patterns ──
|
|
118
|
-
|
|
119
|
-
if (result.emerging.length > 0) {
|
|
120
|
-
console.log(` ${c.bold(c.yellow("Emerging (2x — one more to graduate)"))}\n`);
|
|
121
|
-
for (const group of result.emerging) {
|
|
122
|
-
console.log(` ${c.cyan(`[${group.domain}]`)} ${c.bold(`${group.entries.length}x`)}`);
|
|
123
|
-
for (const entry of group.entries) {
|
|
124
|
-
const sourceType = entry.source.startsWith("failure:") ? "failure" : "learning";
|
|
125
|
-
const tag =
|
|
126
|
-
sourceType === "failure" ? c.red(`[${sourceType}]`) : c.yellow(`[${sourceType}]`);
|
|
127
|
-
console.log(
|
|
128
|
-
` ${c.dim(entry.date || "unknown")} ${tag} ${entry.text.slice(0, 80)}`
|
|
129
|
-
);
|
|
130
|
-
}
|
|
131
|
-
console.log(" Files:");
|
|
132
|
-
for (const entry of group.entries) {
|
|
133
|
-
console.log(` ${c.dim(entry.path)}`);
|
|
134
|
-
}
|
|
135
|
-
console.log("");
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// ── Recommendations ──
|
|
140
|
-
|
|
141
|
-
if (result.recommendations.length > 0) {
|
|
142
|
-
console.log(` ${c.bold("Recommendations:")}\n`);
|
|
143
|
-
for (const rec of result.recommendations) {
|
|
144
|
-
console.log(` ${rec}`);
|
|
145
|
-
}
|
|
146
|
-
console.log("");
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
if (result.candidates.length > 0) {
|
|
150
|
-
console.log(` To crystallize: add a line to the wisdom frame file.`);
|
|
151
|
-
console.log(` Format: ${c.green("- Your principle here [CRYSTAL: 85%]")}\n`);
|
|
152
|
-
}
|