portable-agent-layer 0.35.0 → 0.37.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 +2 -1
- package/assets/skills/analyze-pdf/tools/pdf-download.ts +1 -1
- package/assets/skills/analyze-youtube/tools/youtube-analyze.ts +1 -1
- package/assets/skills/consulting-report/tools/dev.ts +2 -2
- package/assets/skills/consulting-report/tools/generate-pdf.ts +9 -9
- package/assets/skills/consulting-report/tools/scaffold.ts +2 -2
- package/assets/skills/create-pdf/tools/md-to-html-pdf.ts +2 -2
- package/assets/skills/opinion/tools/opinion.ts +3 -2
- package/assets/skills/presentation/SKILL.md +1 -1
- package/assets/skills/presentation/tools/doctor.ts +2 -5
- package/assets/skills/presentation/tools/lib/inline.ts +6 -11
- package/assets/skills/presentation/tools/lib/lint-helpers.ts +2 -2
- package/assets/skills/presentation/tools/lib/lint-rules.ts +5 -2
- package/assets/skills/presentation/tools/setup-template.ts +10 -7
- package/assets/skills/projects/SKILL.md +44 -21
- package/assets/skills/research/tools/gemini-search.ts +2 -2
- package/assets/skills/research/tools/grok-search.ts +2 -2
- package/assets/skills/research/tools/perplexity-search.ts +2 -2
- package/assets/skills/telos/SKILL.md +7 -52
- package/assets/skills/telos/tools/update-telos.ts +0 -1
- package/assets/templates/PAL/ALGORITHM.md +54 -5
- 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/hooks.codex.json +44 -0
- package/assets/templates/hooks.cursor.json +11 -5
- package/assets/templates/pal-settings.json +1 -3
- package/assets/templates/settings.claude.json +2 -1
- package/package.json +2 -1
- package/src/cli/index.ts +112 -14
- package/src/cli/migrate.ts +299 -0
- package/src/cli/setup-identity.ts +3 -3
- package/src/cli/setup-telos.ts +12 -80
- package/src/hooks/CompactRecover.ts +11 -5
- package/src/hooks/LoadContext.ts +35 -11
- package/src/hooks/PreCompactPersist.ts +26 -34
- package/src/hooks/SecurityValidator.ts +43 -21
- package/src/hooks/StopOrchestrator.ts +4 -1
- package/src/hooks/UserPromptOrchestrator.ts +4 -2
- package/src/hooks/handlers/auto-graduate.ts +2 -2
- package/src/hooks/handlers/backup.ts +3 -3
- package/src/hooks/handlers/context-digests.ts +74 -0
- package/src/hooks/handlers/failure.ts +5 -3
- package/src/hooks/handlers/inject-retrieval.ts +29 -6
- package/src/hooks/handlers/persist-last-exchange.ts +76 -0
- package/src/hooks/handlers/rating.ts +2 -1
- package/src/hooks/handlers/readme-sync.ts +3 -2
- package/src/hooks/handlers/session-intelligence.ts +17 -93
- package/src/hooks/handlers/session-name.ts +2 -2
- package/src/hooks/handlers/synthesis.ts +5 -2
- package/src/hooks/handlers/update-counts.ts +3 -2
- package/src/hooks/lib/agent.ts +20 -18
- package/src/hooks/lib/claude-md.ts +69 -14
- package/src/hooks/lib/context.ts +92 -246
- package/src/hooks/lib/entities.ts +7 -7
- package/src/hooks/lib/frontmatter.ts +4 -4
- package/src/hooks/lib/graduation.ts +7 -6
- package/src/hooks/lib/inference.ts +6 -2
- package/src/hooks/lib/learning-category.ts +1 -1
- package/src/hooks/lib/learning-store.ts +6 -1
- package/src/hooks/lib/notify.ts +2 -2
- package/src/hooks/lib/opinions.ts +3 -3
- package/src/hooks/lib/paths.ts +2 -0
- package/src/hooks/lib/projects.ts +142 -74
- package/src/hooks/lib/readme-sync.ts +1 -1
- package/src/hooks/lib/relationship.ts +4 -16
- package/src/hooks/lib/retrieval-index.ts +5 -3
- package/src/hooks/lib/retrieval.ts +11 -12
- package/src/hooks/lib/security.ts +24 -18
- package/src/hooks/lib/semi-static.ts +188 -0
- package/src/hooks/lib/session-names.ts +1 -1
- package/src/hooks/lib/settings.ts +1 -1
- package/src/hooks/lib/setup.ts +2 -65
- package/src/hooks/lib/signals.ts +2 -2
- package/src/hooks/lib/stdin.ts +1 -1
- package/src/hooks/lib/stop.ts +16 -6
- package/src/hooks/lib/token-usage.ts +1 -2
- package/src/hooks/lib/transcript.ts +1 -1
- package/src/hooks/lib/wisdom.ts +5 -5
- package/src/hooks/lib/work-tracking.ts +8 -14
- package/src/targets/claude/uninstall.ts +1 -1
- package/src/targets/codex/install.ts +95 -0
- package/src/targets/codex/uninstall.ts +70 -0
- 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 +166 -14
- package/src/targets/opencode/install.ts +29 -1
- package/src/targets/opencode/plugin.ts +23 -12
- package/src/targets/opencode/uninstall.ts +30 -3
- package/src/tools/agent/algorithm-reflect.ts +1 -1
- package/src/tools/agent/analyze.ts +18 -18
- package/src/tools/agent/handoff-note.ts +116 -0
- package/src/tools/agent/project.ts +375 -75
- package/src/tools/agent/relationship-note.ts +51 -0
- package/src/tools/agent/synthesize.ts +6 -42
- package/src/tools/agent/thread.ts +15 -14
- package/src/tools/agent/wisdom-frame.ts +9 -3
- package/src/tools/import.ts +1 -1
- package/src/tools/relationship-reflect.ts +15 -13
- package/src/tools/self-model.ts +23 -19
- package/src/tools/session-summary.ts +3 -3
- package/src/tools/token-cost.ts +15 -16
- package/assets/skills/telos/tools/update-projects.ts +0 -106
- package/assets/templates/telos/PROJECTS.md +0 -7
package/src/hooks/lib/context.ts
CHANGED
|
@@ -3,21 +3,19 @@
|
|
|
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";
|
|
15
|
-
import {
|
|
14
|
+
import { loadFailurePatterns, loadSynthesisRecommendations } from "./semi-static";
|
|
16
15
|
import * as settings from "./settings";
|
|
17
|
-
import { isSetupComplete, readSetupState, remainingSteps, STEP_ORDER } from "./setup";
|
|
18
16
|
import { computeSignalTrends, formatTrends } from "./signal-trends";
|
|
19
17
|
import { readFramePrinciples } from "./wisdom";
|
|
20
|
-
import { readProjectHistory
|
|
18
|
+
import { readProjectHistory } from "./work-tracking";
|
|
21
19
|
|
|
22
20
|
/** Load and concatenate loadAtStartup files */
|
|
23
21
|
function loadStartupFiles(): string {
|
|
@@ -41,135 +39,6 @@ function loadStartupFiles(): string {
|
|
|
41
39
|
return sections.join("\n\n---\n\n");
|
|
42
40
|
}
|
|
43
41
|
|
|
44
|
-
/** Count lines in a signals JSONL file */
|
|
45
|
-
export function countSignals(filename: string): number {
|
|
46
|
-
const filepath = resolve(paths.signals(), filename);
|
|
47
|
-
if (!existsSync(filepath)) return 0;
|
|
48
|
-
try {
|
|
49
|
-
const content = readFileSync(filepath, "utf-8").trim();
|
|
50
|
-
return content ? content.split("\n").length : 0;
|
|
51
|
-
} catch {
|
|
52
|
-
return 0;
|
|
53
|
-
}
|
|
54
|
-
}
|
|
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
|
-
/** Load the N most recent session names (fallback for greeting) */
|
|
98
|
-
export function loadRecentSessions(count: number): string[] {
|
|
99
|
-
try {
|
|
100
|
-
const sessions = readSessions();
|
|
101
|
-
if (sessions.length > 0) {
|
|
102
|
-
return sessions
|
|
103
|
-
.slice(-count)
|
|
104
|
-
.reverse()
|
|
105
|
-
.map((s) => s.name);
|
|
106
|
-
}
|
|
107
|
-
// Fallback to session-names.json for backwards compat
|
|
108
|
-
const names = readSessionNames();
|
|
109
|
-
const entries = Object.values(names);
|
|
110
|
-
return entries.slice(-count).reverse();
|
|
111
|
-
} catch {
|
|
112
|
-
return [];
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/** Read cached counts from counts.json, falling back to live counting */
|
|
117
|
-
function loadCachedCounts(): {
|
|
118
|
-
signals: number;
|
|
119
|
-
telos: number;
|
|
120
|
-
skills: number;
|
|
121
|
-
sessions: number;
|
|
122
|
-
} {
|
|
123
|
-
try {
|
|
124
|
-
const countsPath = resolve(paths.state(), "counts.json");
|
|
125
|
-
if (existsSync(countsPath)) {
|
|
126
|
-
return JSON.parse(readFileSync(countsPath, "utf-8"));
|
|
127
|
-
}
|
|
128
|
-
} catch {
|
|
129
|
-
/* fall through */
|
|
130
|
-
}
|
|
131
|
-
// Fallback: count live (first session before any stop has run)
|
|
132
|
-
return {
|
|
133
|
-
signals: countSignals("ratings.jsonl"),
|
|
134
|
-
telos: 0,
|
|
135
|
-
skills: 0,
|
|
136
|
-
sessions: 0,
|
|
137
|
-
};
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/** Build the visible greeting lines for stderr */
|
|
141
|
-
export function buildGreeting(): string[] {
|
|
142
|
-
const counts = loadCachedCounts();
|
|
143
|
-
const work = loadActiveWork();
|
|
144
|
-
const setupState = readSetupState();
|
|
145
|
-
const setupIncomplete = setupState && !isSetupComplete(setupState);
|
|
146
|
-
|
|
147
|
-
const greeting: string[] = [];
|
|
148
|
-
|
|
149
|
-
if (setupIncomplete) {
|
|
150
|
-
const done = STEP_ORDER.length - remainingSteps(setupState).length;
|
|
151
|
-
greeting.push(
|
|
152
|
-
`🔧 PAL setup ${done}/${STEP_ORDER.length} | ${counts.signals} signals`
|
|
153
|
-
);
|
|
154
|
-
} else {
|
|
155
|
-
greeting.push(
|
|
156
|
-
`✅ PAL ready | ${counts.telos} TELOS | ${counts.skills} skills | ${counts.signals} signals | ${counts.sessions} sessions`
|
|
157
|
-
);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
if (work?.summary) {
|
|
161
|
-
greeting.push(`📋 Previous: ${work.summary}`);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
// Show recent session names for quick context
|
|
165
|
-
const recent = loadRecentSessions(3);
|
|
166
|
-
if (recent.length > 0) {
|
|
167
|
-
greeting.push(`📂 Recent: ${recent.join(" | ")}`);
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
return greeting;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
42
|
/** Load high-confidence wisdom principles for injection into system-reminder */
|
|
174
43
|
export function loadWisdomContext(): string {
|
|
175
44
|
try {
|
|
@@ -205,7 +74,7 @@ export function loadLearningDigest(): string {
|
|
|
205
74
|
}
|
|
206
75
|
|
|
207
76
|
/** Load self-model for session context injection */
|
|
208
|
-
|
|
77
|
+
function loadSelfModel(): string {
|
|
209
78
|
try {
|
|
210
79
|
const p = resolve(paths.memory(), "self-model", "current.md");
|
|
211
80
|
if (!existsSync(p)) return "";
|
|
@@ -217,78 +86,6 @@ export function loadSelfModel(): string {
|
|
|
217
86
|
}
|
|
218
87
|
}
|
|
219
88
|
|
|
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
89
|
/** Load signal trends as a formatted string */
|
|
293
90
|
export function loadSignalTrends(): string {
|
|
294
91
|
try {
|
|
@@ -299,7 +96,7 @@ export function loadSignalTrends(): string {
|
|
|
299
96
|
}
|
|
300
97
|
|
|
301
98
|
/** Load per-project session history for the current working directory */
|
|
302
|
-
|
|
99
|
+
function loadProjectHistoryContext(): string {
|
|
303
100
|
try {
|
|
304
101
|
const cwd = process.cwd();
|
|
305
102
|
const entries = readProjectHistory(cwd, 3);
|
|
@@ -317,19 +114,54 @@ export function loadProjectHistoryContext(): string {
|
|
|
317
114
|
}
|
|
318
115
|
}
|
|
319
116
|
|
|
320
|
-
/**
|
|
117
|
+
/**
|
|
118
|
+
* Filter raw relationship note lines:
|
|
119
|
+
* - O entries: stripped (loaded natively via digest)
|
|
120
|
+
* - HTML comments: stripped, but cwd is extracted from session comments
|
|
121
|
+
* - Session entries: kept only if block cwd matches current project (or legacy with no cwd)
|
|
122
|
+
* - W entries and structural lines: always kept
|
|
123
|
+
*/
|
|
124
|
+
function filterRelationshipNotes(notes: string, cwd: string): string {
|
|
125
|
+
const lines = notes.split("\n");
|
|
126
|
+
const out: string[] = [];
|
|
127
|
+
let blockCwd: string | null = null;
|
|
128
|
+
|
|
129
|
+
for (const line of lines) {
|
|
130
|
+
if (/^## \d{2}:\d{2}/.test(line)) {
|
|
131
|
+
blockCwd = null;
|
|
132
|
+
out.push(line);
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
const cwdMatch = new RegExp(/<!--.*cwd:(\S+)/).exec(line);
|
|
136
|
+
if (cwdMatch) {
|
|
137
|
+
blockCwd = cwdMatch[1];
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
if (/^\s*<!--/.test(line)) continue;
|
|
141
|
+
if (/^\s*- O\(/.test(line)) continue;
|
|
142
|
+
if (/^\s*- Session:/.test(line)) {
|
|
143
|
+
if (blockCwd === null || blockCwd === cwd) out.push(line);
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
out.push(line);
|
|
147
|
+
}
|
|
148
|
+
return out.join("\n");
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Load recent relationship notes (today + yesterday), scoped to current project */
|
|
321
152
|
export function loadRelationshipContext(): string {
|
|
322
153
|
try {
|
|
323
154
|
const notes = loadRecentNotes(2);
|
|
324
155
|
if (!notes) return "";
|
|
325
|
-
|
|
156
|
+
const filtered = filterRelationshipNotes(notes, process.cwd());
|
|
157
|
+
return capSection(`## Recent Interaction Notes\n${filtered}`, 1500);
|
|
326
158
|
} catch {
|
|
327
159
|
return "";
|
|
328
160
|
}
|
|
329
161
|
}
|
|
330
162
|
|
|
331
163
|
/** Load session intelligence from compact synthesis state */
|
|
332
|
-
|
|
164
|
+
function loadSessionIntelligence(): string {
|
|
333
165
|
try {
|
|
334
166
|
const p = resolve(paths.state(), "synthesis.json");
|
|
335
167
|
if (!existsSync(p)) return "";
|
|
@@ -337,32 +169,13 @@ export function loadSessionIntelligence(): string {
|
|
|
337
169
|
|
|
338
170
|
const lines: string[] = ["## Session Intelligence"];
|
|
339
171
|
|
|
340
|
-
// Open Threads — project-specific first, then global
|
|
341
|
-
if (state.threads?.length > 0) {
|
|
342
|
-
const cwd = process.cwd();
|
|
343
|
-
const here = state.threads.filter((t: { cwd?: string }) => t.cwd === cwd);
|
|
344
|
-
const other = state.threads.filter((t: { cwd?: string }) => t.cwd !== cwd);
|
|
345
|
-
|
|
346
|
-
if (here.length > 0) {
|
|
347
|
-
lines.push("");
|
|
348
|
-
lines.push(`**Open threads — this project (${here.length}):**`);
|
|
349
|
-
for (const t of here) {
|
|
350
|
-
lines.push(`- ${t.title} (opened ${t.opened})`);
|
|
351
|
-
if (t.context) lines.push(` ${t.context}`);
|
|
352
|
-
}
|
|
353
|
-
lines.push("→ These are directly relevant to your current work.");
|
|
354
|
-
}
|
|
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
|
-
}
|
|
359
|
-
|
|
360
172
|
// Rating Trend
|
|
361
173
|
if (state.ratings?.count > 0) {
|
|
362
174
|
const r = state.ratings;
|
|
363
|
-
|
|
175
|
+
const lowNote = r.lowCount > 0 ? ` ${r.lowCount} low ratings.` : "";
|
|
364
176
|
lines.push(
|
|
365
|
-
|
|
177
|
+
"",
|
|
178
|
+
`**Rating trend:** ${r.avg}/10 avg (last 10: ${r.recentAvg}/10, ${r.trend}).${lowNote}`
|
|
366
179
|
);
|
|
367
180
|
if (r.trend === "declining") {
|
|
368
181
|
lines.push(
|
|
@@ -380,8 +193,8 @@ export function loadSessionIntelligence(): string {
|
|
|
380
193
|
// Algorithm Performance
|
|
381
194
|
if (state.algorithm?.reflectionCount > 0) {
|
|
382
195
|
const a = state.algorithm;
|
|
383
|
-
lines.push("");
|
|
384
196
|
lines.push(
|
|
197
|
+
"",
|
|
385
198
|
`**Algorithm:** ${a.reflectionCount} reflections, ${a.passRate}% criteria pass rate, ${a.avgSentiment}/10 sentiment.`
|
|
386
199
|
);
|
|
387
200
|
if (a.passRate < 80) {
|
|
@@ -403,14 +216,14 @@ export function loadSessionIntelligence(): string {
|
|
|
403
216
|
}
|
|
404
217
|
}
|
|
405
218
|
|
|
406
|
-
return lines.length > 1 ? lines.join("\n") : "";
|
|
219
|
+
return lines.length > 1 ? capSection(lines.join("\n"), 2000) : "";
|
|
407
220
|
} catch {
|
|
408
221
|
return "";
|
|
409
222
|
}
|
|
410
223
|
}
|
|
411
224
|
|
|
412
225
|
/** Load handoff state for the current project */
|
|
413
|
-
|
|
226
|
+
function loadHandoff(): string {
|
|
414
227
|
try {
|
|
415
228
|
const p = resolve(paths.state(), "last-handoff.json");
|
|
416
229
|
if (!existsSync(p)) return "";
|
|
@@ -434,17 +247,46 @@ export function loadHandoff(): string {
|
|
|
434
247
|
}
|
|
435
248
|
}
|
|
436
249
|
|
|
250
|
+
/** Truncate text to maxChars at the last complete line boundary */
|
|
251
|
+
function capSection(text: string, maxChars: number): string {
|
|
252
|
+
if (text.length <= maxChars) return text;
|
|
253
|
+
const lines = text.split("\n");
|
|
254
|
+
const kept: string[] = [];
|
|
255
|
+
let total = 0;
|
|
256
|
+
for (const line of lines) {
|
|
257
|
+
const next = total + line.length + 1;
|
|
258
|
+
if (next > maxChars) break;
|
|
259
|
+
kept.push(line);
|
|
260
|
+
total = next;
|
|
261
|
+
}
|
|
262
|
+
return kept.join("\n");
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** Agent targets — determines which context sections are skipped due to native loading. */
|
|
266
|
+
export type AgentTarget = "claude" | "opencode" | "cursor" | "copilot";
|
|
267
|
+
|
|
437
268
|
/**
|
|
438
269
|
* Build the <system-reminder> content for the AI.
|
|
439
270
|
*
|
|
440
271
|
* Static context (TELOS, setup prompt) lives in AGENTS.md / CLAUDE.md and is
|
|
441
272
|
* loaded natively by Claude Code / opencode. This injects dynamic context only —
|
|
442
273
|
* things that change per-session and can't live in a static file.
|
|
274
|
+
*
|
|
275
|
+
* opts.agent — agent target; Claude Code skips semi-static sections (self-model,
|
|
276
|
+
* wisdom, opinions) that load natively via @imports in CLAUDE.md.
|
|
443
277
|
*/
|
|
444
|
-
export function buildSystemReminder(): string {
|
|
278
|
+
export function buildSystemReminder(opts: { agent?: AgentTarget } = {}): string {
|
|
279
|
+
// Semi-static sections loaded natively via @imports (Claude Code) or
|
|
280
|
+
// instructions[] (opencode). Skip them from hook output for those agents.
|
|
281
|
+
const skipSemiStatic =
|
|
282
|
+
opts.agent === "claude" ||
|
|
283
|
+
opts.agent === "opencode" ||
|
|
284
|
+
opts.agent === "cursor" ||
|
|
285
|
+
opts.agent === "copilot";
|
|
286
|
+
|
|
445
287
|
const startup = loadStartupFiles();
|
|
446
|
-
const
|
|
447
|
-
|
|
288
|
+
const wisdom =
|
|
289
|
+
!skipSemiStatic && settings.isEnabled("wisdom") ? loadWisdomContext() : "";
|
|
448
290
|
const relationship = settings.isEnabled("relationship")
|
|
449
291
|
? loadRelationshipContext()
|
|
450
292
|
: "";
|
|
@@ -456,10 +298,16 @@ export function buildSystemReminder(): string {
|
|
|
456
298
|
? loadActiveProjectsContext()
|
|
457
299
|
: "";
|
|
458
300
|
const trends = settings.isEnabled("signalTrends") ? loadSignalTrends() : "";
|
|
459
|
-
const failures =
|
|
460
|
-
|
|
461
|
-
const
|
|
462
|
-
|
|
301
|
+
const failures =
|
|
302
|
+
settings.isEnabled("failurePatterns") && !skipSemiStatic ? loadFailurePatterns() : "";
|
|
303
|
+
const synthesis =
|
|
304
|
+
settings.isEnabled("synthesis") && !skipSemiStatic
|
|
305
|
+
? loadSynthesisRecommendations()
|
|
306
|
+
: "";
|
|
307
|
+
const opinions =
|
|
308
|
+
!skipSemiStatic && settings.isEnabled("opinions") ? loadOpinionContext() : "";
|
|
309
|
+
const selfModel =
|
|
310
|
+
!skipSemiStatic && settings.isEnabled("selfModel") ? loadSelfModel() : "";
|
|
463
311
|
const intelligence = settings.isEnabled("sessionIntelligence")
|
|
464
312
|
? loadSessionIntelligence()
|
|
465
313
|
: "";
|
|
@@ -478,8 +326,6 @@ export function buildSystemReminder(): string {
|
|
|
478
326
|
if (synthesis) parts.push(synthesis);
|
|
479
327
|
if (trends) parts.push(trends);
|
|
480
328
|
if (failures) parts.push(failures);
|
|
481
|
-
if (work) parts.push(work.text);
|
|
482
|
-
|
|
483
329
|
if (parts.length === 0) return "";
|
|
484
330
|
|
|
485
331
|
const now = new Date();
|
|
@@ -12,7 +12,7 @@ import { ensureDir, paths } from "./paths";
|
|
|
12
12
|
|
|
13
13
|
// --- Types ---
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
interface PersonEntity {
|
|
16
16
|
id: string;
|
|
17
17
|
name: string;
|
|
18
18
|
first_seen: string;
|
|
@@ -20,7 +20,7 @@ export interface PersonEntity {
|
|
|
20
20
|
source_ids: string[];
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
interface CompanyEntity {
|
|
24
24
|
id: string;
|
|
25
25
|
name: string;
|
|
26
26
|
domain: string | null;
|
|
@@ -29,7 +29,7 @@ export interface CompanyEntity {
|
|
|
29
29
|
source_ids: string[];
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
interface LinkEntity {
|
|
33
33
|
id: string;
|
|
34
34
|
url: string;
|
|
35
35
|
first_seen: string;
|
|
@@ -37,7 +37,7 @@ export interface LinkEntity {
|
|
|
37
37
|
source_ids: string[];
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
|
|
40
|
+
interface SourceEntity {
|
|
41
41
|
id: string;
|
|
42
42
|
url: string | null;
|
|
43
43
|
author: string | null;
|
|
@@ -47,7 +47,7 @@ export interface SourceEntity {
|
|
|
47
47
|
source_ids: string[];
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
|
|
50
|
+
interface EntityIndex {
|
|
51
51
|
version: string;
|
|
52
52
|
last_updated: string;
|
|
53
53
|
people: Record<string, PersonEntity>;
|
|
@@ -100,8 +100,8 @@ function emptyIndex(): EntityIndex {
|
|
|
100
100
|
|
|
101
101
|
/** Migrate older indexes that lack links/sources. */
|
|
102
102
|
function ensureShape(index: EntityIndex): EntityIndex {
|
|
103
|
-
|
|
104
|
-
|
|
103
|
+
index.links ??= {};
|
|
104
|
+
index.sources ??= {};
|
|
105
105
|
return index;
|
|
106
106
|
}
|
|
107
107
|
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Supports strings, numbers, booleans, and inline JSON arrays.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
interface Parsed<T = Record<string, string>> {
|
|
9
9
|
meta: T;
|
|
10
10
|
body: string;
|
|
11
11
|
}
|
|
@@ -30,7 +30,7 @@ export function parse<T = Record<string, string>>(content: string): Parsed<T> {
|
|
|
30
30
|
|
|
31
31
|
const meta: Record<string, unknown> = {};
|
|
32
32
|
for (const line of rawMeta.split("\n")) {
|
|
33
|
-
const match =
|
|
33
|
+
const match = new RegExp(/^(\w[\w-]*)\s*:\s*(.*)$/).exec(line);
|
|
34
34
|
if (!match) continue;
|
|
35
35
|
const [, key, rawValue] = match;
|
|
36
36
|
const value = rawValue.trim();
|
|
@@ -50,7 +50,7 @@ export function parse<T = Record<string, string>>(content: string): Parsed<T> {
|
|
|
50
50
|
(value.startsWith('"') && value.endsWith('"')) ||
|
|
51
51
|
(value.startsWith("'") && value.endsWith("'"))
|
|
52
52
|
) {
|
|
53
|
-
meta[key] = value.slice(1, -1).
|
|
53
|
+
meta[key] = value.slice(1, -1).replaceAll('\\"', '"');
|
|
54
54
|
continue;
|
|
55
55
|
}
|
|
56
56
|
|
|
@@ -77,7 +77,7 @@ export function stringify(meta: Record<string, unknown>, body: string): string {
|
|
|
77
77
|
if (Array.isArray(value)) {
|
|
78
78
|
lines.push(`${key}: ${JSON.stringify(value)}`);
|
|
79
79
|
} else if (typeof value === "string") {
|
|
80
|
-
lines.push(`${key}: "${value.
|
|
80
|
+
lines.push(`${key}: "${value.replaceAll('"', '\\"')}"`);
|
|
81
81
|
} else {
|
|
82
82
|
lines.push(`${key}: ${String(value)}`);
|
|
83
83
|
}
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
14
14
|
import { resolve } from "node:path";
|
|
15
|
+
import { hasApiKey } from "./inference";
|
|
15
16
|
import {
|
|
16
17
|
type FailureEntry,
|
|
17
18
|
type LearningEntry,
|
|
@@ -24,14 +25,14 @@ import { extractKeywords, similarity } from "./text-similarity";
|
|
|
24
25
|
|
|
25
26
|
// ── Types ──
|
|
26
27
|
|
|
27
|
-
|
|
28
|
+
interface AnalysisEntry {
|
|
28
29
|
source: string;
|
|
29
30
|
path: string;
|
|
30
31
|
text: string;
|
|
31
32
|
date: string;
|
|
32
33
|
}
|
|
33
34
|
|
|
34
|
-
|
|
35
|
+
interface PatternGroup {
|
|
35
36
|
pattern: string;
|
|
36
37
|
entries: AnalysisEntry[];
|
|
37
38
|
domain: string;
|
|
@@ -51,7 +52,7 @@ interface GraduationState {
|
|
|
51
52
|
graduated: GraduatedEntry[];
|
|
52
53
|
}
|
|
53
54
|
|
|
54
|
-
|
|
55
|
+
interface RatingsSummary {
|
|
55
56
|
total: number;
|
|
56
57
|
average: number;
|
|
57
58
|
low: { count: number; examples: string[] };
|
|
@@ -214,7 +215,7 @@ async function generateRecommendations(
|
|
|
214
215
|
ratings: RatingsSummary | null
|
|
215
216
|
): Promise<string[]> {
|
|
216
217
|
if (candidates.length === 0 && !ratings) return [];
|
|
217
|
-
if (!
|
|
218
|
+
if (!hasApiKey()) {
|
|
218
219
|
return candidates
|
|
219
220
|
.slice(0, 3)
|
|
220
221
|
.map(
|
|
@@ -298,7 +299,7 @@ function writeState(state: GraduationState): void {
|
|
|
298
299
|
function synthesizePrinciple(group: PatternGroup): string {
|
|
299
300
|
const sorted = [...group.entries].sort((a, b) => a.text.length - b.text.length);
|
|
300
301
|
let principle = sorted[0].text;
|
|
301
|
-
const firstSentence =
|
|
302
|
+
const firstSentence = new RegExp(/^[^.!?]+[.!?]?/).exec(principle);
|
|
302
303
|
if (firstSentence) principle = firstSentence[0];
|
|
303
304
|
if (principle.length > 120) principle = `${principle.slice(0, 117)}...`;
|
|
304
305
|
return principle.trim();
|
|
@@ -306,7 +307,7 @@ function synthesizePrinciple(group: PatternGroup): string {
|
|
|
306
307
|
|
|
307
308
|
// ── Main Analysis ──
|
|
308
309
|
|
|
309
|
-
|
|
310
|
+
interface AnalyzeOptions {
|
|
310
311
|
/** Generate actionable recommendations via inference. Default: false (patterns only). */
|
|
311
312
|
actionable?: boolean;
|
|
312
313
|
}
|
|
@@ -4,7 +4,11 @@
|
|
|
4
4
|
|
|
5
5
|
import { HAIKU_MODEL } from "./models";
|
|
6
6
|
|
|
7
|
-
export
|
|
7
|
+
export function hasApiKey(): boolean {
|
|
8
|
+
return !!process.env.PAL_ANTHROPIC_API_KEY;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface InferenceOptions {
|
|
8
12
|
system?: string;
|
|
9
13
|
user: string;
|
|
10
14
|
model?: string;
|
|
@@ -14,7 +18,7 @@ export interface InferenceOptions {
|
|
|
14
18
|
jsonSchema?: Record<string, unknown>;
|
|
15
19
|
}
|
|
16
20
|
|
|
17
|
-
|
|
21
|
+
interface InferenceResult {
|
|
18
22
|
success: boolean;
|
|
19
23
|
output?: string;
|
|
20
24
|
usage?: { inputTokens: number; outputTokens: number };
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Used by both learning.ts and work-learning.ts handlers.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
type LearningCategory = "system" | "algorithm";
|
|
7
7
|
|
|
8
8
|
const SYSTEM_KEYWORDS =
|
|
9
9
|
/\b(config|setting|install|deploy|build|lint|format|biome|typescript|tsc|hook|plugin|ci|cd|pipeline|docker|package|dependency|migration|schema|database|env|permission|security|git|commit|branch|merge)\b/i;
|
|
@@ -19,6 +19,7 @@ export interface FailureEntry {
|
|
|
19
19
|
principle: string;
|
|
20
20
|
date: string;
|
|
21
21
|
ts: string;
|
|
22
|
+
cwd: string;
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
export interface LearningEntry {
|
|
@@ -76,6 +77,7 @@ export function readFailures(baseDir: string, limit?: number): FailureEntry[] {
|
|
|
76
77
|
date?: string;
|
|
77
78
|
ts?: string;
|
|
78
79
|
slug?: string;
|
|
80
|
+
cwd?: string;
|
|
79
81
|
}>(content);
|
|
80
82
|
|
|
81
83
|
if (!meta.context) continue;
|
|
@@ -88,6 +90,7 @@ export function readFailures(baseDir: string, limit?: number): FailureEntry[] {
|
|
|
88
90
|
principle: meta.principle || "",
|
|
89
91
|
date: meta.date || (meta.ts ? String(meta.ts).slice(0, 10) : ""),
|
|
90
92
|
ts: meta.ts ? String(meta.ts) : "",
|
|
93
|
+
cwd: meta.cwd || "",
|
|
91
94
|
});
|
|
92
95
|
|
|
93
96
|
if (limit && entries.length >= limit) return entries;
|
|
@@ -128,7 +131,9 @@ export function readLearnings(baseDir: string, limit?: number): LearningEntry[]
|
|
|
128
131
|
|
|
129
132
|
if (!meta.title) continue;
|
|
130
133
|
|
|
131
|
-
const insightsMatch =
|
|
134
|
+
const insightsMatch = new RegExp(/## Insights\n([\s\S]*?)(?=\n##|$)/).exec(
|
|
135
|
+
body
|
|
136
|
+
);
|
|
132
137
|
|
|
133
138
|
entries.push({
|
|
134
139
|
filename: file,
|
package/src/hooks/lib/notify.ts
CHANGED
|
@@ -20,11 +20,11 @@ function spawnSilent(cmd: string, args: string[]): Promise<void> {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
function escapeAppleScript(s: string): string {
|
|
23
|
-
return s.
|
|
23
|
+
return s.replaceAll("\\", "\\\\").replaceAll('"', '\\"');
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
function escapePowerShellSingle(s: string): string {
|
|
27
|
-
return s.
|
|
27
|
+
return s.replaceAll("'", "''");
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
export async function notify(title: string, body: string): Promise<void> {
|