portable-agent-layer 0.23.0 → 0.23.1

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.
@@ -216,7 +216,26 @@ Only add threads that genuinely need follow-up. Resolve existing threads if this
216
216
  bun ~/.pal/tools/thread.ts --resolve --id <id>
217
217
  ```
218
218
 
219
- **4. Wisdom Frame** (Extended+ only) if the session produced a genuine, reusable insight:
219
+ **4. Opinion capture** — scan the conversation for moments where the user:
220
+ - Confirmed something you did: "yes exactly", "keep doing that", "10 rated", accepted without pushback
221
+ - Corrected something you did: "no", "don't do that", "stop", "that's not what I meant"
222
+ - Revealed a preference by repeating a pattern (asked for concise answers twice, always checked PAI first, etc.)
223
+
224
+ For each, invoke the opinion tool:
225
+ ```bash
226
+ # User confirmed a preference
227
+ bun ~/.pal/skills/opinion/tools/opinion.ts evidence "matching keywords" --confirmation "what they confirmed"
228
+
229
+ # User corrected a preference
230
+ bun ~/.pal/skills/opinion/tools/opinion.ts evidence "matching keywords" --contradiction "what they corrected"
231
+
232
+ # New pattern observed (no existing opinion matches)
233
+ bun ~/.pal/skills/opinion/tools/opinion.ts add "the preference" --category communication|technical|workflow|general
234
+ ```
235
+
236
+ Skip if nothing in the conversation touched preferences or working style.
237
+
238
+ **5. Wisdom Frame** (Extended+ only) — if the session produced a genuine, reusable insight:
220
239
 
221
240
  ```bash
222
241
  bun ~/.pal/tools/wisdom-frame.ts --domain <domain> --observation "insight" [--type principle|contextual-rule|anti-pattern|evolution]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "portable-agent-layer",
3
- "version": "0.23.0",
3
+ "version": "0.23.1",
4
4
  "description": "PAL — Portable Agent Layer: persistent personal context for AI coding assistants",
5
5
  "type": "module",
