portable-agent-layer 0.71.0 → 0.72.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.
Files changed (45) hide show
  1. package/package.json +1 -1
  2. package/src/cli/migrate.ts +1 -1
  3. package/src/cli/skill.ts +1 -1
  4. package/src/hooks/CompactRecover.ts +28 -86
  5. package/src/hooks/LedgerUnapplied.ts +3 -28
  6. package/src/hooks/LoadContext.ts +33 -60
  7. package/src/hooks/SecurityValidator.ts +16 -109
  8. package/src/hooks/handlers/failure-principle.ts +19 -44
  9. package/src/hooks/handlers/session-intelligence.ts +13 -70
  10. package/src/hooks/lib/capture-store.ts +103 -0
  11. package/src/hooks/lib/compact-recall.ts +89 -0
  12. package/src/hooks/lib/failure-principle.ts +98 -0
  13. package/src/hooks/lib/ledger-hook.ts +35 -0
  14. package/src/hooks/lib/ledger.ts +48 -1
  15. package/src/hooks/lib/security-gate.ts +159 -0
  16. package/src/hooks/lib/session-context.ts +74 -0
  17. package/src/tools/agent/algorithm-reflect.ts +28 -97
  18. package/src/tools/agent/analyze.ts +19 -120
  19. package/src/tools/agent/handoff-note.ts +29 -77
  20. package/src/tools/agent/project.ts +13 -134
  21. package/src/tools/agent/relationship-note.ts +27 -46
  22. package/src/tools/agent/synthesize.ts +1 -1
  23. package/src/tools/agent/thread.ts +43 -123
  24. package/src/tools/control-room/data.ts +2 -2
  25. package/src/tools/control-room/matrix.ts +1 -1
  26. package/src/tools/control-room/ui/ledger.tsx +2 -1
  27. package/src/tools/ledger/view.ts +3 -0
  28. package/src/tools/lib/algorithm-reflect.ts +84 -0
  29. package/src/tools/lib/analyze-report.ts +120 -0
  30. package/src/tools/lib/handoff-note.ts +88 -0
  31. package/src/tools/lib/note-flags.ts +59 -0
  32. package/src/tools/lib/project-isc.ts +151 -0
  33. package/src/tools/lib/relationship-reflect.ts +402 -0
  34. package/src/tools/lib/self-model.ts +499 -0
  35. package/src/tools/lib/session-usage.ts +216 -0
  36. package/src/tools/lib/skill-doctor.ts +457 -0
  37. package/src/tools/lib/thread.ts +119 -0
  38. package/src/tools/lib/token-report.ts +173 -0
  39. package/src/tools/lib/transcript-usage.ts +42 -0
  40. package/src/tools/lib/usage-buckets.ts +329 -0
  41. package/src/tools/relationship-reflect.ts +48 -412
  42. package/src/tools/self-model.ts +76 -558
  43. package/src/tools/session-summary.ts +8 -215
  44. package/src/tools/skill-doctor.ts +9 -444
  45. package/src/tools/token-cost.ts +18 -428
@@ -13,371 +13,49 @@
13
13
  * bun run tool:reflect -- --dry-run # Preview without writing
14
14
  */
15
15
 
16
- import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
16
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
17
17
  import { resolve } from "node:path";
18
18
  import { parseArgs } from "node:util";
19
- import {
20
- addEvidence,
21
- createOpinion,
22
- findSimilarOpinion,
23
- readOpinions,
24
- saveOpinion,
25
- setLastReflectDate,
26
- } from "../hooks/lib/opinions";
19
+ import { readOpinions, saveOpinion, setLastReflectDate } from "../hooks/lib/opinions";
27
20
  import { palHome } from "../hooks/lib/paths";
28
- import { similarity } from "../hooks/lib/text-similarity";
29
21
  import { emit } from "./lib/emit";
