portable-agent-layer 0.35.0 → 0.36.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/README.md +1 -1
- package/assets/skills/projects/SKILL.md +0 -1
- package/assets/skills/telos/SKILL.md +7 -52
- package/assets/templates/PAL/ALGORITHM.md +28 -3
- package/assets/templates/PAL/PROJECT_LIFECYCLE.md +48 -0
- package/assets/templates/PAL/README.md +1 -1
- package/assets/templates/PAL/STEERING_RULES.md +4 -0
- package/assets/templates/PAL/SYSTEM_ARCHITECTURE.md +32 -17
- package/assets/templates/PAL/WORK_TRACKING.md +1 -1
- package/assets/templates/pal-settings.json +1 -3
- package/assets/templates/settings.claude.json +2 -1
- package/package.json +1 -1
- package/src/cli/setup-telos.ts +12 -79
- package/src/hooks/LoadContext.ts +22 -10
- package/src/hooks/handlers/context-digests.ts +74 -0
- package/src/hooks/handlers/session-intelligence.ts +9 -86
- package/src/hooks/lib/claude-md.ts +69 -14
- package/src/hooks/lib/context.ts +57 -139
- package/src/hooks/lib/relationship.ts +3 -3
- package/src/hooks/lib/security.ts +2 -0
- package/src/hooks/lib/semi-static.ts +186 -0
- package/src/hooks/lib/setup.ts +0 -5
- package/src/hooks/lib/stop.ts +3 -0
- package/src/targets/claude/uninstall.ts +1 -1
- package/src/targets/copilot/install.ts +39 -8
- package/src/targets/copilot/uninstall.ts +58 -17
- package/src/targets/cursor/install.ts +8 -0
- package/src/targets/cursor/uninstall.ts +18 -1
- package/src/targets/lib.ts +26 -0
- package/src/targets/opencode/install.ts +29 -1
- package/src/targets/opencode/plugin.ts +1 -1
- package/src/targets/opencode/uninstall.ts +30 -3
- package/src/tools/agent/handoff-note.ts +116 -0
- package/src/tools/agent/relationship-note.ts +51 -0
- package/src/tools/relationship-reflect.ts +2 -2
- package/src/tools/self-model.ts +4 -4
- package/assets/templates/telos/PROJECTS.md +0 -7
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Stop handler: unified session intelligence capture.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* Writes: session learning file, project history, relationship notes, last-handoff.
|
|
4
|
+
* Produces: title, summary, insights via Haiku.
|
|
5
|
+
* Writes: session learning file, project history.
|
|
7
6
|
*
|
|
8
|
-
*
|
|
7
|
+
* Relationship notes → written in ALGORITHM LEARN phase via relationship-note.ts
|
|
8
|
+
* Handoff notes → written in ALGORITHM LEARN phase via handoff-note.ts
|
|
9
|
+
*
|
|
10
|
+
* Replaces: work-learning.ts (still exists but is bypassed).
|
|
9
11
|
*/
|
|
10
12
|
|
|
11
13
|
import { existsSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
@@ -15,7 +17,6 @@ import { inference } from "../lib/inference";
|
|
|
15
17
|
import { categorizeLearning } from "../lib/learning-category";
|
|
16
18
|
import { logDebug, logError } from "../lib/log";
|
|
17
19
|
import { ensureDir, paths } from "../lib/paths";
|
|
18
|
-
import { appendNotes, hasSessionNotes, type RelationshipNote } from "../lib/relationship";
|
|
19
20
|
import { fileTimestamp, monthPath } from "../lib/time";
|
|
20
21
|
import { logTokenUsage } from "../lib/token-usage";
|
|
21
22
|
import {
|
|
@@ -110,25 +111,8 @@ const INTELLIGENCE_SCHEMA = {
|
|
|
110
111
|
description:
|
|
111
112
|
"If status is in-progress: what remains to be done, key decisions made, blockers. If completed: empty string.",
|
|
112
113
|
},
|
|
113
|
-
observations: {
|
|
114
|
-
type: "array" as const,
|
|
115
|
-
items: {
|
|
116
|
-
type: "object" as const,
|
|
117
|
-
additionalProperties: false,
|
|
118
|
-
properties: {
|
|
119
|
-
type: {
|
|
120
|
-
type: "string" as const,
|
|
121
|
-
enum: ["O", "W", "B"],
|
|
122
|
-
description: "O=preference, W=world fact, B=what AI did",
|
|
123
|
-
},
|
|
124
|
-
text: { type: "string" as const },
|
|
125
|
-
confidence: { type: "number" as const },
|
|
126
|
-
},
|
|
127
|
-
required: ["type", "text", "confidence"] as const,
|
|
128
|
-
},
|
|
129
|
-
},
|
|
130
114
|
},
|
|
131
|
-
required: ["title", "summary", "insights", "handoff"
|
|
115
|
+
required: ["title", "summary", "insights", "handoff"] as const,
|
|
132
116
|
};
|
|
133
117
|
|
|
134
118
|
interface IntelligenceOutput {
|
|
@@ -136,7 +120,6 @@ interface IntelligenceOutput {
|
|
|
136
120
|
summary: string;
|
|
137
121
|
insights: string;
|
|
138
122
|
handoff: string;
|
|
139
|
-
observations: Array<{ type: "O" | "W" | "B"; text: string; confidence: number }>;
|
|
140
123
|
}
|
|
141
124
|
|
|
142
125
|
// ── Main handler ──
|
|
@@ -160,9 +143,6 @@ export async function captureSessionIntelligence(
|
|
|
160
143
|
return;
|
|
161
144
|
}
|
|
162
145
|
|
|
163
|
-
// Relationship dedup — skip relationship capture if already done for this session
|
|
164
|
-
const skipRelationship = sessionId ? hasSessionNotes(sessionId) : false;
|
|
165
|
-
|
|
166
146
|
// Extract transcript windows
|
|
167
147
|
const userMessages = messages
|
|
168
148
|
.filter((m) => m.role === "user")
|
|
@@ -174,8 +154,7 @@ export async function captureSessionIntelligence(
|
|
|
174
154
|
const lastUser = extractLastUser(messages);
|
|
175
155
|
const status = detectStatus(lastAssistantText);
|
|
176
156
|
|
|
177
|
-
|
|
178
|
-
const userWindow = userMessages.slice(-15).map((t) => t.slice(0, 200));
|
|
157
|
+
const userWindow = userMessages.slice(-10).map((t) => t.slice(0, 200));
|
|
179
158
|
const assistantWindow = lastAssistantText.slice(0, 600);
|
|
180
159
|
|
|
181
160
|
if (userWindow.length < 3) return;
|
|
@@ -195,12 +174,9 @@ export async function captureSessionIntelligence(
|
|
|
195
174
|
status === "in-progress"
|
|
196
175
|
? "4. handoff: what remains unfinished — decisions made so far, next steps, blockers (2-4 sentences)"
|
|
197
176
|
: "4. handoff: empty string (session completed)",
|
|
198
|
-
skipRelationship
|
|
199
|
-
? "5. observations: empty array (already captured)"
|
|
200
|
-
: "5. observations: 0-3 relationship observations. O=preference/opinion, W=world fact, B=what AI did this session (first-person). Be concise.",
|
|
201
177
|
].join("\n"),
|
|
202
178
|
user: `User messages:\n${userWindow.map((m, i) => `${i + 1}. ${m}`).join("\n")}\n\nLast AI response:\n${assistantWindow}`,
|
|
203
|
-
maxTokens:
|
|
179
|
+
maxTokens: 350,
|
|
204
180
|
timeout: 15000,
|
|
205
181
|
jsonSchema: INTELLIGENCE_SCHEMA,
|
|
206
182
|
});
|
|
@@ -218,8 +194,6 @@ export async function captureSessionIntelligence(
|
|
|
218
194
|
const title = output?.title || extractContent(lastUser).slice(0, 80) || "session";
|
|
219
195
|
const summary = output?.summary || lastAssistantText.slice(0, 600);
|
|
220
196
|
const insights = output?.insights || "";
|
|
221
|
-
const handoff = output?.handoff || "";
|
|
222
|
-
|
|
223
197
|
// ── Write session learning file ──
|
|
224
198
|
|
|
225
199
|
const category = categorizeLearning(title, summary);
|
|
@@ -241,7 +215,6 @@ export async function captureSessionIntelligence(
|
|
|
241
215
|
"",
|
|
242
216
|
"## Insights",
|
|
243
217
|
insights || "*No insights captured.*",
|
|
244
|
-
...(handoff ? ["", "## Handoff", handoff] : []),
|
|
245
218
|
].join("\n");
|
|
246
219
|
|
|
247
220
|
const content = stringify(meta, body);
|
|
@@ -271,54 +244,4 @@ export async function captureSessionIntelligence(
|
|
|
271
244
|
|
|
272
245
|
if (sessionId) markCaptured(sessionId, filepath, messages.length);
|
|
273
246
|
logDebug("session-intelligence", `Learning captured: ${title}`);
|
|
274
|
-
|
|
275
|
-
// ── Write relationship notes ──
|
|
276
|
-
|
|
277
|
-
if (!skipRelationship && output?.observations && output.observations.length > 0) {
|
|
278
|
-
try {
|
|
279
|
-
const notes: RelationshipNote[] = output.observations.map((o) => ({
|
|
280
|
-
type: o.type,
|
|
281
|
-
text: o.text,
|
|
282
|
-
confidence: o.confidence,
|
|
283
|
-
}));
|
|
284
|
-
appendNotes(notes, sessionId);
|
|
285
|
-
logDebug(
|
|
286
|
-
"session-intelligence",
|
|
287
|
-
`${notes.length} relationship observations captured`
|
|
288
|
-
);
|
|
289
|
-
} catch (err) {
|
|
290
|
-
logError("session-intelligence:relationship", err);
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
// ── Write handoff state ──
|
|
295
|
-
|
|
296
|
-
if (handoff && status === "in-progress") {
|
|
297
|
-
try {
|
|
298
|
-
const handoffPath = resolve(ensureDir(paths.state()), "last-handoff.json");
|
|
299
|
-
let handoffs: Record<string, unknown> = {};
|
|
300
|
-
if (existsSync(handoffPath)) {
|
|
301
|
-
try {
|
|
302
|
-
handoffs = JSON.parse(readFileSync(handoffPath, "utf-8"));
|
|
303
|
-
} catch {
|
|
304
|
-
/* fresh */
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
handoffs[process.cwd()] = {
|
|
308
|
-
timestamp: new Date().toISOString(),
|
|
309
|
-
sessionId,
|
|
310
|
-
title,
|
|
311
|
-
status,
|
|
312
|
-
handoff,
|
|
313
|
-
artifacts: [],
|
|
314
|
-
};
|
|
315
|
-
// Keep last 20 projects
|
|
316
|
-
const entries = Object.entries(handoffs);
|
|
317
|
-
if (entries.length > 20) handoffs = Object.fromEntries(entries.slice(-20));
|
|
318
|
-
writeFileSync(handoffPath, JSON.stringify(handoffs, null, 2), "utf-8");
|
|
319
|
-
logDebug("session-intelligence", "Handoff state written");
|
|
320
|
-
} catch (err) {
|
|
321
|
-
logError("session-intelligence:handoff", err);
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
247
|
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Dynamic AGENTS.md generation.
|
|
2
|
+
* Dynamic AGENTS.md / CLAUDE.md generation.
|
|
3
3
|
*
|
|
4
|
-
* AGENTS.md is regenerated when setup.json or any
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* AGENTS.md (opencode, codex, copilot) is regenerated when setup.json or any
|
|
5
|
+
* telos file is newer. CLAUDE.md (Claude Code) is a real file — not a symlink —
|
|
6
|
+
* and prepends an @import for the self-model so that large static context loads
|
|
7
|
+
* natively rather than through the hook's stdout.
|
|
7
8
|
*/
|
|
8
9
|
|
|
9
10
|
import {
|
|
@@ -19,6 +20,7 @@ import {
|
|
|
19
20
|
} from "node:fs";
|
|
20
21
|
import { dirname, relative, resolve } from "node:path";
|
|
21
22
|
import { assets, ensureDir, paths, platform } from "./paths";
|
|
23
|
+
import { getSemiStaticSources } from "./semi-static";
|
|
22
24
|
|
|
23
25
|
const TEMPLATE_PATH = assets.agentsMdTemplate();
|
|
24
26
|
|
|
@@ -70,16 +72,12 @@ function ensureOneSymlink(linkPath: string, targetPath: string): void {
|
|
|
70
72
|
}
|
|
71
73
|
}
|
|
72
74
|
|
|
73
|
-
/** Ensure
|
|
75
|
+
/** Ensure codex symlink points to the canonical AGENTS.md.
|
|
76
|
+
* CLAUDE.md for Claude Code is a real file written by ensureClaudeCodeMd().
|
|
77
|
+
* Copilot uses ~/.copilot/instructions/*.instructions.md — no symlink needed. */
|
|
74
78
|
function ensureSymlinks(): void {
|
|
75
|
-
const { outputPath
|
|
76
|
-
ensureOneSymlink(symlinkPath, outputPath);
|
|
79
|
+
const { outputPath } = getOutputPaths();
|
|
77
80
|
ensureOneSymlink(resolve(platform.codexDir(), "AGENTS.md"), outputPath);
|
|
78
|
-
// Copilot instructions — only create if ~/.copilot/ already exists (i.e. Copilot is installed)
|
|
79
|
-
const copilotDir = platform.copilotDir();
|
|
80
|
-
if (existsSync(copilotDir)) {
|
|
81
|
-
ensureOneSymlink(resolve(copilotDir, "copilot-instructions.md"), outputPath);
|
|
82
|
-
}
|
|
83
81
|
}
|
|
84
82
|
|
|
85
83
|
/** Returns true if AGENTS.md needs to be regenerated */
|
|
@@ -89,11 +87,12 @@ export function needsRebuild(): boolean {
|
|
|
89
87
|
|
|
90
88
|
const outputMtime = statSync(outputPath).mtimeMs;
|
|
91
89
|
|
|
92
|
-
// Collect source files: template + setup.json + identity + PAL docs
|
|
90
|
+
// Collect source files: template + setup.json + identity + PAL docs + @import candidates
|
|
93
91
|
const sources: string[] = [
|
|
94
92
|
TEMPLATE_PATH,
|
|
95
93
|
resolve(paths.state(), "setup.json"),
|
|
96
94
|
resolve(paths.memory(), "pal-settings.json"),
|
|
95
|
+
...getSemiStaticSources().map((s) => s.path),
|
|
97
96
|
];
|
|
98
97
|
|
|
99
98
|
// Track PAL doc sources for rebuild detection
|
|
@@ -124,15 +123,71 @@ export function buildClaudeMd(): string {
|
|
|
124
123
|
.replaceAll("{{PRINCIPAL_NAME}}", id.principal.name);
|
|
125
124
|
}
|
|
126
125
|
|
|
127
|
-
/**
|
|
126
|
+
/** Build @import header lines for CLAUDE.md — one line per semi-static file that exists. */
|
|
127
|
+
function buildClaudeCodeImports(): string {
|
|
128
|
+
const claudeDir = platform.claudeDir();
|
|
129
|
+
|
|
130
|
+
const lines = getSemiStaticSources()
|
|
131
|
+
.map((s) => s.path)
|
|
132
|
+
.filter((p) => existsSync(p))
|
|
133
|
+
.map((p) => `@${relative(claudeDir, p).replaceAll("\\", "/")}`);
|
|
134
|
+
|
|
135
|
+
return lines.length > 0 ? `${lines.join("\n")}\n\n` : "";
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Build CLAUDE.md content for Claude Code — prepends @import for self-model. */
|
|
139
|
+
export function buildClaudeCodeMd(): string {
|
|
140
|
+
return buildClaudeCodeImports() + buildClaudeMd();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Write ~/.claude/CLAUDE.md as a real file (upgrading from symlink if needed).
|
|
144
|
+
* Also rewrites if the @import header has changed (new digest files appeared). */
|
|
145
|
+
function ensureClaudeCodeMd(): void {
|
|
146
|
+
const claudeDir = platform.claudeDir();
|
|
147
|
+
if (!claudeDir) return;
|
|
148
|
+
const claudeMdPath = resolve(claudeDir, "CLAUDE.md");
|
|
149
|
+
const expected = buildClaudeCodeMd();
|
|
150
|
+
try {
|
|
151
|
+
if (existsSync(claudeMdPath) && !lstatSync(claudeMdPath).isSymbolicLink()) {
|
|
152
|
+
const current = readFileSync(claudeMdPath, "utf-8");
|
|
153
|
+
if (current === expected) return; // no change needed
|
|
154
|
+
// @imports changed — rewrite
|
|
155
|
+
} else if (existsSync(claudeMdPath)) {
|
|
156
|
+
unlinkSync(claudeMdPath); // remove symlink
|
|
157
|
+
}
|
|
158
|
+
} catch {
|
|
159
|
+
/* fall through */
|
|
160
|
+
}
|
|
161
|
+
try {
|
|
162
|
+
ensureDir(claudeDir);
|
|
163
|
+
writeFileSync(claudeMdPath, expected, "utf-8");
|
|
164
|
+
} catch {
|
|
165
|
+
/* ignore write errors — non-fatal */
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Regenerate AGENTS.md if any source file is newer, write real CLAUDE.md, ensure other symlinks. Returns true if rebuilt. */
|
|
128
170
|
export function regenerateIfNeeded(): boolean {
|
|
129
171
|
const { outputPath } = getOutputPaths();
|
|
130
172
|
if (!needsRebuild()) {
|
|
131
173
|
ensureSymlinks();
|
|
174
|
+
ensureClaudeCodeMd();
|
|
132
175
|
return false;
|
|
133
176
|
}
|
|
134
177
|
ensureDir(dirname(outputPath));
|
|
135
178
|
writeFileSync(outputPath, buildClaudeMd(), "utf-8");
|
|
179
|
+
// Write Claude Code's CLAUDE.md as a real file (removing any existing symlink)
|
|
180
|
+
const claudeDir = platform.claudeDir();
|
|
181
|
+
if (claudeDir) {
|
|
182
|
+
const claudeMdPath = resolve(claudeDir, "CLAUDE.md");
|
|
183
|
+
try {
|
|
184
|
+
if (existsSync(claudeMdPath)) unlinkSync(claudeMdPath);
|
|
185
|
+
ensureDir(claudeDir);
|
|
186
|
+
writeFileSync(claudeMdPath, buildClaudeCodeMd(), "utf-8");
|
|
187
|
+
} catch {
|
|
188
|
+
/* ignore — CLAUDE.md write failure is non-fatal */
|
|
189
|
+
}
|
|
190
|
+
}
|
|
136
191
|
ensureSymlinks();
|
|
137
192
|
return true;
|
|
138
193
|
}
|
package/src/hooks/lib/context.ts
CHANGED
|
@@ -3,21 +3,21 @@
|
|
|
3
3
|
* Used by LoadContext.ts (Claude Code) and the opencode plugin.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { existsSync,
|
|
6
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
7
7
|
import { homedir } from "node:os";
|
|
8
8
|
import { resolve } from "node:path";
|
|
9
|
-
import {
|
|
10
|
-
import { readFailures, readLearnings } from "./learning-store";
|
|
9
|
+
import { readLearnings } from "./learning-store";
|
|
11
10
|
import { loadOpinionContext } from "./opinions";
|
|
12
11
|
import { paths } from "./paths";
|
|
13
12
|
import { loadActiveProjectsContext } from "./projects";
|
|
14
13
|
import { loadRecentNotes } from "./relationship";
|
|
14
|
+
import { loadFailurePatterns, loadSynthesisRecommendations } from "./semi-static";
|
|
15
15
|
import { readSessionNames } from "./session-names";
|
|
16
16
|
import * as settings from "./settings";
|
|
17
17
|
import { isSetupComplete, readSetupState, remainingSteps, STEP_ORDER } from "./setup";
|
|
18
18
|
import { computeSignalTrends, formatTrends } from "./signal-trends";
|
|
19
19
|
import { readFramePrinciples } from "./wisdom";
|
|
20
|
-
import { readProjectHistory, readSessions
|
|
20
|
+
import { readProjectHistory, readSessions } from "./work-tracking";
|
|
21
21
|
|
|
22
22
|
/** Load and concatenate loadAtStartup files */
|
|
23
23
|
function loadStartupFiles(): string {
|
|
@@ -53,47 +53,6 @@ export function countSignals(filename: string): number {
|
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
/** Load structured session history + project dashboard */
|
|
57
|
-
export function loadActiveWork(): { text: string; summary: string | null } | null {
|
|
58
|
-
try {
|
|
59
|
-
const cwd = process.cwd();
|
|
60
|
-
const allRecent = recentSessions(48);
|
|
61
|
-
|
|
62
|
-
if (allRecent.length === 0) return null;
|
|
63
|
-
|
|
64
|
-
const lines: string[] = [];
|
|
65
|
-
|
|
66
|
-
lines.push("## Recent Work (last 48h)");
|
|
67
|
-
for (const s of allRecent.slice(-10).reverse()) {
|
|
68
|
-
const ago = formatAgo(s.ts);
|
|
69
|
-
const here = s.cwd === cwd ? " *" : "";
|
|
70
|
-
lines.push(`- [${s.status}] ${s.name} — ${ago}${here}`);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// Summary from most recent session
|
|
74
|
-
const cwdSessions = allRecent.filter((s) => s.cwd === cwd);
|
|
75
|
-
const last = cwdSessions.length > 0 ? cwdSessions[cwdSessions.length - 1] : null;
|
|
76
|
-
const summary = last?.summary?.slice(0, 60) || null;
|
|
77
|
-
|
|
78
|
-
return {
|
|
79
|
-
text: lines.join("\n"),
|
|
80
|
-
summary: summary ? `"${summary}"` : null,
|
|
81
|
-
};
|
|
82
|
-
} catch {
|
|
83
|
-
return null;
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/** Format a timestamp as a human-readable "X ago" string */
|
|
88
|
-
function formatAgo(ts: string): string {
|
|
89
|
-
const diff = Date.now() - new Date(ts).getTime();
|
|
90
|
-
const hours = Math.floor(diff / (1000 * 60 * 60));
|
|
91
|
-
if (hours < 1) return "just now";
|
|
92
|
-
if (hours < 24) return `${hours}h ago`;
|
|
93
|
-
const days = Math.floor(hours / 24);
|
|
94
|
-
return `${days}d ago`;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
56
|
/** Load the N most recent session names (fallback for greeting) */
|
|
98
57
|
export function loadRecentSessions(count: number): string[] {
|
|
99
58
|
try {
|
|
@@ -140,7 +99,6 @@ function loadCachedCounts(): {
|
|
|
140
99
|
/** Build the visible greeting lines for stderr */
|
|
141
100
|
export function buildGreeting(): string[] {
|
|
142
101
|
const counts = loadCachedCounts();
|
|
143
|
-
const work = loadActiveWork();
|
|
144
102
|
const setupState = readSetupState();
|
|
145
103
|
const setupIncomplete = setupState && !isSetupComplete(setupState);
|
|
146
104
|
|
|
@@ -157,10 +115,6 @@ export function buildGreeting(): string[] {
|
|
|
157
115
|
);
|
|
158
116
|
}
|
|
159
117
|
|
|
160
|
-
if (work?.summary) {
|
|
161
|
-
greeting.push(`📋 Previous: ${work.summary}`);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
118
|
// Show recent session names for quick context
|
|
165
119
|
const recent = loadRecentSessions(3);
|
|
166
120
|
if (recent.length > 0) {
|
|
@@ -217,78 +171,6 @@ export function loadSelfModel(): string {
|
|
|
217
171
|
}
|
|
218
172
|
}
|
|
219
173
|
|
|
220
|
-
/** Load 5 most recent failure contexts as an "avoid" list */
|
|
221
|
-
export function loadFailurePatterns(): string {
|
|
222
|
-
try {
|
|
223
|
-
const entries = readFailures(paths.failures(), 5);
|
|
224
|
-
if (entries.length === 0) return "";
|
|
225
|
-
|
|
226
|
-
const lines = entries.map((e) => {
|
|
227
|
-
const label = e.rating ? `[${e.rating}/10]` : "";
|
|
228
|
-
const text = e.principle || e.context;
|
|
229
|
-
return `- ${label} ${text}`.trim();
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
return ["## Lessons from Recent Failures — Apply These Now", ...lines].join("\n");
|
|
233
|
-
} catch {
|
|
234
|
-
return "";
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
/** Load recommendations from the most recent synthesis report */
|
|
239
|
-
export function loadSynthesisRecommendations(): string {
|
|
240
|
-
try {
|
|
241
|
-
const synthDir = paths.synthesis();
|
|
242
|
-
if (!existsSync(synthDir)) return "";
|
|
243
|
-
|
|
244
|
-
// Find most recent month directory
|
|
245
|
-
const months = readdirSync(synthDir).sort().reverse();
|
|
246
|
-
for (const month of months) {
|
|
247
|
-
const monthDir = resolve(synthDir, month);
|
|
248
|
-
try {
|
|
249
|
-
const files = readdirSync(monthDir)
|
|
250
|
-
.filter((f) => f.endsWith(".md"))
|
|
251
|
-
.sort()
|
|
252
|
-
.reverse();
|
|
253
|
-
if (files.length === 0) continue;
|
|
254
|
-
|
|
255
|
-
const content = readFileSync(resolve(monthDir, files[0]), "utf-8");
|
|
256
|
-
|
|
257
|
-
// Extract recommendations section
|
|
258
|
-
const recMatch = content.match(/## Recommendations\n\n([\s\S]*?)(?:\n##|\n$|$)/);
|
|
259
|
-
if (!recMatch?.[1]?.trim()) continue;
|
|
260
|
-
|
|
261
|
-
const recs = recMatch[1]
|
|
262
|
-
.trim()
|
|
263
|
-
.split("\n")
|
|
264
|
-
.filter((l) => l.trim())
|
|
265
|
-
.slice(0, 4);
|
|
266
|
-
|
|
267
|
-
if (recs.length === 0) continue;
|
|
268
|
-
|
|
269
|
-
const { meta } = parse<{
|
|
270
|
-
period?: string;
|
|
271
|
-
average_rating?: string;
|
|
272
|
-
}>(content);
|
|
273
|
-
const period = meta.period || "";
|
|
274
|
-
const avgRating = meta.average_rating ? `${meta.average_rating}/10` : "";
|
|
275
|
-
|
|
276
|
-
const header = [
|
|
277
|
-
"## Pattern Synthesis",
|
|
278
|
-
period ? `*${period} — ${avgRating}*` : "",
|
|
279
|
-
]
|
|
280
|
-
.filter(Boolean)
|
|
281
|
-
.join("\n");
|
|
282
|
-
|
|
283
|
-
return [header, ...recs].join("\n");
|
|
284
|
-
} catch {}
|
|
285
|
-
}
|
|
286
|
-
return "";
|
|
287
|
-
} catch {
|
|
288
|
-
return "";
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
|
|
292
174
|
/** Load signal trends as a formatted string */
|
|
293
175
|
export function loadSignalTrends(): string {
|
|
294
176
|
try {
|
|
@@ -322,7 +204,12 @@ export function loadRelationshipContext(): string {
|
|
|
322
204
|
try {
|
|
323
205
|
const notes = loadRecentNotes(2);
|
|
324
206
|
if (!notes) return "";
|
|
325
|
-
|
|
207
|
+
// Strip O entries (opinions loaded natively via digest) and HTML comment lines
|
|
208
|
+
const filtered = notes
|
|
209
|
+
.split("\n")
|
|
210
|
+
.filter((l) => !/^\s*- O\(/.test(l) && !/^\s*<!--/.test(l))
|
|
211
|
+
.join("\n");
|
|
212
|
+
return capSection(`## Recent Interaction Notes\n${filtered}`, 1500);
|
|
326
213
|
} catch {
|
|
327
214
|
return "";
|
|
328
215
|
}
|
|
@@ -337,11 +224,10 @@ export function loadSessionIntelligence(): string {
|
|
|
337
224
|
|
|
338
225
|
const lines: string[] = ["## Session Intelligence"];
|
|
339
226
|
|
|
340
|
-
// Open Threads — project-specific
|
|
227
|
+
// Open Threads — project-specific only
|
|
341
228
|
if (state.threads?.length > 0) {
|
|
342
229
|
const cwd = process.cwd();
|
|
343
230
|
const here = state.threads.filter((t: { cwd?: string }) => t.cwd === cwd);
|
|
344
|
-
const other = state.threads.filter((t: { cwd?: string }) => t.cwd !== cwd);
|
|
345
231
|
|
|
346
232
|
if (here.length > 0) {
|
|
347
233
|
lines.push("");
|
|
@@ -350,11 +236,10 @@ export function loadSessionIntelligence(): string {
|
|
|
350
236
|
lines.push(`- ${t.title} (opened ${t.opened})`);
|
|
351
237
|
if (t.context) lines.push(` ${t.context}`);
|
|
352
238
|
}
|
|
353
|
-
lines.push(
|
|
239
|
+
lines.push(
|
|
240
|
+
"→ Continue this work or explicitly close it before starting something new."
|
|
241
|
+
);
|
|
354
242
|
}
|
|
355
|
-
// Cross-project threads intentionally omitted — they were noise in 90%+ of sessions.
|
|
356
|
-
// To surface them on demand, add a `threads` slash-command or a flag in pal-settings.
|
|
357
|
-
void other;
|
|
358
243
|
}
|
|
359
244
|
|
|
360
245
|
// Rating Trend
|
|
@@ -403,7 +288,7 @@ export function loadSessionIntelligence(): string {
|
|
|
403
288
|
}
|
|
404
289
|
}
|
|
405
290
|
|
|
406
|
-
return lines.length > 1 ? lines.join("\n") : "";
|
|
291
|
+
return lines.length > 1 ? capSection(lines.join("\n"), 2000) : "";
|
|
407
292
|
} catch {
|
|
408
293
|
return "";
|
|
409
294
|
}
|
|
@@ -434,17 +319,46 @@ export function loadHandoff(): string {
|
|
|
434
319
|
}
|
|
435
320
|
}
|
|
436
321
|
|
|
322
|
+
/** Truncate text to maxChars at the last complete line boundary */
|
|
323
|
+
function capSection(text: string, maxChars: number): string {
|
|
324
|
+
if (text.length <= maxChars) return text;
|
|
325
|
+
const lines = text.split("\n");
|
|
326
|
+
const kept: string[] = [];
|
|
327
|
+
let total = 0;
|
|
328
|
+
for (const line of lines) {
|
|
329
|
+
const next = total + line.length + 1;
|
|
330
|
+
if (next > maxChars) break;
|
|
331
|
+
kept.push(line);
|
|
332
|
+
total = next;
|
|
333
|
+
}
|
|
334
|
+
return kept.join("\n");
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/** Agent targets — determines which context sections are skipped due to native loading. */
|
|
338
|
+
export type AgentTarget = "claude" | "opencode" | "cursor" | "copilot";
|
|
339
|
+
|
|
437
340
|
/**
|
|
438
341
|
* Build the <system-reminder> content for the AI.
|
|
439
342
|
*
|
|
440
343
|
* Static context (TELOS, setup prompt) lives in AGENTS.md / CLAUDE.md and is
|
|
441
344
|
* loaded natively by Claude Code / opencode. This injects dynamic context only —
|
|
442
345
|
* things that change per-session and can't live in a static file.
|
|
346
|
+
*
|
|
347
|
+
* opts.agent — agent target; Claude Code skips semi-static sections (self-model,
|
|
348
|
+
* wisdom, opinions) that load natively via @imports in CLAUDE.md.
|
|
443
349
|
*/
|
|
444
|
-
export function buildSystemReminder(): string {
|
|
350
|
+
export function buildSystemReminder(opts: { agent?: AgentTarget } = {}): string {
|
|
351
|
+
// Semi-static sections loaded natively via @imports (Claude Code) or
|
|
352
|
+
// instructions[] (opencode). Skip them from hook output for those agents.
|
|
353
|
+
const skipSemiStatic =
|
|
354
|
+
opts.agent === "claude" ||
|
|
355
|
+
opts.agent === "opencode" ||
|
|
356
|
+
opts.agent === "cursor" ||
|
|
357
|
+
opts.agent === "copilot";
|
|
358
|
+
|
|
445
359
|
const startup = loadStartupFiles();
|
|
446
|
-
const
|
|
447
|
-
|
|
360
|
+
const wisdom =
|
|
361
|
+
!skipSemiStatic && settings.isEnabled("wisdom") ? loadWisdomContext() : "";
|
|
448
362
|
const relationship = settings.isEnabled("relationship")
|
|
449
363
|
? loadRelationshipContext()
|
|
450
364
|
: "";
|
|
@@ -456,10 +370,16 @@ export function buildSystemReminder(): string {
|
|
|
456
370
|
? loadActiveProjectsContext()
|
|
457
371
|
: "";
|
|
458
372
|
const trends = settings.isEnabled("signalTrends") ? loadSignalTrends() : "";
|
|
459
|
-
const failures =
|
|
460
|
-
|
|
461
|
-
const
|
|
462
|
-
|
|
373
|
+
const failures =
|
|
374
|
+
settings.isEnabled("failurePatterns") && !skipSemiStatic ? loadFailurePatterns() : "";
|
|
375
|
+
const synthesis =
|
|
376
|
+
settings.isEnabled("synthesis") && !skipSemiStatic
|
|
377
|
+
? loadSynthesisRecommendations()
|
|
378
|
+
: "";
|
|
379
|
+
const opinions =
|
|
380
|
+
!skipSemiStatic && settings.isEnabled("opinions") ? loadOpinionContext() : "";
|
|
381
|
+
const selfModel =
|
|
382
|
+
!skipSemiStatic && settings.isEnabled("selfModel") ? loadSelfModel() : "";
|
|
463
383
|
const intelligence = settings.isEnabled("sessionIntelligence")
|
|
464
384
|
? loadSessionIntelligence()
|
|
465
385
|
: "";
|
|
@@ -478,8 +398,6 @@ export function buildSystemReminder(): string {
|
|
|
478
398
|
if (synthesis) parts.push(synthesis);
|
|
479
399
|
if (trends) parts.push(trends);
|
|
480
400
|
if (failures) parts.push(failures);
|
|
481
|
-
if (work) parts.push(work.text);
|
|
482
|
-
|
|
483
401
|
if (parts.length === 0) return "";
|
|
484
402
|
|
|
485
403
|
const now = new Date();
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Notes live at memory/relationship/YYYY-MM/YYYY-MM-DD.md
|
|
6
6
|
* W = world (facts about user's situation)
|
|
7
7
|
* O = opinion (preference with confidence)
|
|
8
|
-
*
|
|
8
|
+
* Session = what the AI did this session (first-person)
|
|
9
9
|
*
|
|
10
10
|
* Extraction is handled by the relationship handler via Haiku inference.
|
|
11
11
|
* This lib provides storage and reading utilities only.
|
|
@@ -15,7 +15,7 @@ import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
|
15
15
|
import { resolve } from "node:path";
|
|
16
16
|
import { ensureDir, paths } from "./paths";
|
|
17
17
|
|
|
18
|
-
export type NoteType = "W" | "O" | "
|
|
18
|
+
export type NoteType = "W" | "O" | "Session";
|
|
19
19
|
|
|
20
20
|
export interface RelationshipNote {
|
|
21
21
|
type: NoteType;
|
|
@@ -31,7 +31,7 @@ function dailyFilePath(date: Date): string {
|
|
|
31
31
|
return resolve(monthDir, `${yyyy}-${mm}-${dd}.md`);
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
/**
|
|
34
|
+
/** @deprecated No longer called — relationship notes are written in ALGORITHM LEARN phase */
|
|
35
35
|
export function hasSessionNotes(sessionId: string): boolean {
|
|
36
36
|
const filepath = dailyFilePath(new Date());
|
|
37
37
|
if (!existsSync(filepath)) return false;
|
|
@@ -63,6 +63,8 @@ export const PROTECTED_PATHS: RegExp[] = [
|
|
|
63
63
|
/^\/System\//,
|
|
64
64
|
/\.ssh\/(?!config)/,
|
|
65
65
|
/\.gnupg\//,
|
|
66
|
+
// Claude Code auto-memory — PAL owns memory; writes here indicate wrong system is being used
|
|
67
|
+
/\.claude\/projects\/[^/]+\/memory\//,
|
|
66
68
|
// Derived from HOOK_MANAGED_FILES — scoped to managed roots only
|
|
67
69
|
...HOOK_MANAGED_FILES.map(
|
|
68
70
|
(name) =>
|