portable-agent-layer 0.8.0 → 0.9.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 +1 -2
- package/src/hooks/LoadContext.ts +1 -4
- package/src/hooks/handlers/rating.ts +5 -2
- package/src/hooks/handlers/update-check.ts +1 -4
- package/src/hooks/lib/log.ts +1 -3
- package/src/hooks/lib/security.ts +2 -0
- package/src/hooks/lib/work-tracking.ts +1 -1
- package/src/targets/opencode/install.ts +6 -2
- package/src/targets/opencode/plugin.ts +20 -171
- package/src/hooks/lib/prompts.ts +0 -11
- package/src/tools/eval-principles.ts +0 -234
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "portable-agent-layer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "PAL — Portable Agent Layer: persistent personal context for AI coding assistants",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -48,7 +48,6 @@
|
|
|
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",
|
|
52
51
|
"tool:analyze": "bun run src/tools/analyze.ts",
|
|
53
52
|
"tool:reflect": "bun run src/tools/relationship-reflect.ts",
|
|
54
53
|
"tool:export": "bun run src/tools/export.ts",
|
package/src/hooks/LoadContext.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { regenerateIfNeeded } from "./lib/claude-md";
|
|
10
|
-
import {
|
|
10
|
+
import { buildSystemReminder } from "./lib/context";
|
|
11
11
|
import { logDebug, logError } from "./lib/log";
|
|
12
12
|
|
|
13
13
|
// --- Skip heavy context for subagents ---
|
|
@@ -28,9 +28,6 @@ try {
|
|
|
28
28
|
logError("LoadContext:regenerate", err);
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
// --- Visible greeting to stderr ---
|
|
32
|
-
process.stderr.write(`${buildGreeting().join("\n")}\n`);
|
|
33
|
-
|
|
34
31
|
// --- Dynamic system-reminder to stdout (empty = nothing injected) ---
|
|
35
32
|
try {
|
|
36
33
|
const reminder = buildSystemReminder();
|
|
@@ -46,11 +46,11 @@ function getLastResponse(sessionId?: string): string {
|
|
|
46
46
|
* Matches: "7", "8 - good work", "6: needs work", "9 excellent", "10!"
|
|
47
47
|
* Rejects: "3 items", "5 things to fix", "7th thing", "10/10"
|
|
48
48
|
*/
|
|
49
|
-
function parseExplicitRating(
|
|
49
|
+
export function parseExplicitRating(
|
|
50
50
|
prompt: string
|
|
51
51
|
): { rating: number; comment?: string } | null {
|
|
52
52
|
const trimmed = prompt.trim();
|
|
53
|
-
const match = trimmed.match(/^(10|[1-9])(?:\s*[
|
|
53
|
+
const match = trimmed.match(/^(10|[1-9])(?:\s*[-:,]\s*|\s+)?(.*)$/);
|
|
54
54
|
if (!match) return null;
|
|
55
55
|
|
|
56
56
|
const rating = parseInt(match[1], 10);
|
|
@@ -67,6 +67,9 @@ function parseExplicitRating(
|
|
|
67
67
|
const sentenceStarters =
|
|
68
68
|
/^(items?|things?|steps?|files?|lines?|bugs?|issues?|errors?|times?|minutes?|hours?|days?|seconds?|percent|%|th\b|st\b|nd\b|rd\b|of\b|in\b|at\b|to\b|the\b|a\b|an\b)/i;
|
|
69
69
|
if (sentenceStarters.test(rest)) return null;
|
|
70
|
+
|
|
71
|
+
// Reject item selections: "1 and 2", "2 3 5", "1, 3, 5", "1-3"
|
|
72
|
+
if (/^(and\b|\d|,\s*\d|-\d)/.test(rest)) return null;
|
|
70
73
|
}
|
|
71
74
|
|
|
72
75
|
return { rating, comment: rest };
|
|
@@ -183,10 +183,7 @@ export function getUpdateNotice(): string | null {
|
|
|
183
183
|
const cache = JSON.parse(readFileSync(fp, "utf-8")) as UpdateCache;
|
|
184
184
|
if (!cache.available) return null;
|
|
185
185
|
|
|
186
|
-
|
|
187
|
-
return `📦 Update available: ${cache.current} → ${cache.latest} (git pull)`;
|
|
188
|
-
}
|
|
189
|
-
return `📦 Update available: ${cache.current} → ${cache.latest} (bun update -g portable-agent-layer)`;
|
|
186
|
+
return `📦 Update available: ${cache.current} → ${cache.latest} (pal cli update)`;
|
|
190
187
|
} catch {
|
|
191
188
|
return null;
|
|
192
189
|
}
|
package/src/hooks/lib/log.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Only writes when PAL_DEBUG=1 or when called via logError (always logged).
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { appendFileSync, existsSync, statSync, writeFileSync } from "node:fs";
|
|
8
|
+
import { appendFileSync, existsSync, renameSync, statSync, writeFileSync } from "node:fs";
|
|
9
9
|
import { resolve } from "node:path";
|
|
10
10
|
import { paths } from "./paths";
|
|
11
11
|
|
|
@@ -21,8 +21,6 @@ function rotateIfNeeded(): void {
|
|
|
21
21
|
if (existsSync(LOG_FILE) && statSync(LOG_FILE).size > MAX_LOG_SIZE) {
|
|
22
22
|
const prev = `${LOG_FILE}.prev`;
|
|
23
23
|
writeFileSync(prev, "");
|
|
24
|
-
// Swap: current → prev, start fresh
|
|
25
|
-
const { renameSync } = require("node:fs");
|
|
26
24
|
renameSync(LOG_FILE, prev);
|
|
27
25
|
}
|
|
28
26
|
} catch {
|
|
@@ -31,6 +31,7 @@ export const HOOK_MANAGED_FILES = [
|
|
|
31
31
|
"token-usage.jsonl",
|
|
32
32
|
"graduated.json",
|
|
33
33
|
"update-available.json",
|
|
34
|
+
"debug.log.prev",
|
|
34
35
|
];
|
|
35
36
|
|
|
36
37
|
/** Hook-managed directories — AI must not write to or delete from these */
|
|
@@ -40,6 +41,7 @@ export const HOOK_MANAGED_DIRS = [
|
|
|
40
41
|
"memory/learning/session",
|
|
41
42
|
"memory/learning/synthesis",
|
|
42
43
|
"memory/relationship",
|
|
44
|
+
"memory/wisdom/state",
|
|
43
45
|
];
|
|
44
46
|
|
|
45
47
|
/** Escape a string for use in a RegExp */
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Structured work tracking: session history + persistent projects.
|
|
3
|
-
*
|
|
3
|
+
* Used by both Claude Code (StopOrchestrator) and opencode (plugin).
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Deploys plugin, installs skills, generates AGENTS.md.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
7
7
|
import { resolve } from "node:path";
|
|
8
8
|
import { regenerateIfNeeded } from "../../hooks/lib/claude-md";
|
|
9
9
|
import { palPkg, platform } from "../../hooks/lib/paths";
|
|
@@ -15,7 +15,11 @@ const OC_PLUGINS_DIR = resolve(OC_GLOBAL_DIR, "plugins");
|
|
|
15
15
|
|
|
16
16
|
mkdirSync(OC_PLUGINS_DIR, { recursive: true });
|
|
17
17
|
|
|
18
|
-
// --- 1. Deploy plugin ---
|
|
18
|
+
// --- 1. Deploy plugin (clean up legacy filename) ---
|
|
19
|
+
const legacyPlugin = resolve(OC_PLUGINS_DIR, "pai-plugin.ts");
|
|
20
|
+
if (existsSync(legacyPlugin)) {
|
|
21
|
+
unlinkSync(legacyPlugin);
|
|
22
|
+
}
|
|
19
23
|
const pluginSrc = resolve(PKG_ROOT, "src", "targets", "opencode", "plugin.ts");
|
|
20
24
|
const pluginDst = resolve(OC_PLUGINS_DIR, "pal-plugin.ts");
|
|
21
25
|
// Embed PKG_ROOT as a hardcoded constant so no env config is needed
|
|
@@ -5,7 +5,6 @@
|
|
|
5
5
|
* This plugin just wires opencode's hook API to those shared functions.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { writeFileSync } from "node:fs";
|
|
9
8
|
import { resolve } from "node:path";
|
|
10
9
|
import type { Plugin, PluginInput } from "@opencode-ai/plugin";
|
|
11
10
|
|
|
@@ -20,25 +19,21 @@ type TranscriptMessage = { role: string; content: string };
|
|
|
20
19
|
|
|
21
20
|
const PALPlugin: Plugin = async ({ directory, client }: PluginInput) => {
|
|
22
21
|
// Pre-load shared modules
|
|
23
|
-
const {
|
|
22
|
+
const { buildSystemReminder } =
|
|
24
23
|
await lib<typeof import("../../hooks/lib/context")>("context.ts");
|
|
25
24
|
const { checkBashCommand, checkFilePath } =
|
|
26
25
|
await lib<typeof import("../../hooks/lib/security")>("security.ts");
|
|
27
|
-
const { paths, ensureDir } =
|
|
28
|
-
await lib<typeof import("../../hooks/lib/paths")>("paths.ts");
|
|
29
|
-
const { emitRating } =
|
|
30
|
-
await lib<typeof import("../../hooks/lib/signals")>("signals.ts");
|
|
31
|
-
const { now } = await lib<typeof import("../../hooks/lib/time")>("time.ts");
|
|
32
|
-
const { monthPath, fileTimestamp } =
|
|
33
|
-
await lib<typeof import("../../hooks/lib/time")>("time.ts");
|
|
34
26
|
const { logDebug, logError } =
|
|
35
27
|
await lib<typeof import("../../hooks/lib/log")>("log.ts");
|
|
36
28
|
|
|
37
|
-
// Load shared
|
|
29
|
+
// Load shared handlers
|
|
38
30
|
const { runStopHandlers } = await lib<typeof import("../../hooks/lib/stop")>("stop.ts");
|
|
39
31
|
const { captureSessionName } = await lib<
|
|
40
32
|
typeof import("../../hooks/handlers/session-name")
|
|
41
33
|
>("../handlers/session-name.ts");
|
|
34
|
+
const { captureRating } = await lib<typeof import("../../hooks/handlers/rating")>(
|
|
35
|
+
"../handlers/rating.ts"
|
|
36
|
+
);
|
|
42
37
|
|
|
43
38
|
function partsToText(parts: Array<Record<string, unknown>>): string {
|
|
44
39
|
return parts
|
|
@@ -76,59 +71,6 @@ const PALPlugin: Plugin = async ({ directory, client }: PluginInput) => {
|
|
|
76
71
|
.filter((m) => m.content.length > 0);
|
|
77
72
|
}
|
|
78
73
|
|
|
79
|
-
const { categorizeLearning } =
|
|
80
|
-
await lib<typeof import("../../hooks/lib/learning-category")>("learning-category.ts");
|
|
81
|
-
|
|
82
|
-
// Local helpers for rating (thin wrappers around shared signals)
|
|
83
|
-
function handleRating(
|
|
84
|
-
rating: number,
|
|
85
|
-
context: string,
|
|
86
|
-
source: string,
|
|
87
|
-
detailedContext?: string,
|
|
88
|
-
userMessage?: string
|
|
89
|
-
): void {
|
|
90
|
-
emitRating(rating, context, source);
|
|
91
|
-
|
|
92
|
-
if (rating < 5) {
|
|
93
|
-
const category = categorizeLearning(context, detailedContext ?? "");
|
|
94
|
-
const dir = ensureDir(resolve(paths.sessionLearning(), monthPath()));
|
|
95
|
-
const filename = `${fileTimestamp()}_${source}-rating-${rating}_${category}.md`;
|
|
96
|
-
writeFileSync(
|
|
97
|
-
resolve(dir, filename),
|
|
98
|
-
[
|
|
99
|
-
"---",
|
|
100
|
-
`title: "${(context.slice(0, 100) || "(low rating)").replace(/"/g, '\\"')}"`,
|
|
101
|
-
`category: "${category}"`,
|
|
102
|
-
`date: "${new Date().toISOString().slice(0, 10)}"`,
|
|
103
|
-
`rating: ${rating}`,
|
|
104
|
-
`source: "${source}"`,
|
|
105
|
-
"---",
|
|
106
|
-
"",
|
|
107
|
-
"## Context",
|
|
108
|
-
context || "*(unavailable)*",
|
|
109
|
-
"",
|
|
110
|
-
...(detailedContext ? ["## Analysis", detailedContext, ""] : []),
|
|
111
|
-
].join("\n")
|
|
112
|
-
);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
if (rating <= 3) {
|
|
116
|
-
const userPreview = userMessage?.slice(0, 400);
|
|
117
|
-
writeFileSync(
|
|
118
|
-
resolve(paths.state(), "pending-failure.json"),
|
|
119
|
-
JSON.stringify(
|
|
120
|
-
{ rating, context, source, detailedContext, userPreview, ts: now() },
|
|
121
|
-
null,
|
|
122
|
-
2
|
|
123
|
-
),
|
|
124
|
-
"utf-8"
|
|
125
|
-
);
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
const PRAISE_PATTERNS =
|
|
130
|
-
/^(great\s*job|nice|perfect|awesome|excellent|thanks|thank\s*you|well\s*done|good\s*job|love\s*it|amazing|brilliant|fantastic|wonderful|superb|nailed\s*it)[.!?]?$/i;
|
|
131
|
-
|
|
132
74
|
return {
|
|
133
75
|
// --- Per-message: Inject dynamic system reminder ---
|
|
134
76
|
"experimental.chat.system.transform": async (_input, output) => {
|
|
@@ -144,7 +86,6 @@ const PALPlugin: Plugin = async ({ directory, client }: PluginInput) => {
|
|
|
144
86
|
const { regenerateIfNeeded } =
|
|
145
87
|
await lib<typeof import("../../hooks/lib/claude-md")>("claude-md.ts");
|
|
146
88
|
regenerateIfNeeded();
|
|
147
|
-
console.log(buildGreeting().join("\n"));
|
|
148
89
|
}
|
|
149
90
|
|
|
150
91
|
if (event.type === "session.idle" || event.type === "session.diff") {
|
|
@@ -161,13 +102,15 @@ const PALPlugin: Plugin = async ({ directory, client }: PluginInput) => {
|
|
|
161
102
|
logDebug("opencode:event", `Got ${messages.length} transcript messages`);
|
|
162
103
|
if (messages.length < 2) return;
|
|
163
104
|
|
|
164
|
-
//
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
}
|
|
105
|
+
// Extract last assistant message for response caching (parity with Claude Code)
|
|
106
|
+
const lastAssistant = messages
|
|
107
|
+
.filter((m: TranscriptMessage) => m.role === "assistant")
|
|
108
|
+
.pop();
|
|
169
109
|
|
|
170
|
-
await runStopHandlers(JSON.stringify(messages), {
|
|
110
|
+
await runStopHandlers(JSON.stringify(messages), {
|
|
111
|
+
sessionId: sessionID,
|
|
112
|
+
lastAssistantMessage: lastAssistant?.content,
|
|
113
|
+
});
|
|
171
114
|
logDebug("opencode:event", "Stop handlers complete");
|
|
172
115
|
} catch (err) {
|
|
173
116
|
logError("opencode:session.stop", err);
|
|
@@ -175,98 +118,19 @@ const PALPlugin: Plugin = async ({ directory, client }: PluginInput) => {
|
|
|
175
118
|
}
|
|
176
119
|
},
|
|
177
120
|
|
|
178
|
-
// --- Capture ratings from user messages ---
|
|
179
|
-
"chat.message": async (
|
|
121
|
+
// --- Capture ratings + session naming from user messages (shared handlers) ---
|
|
122
|
+
"chat.message": async (input, output) => {
|
|
180
123
|
const text =
|
|
181
124
|
output.parts
|
|
182
125
|
?.filter((p) => p.type === "text")
|
|
183
126
|
.map((p) => p.text || "")
|
|
184
127
|
.join(" ") ?? "";
|
|
185
128
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
const rating = parseInt(match[1], 10);
|
|
192
|
-
if (rating >= 1 && rating <= 10) {
|
|
193
|
-
handleRating(rating, text.slice(0, 200), "explicit", undefined, text);
|
|
194
|
-
return;
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
// Implicit sentiment: auto-enabled when ANTHROPIC_API_KEY is set
|
|
199
|
-
if (process.env.ANTHROPIC_API_KEY) {
|
|
200
|
-
const trimmed = text.trim();
|
|
201
|
-
if (PRAISE_PATTERNS.test(trimmed)) {
|
|
202
|
-
handleRating(8, trimmed, "implicit", undefined, trimmed);
|
|
203
|
-
return;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
// Full implicit via API — only for medium-length messages
|
|
207
|
-
if (
|
|
208
|
-
trimmed.length >= 5 &&
|
|
209
|
-
trimmed.length <= 500 &&
|
|
210
|
-
!/^[/$`{]/.test(trimmed) &&
|
|
211
|
-
!trimmed.includes("\n\n")
|
|
212
|
-
) {
|
|
213
|
-
const apiKey = process.env.ANTHROPIC_API_KEY;
|
|
214
|
-
if (apiKey) {
|
|
215
|
-
try {
|
|
216
|
-
const response = await fetch("https://api.anthropic.com/v1/messages", {
|
|
217
|
-
method: "POST",
|
|
218
|
-
headers: {
|
|
219
|
-
"x-api-key": apiKey,
|
|
220
|
-
"anthropic-version": "2023-06-01",
|
|
221
|
-
"content-type": "application/json",
|
|
222
|
-
},
|
|
223
|
-
body: JSON.stringify({
|
|
224
|
-
model: (await lib<{ HAIKU_MODEL: string }>("models")).HAIKU_MODEL,
|
|
225
|
-
max_tokens: 100,
|
|
226
|
-
messages: [
|
|
227
|
-
{
|
|
228
|
-
role: "user",
|
|
229
|
-
content: `Rate the sentiment of this user message toward an AI assistant on a 1-10 scale (1=very negative, 5=neutral, 10=very positive). If the message has no clear sentiment toward the assistant, respond with just "neutral". Otherwise respond with just a JSON object: {"rating": N, "sentiment": "one-word"}\n\nMessage: "${trimmed.slice(0, 300)}"`,
|
|
230
|
-
},
|
|
231
|
-
],
|
|
232
|
-
}),
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
if (response.ok) {
|
|
236
|
-
const data = (await response.json()) as {
|
|
237
|
-
content?: Array<{ text?: string }>;
|
|
238
|
-
};
|
|
239
|
-
const rText = data?.content?.[0]?.text?.trim();
|
|
240
|
-
if (rText && rText !== "neutral") {
|
|
241
|
-
try {
|
|
242
|
-
const parsed = JSON.parse(rText) as {
|
|
243
|
-
rating?: number;
|
|
244
|
-
sentiment?: string;
|
|
245
|
-
};
|
|
246
|
-
if (
|
|
247
|
-
typeof parsed.rating === "number" &&
|
|
248
|
-
parsed.rating >= 1 &&
|
|
249
|
-
parsed.rating <= 10 &&
|
|
250
|
-
parsed.rating !== 5
|
|
251
|
-
) {
|
|
252
|
-
handleRating(
|
|
253
|
-
parsed.rating,
|
|
254
|
-
`${parsed.sentiment || "inferred"}: ${trimmed.slice(0, 150)}`,
|
|
255
|
-
"implicit",
|
|
256
|
-
undefined,
|
|
257
|
-
trimmed
|
|
258
|
-
);
|
|
259
|
-
}
|
|
260
|
-
} catch {
|
|
261
|
-
// Ignore parse errors
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
} catch {
|
|
266
|
-
// Ignore API errors
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
}
|
|
129
|
+
if (text.trim()) {
|
|
130
|
+
await Promise.allSettled([
|
|
131
|
+
captureRating(text, input.sessionID),
|
|
132
|
+
captureSessionName(text, input.sessionID),
|
|
133
|
+
]);
|
|
270
134
|
}
|
|
271
135
|
},
|
|
272
136
|
|
|
@@ -298,21 +162,6 @@ const PALPlugin: Plugin = async ({ directory, client }: PluginInput) => {
|
|
|
298
162
|
}
|
|
299
163
|
},
|
|
300
164
|
|
|
301
|
-
// --- Capture work state after tool use ---
|
|
302
|
-
"tool.execute.after": async (
|
|
303
|
-
input: { tool: string; sessionID: string; callID: string; args: unknown },
|
|
304
|
-
_output: { title: string; output: string; metadata: unknown }
|
|
305
|
-
) => {
|
|
306
|
-
try {
|
|
307
|
-
writeFileSync(
|
|
308
|
-
resolve(ensureDir(paths.state()), "current-work.json"),
|
|
309
|
-
JSON.stringify({ ts: now(), tool: input.tool, cwd: directory }, null, 2)
|
|
310
|
-
);
|
|
311
|
-
} catch {
|
|
312
|
-
// Ignore write errors
|
|
313
|
-
}
|
|
314
|
-
},
|
|
315
|
-
|
|
316
165
|
// --- Inject PAL_DIR into shell environment ---
|
|
317
166
|
"shell.env": async (
|
|
318
167
|
_input: { cwd: string; sessionID?: string; callID?: string },
|
package/src/hooks/lib/prompts.ts
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
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.";
|
|
@@ -1,234 +0,0 @@
|
|
|
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
|
-
);
|