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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "portable-agent-layer",
3
- "version": "0.8.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",
@@ -7,7 +7,7 @@
7
7
  */
8
8
 
9
9
  import { regenerateIfNeeded } from "./lib/claude-md";
10
- import { buildGreeting, buildSystemReminder } from "./lib/context";
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*[-:]\s*|\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
- if (cache.mode === "repo") {
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
  }
@@ -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
- * Replaces the single-file current-work.json approach.
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 { buildGreeting, buildSystemReminder } =
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 stop-orchestrator handler
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
- // Name session from first user message (if not already named)
165
- const firstUser = messages.find((m: TranscriptMessage) => m.role === "user");
166
- if (firstUser) {
167
- await captureSessionName(firstUser.content, sessionID);
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), { sessionId: sessionID });
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 (_input, output) => {
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
- // Explicit rating
187
- const match = text.match(
188
- /(?:^|rating:?\s*|score:?\s*)(\d|10)(?:\s*(?:\/10|[-.])|$|\s)/i
189
- );
190
- if (match) {
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 },
@@ -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
- );