portable-agent-layer 0.23.0 → 0.24.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/assets/templates/PAL/ALGORITHM.md +20 -1
- package/assets/templates/pal-settings.json +2 -1
- package/package.json +1 -1
- package/src/cli/setup-identity.ts +6 -30
- package/src/hooks/handlers/failure.ts +3 -1
- package/src/hooks/handlers/rating.ts +17 -2
- package/src/hooks/handlers/reflect-trigger.ts +4 -4
- package/src/hooks/handlers/self-model-trigger.ts +27 -0
- package/src/hooks/lib/claude-md.ts +8 -47
- package/src/hooks/lib/context.ts +34 -41
- package/src/hooks/lib/models.ts +1 -0
- package/src/hooks/lib/settings.ts +112 -0
- package/src/hooks/lib/stop.ts +52 -1
- package/src/targets/lib.ts +0 -18
- package/src/tools/self-model.ts +668 -0
- package/src/hooks/handlers/relationship.ts +0 -116
- package/src/hooks/handlers/work-learning.ts +0 -196
- package/src/hooks/setup-check.ts +0 -42
|
@@ -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.
|
|
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
|
@@ -3,43 +3,19 @@
|
|
|
3
3
|
* Called during `pal install`. Skips fields that already have values.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
7
|
-
import { resolve } from "node:path";
|
|
8
6
|
import * as clack from "@clack/prompts";
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
principal?: { name?: string; timezone?: string };
|
|
15
|
-
};
|
|
16
|
-
[key: string]: unknown;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function settingsPath(): string {
|
|
20
|
-
return resolve(palHome(), "memory", "pal-settings.json");
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function readSettings(): PalSettings {
|
|
24
|
-
const p = settingsPath();
|
|
25
|
-
if (!existsSync(p)) return {};
|
|
26
|
-
try {
|
|
27
|
-
return JSON.parse(readFileSync(p, "utf-8"));
|
|
28
|
-
} catch {
|
|
29
|
-
return {};
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function writeSettings(settings: PalSettings): void {
|
|
34
|
-
writeFileSync(settingsPath(), `${JSON.stringify(settings, null, 2)}\n`, "utf-8");
|
|
35
|
-
}
|
|
7
|
+
import {
|
|
8
|
+
type PalSettingsData,
|
|
9
|
+
raw as readSettings,
|
|
10
|
+
write as writeSettings,
|
|
11
|
+
} from "../hooks/lib/settings";
|
|
36
12
|
|
|
37
13
|
/** Prompt for missing identity fields. Skips any field that already has a value. */
|
|
38
14
|
export async function promptIdentity(): Promise<void> {
|
|
39
15
|
// Skip interactive prompts in non-TTY environments (tests, CI)
|
|
40
16
|
if (!process.stdin.isTTY) return;
|
|
41
17
|
|
|
42
|
-
const settings = readSettings();
|
|
18
|
+
const settings: PalSettingsData = { ...readSettings() };
|
|
43
19
|
if (!settings.identity) settings.identity = {};
|
|
44
20
|
if (!settings.identity.ai) settings.identity.ai = {};
|
|
45
21
|
if (!settings.identity.principal) settings.identity.principal = {};
|
|
@@ -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: [
|
|
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
|
-
* -
|
|
4
|
-
* -
|
|
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 =
|
|
16
|
-
const MIN_NEW_NOTES =
|
|
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();
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-trigger for self-model synthesis — runs daily.
|
|
3
|
+
* writeSelfModel has a 24h TTL guard, so this is safe to call every session.
|
|
4
|
+
* Respects dynamicContext.selfModel — if disabled, skips generation entirely.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { writeSelfModel } from "../../tools/self-model";
|
|
8
|
+
import { logDebug } from "../lib/log";
|
|
9
|
+
import { isEnabled } from "../lib/settings";
|
|
10
|
+
|
|
11
|
+
export async function checkSelfModelTrigger(): Promise<void> {
|
|
12
|
+
if (!isEnabled("selfModel")) {
|
|
13
|
+
logDebug("self-model-trigger", "Disabled in pal-settings.json");
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
const result = await writeSelfModel(30);
|
|
19
|
+
if (result.skipped) {
|
|
20
|
+
logDebug("self-model-trigger", "Skipped — last synthesis < 24h ago");
|
|
21
|
+
} else {
|
|
22
|
+
logDebug("self-model-trigger", `Self-model written: ${result.path}`);
|
|
23
|
+
}
|
|
24
|
+
} catch {
|
|
25
|
+
// Non-critical — self-model is best-effort
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
writeFileSync,
|
|
19
19
|
} from "node:fs";
|
|
20
20
|
import { dirname, relative, resolve } from "node:path";
|
|
21
|
-
import { assets, ensureDir,
|
|
21
|
+
import { assets, ensureDir, paths, platform } from "./paths";
|
|
22
22
|
import { buildSetupPrompt, readSetupState } from "./setup";
|
|
23
23
|
|
|
24
24
|
const TEMPLATE_PATH = assets.agentsMdTemplate();
|
|
@@ -89,7 +89,7 @@ export function needsRebuild(): boolean {
|
|
|
89
89
|
const sources: string[] = [
|
|
90
90
|
TEMPLATE_PATH,
|
|
91
91
|
resolve(paths.state(), "setup.json"),
|
|
92
|
-
|
|
92
|
+
resolve(paths.memory(), "pal-settings.json"),
|
|
93
93
|
];
|
|
94
94
|
|
|
95
95
|
// Track PAL doc sources for rebuild detection
|
|
@@ -103,46 +103,7 @@ export function needsRebuild(): boolean {
|
|
|
103
103
|
return latestMtime(...sources) > outputMtime;
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
-
|
|
107
|
-
ai: { name: string; displayName: string; catchphrase: string };
|
|
108
|
-
principal: { name: string };
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
const IDENTITY_DEFAULTS: Identity = {
|
|
112
|
-
ai: { name: "Assistant", displayName: "ASSISTANT", catchphrase: "" },
|
|
113
|
-
principal: { name: "" },
|
|
114
|
-
};
|
|
115
|
-
|
|
116
|
-
function palSettingsPath(): string {
|
|
117
|
-
return resolve(palHome(), "memory", "pal-settings.json");
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/** Load identity from pal-settings.json */
|
|
121
|
-
export function loadIdentity(): Identity {
|
|
122
|
-
const p = palSettingsPath();
|
|
123
|
-
if (!existsSync(p)) return IDENTITY_DEFAULTS;
|
|
124
|
-
|
|
125
|
-
try {
|
|
126
|
-
const data = JSON.parse(readFileSync(p, "utf-8"));
|
|
127
|
-
const ai = data.identity?.ai ?? {};
|
|
128
|
-
const principal = data.identity?.principal ?? {};
|
|
129
|
-
const name = ai.name || IDENTITY_DEFAULTS.ai.name;
|
|
130
|
-
const catchphrase = (ai.catchphrase || "").replace("{name}", name);
|
|
131
|
-
|
|
132
|
-
return {
|
|
133
|
-
ai: {
|
|
134
|
-
name,
|
|
135
|
-
displayName: ai.displayName || IDENTITY_DEFAULTS.ai.displayName,
|
|
136
|
-
catchphrase,
|
|
137
|
-
},
|
|
138
|
-
principal: {
|
|
139
|
-
name: principal.name || IDENTITY_DEFAULTS.principal.name,
|
|
140
|
-
},
|
|
141
|
-
};
|
|
142
|
-
} catch {
|
|
143
|
-
return IDENTITY_DEFAULTS;
|
|
144
|
-
}
|
|
145
|
-
}
|
|
106
|
+
import { identity } from "./settings";
|
|
146
107
|
|
|
147
108
|
/** Render AGENTS.md from the template using current state */
|
|
148
109
|
export function buildClaudeMd(): string {
|
|
@@ -152,14 +113,14 @@ export function buildClaudeMd(): string {
|
|
|
152
113
|
|
|
153
114
|
const state = readSetupState();
|
|
154
115
|
const setupPrompt = state ? buildSetupPrompt(state) : null;
|
|
155
|
-
const
|
|
116
|
+
const id = identity();
|
|
156
117
|
|
|
157
118
|
return template
|
|
158
119
|
.replace("{{SETUP_PROMPT}}", setupPrompt ? `${setupPrompt}\n` : "")
|
|
159
|
-
.replaceAll("{{IDENTITY_NAME}}",
|
|
160
|
-
.replaceAll("{{IDENTITY_DISPLAY}}",
|
|
161
|
-
.replaceAll("{{IDENTITY_CATCHPHRASE}}",
|
|
162
|
-
.replaceAll("{{PRINCIPAL_NAME}}",
|
|
120
|
+
.replaceAll("{{IDENTITY_NAME}}", id.ai.name)
|
|
121
|
+
.replaceAll("{{IDENTITY_DISPLAY}}", id.ai.displayName)
|
|
122
|
+
.replaceAll("{{IDENTITY_CATCHPHRASE}}", id.ai.catchphrase)
|
|
123
|
+
.replaceAll("{{PRINCIPAL_NAME}}", id.principal.name);
|
|
163
124
|
}
|
|
164
125
|
|
|
165
126
|
/** Regenerate AGENTS.md if any source file is newer, and ensure CLAUDE.md symlink exists. Returns true if rebuilt. */
|
package/src/hooks/lib/context.ts
CHANGED
|
@@ -12,36 +12,16 @@ import { loadOpinionContext } from "./opinions";
|
|
|
12
12
|
import { paths } from "./paths";
|
|
13
13
|
import { loadRecentNotes } from "./relationship";
|
|
14
14
|
import { readSessionNames } from "./session-names";
|
|
15
|
+
import * as settings from "./settings";
|
|
15
16
|
import { buildSetupPrompt, readSetupState, remainingSteps, STEP_ORDER } from "./setup";
|
|
16
17
|
import { computeSignalTrends, formatTrends } from "./signal-trends";
|
|
17
18
|
import { readFramePrinciples } from "./wisdom";
|
|
18
19
|
import { readProjectHistory, readSessions, recentSessions } from "./work-tracking";
|
|
19
20
|
|
|
20
|
-
interface PalSettings {
|
|
21
|
-
loadAtStartup?: { files?: string[] };
|
|
22
|
-
dynamicContext?: Record<string, boolean>;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/** Load pal-settings.json from memory/ */
|
|
26
|
-
function loadPalSettings(): PalSettings {
|
|
27
|
-
const p = resolve(paths.memory(), "pal-settings.json");
|
|
28
|
-
if (!existsSync(p)) return {};
|
|
29
|
-
try {
|
|
30
|
-
return JSON.parse(readFileSync(p, "utf-8"));
|
|
31
|
-
} catch {
|
|
32
|
-
return {};
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/** Check if a dynamic context section is enabled (defaults to true) */
|
|
37
|
-
function isEnabled(settings: PalSettings, key: string): boolean {
|
|
38
|
-
return settings.dynamicContext?.[key] !== false;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
21
|
/** Load and concatenate loadAtStartup files */
|
|
42
|
-
function loadStartupFiles(
|
|
43
|
-
const files = settings.
|
|
44
|
-
if (
|
|
22
|
+
function loadStartupFiles(): string {
|
|
23
|
+
const files = settings.startupFiles();
|
|
24
|
+
if (files.length === 0) return "";
|
|
45
25
|
|
|
46
26
|
const home = homedir();
|
|
47
27
|
const sections: string[] = [];
|
|
@@ -223,6 +203,19 @@ export function loadLearningDigest(): string {
|
|
|
223
203
|
}
|
|
224
204
|
}
|
|
225
205
|
|
|
206
|
+
/** Load self-model for session context injection */
|
|
207
|
+
export function loadSelfModel(): string {
|
|
208
|
+
try {
|
|
209
|
+
const p = resolve(paths.memory(), "self-model", "current.md");
|
|
210
|
+
if (!existsSync(p)) return "";
|
|
211
|
+
const content = readFileSync(p, "utf-8").trim();
|
|
212
|
+
if (!content) return "";
|
|
213
|
+
return content;
|
|
214
|
+
} catch {
|
|
215
|
+
return "";
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
226
219
|
/** Load 5 most recent failure contexts as an "avoid" list */
|
|
227
220
|
export function loadFailurePatterns(): string {
|
|
228
221
|
try {
|
|
@@ -231,10 +224,11 @@ export function loadFailurePatterns(): string {
|
|
|
231
224
|
|
|
232
225
|
const lines = entries.map((e) => {
|
|
233
226
|
const label = e.rating ? `[${e.rating}/10]` : "";
|
|
234
|
-
|
|
227
|
+
const text = e.principle || e.context;
|
|
228
|
+
return `- ${label} ${text}`.trim();
|
|
235
229
|
});
|
|
236
230
|
|
|
237
|
-
return ["## Recent
|
|
231
|
+
return ["## Lessons from Recent Failures — Apply These Now", ...lines].join("\n");
|
|
238
232
|
} catch {
|
|
239
233
|
return "";
|
|
240
234
|
}
|
|
@@ -451,30 +445,29 @@ export function loadHandoff(): string {
|
|
|
451
445
|
* things that change per-session and can't live in a static file.
|
|
452
446
|
*/
|
|
453
447
|
export function buildSystemReminder(): string {
|
|
454
|
-
const
|
|
455
|
-
const
|
|
456
|
-
const
|
|
457
|
-
const
|
|
458
|
-
const relationship = isEnabled(settings, "relationship")
|
|
448
|
+
const startup = loadStartupFiles();
|
|
449
|
+
const work = settings.isEnabled("activeWork") ? loadActiveWork() : null;
|
|
450
|
+
const wisdom = settings.isEnabled("wisdom") ? loadWisdomContext() : "";
|
|
451
|
+
const relationship = settings.isEnabled("relationship")
|
|
459
452
|
? loadRelationshipContext()
|
|
460
453
|
: "";
|
|
461
|
-
const digest = isEnabled(
|
|
462
|
-
const projectHistory = isEnabled(
|
|
454
|
+
const digest = settings.isEnabled("learningDigest") ? loadLearningDigest() : "";
|
|
455
|
+
const projectHistory = settings.isEnabled("projectHistory")
|
|
463
456
|
? loadProjectHistoryContext()
|
|
464
457
|
: "";
|
|
465
|
-
const trends = isEnabled(
|
|
466
|
-
const failures = isEnabled(
|
|
467
|
-
const synthesis = isEnabled(
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
const
|
|
471
|
-
const intelligence = isEnabled(settings, "sessionIntelligence")
|
|
458
|
+
const trends = settings.isEnabled("signalTrends") ? loadSignalTrends() : "";
|
|
459
|
+
const failures = settings.isEnabled("failurePatterns") ? loadFailurePatterns() : "";
|
|
460
|
+
const synthesis = settings.isEnabled("synthesis") ? loadSynthesisRecommendations() : "";
|
|
461
|
+
const opinions = settings.isEnabled("opinions") ? loadOpinionContext() : "";
|
|
462
|
+
const selfModel = settings.isEnabled("selfModel") ? loadSelfModel() : "";
|
|
463
|
+
const intelligence = settings.isEnabled("sessionIntelligence")
|
|
472
464
|
? loadSessionIntelligence()
|
|
473
465
|
: "";
|
|
474
|
-
const handoff = isEnabled(
|
|
466
|
+
const handoff = settings.isEnabled("handoff") ? loadHandoff() : "";
|
|
475
467
|
const parts: string[] = [];
|
|
476
468
|
if (startup) parts.push(startup);
|
|
477
469
|
if (handoff) parts.push(handoff);
|
|
470
|
+
if (selfModel) parts.push(selfModel);
|
|
478
471
|
if (wisdom) parts.push(wisdom);
|
|
479
472
|
if (opinions) parts.push(opinions);
|
|
480
473
|
if (intelligence) parts.push(intelligence);
|
package/src/hooks/lib/models.ts
CHANGED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PalSettings — single source of truth for pal-settings.json.
|
|
3
|
+
*
|
|
4
|
+
* Reads once, caches in memory for the process lifetime.
|
|
5
|
+
* All consumers import from here instead of reading the file directly.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
9
|
+
import { resolve } from "node:path";
|
|
10
|
+
import { paths } from "./paths";
|
|
11
|
+
|
|
12
|
+
// ── Types ──
|
|
13
|
+
|
|
14
|
+
export interface Identity {
|
|
15
|
+
ai: { name: string; fullName: string; displayName: string; catchphrase: string };
|
|
16
|
+
principal: { name: string; timezone: string };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface PalSettingsData {
|
|
20
|
+
identity?: {
|
|
21
|
+
ai?: { name?: string; fullName?: string; displayName?: string; catchphrase?: string };
|
|
22
|
+
principal?: { name?: string; timezone?: string };
|
|
23
|
+
};
|
|
24
|
+
loadAtStartup?: { files?: string[] };
|
|
25
|
+
dynamicContext?: Record<string, boolean>;
|
|
26
|
+
[key: string]: unknown;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const IDENTITY_DEFAULTS: Identity = {
|
|
30
|
+
ai: {
|
|
31
|
+
name: "Assistant",
|
|
32
|
+
fullName: "AI Assistant",
|
|
33
|
+
displayName: "ASSISTANT",
|
|
34
|
+
catchphrase: "",
|
|
35
|
+
},
|
|
36
|
+
principal: { name: "User", timezone: "" },
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// ── Singleton ──
|
|
40
|
+
|
|
41
|
+
let cached: PalSettingsData | null = null;
|
|
42
|
+
|
|
43
|
+
function settingsPath(): string {
|
|
44
|
+
return resolve(paths.memory(), "pal-settings.json");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function load(): PalSettingsData {
|
|
48
|
+
if (cached) return cached;
|
|
49
|
+
const p = settingsPath();
|
|
50
|
+
if (!existsSync(p)) {
|
|
51
|
+
cached = {};
|
|
52
|
+
return cached;
|
|
53
|
+
}
|
|
54
|
+
try {
|
|
55
|
+
cached = JSON.parse(readFileSync(p, "utf-8")) as PalSettingsData;
|
|
56
|
+
return cached;
|
|
57
|
+
} catch {
|
|
58
|
+
cached = {};
|
|
59
|
+
return cached;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Force re-read from disk (useful after writes) */
|
|
64
|
+
export function reload(): PalSettingsData {
|
|
65
|
+
cached = null;
|
|
66
|
+
return load();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ── Public API ──
|
|
70
|
+
|
|
71
|
+
/** Get the raw settings data */
|
|
72
|
+
export function raw(): PalSettingsData {
|
|
73
|
+
return load();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Get resolved identity with defaults */
|
|
77
|
+
export function identity(): Identity {
|
|
78
|
+
const data = load();
|
|
79
|
+
const ai = data.identity?.ai ?? {};
|
|
80
|
+
const principal = data.identity?.principal ?? {};
|
|
81
|
+
const name = ai.name || IDENTITY_DEFAULTS.ai.name;
|
|
82
|
+
const catchphrase = (ai.catchphrase || "").replace("{name}", name);
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
ai: {
|
|
86
|
+
name,
|
|
87
|
+
fullName: ai.fullName || IDENTITY_DEFAULTS.ai.fullName,
|
|
88
|
+
displayName: ai.displayName || IDENTITY_DEFAULTS.ai.displayName,
|
|
89
|
+
catchphrase,
|
|
90
|
+
},
|
|
91
|
+
principal: {
|
|
92
|
+
name: principal.name || IDENTITY_DEFAULTS.principal.name,
|
|
93
|
+
timezone: principal.timezone || IDENTITY_DEFAULTS.principal.timezone,
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Check if a dynamic context section is enabled (defaults to true) */
|
|
99
|
+
export function isEnabled(key: string): boolean {
|
|
100
|
+
return load().dynamicContext?.[key] !== false;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Get the loadAtStartup file list */
|
|
104
|
+
export function startupFiles(): string[] {
|
|
105
|
+
return load().loadAtStartup?.files ?? [];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Write settings back to disk and bust cache */
|
|
109
|
+
export function write(data: PalSettingsData): void {
|
|
110
|
+
writeFileSync(settingsPath(), `${JSON.stringify(data, null, 2)}\n`, "utf-8");
|
|
111
|
+
cached = null;
|
|
112
|
+
}
|
package/src/hooks/lib/stop.ts
CHANGED
|
@@ -8,11 +8,13 @@ import { resolve } from "node:path";
|
|
|
8
8
|
import { autoBackup } from "../handlers/backup";
|
|
9
9
|
import { captureFailure } from "../handlers/failure";
|
|
10
10
|
import { checkReflectTrigger } from "../handlers/reflect-trigger";
|
|
11
|
+
import { checkSelfModelTrigger } from "../handlers/self-model-trigger";
|
|
11
12
|
import { captureSessionIntelligence } from "../handlers/session-intelligence";
|
|
12
13
|
import { runSynthesis } from "../handlers/synthesis";
|
|
13
14
|
import { resetTab } from "../handlers/tab";
|
|
14
15
|
import { updateCounts } from "../handlers/update-counts";
|
|
15
16
|
import { captureWorkSession } from "../handlers/work-session";
|
|
17
|
+
import { inference } from "./inference";
|
|
16
18
|
import { logDebug, logError } from "./log";
|
|
17
19
|
import { ensureDir, paths } from "./paths";
|
|
18
20
|
import { extractContent, extractLastAssistant, parseMessages } from "./transcript";
|
|
@@ -44,6 +46,7 @@ export async function runStopHandlers(
|
|
|
44
46
|
updateCounts(),
|
|
45
47
|
autoBackup(),
|
|
46
48
|
checkReflectTrigger(),
|
|
49
|
+
checkSelfModelTrigger(),
|
|
47
50
|
runSynthesis(),
|
|
48
51
|
]);
|
|
49
52
|
|
|
@@ -137,15 +140,63 @@ async function checkPendingFailure(transcript: string): Promise<void> {
|
|
|
137
140
|
rating: number;
|
|
138
141
|
context: string;
|
|
139
142
|
detailedContext?: string;
|
|
143
|
+
principle?: string;
|
|
140
144
|
responsePreview?: string;
|
|
141
145
|
userPreview?: string;
|
|
142
146
|
};
|
|
143
147
|
unlinkSync(pendingPath);
|
|
148
|
+
|
|
149
|
+
// Extract principle from full transcript if not already present
|
|
150
|
+
let { principle, detailedContext } = pending;
|
|
151
|
+
if (!principle) {
|
|
152
|
+
try {
|
|
153
|
+
const msgs = parseMessages(transcript);
|
|
154
|
+
const recent = msgs
|
|
155
|
+
.slice(-10)
|
|
156
|
+
.map((m) => `${m.role.toUpperCase()}: ${extractContent(m).slice(0, 300)}`)
|
|
157
|
+
.join("\n\n");
|
|
158
|
+
|
|
159
|
+
const result = await inference({
|
|
160
|
+
system: `Analyze this failed AI interaction. The user rated it ${pending.rating}/10.
|
|
161
|
+
|
|
162
|
+
Return JSON:
|
|
163
|
+
{
|
|
164
|
+
"principle": "<one actionable rule the AI should follow, 10-20 words. Start with a verb: 'Verify...', 'Always...', 'Never...', 'Ask before...'>",
|
|
165
|
+
"detailed_context": "<what went wrong and why, 50-150 words>"
|
|
166
|
+
}`,
|
|
167
|
+
user: `User feedback: ${pending.context}\n\nConversation:\n${recent}`,
|
|
168
|
+
maxTokens: 400,
|
|
169
|
+
timeout: 10000,
|
|
170
|
+
jsonSchema: {
|
|
171
|
+
type: "object" as const,
|
|
172
|
+
properties: {
|
|
173
|
+
principle: { type: "string" as const },
|
|
174
|
+
detailed_context: { type: "string" as const },
|
|
175
|
+
},
|
|
176
|
+
required: ["principle", "detailed_context"],
|
|
177
|
+
additionalProperties: false,
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
if (result.success && result.output) {
|
|
182
|
+
const parsed = JSON.parse(result.output) as {
|
|
183
|
+
principle?: string;
|
|
184
|
+
detailed_context?: string;
|
|
185
|
+
};
|
|
186
|
+
principle = parsed.principle || undefined;
|
|
187
|
+
if (!detailedContext) detailedContext = parsed.detailed_context || undefined;
|
|
188
|
+
}
|
|
189
|
+
} catch {
|
|
190
|
+
/* graceful fallback — capture without principle */
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
144
194
|
await captureFailure(
|
|
145
195
|
pending.rating,
|
|
146
196
|
pending.context,
|
|
147
197
|
transcript,
|
|
148
|
-
|
|
198
|
+
detailedContext,
|
|
199
|
+
principle
|
|
149
200
|
);
|
|
150
201
|
} catch {
|
|
151
202
|
// Non-critical
|
package/src/targets/lib.ts
CHANGED
|
@@ -660,21 +660,3 @@ export function countMd(dir: string): number {
|
|
|
660
660
|
return 0;
|
|
661
661
|
}
|
|
662
662
|
}
|
|
663
|
-
|
|
664
|
-
/** Read skill frontmatter field */
|
|
665
|
-
export function readSkillField(skillPath: string, field: string): string {
|
|
666
|
-
try {
|
|
667
|
-
const content = readFileSync(skillPath, "utf-8");
|
|
668
|
-
const match = content.match(new RegExp(`^${field}:\\s*(.+)$`, "m"));
|
|
669
|
-
return match?.[1]?.trim() ?? "";
|
|
670
|
-
} catch {
|
|
671
|
-
return "";
|
|
672
|
-
}
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
/** Strip frontmatter from a skill file (content after second ---) */
|
|
676
|
-
export function skillBody(skillPath: string): string {
|
|
677
|
-
const content = readFileSync(skillPath, "utf-8");
|
|
678
|
-
const parts = content.split(/^---\s*$/m);
|
|
679
|
-
return parts.length >= 3 ? parts.slice(2).join("---").trim() : content.trim();
|
|
680
|
-
}
|