22
+ import {
23
+ consoleLines,
24
+ formatReport,
25
+ highConfidenceLines,
26
+ loadNotes,
27
+ loadRatings,
28
+ planPromotions,
29
+ reportPath,
30
+ } from "./lib/relationship-reflect";
31
+
32
+ const HELP = `
33
+ RelationshipReflect — Periodic reflection + opinion promotion
30
34
 
31
- // ── Types ──
32
-
33
- interface Rating {
34
- ts: string;
35
- rating: number;
36
- context: string;
37
- source: "explicit" | "implicit";
38
- }
39
-
40
- interface ParsedNote {
41
- type: "W" | "O" | "Session";
42
- text: string;
43
- confidence?: number;
44
- date: string;
45
- time: string;
46
- }
47
-
48
- interface OpinionChange {
49
- statement: string;
50
- action: "created" | "strengthened";
51
- oldConfidence?: number;
52
- newConfidence: number;
53
- }
54
-
55
- // ── Note Parsing ──
56
-
57
- export function loadNotes(daysBack: number): ParsedNote[] {
58
- const relationshipDir = resolve(palHome(), "memory", "relationship");
59
- if (!existsSync(relationshipDir)) return [];
60
-
61
- const cutoff = new Date();
62
- cutoff.setDate(cutoff.getDate() - daysBack);
63
- const notes: ParsedNote[] = [];
64
-
65
- for (const monthDir of readdirSync(relationshipDir).sort().reverse()) {
66
- if (!/^\d{4}-\d{2}$/.test(monthDir)) continue;
67
- const monthPath = resolve(relationshipDir, monthDir);
68
-
69
- let files: string[];
70
- try {
71
- files = readdirSync(monthPath)
72
- .filter((f) => f.endsWith(".md"))
73
- .sort()
74
- .reverse();
75
- } catch {
76
- continue;
77
- }
78
-
79
- for (const file of files) {
80
- const dateStr = file.replace(".md", "");
81
- if (new Date(dateStr) < cutoff) continue;
82
-
83
- try {
84
- const content = readFileSync(resolve(monthPath, file), "utf-8");
85
- let currentTime = "";
86
-
87
- for (const line of content.split("\n")) {
88
- const timeMatch = new RegExp(/^## (\d{2}:\d{2})/).exec(line);
89
- if (timeMatch) {
90
- currentTime = timeMatch[1];
91
- continue;
92
- }
93
-
94
- // O(c=0.85): text or B(c=0.85): text
95
- const obMatch = new RegExp(/^- ([OB])\(c=([\d.]+)\):\s*(.+)$/).exec(line);
96
- if (obMatch) {
97
- notes.push({
98
- type: obMatch[1] as "O" | "Session",
99
- confidence: Number.parseFloat(obMatch[2]),
100
- text: obMatch[3],
101
- date: dateStr,
102
- time: currentTime,
103
- });
104
- continue;
105
- }
106
-
107
- // W: text
108
- const worldMatch = new RegExp(/^- W:\s*(.+)$/).exec(line);
109
- if (worldMatch) {
110
- notes.push({
111
- type: "W",
112
- text: worldMatch[1],
113
- date: dateStr,
114
- time: currentTime,
115
- });
116
- }
117
- }
118
- } catch {
119
- // skip
120
- }
121
- }
122
- }
123
-
124
- return notes;
125
- }
126
-
127
- // ── Ratings ──
128
-
129
- export function loadRatings(daysBack: number): Rating[] {
130
- const ratingsFile = resolve(palHome(), "memory", "signals", "ratings.jsonl");
131
- if (!existsSync(ratingsFile)) return [];
132
-
133
- const cutoff = new Date();
134
- cutoff.setDate(cutoff.getDate() - daysBack);
135
-
136
- return readFileSync(ratingsFile, "utf-8")
137
- .split("\n")
138
- .filter((l) => l.trim())
139
- .map((l) => {
140
- try {
141
- return JSON.parse(l) as Rating;
142
- } catch {
143
- return null;
144
- }
145
- })
146
- .filter(
147
- (r): r is Rating => r !== null && new Date(r.ts).getTime() >= cutoff.getTime()
148
- );
149
- }
150
-
151
- // ── Opinion Promotion ──
152
-
153
- export function promoteToOpinions(notes: ParsedNote[], dryRun: boolean): OpinionChange[] {
154
- const changes: OpinionChange[] = [];
155
- const opinions = readOpinions();
156
-
157
- const opinionNotes = notes.filter((n) => n.type === "O");
158
-
159
- // Group similar notes together
160
- const groups = new Map<string, ParsedNote[]>();
161
- for (const note of opinionNotes) {
162
- let matched = false;
163
- for (const [key, group] of groups) {
164
- if (similarity(note.text, key) >= 0.3) {
165
- group.push(note);
166
- matched = true;
167
- break;
168
- }
169
- }
170
- if (!matched) {
171
- groups.set(note.text, [note]);
172
- }
173
- }
174
-
175
- for (const [representative, group] of groups) {
176
- const existing = findSimilarOpinion(representative, opinions);
177
-
178
- if (existing) {
179
- let updated = existing;
180
- for (const note of group) {
181
- updated = addEvidence(updated, "supporting", note.text.slice(0, 120));
182
- }
183
-
184
- if (updated.confidence !== existing.confidence) {
185
- changes.push({
186
- statement: existing.statement,
187
- action: "strengthened",
188
- oldConfidence: existing.confidence,
189
- newConfidence: updated.confidence,
190
- });
191
- if (!dryRun) saveOpinion(updated);
192
- }
193
- } else if (group.length >= 2) {
194
- let opinion = createOpinion(representative, group[0].text.slice(0, 120));
195
- for (const note of group.slice(1)) {
196
- opinion = addEvidence(opinion, "supporting", note.text.slice(0, 120));
197
- }
198
- changes.push({
199
- statement: representative,
200
- action: "created",
201
- newConfidence: opinion.confidence,
202
- });
203
- if (!dryRun) saveOpinion(opinion);
204
- }
205
- }
206
-
207
- return changes;
208
- }
209
-
210
- // ── Analysis ──
211
-
212
- interface OpinionSummary {
213
- text: string;
214
- occurrences: number;
215
- avgConfidence: number;
216
- dates: string[];
217
- }
218
-
219
- export function groupNoteOccurrences(notes: ParsedNote[]): OpinionSummary[] {
220
- const opNotes = notes.filter((n) => n.type === "O");
221
- const groups = new Map<
222
- string,
223
- { confidences: number[]; dates: string[]; text: string }
224
- >();
225
-
226
- for (const note of opNotes) {
227
- let matched = false;
228
- for (const [key, group] of groups) {
229
- if (similarity(note.text, key) >= 0.3) {
230
- if (note.confidence !== undefined) group.confidences.push(note.confidence);
231
- group.dates.push(note.date);
232
- matched = true;
233
- break;
234
- }
235
- }
236
- if (!matched) {
237
- groups.set(note.text, {
238
- text: note.text,
239
- confidences: note.confidence !== undefined ? [note.confidence] : [],
240
- dates: [note.date],
241
- });
242
- }
243
- }
244
-
245
- return [...groups.values()]
246
- .map((g) => ({
247
- text: g.text,
248
- occurrences: g.dates.length,
249
- avgConfidence:
250
- g.confidences.length > 0
251
- ? g.confidences.reduce((a, b) => a + b, 0) / g.confidences.length
252
- : 0,
253
- dates: [...new Set(g.dates)],
254
- }))
255
- .sort((a, b) => b.occurrences - a.occurrences);
256
- }
257
-
258
- function correlateRatings(ratings: Rating[]): string[] {
259
- const correlations: string[] = [];
260
-
261
- const lowRatings = ratings.filter((r) => r.rating <= 4);
262
- const highRatings = ratings.filter((r) => r.rating >= 7);
263
-
264
- if (lowRatings.length > 0) {
265
- const lowContexts = lowRatings
266
- .slice(0, 3)
267
- .map((r) => `"${r.context.slice(0, 60)}"`)
268
- .join(", ");
269
- correlations.push(
270
- `${lowRatings.length} low ratings (<=4) — common contexts: ${lowContexts}`
271
- );
272
- }
273
- if (highRatings.length > 0) {
274
- const highContexts = highRatings
275
- .slice(0, 3)
276
- .map((r) => `"${r.context.slice(0, 60)}"`)
277
- .join(", ");
278
- correlations.push(
279
- `${highRatings.length} high ratings (>=7) — common contexts: ${highContexts}`
280
- );
281
- }
282
-
283
- if (ratings.length > 0) {
284
- const explicitCount = ratings.filter((r) => r.source === "explicit").length;
285
- const implicitCount = ratings.filter((r) => r.source === "implicit").length;
286
- correlations.push(`Source mix: ${explicitCount} explicit, ${implicitCount} implicit`);
287
- }
288
-
289
- return correlations;
290
- }
291
-
292
- // ── Report ──
293
-
294
- export function formatReport(
295
- period: string,
296
- notes: ParsedNote[],
297
- ratings: Rating[],
298
- opinionChanges: OpinionChange[]
299
- ): string {
300
- const date = new Date().toISOString().slice(0, 10);
301
- const avgRating =
302
- ratings.length > 0 ? ratings.reduce((s, r) => s + r.rating, 0) / ratings.length : 0;
303
- const summaries = groupNoteOccurrences(notes);
304
- const worldFacts = notes.filter((n) => n.type === "W").map((n) => n.text);
305
- const ratingInsights = correlateRatings(ratings);
306
-
307
- const lines: string[] = [
308
- "# Relationship Reflection",
309
- "",
310
- `**Period:** ${period}`,
311
- `**Generated:** ${date}`,
312
- `**Notes analyzed:** ${notes.length}`,
313
- `**Ratings analyzed:** ${ratings.length}`,
314
- `**Average Rating:** ${avgRating.toFixed(1)}/10`,
315
- "",
316
- "---",
317
- "",
318
- ];
319
-
320
- if (opinionChanges.length > 0) {
321
- lines.push("## Opinion Changes", "");
322
- for (const change of opinionChanges) {
323
- if (change.action === "created") {
324
- lines.push(
325
- `- **NEW** (${Math.round(change.newConfidence * 100)}%): ${change.statement}`
326
- );
327
- } else {
328
- lines.push(
329
- `- **+** ${Math.round(change.oldConfidence ?? 0 * 100)}% → ${Math.round(change.newConfidence * 100)}%: ${change.statement}`
330
- );
331
- }
332
- }
333
- lines.push("");
334
- }
335
-
336
- if (summaries.length > 0) {
337
- lines.push("## Recurring Opinions", "");
338
- for (const op of summaries) {
339
- lines.push(
340
- `- **${op.text}**`,
341
- ` Seen ${op.occurrences}x | Avg confidence: ${op.avgConfidence.toFixed(2)} | Dates: ${op.dates.join(", ")}`,
342
- ""
343
- );
344
- }
345
- }
346
-
347
- if (worldFacts.length > 0) {
348
- lines.push("## World Facts Observed", "");
349
- for (const fact of worldFacts.slice(0, 10)) {
350
- lines.push(`- ${fact}`);
351
- }
352
- lines.push("");
353
- }
354
-
355
- if (ratingInsights.length > 0) {
356
- lines.push("## Rating Insights", "");
357
- for (const insight of ratingInsights) {
358
- lines.push(`- ${insight}`);
359
- }
360
- lines.push("");
361
- }
35
+ Reads recent relationship notes and ratings. Promotes recurring
36
+ observations (O type) into tracked opinions with confidence scoring.
362
37
 
363
- return lines.join("\n");
364
- }
38
+ Usage:
39
+ bun run tool:reflect Reflect on last 7 days (default)
40
+ bun run tool:reflect -- --month Reflect on last 30 days
41
+ bun run tool:reflect -- --dry-run Preview without writing
365
42
 
366
- export function writeReport(report: string, period: string): string {
367
- const reflectionDir = resolve(palHome(), "memory", "relationship", "reflections");
368
- if (!existsSync(reflectionDir)) mkdirSync(reflectionDir, { recursive: true });
43
+ Output:
44
+ - Updates memory/relationship/opinions.json (confidence tracking)
45
+ - Creates reflection report in memory/relationship/reflections/
46
+ `;
369
47
 
