portable-agent-layer 0.4.0 → 0.6.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/package.json +2 -1
- package/src/cli/index.ts +0 -13
- package/src/hooks/handlers/failure.ts +9 -16
- package/src/hooks/handlers/work-learning.ts +8 -15
- package/src/hooks/lib/graduation.ts +21 -33
- package/src/hooks/lib/prompts.ts +11 -0
- package/src/hooks/lib/security.ts +0 -1
- package/src/tools/eval-principles.ts +234 -0
- package/src/tools/graduate.ts +41 -13
- package/src/hooks/lib/tags.ts +0 -89
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "portable-agent-layer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "PAL — Portable Agent Layer: persistent personal context for AI coding assistants",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -48,6 +48,7 @@
|
|
|
48
48
|
"ai:fyzz-api": "bun run src/tools/fyzz-api.ts",
|
|
49
49
|
"ai:pdf-download": "bun run src/tools/pdf-download.ts",
|
|
50
50
|
"ai:youtube-analyze": "bun run src/tools/youtube-analyze.ts",
|
|
51
|
+
"tool:eval": "bun run src/tools/eval-principles.ts",
|
|
51
52
|
"tool:graduate": "bun run src/tools/graduate.ts",
|
|
52
53
|
"tool:patterns": "bun run src/tools/pattern-synthesis.ts",
|
|
53
54
|
"tool:reflect": "bun run src/tools/relationship-reflect.ts",
|
package/src/cli/index.ts
CHANGED
|
@@ -21,7 +21,6 @@ import { existsSync, mkdirSync, readdirSync, readFileSync, statSync } from "node
|
|
|
21
21
|
import { homedir } from "node:os";
|
|
22
22
|
import { resolve } from "node:path";
|
|
23
23
|
import { palHome, palPkg, platform } from "../hooks/lib/paths";
|
|
24
|
-
import { getPendingSuggestions } from "../hooks/lib/tags";
|
|
25
24
|
import { log } from "../targets/lib";
|
|
26
25
|
|
|
27
26
|
const allArgs = process.argv.slice(2);
|
|
@@ -361,18 +360,6 @@ function doctor(silent = false): DoctorResult {
|
|
|
361
360
|
}
|
|
362
361
|
}
|
|
363
362
|
|
|
364
|
-
// Pending tag suggestions
|
|
365
|
-
const pending = getPendingSuggestions();
|
|
366
|
-
const pendingEntries = Object.entries(pending).sort((a, b) => b[1] - a[1]);
|
|
367
|
-
if (pendingEntries.length > 0) {
|
|
368
|
-
warn(`Tags: ${pendingEntries.length} pending suggestion(s)`);
|
|
369
|
-
for (const [tag, count] of pendingEntries.slice(0, 5)) {
|
|
370
|
-
log.info(` "${tag}" (${count}/3 to promote)`);
|
|
371
|
-
}
|
|
372
|
-
} else {
|
|
373
|
-
ok("Tags: no pending suggestions");
|
|
374
|
-
}
|
|
375
|
-
|
|
376
363
|
if (!hasAgent) {
|
|
377
364
|
console.log("");
|
|
378
365
|
log.error("No supported agent found. Install Claude Code or opencode.");
|
|
@@ -11,7 +11,7 @@ import { resolve } from "node:path";
|
|
|
11
11
|
import { stringify } from "../lib/frontmatter";
|
|
12
12
|
import { inference } from "../lib/inference";
|
|
13
13
|
import { ensureDir, paths } from "../lib/paths";
|
|
14
|
-
import {
|
|
14
|
+
import { FAILURE_PRINCIPLE_PROMPT } from "../lib/prompts";
|
|
15
15
|
import { fileTimestamp, monthPath } from "../lib/time";
|
|
16
16
|
import { logTokenUsage } from "../lib/token-usage";
|
|
17
17
|
import {
|
|
@@ -55,14 +55,13 @@ export async function captureFailure(
|
|
|
55
55
|
resolve(paths.failures(), monthPath(), `${fileTimestamp()}_${slug}`)
|
|
56
56
|
);
|
|
57
57
|
|
|
58
|
-
// Attempt inference to fill root cause analysis +
|
|
58
|
+
// Attempt inference to fill root cause analysis + candidate principle
|
|
59
59
|
let whatWentWrong = "";
|
|
60
60
|
let whatToDoDifferently = "";
|
|
61
|
-
let
|
|
61
|
+
let principle = "";
|
|
62
62
|
try {
|
|
63
|
-
const vocab = getVocabulary();
|
|
64
63
|
const analysisResult = await inference({
|
|
65
|
-
system: `You are analyzing a failed AI assistant interaction. Based on the context, identify what went wrong and what should be done differently. Be specific and actionable. Also
|
|
64
|
+
system: `You are analyzing a failed AI assistant interaction. Based on the context, identify what went wrong and what should be done differently. Be specific and actionable. Also write a principle — ${FAILURE_PRINCIPLE_PROMPT}`,
|
|
66
65
|
user: [
|
|
67
66
|
`Rating: ${rating}/10`,
|
|
68
67
|
`Context: ${context}`,
|
|
@@ -80,13 +79,9 @@ export async function captureFailure(
|
|
|
80
79
|
properties: {
|
|
81
80
|
what_went_wrong: { type: "string" as const },
|
|
82
81
|
what_to_do_differently: { type: "string" as const },
|
|
83
|
-
|
|
84
|
-
type: "array" as const,
|
|
85
|
-
items: { type: "string" as const },
|
|
86
|
-
},
|
|
87
|
-
suggested_tag: { type: "string" as const },
|
|
82
|
+
principle: { type: "string" as const },
|
|
88
83
|
},
|
|
89
|
-
required: ["what_went_wrong", "what_to_do_differently", "
|
|
84
|
+
required: ["what_went_wrong", "what_to_do_differently", "principle"],
|
|
90
85
|
},
|
|
91
86
|
});
|
|
92
87
|
if (analysisResult.usage) logTokenUsage("failure", analysisResult.usage);
|
|
@@ -94,13 +89,11 @@ export async function captureFailure(
|
|
|
94
89
|
const parsed = JSON.parse(analysisResult.output) as {
|
|
95
90
|
what_went_wrong?: string;
|
|
96
91
|
what_to_do_differently?: string;
|
|
97
|
-
|
|
98
|
-
suggested_tag?: string;
|
|
92
|
+
principle?: string;
|
|
99
93
|
};
|
|
100
94
|
whatWentWrong = parsed.what_went_wrong ?? "";
|
|
101
95
|
whatToDoDifferently = parsed.what_to_do_differently ?? "";
|
|
102
|
-
|
|
103
|
-
if (parsed.suggested_tag) recordSuggestedTag(parsed.suggested_tag);
|
|
96
|
+
principle = parsed.principle ?? "";
|
|
104
97
|
}
|
|
105
98
|
} catch {
|
|
106
99
|
// Graceful fallback — empty sections are still useful with the other context
|
|
@@ -113,7 +106,7 @@ export async function captureFailure(
|
|
|
113
106
|
ts: new Date().toISOString(),
|
|
114
107
|
slug,
|
|
115
108
|
};
|
|
116
|
-
if (
|
|
109
|
+
if (principle) meta.principle = principle;
|
|
117
110
|
|
|
118
111
|
const body = [
|
|
119
112
|
"## Last User Message",
|
|
@@ -11,7 +11,7 @@ import { stringify } from "../lib/frontmatter";
|
|
|
11
11
|
import { inference } from "../lib/inference";
|
|
12
12
|
import { categorizeLearning } from "../lib/learning-category";
|
|
13
13
|
import { ensureDir, paths } from "../lib/paths";
|
|
14
|
-
import {
|
|
14
|
+
import { LEARNING_PRINCIPLE_PROMPT } from "../lib/prompts";
|
|
15
15
|
import { fileTimestamp, monthPath } from "../lib/time";
|
|
16
16
|
import { logTokenUsage } from "../lib/token-usage";
|
|
17
17
|
import {
|
|
@@ -108,16 +108,15 @@ export async function captureWorkLearning(
|
|
|
108
108
|
let title = rawTitle;
|
|
109
109
|
let summary = rawSummary;
|
|
110
110
|
let insights = "";
|
|
111
|
-
let
|
|
111
|
+
let principle = "";
|
|
112
112
|
try {
|
|
113
|
-
const vocab = getVocabulary();
|
|
114
113
|
const userMessages = messages
|
|
115
114
|
.filter((m) => m.role === "user")
|
|
116
115
|
.map((m) => extractContent(m).slice(0, 100))
|
|
117
116
|
.slice(-8)
|
|
118
117
|
.join("\n");
|
|
119
118
|
const result = await inference({
|
|
120
|
-
system: `You summarize AI coding sessions between a human user and an AI assistant. The 'Human messages' are what the user said. The 'AI response' is what the assistant said. Produce: 1) a short title (5-10 words) describing what was accomplished, 2) a summary of what the AI assistant did for the user (2-4 sentences, write from the AI's perspective using 'we'), 3) insights — what worked well, what was surprising, or what should be done differently next time (2-3 bullet points, no markdown), 4)
|
|
119
|
+
system: `You summarize AI coding sessions between a human user and an AI assistant. The 'Human messages' are what the user said. The 'AI response' is what the assistant said. Produce: 1) a short title (5-10 words) describing what was accomplished, 2) a summary of what the AI assistant did for the user (2-4 sentences, write from the AI's perspective using 'we'), 3) insights — what worked well, what was surprising, or what should be done differently next time (2-3 bullet points, no markdown), 4) principle — ${LEARNING_PRINCIPLE_PROMPT}`,
|
|
121
120
|
user: `Human messages:\n${userMessages}\n\nAI response:\n${rawSummary.slice(0, 400)}`,
|
|
122
121
|
maxTokens: 350,
|
|
123
122
|
timeout: 15000,
|
|
@@ -128,13 +127,9 @@ export async function captureWorkLearning(
|
|
|
128
127
|
title: { type: "string" as const },
|
|
129
128
|
summary: { type: "string" as const },
|
|
130
129
|
insights: { type: "string" as const },
|
|
131
|
-
|
|
132
|
-
type: "array" as const,
|
|
133
|
-
items: { type: "string" as const },
|
|
134
|
-
},
|
|
135
|
-
suggested_tag: { type: "string" as const },
|
|
130
|
+
principle: { type: "string" as const },
|
|
136
131
|
},
|
|
137
|
-
required: ["title", "summary", "insights", "
|
|
132
|
+
required: ["title", "summary", "insights", "principle"],
|
|
138
133
|
},
|
|
139
134
|
});
|
|
140
135
|
if (result.usage) logTokenUsage("work-learning", result.usage);
|
|
@@ -143,14 +138,12 @@ export async function captureWorkLearning(
|
|
|
143
138
|
title?: string;
|
|
144
139
|
summary?: string;
|
|
145
140
|
insights?: string;
|
|
146
|
-
|
|
147
|
-
suggested_tag?: string;
|
|
141
|
+
principle?: string;
|
|
148
142
|
};
|
|
149
143
|
if (parsed.title) title = parsed.title.slice(0, 100);
|
|
150
144
|
if (parsed.summary) summary = parsed.summary;
|
|
151
145
|
if (parsed.insights) insights = parsed.insights;
|
|
152
|
-
if (parsed.
|
|
153
|
-
if (parsed.suggested_tag) recordSuggestedTag(parsed.suggested_tag);
|
|
146
|
+
if (parsed.principle) principle = parsed.principle;
|
|
154
147
|
}
|
|
155
148
|
} catch {
|
|
156
149
|
// Fallback to raw values
|
|
@@ -166,7 +159,7 @@ export async function captureWorkLearning(
|
|
|
166
159
|
category,
|
|
167
160
|
date: new Date().toISOString().slice(0, 10),
|
|
168
161
|
};
|
|
169
|
-
if (
|
|
162
|
+
if (principle) meta.principle = principle;
|
|
170
163
|
if (sessionId) meta.session = sessionId;
|
|
171
164
|
|
|
172
165
|
const body = [
|
|
@@ -21,7 +21,7 @@ interface LearningEntry {
|
|
|
21
21
|
source: string; // "failure:{slug}" or "learning:{filename}"
|
|
22
22
|
text: string; // context or title+insights
|
|
23
23
|
date: string; // YYYY-MM-DD
|
|
24
|
-
|
|
24
|
+
principle: string; // candidate principle from inference
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
interface PatternGroup {
|
|
@@ -46,6 +46,7 @@ interface GraduationState {
|
|
|
46
46
|
|
|
47
47
|
export interface GraduationResult {
|
|
48
48
|
candidates: PatternGroup[];
|
|
49
|
+
emerging: PatternGroup[];
|
|
49
50
|
graduated: GraduatedEntry[];
|
|
50
51
|
updated: GraduatedEntry[];
|
|
51
52
|
}
|
|
@@ -171,7 +172,7 @@ function extractKeywords(text: string): Set<string> {
|
|
|
171
172
|
);
|
|
172
173
|
}
|
|
173
174
|
|
|
174
|
-
function similarity(a: string, b: string): number {
|
|
175
|
+
export function similarity(a: string, b: string): number {
|
|
175
176
|
const ka = extractKeywords(a);
|
|
176
177
|
const kb = extractKeywords(b);
|
|
177
178
|
if (ka.size === 0 || kb.size === 0) return 0;
|
|
@@ -199,7 +200,7 @@ function collectFailures(): LearningEntry[] {
|
|
|
199
200
|
for (const slug of readdirSync(monthDir)) {
|
|
200
201
|
let context = "";
|
|
201
202
|
let ts = "";
|
|
202
|
-
let
|
|
203
|
+
let entryPrinciple = "";
|
|
203
204
|
|
|
204
205
|
// Try capture.md (new format)
|
|
205
206
|
const capturePath = resolve(monthDir, slug, "capture.md");
|
|
@@ -209,11 +210,11 @@ function collectFailures(): LearningEntry[] {
|
|
|
209
210
|
const { meta } = parse<{
|
|
210
211
|
context?: string;
|
|
211
212
|
ts?: string;
|
|
212
|
-
|
|
213
|
+
principle?: string;
|
|
213
214
|
}>(content);
|
|
214
215
|
context = meta.context || "";
|
|
215
216
|
ts = (meta.ts as string) || "";
|
|
216
|
-
|
|
217
|
+
entryPrinciple = meta.principle || "";
|
|
217
218
|
} catch {
|
|
218
219
|
/* fallback below */
|
|
219
220
|
}
|
|
@@ -237,7 +238,7 @@ function collectFailures(): LearningEntry[] {
|
|
|
237
238
|
source: `failure:${slug}`,
|
|
238
239
|
text: context.slice(0, 300),
|
|
239
240
|
date: ts.slice(0, 10),
|
|
240
|
-
|
|
241
|
+
principle: entryPrinciple,
|
|
241
242
|
});
|
|
242
243
|
}
|
|
243
244
|
}
|
|
@@ -265,16 +266,16 @@ function collectLearnings(): LearningEntry[] {
|
|
|
265
266
|
const content = readFileSync(resolve(monthDir, file), "utf-8");
|
|
266
267
|
let title = "";
|
|
267
268
|
let insights = "";
|
|
268
|
-
let
|
|
269
|
+
let entryPrinciple = "";
|
|
269
270
|
|
|
270
271
|
if (hasFrontmatter(content)) {
|
|
271
272
|
// New format
|
|
272
273
|
const { meta, body } = parse<{
|
|
273
274
|
title?: string;
|
|
274
|
-
|
|
275
|
+
principle?: string;
|
|
275
276
|
}>(content);
|
|
276
277
|
title = meta.title || "";
|
|
277
|
-
|
|
278
|
+
entryPrinciple = meta.principle || "";
|
|
278
279
|
const insightsMatch = body.match(/## Insights\n([\s\S]*?)(?=\n##|$)/);
|
|
279
280
|
insights = insightsMatch?.[1]?.trim() || "";
|
|
280
281
|
} else {
|
|
@@ -295,7 +296,7 @@ function collectLearnings(): LearningEntry[] {
|
|
|
295
296
|
source: `learning:${file}`,
|
|
296
297
|
text: text.slice(0, 300),
|
|
297
298
|
date,
|
|
298
|
-
|
|
299
|
+
principle: entryPrinciple,
|
|
299
300
|
});
|
|
300
301
|
}
|
|
301
302
|
} catch {
|
|
@@ -313,7 +314,7 @@ function collectLearnings(): LearningEntry[] {
|
|
|
313
314
|
|
|
314
315
|
// ── Grouping ──
|
|
315
316
|
|
|
316
|
-
const SIMILARITY_THRESHOLD = 0.35;
|
|
317
|
+
export const SIMILARITY_THRESHOLD = 0.35;
|
|
317
318
|
const MIN_OCCURRENCES = 3;
|
|
318
319
|
const MIN_TEXT_LENGTH = 30;
|
|
319
320
|
|
|
@@ -329,33 +330,17 @@ function isActionable(text: string): boolean {
|
|
|
329
330
|
return true;
|
|
330
331
|
}
|
|
331
332
|
|
|
332
|
-
/** Check if two entries share at least one tag. */
|
|
333
|
-
function hasSharedTag(a: string[], b: string[]): boolean {
|
|
334
|
-
if (a.length === 0 || b.length === 0) return false;
|
|
335
|
-
return a.some((t) => b.includes(t));
|
|
336
|
-
}
|
|
337
|
-
|
|
338
333
|
function groupPatterns(entries: LearningEntry[]): PatternGroup[] {
|
|
339
334
|
const groups: PatternGroup[] = [];
|
|
340
335
|
const actionable = entries.filter((e) => isActionable(e.text));
|
|
341
336
|
|
|
342
337
|
for (const entry of actionable) {
|
|
338
|
+
// Use principle for matching if available, fall back to raw text
|
|
339
|
+
const matchText = entry.principle || entry.text;
|
|
343
340
|
let matched = false;
|
|
344
341
|
for (const group of groups) {
|
|
345
|
-
|
|
346
|
-
if (
|
|
347
|
-
entry.tags.length > 0 &&
|
|
348
|
-
group.entries.some((e) => hasSharedTag(e.tags, entry.tags))
|
|
349
|
-
) {
|
|
350
|
-
group.entries.push(entry);
|
|
351
|
-
matched = true;
|
|
352
|
-
break;
|
|
353
|
-
}
|
|
354
|
-
// Text similarity fallback (for untagged/legacy entries)
|
|
355
|
-
if (
|
|
356
|
-
entry.tags.length === 0 &&
|
|
357
|
-
similarity(entry.text, group.pattern) >= SIMILARITY_THRESHOLD
|
|
358
|
-
) {
|
|
342
|
+
const groupText = group.entries[0]?.principle || group.pattern;
|
|
343
|
+
if (similarity(matchText, groupText) >= SIMILARITY_THRESHOLD) {
|
|
359
344
|
group.entries.push(entry);
|
|
360
345
|
matched = true;
|
|
361
346
|
break;
|
|
@@ -370,7 +355,7 @@ function groupPatterns(entries: LearningEntry[]): PatternGroup[] {
|
|
|
370
355
|
}
|
|
371
356
|
}
|
|
372
357
|
|
|
373
|
-
return groups.filter((g) => g.entries.length >=
|
|
358
|
+
return groups.filter((g) => g.entries.length >= 2);
|
|
374
359
|
}
|
|
375
360
|
|
|
376
361
|
// ── State Management ──
|
|
@@ -425,9 +410,12 @@ export function graduate(): GraduationResult {
|
|
|
425
410
|
`Collected ${failures.length} failures, ${learnings.length} learnings`
|
|
426
411
|
);
|
|
427
412
|
|
|
428
|
-
const
|
|
413
|
+
const allGroups = groupPatterns(all);
|
|
414
|
+
const candidates = allGroups.filter((g) => g.entries.length >= MIN_OCCURRENCES);
|
|
415
|
+
const emerging = allGroups.filter((g) => g.entries.length === 2);
|
|
429
416
|
const result: GraduationResult = {
|
|
430
417
|
candidates,
|
|
418
|
+
emerging,
|
|
431
419
|
graduated: [],
|
|
432
420
|
updated: [],
|
|
433
421
|
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared prompt fragments — single source of truth for inference instructions.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/** Principle extraction instruction for failed interactions. */
|
|
6
|
+
export const FAILURE_PRINCIPLE_PROMPT =
|
|
7
|
+
"Write one actionable sentence that would prevent this issue from happening again. If no clear lesson, leave principle empty. Be concise.";
|
|
8
|
+
|
|
9
|
+
/** Principle extraction instruction for session learnings. */
|
|
10
|
+
export const LEARNING_PRINCIPLE_PROMPT =
|
|
11
|
+
"If this session taught a reusable lesson, write one actionable sentence that would prevent the same issue in the future. If no clear lesson, leave empty. Be concise.";
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Principle Evaluation — generate, regenerate, or compare candidate principles.
|
|
4
|
+
*
|
|
5
|
+
* Reads failures (capture.md) and learnings (frontmatter .md) and uses Haiku
|
|
6
|
+
* to generate candidate principles. Useful for tuning prompt quality.
|
|
7
|
+
*
|
|
8
|
+
* Modes:
|
|
9
|
+
* --dry-run Preview which files would be updated
|
|
10
|
+
* --evaluate Show current vs new principle for comparison (does not write)
|
|
11
|
+
* --force Regenerate principles even if one already exists
|
|
12
|
+
* (default) Generate missing principles only
|
|
13
|
+
*
|
|
14
|
+
* Usage:
|
|
15
|
+
* bun run tool:eval # generate missing
|
|
16
|
+
* bun run tool:eval -- --dry-run # preview
|
|
17
|
+
* bun run tool:eval -- --evaluate # compare current vs new
|
|
18
|
+
* bun run tool:eval -- --force # regenerate all
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
22
|
+
import { resolve } from "node:path";
|
|
23
|
+
import { hasFrontmatter, parse, stringify } from "../hooks/lib/frontmatter";
|
|
24
|
+
import { inference } from "../hooks/lib/inference";
|
|
25
|
+
import { palHome } from "../hooks/lib/paths";
|
|
26
|
+
import {
|
|
27
|
+
FAILURE_PRINCIPLE_PROMPT,
|
|
28
|
+
LEARNING_PRINCIPLE_PROMPT,
|
|
29
|
+
} from "../hooks/lib/prompts";
|
|
30
|
+
|
|
31
|
+
const args = process.argv.slice(2);
|
|
32
|
+
const dryRun = args.includes("--dry-run");
|
|
33
|
+
const evaluate = args.includes("--evaluate");
|
|
34
|
+
const force = args.includes("--force");
|
|
35
|
+
|
|
36
|
+
const home = palHome();
|
|
37
|
+
let processed = 0;
|
|
38
|
+
let skipped = 0;
|
|
39
|
+
let failed = 0;
|
|
40
|
+
|
|
41
|
+
async function generatePrinciple(systemPrompt: string, context: string): Promise<string> {
|
|
42
|
+
const result = await inference({
|
|
43
|
+
system: systemPrompt,
|
|
44
|
+
user: context,
|
|
45
|
+
maxTokens: 100,
|
|
46
|
+
timeout: 10000,
|
|
47
|
+
jsonSchema: {
|
|
48
|
+
type: "object" as const,
|
|
49
|
+
additionalProperties: false,
|
|
50
|
+
properties: {
|
|
51
|
+
principle: { type: "string" as const },
|
|
52
|
+
},
|
|
53
|
+
required: ["principle"],
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
if (result.success && result.output) {
|
|
58
|
+
const parsed = JSON.parse(result.output) as { principle?: string };
|
|
59
|
+
const principle = parsed.principle?.trim() || "";
|
|
60
|
+
if (principle.length > 10) return principle;
|
|
61
|
+
}
|
|
62
|
+
return "";
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ── Failures ──
|
|
66
|
+
|
|
67
|
+
async function processFailures() {
|
|
68
|
+
const failuresDir = resolve(home, "memory", "learning", "failures");
|
|
69
|
+
if (!existsSync(failuresDir)) return;
|
|
70
|
+
|
|
71
|
+
for (const year of readdirSync(failuresDir)) {
|
|
72
|
+
const yearDir = resolve(failuresDir, year);
|
|
73
|
+
for (const month of readdirSync(yearDir)) {
|
|
74
|
+
const monthDir = resolve(yearDir, month);
|
|
75
|
+
for (const slug of readdirSync(monthDir)) {
|
|
76
|
+
const capturePath = resolve(monthDir, slug, "capture.md");
|
|
77
|
+
if (!existsSync(capturePath)) continue;
|
|
78
|
+
|
|
79
|
+
const content = readFileSync(capturePath, "utf-8");
|
|
80
|
+
if (!hasFrontmatter(content)) continue;
|
|
81
|
+
|
|
82
|
+
const { meta, body } = parse<{
|
|
83
|
+
principle?: string;
|
|
84
|
+
context?: string;
|
|
85
|
+
rating?: number;
|
|
86
|
+
}>(content);
|
|
87
|
+
|
|
88
|
+
const hasPrinciple = !!meta.principle;
|
|
89
|
+
if (hasPrinciple && !force && !evaluate) {
|
|
90
|
+
skipped++;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const context = meta.context || "";
|
|
95
|
+
if (!context) {
|
|
96
|
+
skipped++;
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const inputContext = `Rating: ${meta.rating}/10\nContext: ${context}\n\n${body.slice(0, 400)}`;
|
|
101
|
+
|
|
102
|
+
if (dryRun) {
|
|
103
|
+
console.log(` [failure] ${slug.slice(0, 60)}`);
|
|
104
|
+
processed++;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
const newPrinciple = await generatePrinciple(
|
|
110
|
+
FAILURE_PRINCIPLE_PROMPT,
|
|
111
|
+
inputContext
|
|
112
|
+
);
|
|
113
|
+
if (!newPrinciple) {
|
|
114
|
+
skipped++;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (evaluate) {
|
|
119
|
+
console.log(` [failure] ${slug.slice(0, 50)}`);
|
|
120
|
+
if (hasPrinciple) {
|
|
121
|
+
console.log(` OLD: ${meta.principle}`);
|
|
122
|
+
}
|
|
123
|
+
console.log(` NEW: ${newPrinciple}`);
|
|
124
|
+
console.log("");
|
|
125
|
+
processed++;
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const newMeta = { ...meta, principle: newPrinciple } as Record<string, unknown>;
|
|
130
|
+
writeFileSync(capturePath, stringify(newMeta, body), "utf-8");
|
|
131
|
+
console.log(` [failure] ${slug.slice(0, 60)}`);
|
|
132
|
+
processed++;
|
|
133
|
+
} catch {
|
|
134
|
+
failed++;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ── Learnings ──
|
|
142
|
+
|
|
143
|
+
async function processLearnings() {
|
|
144
|
+
const learningDir = resolve(home, "memory", "learning", "session");
|
|
145
|
+
if (!existsSync(learningDir)) return;
|
|
146
|
+
|
|
147
|
+
for (const year of readdirSync(learningDir)) {
|
|
148
|
+
const yearDir = resolve(learningDir, year);
|
|
149
|
+
for (const month of readdirSync(yearDir)) {
|
|
150
|
+
const monthDir = resolve(yearDir, month);
|
|
151
|
+
for (const file of readdirSync(monthDir).filter((f) => f.endsWith(".md"))) {
|
|
152
|
+
const filepath = resolve(monthDir, file);
|
|
153
|
+
const content = readFileSync(filepath, "utf-8");
|
|
154
|
+
|
|
155
|
+
if (!hasFrontmatter(content)) {
|
|
156
|
+
skipped++;
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const { meta, body } = parse<{
|
|
161
|
+
principle?: string;
|
|
162
|
+
title?: string;
|
|
163
|
+
}>(content);
|
|
164
|
+
|
|
165
|
+
const hasPrinciple = !!meta.principle;
|
|
166
|
+
if (hasPrinciple && !force && !evaluate) {
|
|
167
|
+
skipped++;
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const title = meta.title || "";
|
|
172
|
+
if (!title) {
|
|
173
|
+
skipped++;
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const inputContext = `Title: ${title}\n\n${body.slice(0, 400)}`;
|
|
178
|
+
|
|
179
|
+
if (dryRun) {
|
|
180
|
+
console.log(` [learning] ${file.slice(0, 60)}`);
|
|
181
|
+
processed++;
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
try {
|
|
186
|
+
const newPrinciple = await generatePrinciple(
|
|
187
|
+
LEARNING_PRINCIPLE_PROMPT,
|
|
188
|
+
inputContext
|
|
189
|
+
);
|
|
190
|
+
if (!newPrinciple) {
|
|
191
|
+
skipped++;
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (evaluate) {
|
|
196
|
+
console.log(` [learning] ${file.slice(0, 50)}`);
|
|
197
|
+
if (hasPrinciple) {
|
|
198
|
+
console.log(` OLD: ${meta.principle}`);
|
|
199
|
+
}
|
|
200
|
+
console.log(` NEW: ${newPrinciple}`);
|
|
201
|
+
console.log("");
|
|
202
|
+
processed++;
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const newMeta = { ...meta, principle: newPrinciple } as Record<string, unknown>;
|
|
207
|
+
writeFileSync(filepath, stringify(newMeta, body), "utf-8");
|
|
208
|
+
console.log(` [learning] ${file.slice(0, 60)}`);
|
|
209
|
+
processed++;
|
|
210
|
+
} catch {
|
|
211
|
+
failed++;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// ── Main ──
|
|
219
|
+
|
|
220
|
+
const mode = evaluate
|
|
221
|
+
? "evaluate"
|
|
222
|
+
: force
|
|
223
|
+
? "force regenerate"
|
|
224
|
+
: dryRun
|
|
225
|
+
? "dry run"
|
|
226
|
+
: "backfill";
|
|
227
|
+
console.log(`\n Principle ${mode}...\n`);
|
|
228
|
+
|
|
229
|
+
await processFailures();
|
|
230
|
+
await processLearnings();
|
|
231
|
+
|
|
232
|
+
console.log(
|
|
233
|
+
`\n Done: ${processed} ${evaluate ? "compared" : "processed"}, ${skipped} skipped, ${failed} failed\n`
|
|
234
|
+
);
|
package/src/tools/graduate.ts
CHANGED
|
@@ -13,8 +13,8 @@ import { graduate } from "../hooks/lib/graduation";
|
|
|
13
13
|
|
|
14
14
|
const result = graduate();
|
|
15
15
|
|
|
16
|
-
if (result.candidates.length === 0) {
|
|
17
|
-
console.log("\n No recurring patterns found
|
|
16
|
+
if (result.candidates.length === 0 && result.emerging.length === 0) {
|
|
17
|
+
console.log("\n No recurring patterns found.\n");
|
|
18
18
|
process.exit(0);
|
|
19
19
|
}
|
|
20
20
|
|
|
@@ -22,13 +22,12 @@ console.log(`\n Graduation Report — ${result.candidates.length} pattern(s) de
|
|
|
22
22
|
console.log(" ─────────────────────────────────────────────────\n");
|
|
23
23
|
|
|
24
24
|
for (const candidate of result.candidates) {
|
|
25
|
-
// Collect unique
|
|
26
|
-
const
|
|
25
|
+
// Collect unique candidate principles
|
|
26
|
+
const principles = [
|
|
27
|
+
...new Set(candidate.entries.map((e) => e.principle).filter((p) => p.length > 0)),
|
|
28
|
+
];
|
|
27
29
|
|
|
28
30
|
console.log(` [${candidate.domain}] ${candidate.entries.length}x occurrences`);
|
|
29
|
-
if (allTags.length > 0) {
|
|
30
|
-
console.log(` Tags: ${allTags.join(", ")}`);
|
|
31
|
-
}
|
|
32
31
|
console.log("");
|
|
33
32
|
|
|
34
33
|
// Show each entry with date and source
|
|
@@ -39,13 +38,42 @@ for (const candidate of result.candidates) {
|
|
|
39
38
|
);
|
|
40
39
|
}
|
|
41
40
|
|
|
41
|
+
// Show candidate principles from Haiku
|
|
42
|
+
if (principles.length > 0) {
|
|
43
|
+
console.log("\n Suggested principles:");
|
|
44
|
+
for (const p of principles) {
|
|
45
|
+
console.log(` → ${p}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
42
49
|
console.log("");
|
|
43
|
-
console.log(
|
|
44
|
-
" → Consider adding a principle to:",
|
|
45
|
-
`memory/wisdom/frames/${candidate.domain}.md`
|
|
46
|
-
);
|
|
50
|
+
console.log(" Target frame:", `memory/wisdom/frames/${candidate.domain}.md`);
|
|
47
51
|
console.log(" ─────────────────────────────────────────────────\n");
|
|
48
52
|
}
|
|
49
53
|
|
|
50
|
-
|
|
51
|
-
console.log(
|
|
54
|
+
if (result.emerging.length > 0) {
|
|
55
|
+
console.log(` Emerging (2x — one more to graduate)\n`);
|
|
56
|
+
for (const group of result.emerging) {
|
|
57
|
+
const principles = [
|
|
58
|
+
...new Set(group.entries.map((e) => e.principle).filter((p) => p.length > 0)),
|
|
59
|
+
];
|
|
60
|
+
console.log(` [${group.domain}] ${group.entries.length}x`);
|
|
61
|
+
for (const entry of group.entries) {
|
|
62
|
+
const sourceType = entry.source.startsWith("failure:") ? "failure" : "learning";
|
|
63
|
+
console.log(
|
|
64
|
+
` ${entry.date || "unknown"} [${sourceType}] ${entry.text.slice(0, 80)}`
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
if (principles.length > 0) {
|
|
68
|
+
for (const p of principles) {
|
|
69
|
+
console.log(` → ${p}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
console.log("");
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (result.candidates.length > 0) {
|
|
77
|
+
console.log(" To crystallize: add a line to the wisdom frame file.");
|
|
78
|
+
console.log(" Format: - Your principle here [CRYSTAL: 85%]\n");
|
|
79
|
+
}
|
package/src/hooks/lib/tags.ts
DELETED
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tag vocabulary management for semantic grouping.
|
|
3
|
-
*
|
|
4
|
-
* Tags come from a fixed vocabulary. When Haiku suggests a tag not in the
|
|
5
|
-
* vocabulary, it's tracked as "suggested". Suggested tags that recur 3+
|
|
6
|
-
* times get auto-promoted to the vocabulary.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
10
|
-
import { resolve } from "node:path";
|
|
11
|
-
import { ensureDir, paths } from "./paths";
|
|
12
|
-
|
|
13
|
-
const DEFAULT_VOCABULARY = [
|
|
14
|
-
"versioning",
|
|
15
|
-
"testing",
|
|
16
|
-
"deployment",
|
|
17
|
-
"configuration",
|
|
18
|
-
"communication",
|
|
19
|
-
"documentation",
|
|
20
|
-
"architecture",
|
|
21
|
-
"debugging",
|
|
22
|
-
"performance",
|
|
23
|
-
"security",
|
|
24
|
-
"cross-platform",
|
|
25
|
-
"dependencies",
|
|
26
|
-
"incomplete-work",
|
|
27
|
-
"wrong-approach",
|
|
28
|
-
"tooling",
|
|
29
|
-
];
|
|
30
|
-
|
|
31
|
-
const PROMOTION_THRESHOLD = 3;
|
|
32
|
-
|
|
33
|
-
export interface TagState {
|
|
34
|
-
vocabulary: string[];
|
|
35
|
-
suggested: Record<string, number>;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function stateFilePath(): string {
|
|
39
|
-
return resolve(ensureDir(paths.wisdomState()), "tags.json");
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export function readTagState(): TagState {
|
|
43
|
-
const fp = stateFilePath();
|
|
44
|
-
if (!existsSync(fp)) {
|
|
45
|
-
return { vocabulary: [...DEFAULT_VOCABULARY], suggested: {} };
|
|
46
|
-
}
|
|
47
|
-
try {
|
|
48
|
-
return JSON.parse(readFileSync(fp, "utf-8"));
|
|
49
|
-
} catch {
|
|
50
|
-
return { vocabulary: [...DEFAULT_VOCABULARY], suggested: {} };
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export function writeTagState(state: TagState): void {
|
|
55
|
-
writeFileSync(stateFilePath(), JSON.stringify(state, null, 2), "utf-8");
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/** Get the current vocabulary (for injection into inference prompts). */
|
|
59
|
-
export function getVocabulary(): string[] {
|
|
60
|
-
return readTagState().vocabulary;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Record a suggested tag. If it reaches the promotion threshold,
|
|
65
|
-
* move it to the vocabulary automatically.
|
|
66
|
-
*/
|
|
67
|
-
export function recordSuggestedTag(tag: string): void {
|
|
68
|
-
const state = readTagState();
|
|
69
|
-
const normalized = tag.toLowerCase().trim();
|
|
70
|
-
|
|
71
|
-
// Already in vocabulary — nothing to do
|
|
72
|
-
if (state.vocabulary.includes(normalized)) return;
|
|
73
|
-
|
|
74
|
-
// Increment suggestion count
|
|
75
|
-
state.suggested[normalized] = (state.suggested[normalized] || 0) + 1;
|
|
76
|
-
|
|
77
|
-
// Promote if threshold reached
|
|
78
|
-
if (state.suggested[normalized] >= PROMOTION_THRESHOLD) {
|
|
79
|
-
state.vocabulary.push(normalized);
|
|
80
|
-
delete state.suggested[normalized];
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
writeTagState(state);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/** Get suggested tags and their counts (for reporting). */
|
|
87
|
-
export function getPendingSuggestions(): Record<string, number> {
|
|
88
|
-
return readTagState().suggested;
|
|
89
|
-
}
|