portable-agent-layer 0.23.1 → 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.
@@ -30,6 +30,7 @@
30
30
  "activeWork": true,
31
31
  "projectHistory": true,
32
32
  "sessionIntelligence": true,
33
- "handoff": true
33
+ "handoff": true,
34
+ "selfModel": true
34
35
  }
35
36
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "portable-agent-layer",
3
- "version": "0.23.1",
3
+ "version": "0.24.0",
4
4
  "description": "PAL — Portable Agent Layer: persistent personal context for AI coding assistants",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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 { palHome } from "../hooks/lib/paths";
10
-
11
- interface PalSettings {
12
- identity?: {
13
- ai?: { name?: string; fullName?: string; displayName?: string; catchphrase?: string };
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 = {};
@@ -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, palHome, paths, platform } from "./paths";
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
- palSettingsPath(),
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
- interface Identity {
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 identity = loadIdentity();
116
+ const id = identity();
156
117
 
157
118
  return template
158
119
  .replace("{{SETUP_PROMPT}}", setupPrompt ? `${setupPrompt}\n` : "")
159
- .replaceAll("{{IDENTITY_NAME}}", identity.ai.name)
160
- .replaceAll("{{IDENTITY_DISPLAY}}", identity.ai.displayName)
161
- .replaceAll("{{IDENTITY_CATCHPHRASE}}", identity.ai.catchphrase)
162
- .replaceAll("{{PRINCIPAL_NAME}}", identity.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. */
@@ -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(settings: PalSettings): string {
43
- const files = settings.loadAtStartup?.files;
44
- if (!files || files.length === 0) return "";
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 {
@@ -452,30 +445,29 @@ export function loadHandoff(): string {
452
445
  * things that change per-session and can't live in a static file.
453
446
  */
454
447
  export function buildSystemReminder(): string {
455
- const settings = loadPalSettings();
456
- const startup = loadStartupFiles(settings);
457
- const work = isEnabled(settings, "activeWork") ? loadActiveWork() : null;
458
- const wisdom = isEnabled(settings, "wisdom") ? loadWisdomContext() : "";
459
- 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")
460
452
  ? loadRelationshipContext()
461
453
  : "";
462
- const digest = isEnabled(settings, "learningDigest") ? loadLearningDigest() : "";
463
- const projectHistory = isEnabled(settings, "projectHistory")
454
+ const digest = settings.isEnabled("learningDigest") ? loadLearningDigest() : "";
455
+ const projectHistory = settings.isEnabled("projectHistory")
464
456
  ? loadProjectHistoryContext()
465
457
  : "";
466
- const trends = isEnabled(settings, "signalTrends") ? loadSignalTrends() : "";
467
- const failures = isEnabled(settings, "failurePatterns") ? loadFailurePatterns() : "";
468
- const synthesis = isEnabled(settings, "synthesis")
469
- ? loadSynthesisRecommendations()
470
- : "";
471
- const opinions = isEnabled(settings, "opinions") ? loadOpinionContext() : "";
472
- 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")
473
464
  ? loadSessionIntelligence()
474
465
  : "";
475
- const handoff = isEnabled(settings, "handoff") ? loadHandoff() : "";
466
+ const handoff = settings.isEnabled("handoff") ? loadHandoff() : "";
476
467
  const parts: string[] = [];
477
468
  if (startup) parts.push(startup);
478
469
  if (handoff) parts.push(handoff);
470
+ if (selfModel) parts.push(selfModel);
479
471
  if (wisdom) parts.push(wisdom);
480
472
  if (opinions) parts.push(opinions);
481
473
  if (intelligence) parts.push(intelligence);
@@ -3,6 +3,7 @@
3
3
  */
4
4
 
5
5
  export const HAIKU_MODEL = "claude-haiku-4-5-20251001";
6
+ export const SONNET_MODEL = "claude-sonnet-4-6";
6
7
 
7
8
  /** Pricing per million tokens (USD) — from https://platform.claude.com/docs/en/about-claude/pricing */
8
9
  export const MODEL_PRICING: Record<
@@ -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
+ }
@@ -8,6 +8,7 @@ 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";
@@ -45,6 +46,7 @@ export async function runStopHandlers(
45
46
  updateCounts(),
46
47
  autoBackup(),
47
48
  checkReflectTrigger(),
49
+ checkSelfModelTrigger(),
48
50
  runSynthesis(),
49
51
  ]);
50
52
 
@@ -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
- }