370
- const date = new Date().toISOString().slice(0, 10);
371
- const slug = period.toLowerCase().replace(/\s+/g, "-");
372
- const filename = `${date}_${slug}-reflection.md`;
373
- const filepath = resolve(reflectionDir, filename);
48
+ const relationshipDir = () => resolve(palHome(), "memory", "relationship");
49
+ const ratingsFile = () => resolve(palHome(), "memory", "signals", "ratings.jsonl");
374
50
 
51
+ function saveReport(report: string, period: string): string {
52
+ const dir = resolve(relationshipDir(), "reflections");
53
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
54
+ const filepath = reportPath(dir, period);
375
55
  writeFileSync(filepath, report, "utf-8");
376
56
  return filepath;
377
57
  }
378
58
 
379
- // ── CLI ──
380
-
381
59
  function run() {
382
60
  const { values } = parseArgs({
383
61
  args: Bun.argv.slice(2),
@@ -389,21 +67,7 @@ function run() {
389
67
  });
390
68
 
391
69
  if (values.help) {
392
- console.log(`
393
- RelationshipReflect — Periodic reflection + opinion promotion
394
-
395
- Reads recent relationship notes and ratings. Promotes recurring
396
- observations (O type) into tracked opinions with confidence scoring.
397
-
398
- Usage:
399
- bun run tool:reflect Reflect on last 7 days (default)
400
- bun run tool:reflect -- --month Reflect on last 30 days
401
- bun run tool:reflect -- --dry-run Preview without writing
402
-
403
- Output:
404
- - Updates memory/relationship/opinions.json (confidence tracking)
405
- - Creates reflection report in memory/relationship/reflections/
406
- `);
70
+ console.log(HELP);
407
71
  process.exit(0);
408
72
  }
409
73
 
@@ -411,8 +75,8 @@ Output:
411
75
  const period = values.month ? "Monthly" : "Weekly";
412
76
  const dryRun = values["dry-run"] ?? false;
413
77
 
414
- const notes = loadNotes(daysBack);
415
- const ratings = loadRatings(daysBack);
78
+ const notes = loadNotes(relationshipDir(), daysBack);
79
+ const ratings = loadRatings(ratingsFile(), daysBack);
416
80
 
417
81
  emit.ok(`Loaded ${notes.length} notes from last ${daysBack} days`);
418
82
  emit.ok(`Loaded ${ratings.length} ratings`);
@@ -422,54 +86,26 @@ Output:
422
86
  process.exit(0);
423
87
  }
424
88
 
425
- const opinionChanges = promoteToOpinions(notes, dryRun);
426
-
427
- const avgRating =
428
- ratings.length > 0 ? ratings.reduce((s, r) => s + r.rating, 0) / ratings.length : 0;
429
- emit.ok(`\nAverage Rating: ${avgRating.toFixed(1)}/10`);
89
+ const plan = planPromotions(notes, readOpinions());
90
+ if (!dryRun) for (const opinion of plan.toSave) saveOpinion(opinion);
430
91
 
431
- const summaries = groupNoteOccurrences(notes);
432
- emit.ok(`Observations: ${summaries.length} unique`);
433
-
434
- if (opinionChanges.length > 0) {
435
- emit.ok("\nOpinion changes:");
436
- for (const change of opinionChanges) {
437
- if (change.action === "created") {
438
- emit.ok(
439
- ` + NEW (${Math.round(change.newConfidence * 100)}%) ${change.statement.slice(0, 80)}`
440
- );
441
- } else {
442
- emit.ok(
443
- ` ~ ${Math.round(change.oldConfidence ?? 0 * 100)}% → ${Math.round(change.newConfidence * 100)}% ${change.statement.slice(0, 80)}`
444
- );
445
- }
446
- }
447
- } else {
448
- emit.ok("\nNo opinion changes");
449
- }
92
+ for (const line of consoleLines(notes, ratings, plan.changes)) emit.ok(line);
450
93
 
451
94
  if (dryRun) {
452
95
  emit.data("[DRY RUN] Would write reflection report + update opinions");
453
- } else {
454
- const report = formatReport(period, notes, ratings, opinionChanges);
455
- const filepath = writeReport(report, period);
456
- setLastReflectDate(new Date().toISOString().slice(0, 10));
457
- emit.receipt(filepath, {
458
- period,
459
- notes: notes.length,
460
- ratings: ratings.length,
461
- opinionChanges: opinionChanges.length,
462
- });
463
-
464
- const opinions = readOpinions();
465
- const high = opinions.filter((o) => o.confidence >= 0.85);
466
- if (high.length > 0) {
467
- emit.ok("\nHigh-confidence opinions (injected into context):");
468
- for (const o of high) {
469
- emit.ok(` [${Math.round(o.confidence * 100)}%] ${o.statement.slice(0, 80)}`);
470
- }
471
- }
96
+ return;
472
97
  }
98
+
99
+ const filepath = saveReport(formatReport(period, notes, ratings, plan.changes), period);
100
+ setLastReflectDate(new Date().toISOString().slice(0, 10));
101
+ emit.receipt(filepath, {
102
+ period,
103
+ notes: notes.length,
104
+ ratings: ratings.length,
105
+ opinionChanges: plan.changes.length,
106
+ });
107
+
108
+ for (const line of highConfidenceLines(readOpinions())) emit.ok(line);
473
109
  }
474
110
 
475
111
  if (import.meta.main) run();