portable-agent-layer 0.4.0 → 0.5.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 +15 -31
- 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 +13 -9
- 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.5.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 {
|
|
@@ -171,7 +171,7 @@ function extractKeywords(text: string): Set<string> {
|
|
|
171
171
|
);
|
|
172
172
|
}
|
|
173
173
|
|
|
174
|
-
function similarity(a: string, b: string): number {
|
|
174
|
+
export function similarity(a: string, b: string): number {
|
|
175
175
|
const ka = extractKeywords(a);
|
|
176
176
|
const kb = extractKeywords(b);
|
|
177
177
|
if (ka.size === 0 || kb.size === 0) return 0;
|
|
@@ -199,7 +199,7 @@ function collectFailures(): LearningEntry[] {
|
|
|
199
199
|
for (const slug of readdirSync(monthDir)) {
|
|
200
200
|
let context = "";
|
|
201
201
|
let ts = "";
|
|
202
|
-
let
|
|
202
|
+
let entryPrinciple = "";
|
|
203
203
|
|
|
204
204
|
// Try capture.md (new format)
|
|
205
205
|
const capturePath = resolve(monthDir, slug, "capture.md");
|
|
@@ -209,11 +209,11 @@ function collectFailures(): LearningEntry[] {
|
|
|
209
209
|
const { meta } = parse<{
|
|
210
210
|
context?: string;
|
|
211
211
|
ts?: string;
|
|
212
|
-
|
|
212
|
+
principle?: string;
|
|
213
213
|
}>(content);
|
|
214
214
|
context = meta.context || "";
|
|
215
215
|
ts = (meta.ts as string) || "";
|
|
216
|
-
|
|
216
|
+
entryPrinciple = meta.principle || "";
|
|
217
217
|
} catch {
|
|
218
218
|
/* fallback below */
|
|
219
219
|
}
|
|
@@ -237,7 +237,7 @@ function collectFailures(): LearningEntry[] {
|
|
|
237
237
|
source: `failure:${slug}`,
|
|
238
238
|
text: context.slice(0, 300),
|
|
239
239
|
date: ts.slice(0, 10),
|
|
240
|
-
|
|
240
|
+
principle: entryPrinciple,
|
|
241
241
|
});
|
|
242
242
|
}
|
|
243
243
|
}
|
|
@@ -265,16 +265,16 @@ function collectLearnings(): LearningEntry[] {
|
|
|
265
265
|
const content = readFileSync(resolve(monthDir, file), "utf-8");
|
|
266
266
|
let title = "";
|
|
267
267
|
let insights = "";
|
|
268
|
-
let
|
|
268
|
+
let entryPrinciple = "";
|
|
269
269
|
|
|
270
270
|
if (hasFrontmatter(content)) {
|
|
271
271
|
// New format
|
|
272
272
|
const { meta, body } = parse<{
|
|
273
273
|
title?: string;
|
|
274
|
-
|
|
274
|
+
principle?: string;
|
|
275
275
|
}>(content);
|
|
276
276
|
title = meta.title || "";
|
|
277
|
-
|
|
277
|
+
entryPrinciple = meta.principle || "";
|
|
278
278
|
const insightsMatch = body.match(/## Insights\n([\s\S]*?)(?=\n##|$)/);
|
|
279
279
|
insights = insightsMatch?.[1]?.trim() || "";
|
|
280
280
|
} else {
|
|
@@ -295,7 +295,7 @@ function collectLearnings(): LearningEntry[] {
|
|
|
295
295
|
source: `learning:${file}`,
|
|
296
296
|
text: text.slice(0, 300),
|
|
297
297
|
date,
|
|
298
|
-
|
|
298
|
+
principle: entryPrinciple,
|
|
299
299
|
});
|
|
300
300
|
}
|
|
301
301
|
} catch {
|
|
@@ -313,7 +313,7 @@ function collectLearnings(): LearningEntry[] {
|
|
|
313
313
|
|
|
314
314
|
// ── Grouping ──
|
|
315
315
|
|
|
316
|
-
const SIMILARITY_THRESHOLD = 0.35;
|
|
316
|
+
export const SIMILARITY_THRESHOLD = 0.35;
|
|
317
317
|
const MIN_OCCURRENCES = 3;
|
|
318
318
|
const MIN_TEXT_LENGTH = 30;
|
|
319
319
|
|
|
@@ -329,33 +329,17 @@ function isActionable(text: string): boolean {
|
|
|
329
329
|
return true;
|
|
330
330
|
}
|
|
331
331
|
|
|
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
332
|
function groupPatterns(entries: LearningEntry[]): PatternGroup[] {
|
|
339
333
|
const groups: PatternGroup[] = [];
|
|
340
334
|
const actionable = entries.filter((e) => isActionable(e.text));
|
|
341
335
|
|
|
342
336
|
for (const entry of actionable) {
|
|
337
|
+
// Use principle for matching if available, fall back to raw text
|
|
338
|
+
const matchText = entry.principle || entry.text;
|
|
343
339
|
let matched = false;
|
|
344
340
|
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
|
-
) {
|
|
341
|
+
const groupText = group.entries[0]?.principle || group.pattern;
|
|
342
|
+
if (similarity(matchText, groupText) >= SIMILARITY_THRESHOLD) {
|
|
359
343
|
group.entries.push(entry);
|
|
360
344
|
matched = true;
|
|
361
345
|
break;
|
|
@@ -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
|
@@ -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,11 +38,16 @@ 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
|
|
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
|
-
}
|