6
6
  "bin": {
@@ -30,7 +30,8 @@ export async function captureFailure(
30
30
  rating: number,
31
31
  context: string,
32
32
  transcript: string,
33
- detailedContext?: string
33
+ detailedContext?: string,
34
+ principle?: string
34
35
  ): Promise<void> {
35
36
  const messages = parseMessages(transcript);
36
37
 
@@ -55,6 +56,7 @@ export async function captureFailure(
55
56
  ts: new Date().toISOString(),
56
57
  slug,
57
58
  };
59
+ if (principle) meta.principle = principle;
58
60
 
59
61
  const body = [
60
62
  "## What Happened",
@@ -166,8 +166,16 @@ const SENTIMENT_SCHEMA = {
166
166
  confidence: { type: "number" },
167
167
  summary: { type: "string" },
168
168
  detailed_context: { type: "string" },
169
+ principle: { type: "string" },
169
170
  },
170
- required: ["rating", "sentiment", "confidence", "summary", "detailed_context"],
171
+ required: [
172
+ "rating",
173
+ "sentiment",
174
+ "confidence",
175
+ "summary",
176
+ "detailed_context",
177
+ "principle",
178
+ ],
171
179
  additionalProperties: false,
172
180
  } as const;
173
181
 
@@ -177,6 +185,7 @@ interface SentimentResult {
177
185
  confidence: number;
178
186
  summary: string;
179
187
  detailed_context: string;
188
+ principle: string;
180
189
  }
181
190
 
182
191
  const SENTIMENT_SYSTEM_PROMPT = `Analyze the user's message for emotional sentiment toward the AI assistant.
@@ -187,7 +196,8 @@ OUTPUT FORMAT (JSON only):
187
196
  "sentiment": "positive" | "negative" | "neutral",
188
197
  "confidence": <0.0-1.0>,
189
198
  "summary": "<brief explanation, 10 words max>",
190
- "detailed_context": "<comprehensive analysis, 50-150 words>"
199
+ "detailed_context": "<comprehensive analysis, 50-150 words>",
200
+ "principle": "<one actionable rule the AI should follow to avoid this failure or repeat this success, 10-20 words. Start with a verb: 'Verify...', 'Always...', 'Never...', 'Ask before...'>"
191
201
  }
192
202
 
193
203
  DETAILED_CONTEXT REQUIREMENTS:
@@ -240,6 +250,7 @@ function handleRating(
240
250
  context: string,
241
251
  source: string,
242
252
  detailedContext?: string,
253
+ principle?: string,
243
254
  sessionId?: string,
244
255
  userMessage?: string
245
256
  ): void {
@@ -257,6 +268,7 @@ function handleRating(
257
268
  context,
258
269
  source,
259
270
  detailedContext,
271
+ principle,
260
272
  responsePreview,
261
273
  userPreview,
262
274
  ts: now(),
@@ -284,6 +296,7 @@ async function handleImplicitSentiment(
284
296
  `Direct praise: "${trimmed}"`,
285
297
  "implicit",
286
298
  undefined,
299
+ undefined,
287
300
  sessionId,
288
301
  trimmed
289
302
  );
@@ -328,6 +341,7 @@ async function handleImplicitSentiment(
328
341
  `${parsed.summary}: ${trimmed.slice(0, 200)}`,
329
342
  "implicit",
330
343
  parsed.detailed_context,
344
+ parsed.principle,
331
345
  sessionId,
332
346
  trimmed
333
347
  );
@@ -352,6 +366,7 @@ export async function captureRating(message: string, sessionId?: string): Promis
352
366
  explicit.comment || cleaned.slice(0, 200),
353
367
  "explicit",
354
368
  undefined,
369
+ undefined,
355
370
  sessionId,
356
371
  cleaned
357
372
  );
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Auto-trigger for relationship reflect — runs when conditions are met:
3
- * - 7+ days since last reflect
4
- * - 10+ new relationship notes since last reflect
3
+ * - 1+ days since last reflect
4
+ * - 5+ new relationship notes since last reflect
5
5
  *
6
6
  * Spawns `bun run tool:reflect` as a detached background process.
7
7
  */
@@ -12,8 +12,8 @@ import { logDebug } from "../lib/log";
12
12
  import { getLastReflectDate } from "../lib/opinions";
13
13
  import { palPkg, paths } from "../lib/paths";
14
14
 
15
- const MIN_DAYS_BETWEEN = 7;
16
- const MIN_NEW_NOTES = 10;
15
+ const MIN_DAYS_BETWEEN = 1;
16
+ const MIN_NEW_NOTES = 5;
17
17
 
18
18
  function countNotesSince(since: string): number {
19
19
  const relDir = paths.relationship();
@@ -231,10 +231,11 @@ export function loadFailurePatterns(): string {
231
231
 
232
232
  const lines = entries.map((e) => {
233
233
  const label = e.rating ? `[${e.rating}/10]` : "";
234
- return `- ${label} ${e.context}`.trim();
234
+ const text = e.principle || e.context;
235
+ return `- ${label} ${text}`.trim();
235
236
  });
236
237
 
237
- return ["## Recent Failure Patterns (Avoid)", ...lines].join("\n");
238
+ return ["## Lessons from Recent Failures Apply These Now", ...lines].join("\n");
238
239
  } catch {
239
240
  return "";
240
241
  }
@@ -13,6 +13,7 @@ import { runSynthesis } from "../handlers/synthesis";
13
13
  import { resetTab } from "../handlers/tab";
14
14
  import { updateCounts } from "../handlers/update-counts";
15
15
  import { captureWorkSession } from "../handlers/work-session";
16
+ import { inference } from "./inference";
16
17
  import { logDebug, logError } from "./log";
17
18
  import { ensureDir, paths } from "./paths";
18
19
  import { extractContent, extractLastAssistant, parseMessages } from "./transcript";
@@ -137,15 +138,63 @@ async function checkPendingFailure(transcript: string): Promise<void> {
137
138
  rating: number;
138
139
  context: string;
139
140
  detailedContext?: string;
141
+ principle?: string;
140
142
  responsePreview?: string;
141
143
  userPreview?: string;
142
144
  };
143
145
  unlinkSync(pendingPath);
146
+
147
+ // Extract principle from full transcript if not already present
148
+ let { principle, detailedContext } = pending;
149
+ if (!principle) {
150
+ try {
151
+ const msgs = parseMessages(transcript);
152
+ const recent = msgs
153
+ .slice(-10)
154
+ .map((m) => `${m.role.toUpperCase()}: ${extractContent(m).slice(0, 300)}`)
155
+ .join("\n\n");
156
+
157
+ const result = await inference({
158
+ system: `Analyze this failed AI interaction. The user rated it ${pending.rating}/10.
159
+
160
+ Return JSON:
161
+ {
162
+ "principle": "<one actionable rule the AI should follow, 10-20 words. Start with a verb: 'Verify...', 'Always...', 'Never...', 'Ask before...'>",
163
+ "detailed_context": "<what went wrong and why, 50-150 words>"
164
+ }`,
165
+ user: `User feedback: ${pending.context}\n\nConversation:\n${recent}`,
166
+ maxTokens: 400,
167
+ timeout: 10000,
168
+ jsonSchema: {
169
+ type: "object" as const,
170
+ properties: {
171
+ principle: { type: "string" as const },
172
+ detailed_context: { type: "string" as const },
173
+ },
174
+ required: ["principle", "detailed_context"],
175
+ additionalProperties: false,
176
+ },
177
+ });
178
+
179
+ if (result.success && result.output) {
180
+ const parsed = JSON.parse(result.output) as {
181
+ principle?: string;
182
+ detailed_context?: string;
183
+ };
184
+ principle = parsed.principle || undefined;
185
+ if (!detailedContext) detailedContext = parsed.detailed_context || undefined;
186
+ }
187
+ } catch {
188
+ /* graceful fallback — capture without principle */
189
+ }
190
+ }
191
+
144
192
  await captureFailure(
145
193
  pending.rating,
146
194
  pending.context,
147
195
  transcript,
148
- pending.detailedContext
196
+ detailedContext,
197
+ principle
149
198
  );
150
199
  } catch {
151
200
  // Non-critical