portable-agent-layer 0.36.0 → 0.38.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 -0
- 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 -20
- 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/tools/update-telos.ts +0 -1
- package/assets/templates/PAL/ALGORITHM.md +27 -3
- package/assets/templates/hooks.codex.json +44 -0
- package/assets/templates/hooks.cursor.json +11 -5
- package/package.json +5 -2
- package/src/cli/index.ts +113 -17
- package/src/cli/migrate.ts +299 -0
- package/src/cli/setup-identity.ts +3 -3
- package/src/cli/setup-telos.ts +0 -1
- package/src/hooks/CompactRecover.ts +11 -5
- package/src/hooks/LoadContext.ts +14 -2
- 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/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 +9 -8
- 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/context.ts +45 -117
- package/src/hooks/lib/entities.ts +7 -7
- package/src/hooks/lib/frontmatter.ts +4 -4
- package/src/hooks/lib/graduation.ts +8 -7
- 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 +3 -15
- package/src/hooks/lib/retrieval-index.ts +5 -3
- package/src/hooks/lib/retrieval.ts +11 -12
- package/src/hooks/lib/security.ts +22 -18
- package/src/hooks/lib/semi-static.ts +4 -2
- package/src/hooks/lib/session-names.ts +1 -1
- package/src/hooks/lib/settings.ts +1 -1
- package/src/hooks/lib/setup.ts +2 -60
- package/src/hooks/lib/signals.ts +2 -2
- package/src/hooks/lib/stdin.ts +1 -1
- package/src/hooks/lib/stop.ts +13 -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 +13 -18
- package/src/targets/codex/install.ts +95 -0
- package/src/targets/codex/uninstall.ts +70 -0
- package/src/targets/lib.ts +140 -14
- package/src/targets/opencode/plugin.ts +22 -11
- package/src/tools/agent/algorithm-reflect.ts +1 -1
- package/src/tools/agent/analyze.ts +18 -18
- package/src/tools/agent/handoff-note.ts +1 -1
- package/src/tools/agent/project.ts +375 -75
- 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 +13 -11
- package/src/tools/self-model.ts +20 -16
- 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/src/hooks/lib/agent.ts
CHANGED
|
@@ -1,40 +1,42 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Agent detection and output format adapters.
|
|
3
3
|
*
|
|
4
|
-
* Cursor and Claude Code use different JSON contracts for hook I/O.
|
|
4
|
+
* Cursor, Codex, and Claude Code use different JSON contracts for hook I/O.
|
|
5
5
|
* These helpers normalize the differences so hook handlers stay clean.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
type AgentType = "claude" | "cursor" | "codex";
|
|
9
9
|
|
|
10
10
|
/** Detect which agent is running via environment variables */
|
|
11
|
-
|
|
11
|
+
function detectAgent(): AgentType {
|
|
12
|
+
// PAL_AGENT is set explicitly in hook command prefixes — most reliable signal.
|
|
13
|
+
// IDE env vars (CURSOR_VERSION, CODEX_CLI_VERSION) are NOT reliably forwarded to
|
|
14
|
+
// hook subprocesses, so PAL_AGENT is the primary detection mechanism.
|
|
15
|
+
if (process.env.PAL_AGENT === "cursor") return "cursor";
|
|
16
|
+
if (process.env.PAL_AGENT === "codex") return "codex";
|
|
17
|
+
// Fallbacks for environments that do forward IDE env vars
|
|
12
18
|
if (process.env.CURSOR_VERSION) return "cursor";
|
|
19
|
+
if (process.env.CODEX_CLI_VERSION ?? process.env.OPENAI_CODEX) return "codex";
|
|
13
20
|
return "claude";
|
|
14
21
|
}
|
|
15
22
|
|
|
16
23
|
export const isCursor = () => detectAgent() === "cursor";
|
|
24
|
+
export const isCodex = () => detectAgent() === "codex";
|
|
17
25
|
|
|
18
26
|
/**
|
|
19
27
|
* Format a "block this action" response for the current agent.
|
|
20
|
-
* Claude Code:
|
|
21
|
-
* Cursor:
|
|
28
|
+
* Claude Code: { decision: "block", reason }
|
|
29
|
+
* Cursor preToolUse: { permission: "deny", user_message }
|
|
30
|
+
* Codex PreToolUse: { hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "deny" } }
|
|
22
31
|
*/
|
|
23
|
-
export function blockResponse(reason: string): string {
|
|
32
|
+
export function blockResponse(reason: string, hookEventName?: string): string {
|
|
24
33
|
if (isCursor()) {
|
|
25
34
|
return JSON.stringify({ permission: "deny", user_message: reason });
|
|
26
35
|
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
* Format sessionStart context injection for the current agent.
|
|
32
|
-
* Claude Code: raw text to stdout
|
|
33
|
-
* Cursor: { additional_context: "..." }
|
|
34
|
-
*/
|
|
35
|
-
export function sessionStartOutput(context: string): string {
|
|
36
|
-
if (isCursor()) {
|
|
37
|
-
return JSON.stringify({ additional_context: context });
|
|
36
|
+
if (isCodex() && hookEventName === "PreToolUse") {
|
|
37
|
+
return JSON.stringify({
|
|
38
|
+
hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "deny" },
|
|
39
|
+
});
|
|
38
40
|
}
|
|
39
|
-
return
|
|
41
|
+
return JSON.stringify({ decision: "block", reason });
|
|
40
42
|
}
|
package/src/hooks/lib/context.ts
CHANGED
|
@@ -12,12 +12,10 @@ import { paths } from "./paths";
|
|
|
12
12
|
import { loadActiveProjectsContext } from "./projects";
|
|
13
13
|
import { loadRecentNotes } from "./relationship";
|
|
14
14
|
import { loadFailurePatterns, loadSynthesisRecommendations } from "./semi-static";
|
|
15
|
-
import { readSessionNames } from "./session-names";
|
|
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,89 +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 the N most recent session names (fallback for greeting) */
|
|
57
|
-
export function loadRecentSessions(count: number): string[] {
|
|
58
|
-
try {
|
|
59
|
-
const sessions = readSessions();
|
|
60
|
-
if (sessions.length > 0) {
|
|
61
|
-
return sessions
|
|
62
|
-
.slice(-count)
|
|
63
|
-
.reverse()
|
|
64
|
-
.map((s) => s.name);
|
|
65
|
-
}
|
|
66
|
-
// Fallback to session-names.json for backwards compat
|
|
67
|
-
const names = readSessionNames();
|
|
68
|
-
const entries = Object.values(names);
|
|
69
|
-
return entries.slice(-count).reverse();
|
|
70
|
-
} catch {
|
|
71
|
-
return [];
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/** Read cached counts from counts.json, falling back to live counting */
|
|
76
|
-
function loadCachedCounts(): {
|
|
77
|
-
signals: number;
|
|
78
|
-
telos: number;
|
|
79
|
-
skills: number;
|
|
80
|
-
sessions: number;
|
|
81
|
-
} {
|
|
82
|
-
try {
|
|
83
|
-
const countsPath = resolve(paths.state(), "counts.json");
|
|
84
|
-
if (existsSync(countsPath)) {
|
|
85
|
-
return JSON.parse(readFileSync(countsPath, "utf-8"));
|
|
86
|
-
}
|
|
87
|
-
} catch {
|
|
88
|
-
/* fall through */
|
|
89
|
-
}
|
|
90
|
-
// Fallback: count live (first session before any stop has run)
|
|
91
|
-
return {
|
|
92
|
-
signals: countSignals("ratings.jsonl"),
|
|
93
|
-
telos: 0,
|
|
94
|
-
skills: 0,
|
|
95
|
-
sessions: 0,
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/** Build the visible greeting lines for stderr */
|
|
100
|
-
export function buildGreeting(): string[] {
|
|
101
|
-
const counts = loadCachedCounts();
|
|
102
|
-
const setupState = readSetupState();
|
|
103
|
-
const setupIncomplete = setupState && !isSetupComplete(setupState);
|
|
104
|
-
|
|
105
|
-
const greeting: string[] = [];
|
|
106
|
-
|
|
107
|
-
if (setupIncomplete) {
|
|
108
|
-
const done = STEP_ORDER.length - remainingSteps(setupState).length;
|
|
109
|
-
greeting.push(
|
|
110
|
-
`🔧 PAL setup ${done}/${STEP_ORDER.length} | ${counts.signals} signals`
|
|
111
|
-
);
|
|
112
|
-
} else {
|
|
113
|
-
greeting.push(
|
|
114
|
-
`✅ PAL ready | ${counts.telos} TELOS | ${counts.skills} skills | ${counts.signals} signals | ${counts.sessions} sessions`
|
|
115
|
-
);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// Show recent session names for quick context
|
|
119
|
-
const recent = loadRecentSessions(3);
|
|
120
|
-
if (recent.length > 0) {
|
|
121
|
-
greeting.push(`📂 Recent: ${recent.join(" | ")}`);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
return greeting;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
42
|
/** Load high-confidence wisdom principles for injection into system-reminder */
|
|
128
43
|
export function loadWisdomContext(): string {
|
|
129
44
|
try {
|
|
@@ -159,7 +74,7 @@ export function loadLearningDigest(): string {
|
|
|
159
74
|
}
|
|
160
75
|
|
|
161
76
|
/** Load self-model for session context injection */
|
|
162
|
-
|
|
77
|
+
function loadSelfModel(): string {
|
|
163
78
|
try {
|
|
164
79
|
const p = resolve(paths.memory(), "self-model", "current.md");
|
|
165
80
|
if (!existsSync(p)) return "";
|
|
@@ -181,7 +96,7 @@ export function loadSignalTrends(): string {
|
|
|
181
96
|
}
|
|
182
97
|
|
|
183
98
|
/** Load per-project session history for the current working directory */
|
|
184
|
-
|
|
99
|
+
function loadProjectHistoryContext(): string {
|
|
185
100
|
try {
|
|
186
101
|
const cwd = process.cwd();
|
|
187
102
|
const entries = readProjectHistory(cwd, 3);
|
|
@@ -199,16 +114,46 @@ export function loadProjectHistoryContext(): string {
|
|
|
199
114
|
}
|
|
200
115
|
}
|
|
201
116
|
|
|
202
|
-
/**
|
|
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 */
|
|
203
152
|
export function loadRelationshipContext(): string {
|
|
204
153
|
try {
|
|
205
154
|
const notes = loadRecentNotes(2);
|
|
206
155
|
if (!notes) return "";
|
|
207
|
-
|
|
208
|
-
const filtered = notes
|
|
209
|
-
.split("\n")
|
|
210
|
-
.filter((l) => !/^\s*- O\(/.test(l) && !/^\s*<!--/.test(l))
|
|
211
|
-
.join("\n");
|
|
156
|
+
const filtered = filterRelationshipNotes(notes, process.cwd());
|
|
212
157
|
return capSection(`## Recent Interaction Notes\n${filtered}`, 1500);
|
|
213
158
|
} catch {
|
|
214
159
|
return "";
|
|
@@ -216,7 +161,7 @@ export function loadRelationshipContext(): string {
|
|
|
216
161
|
}
|
|
217
162
|
|
|
218
163
|
/** Load session intelligence from compact synthesis state */
|
|
219
|
-
|
|
164
|
+
function loadSessionIntelligence(): string {
|
|
220
165
|
try {
|
|
221
166
|
const p = resolve(paths.state(), "synthesis.json");
|
|
222
167
|
if (!existsSync(p)) return "";
|
|
@@ -224,30 +169,13 @@ export function loadSessionIntelligence(): string {
|
|
|
224
169
|
|
|
225
170
|
const lines: string[] = ["## Session Intelligence"];
|
|
226
171
|
|
|
227
|
-
// Open Threads — project-specific only
|
|
228
|
-
if (state.threads?.length > 0) {
|
|
229
|
-
const cwd = process.cwd();
|
|
230
|
-
const here = state.threads.filter((t: { cwd?: string }) => t.cwd === cwd);
|
|
231
|
-
|
|
232
|
-
if (here.length > 0) {
|
|
233
|
-
lines.push("");
|
|
234
|
-
lines.push(`**Open threads — this project (${here.length}):**`);
|
|
235
|
-
for (const t of here) {
|
|
236
|
-
lines.push(`- ${t.title} (opened ${t.opened})`);
|
|
237
|
-
if (t.context) lines.push(` ${t.context}`);
|
|
238
|
-
}
|
|
239
|
-
lines.push(
|
|
240
|
-
"→ Continue this work or explicitly close it before starting something new."
|
|
241
|
-
);
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
|
|
245
172
|
// Rating Trend
|
|
246
173
|
if (state.ratings?.count > 0) {
|
|
247
174
|
const r = state.ratings;
|
|
248
|
-
|
|
175
|
+
const lowNote = r.lowCount > 0 ? ` ${r.lowCount} low ratings.` : "";
|
|
249
176
|
lines.push(
|
|
250
|
-
|
|
177
|
+
"",
|
|
178
|
+
`**Rating trend:** ${r.avg}/10 avg (last 10: ${r.recentAvg}/10, ${r.trend}).${lowNote}`
|
|
251
179
|
);
|
|
252
180
|
if (r.trend === "declining") {
|
|
253
181
|
lines.push(
|
|
@@ -265,8 +193,8 @@ export function loadSessionIntelligence(): string {
|
|
|
265
193
|
// Algorithm Performance
|
|
266
194
|
if (state.algorithm?.reflectionCount > 0) {
|
|
267
195
|
const a = state.algorithm;
|
|
268
|
-
lines.push("");
|
|
269
196
|
lines.push(
|
|
197
|
+
"",
|
|
270
198
|
`**Algorithm:** ${a.reflectionCount} reflections, ${a.passRate}% criteria pass rate, ${a.avgSentiment}/10 sentiment.`
|
|
271
199
|
);
|
|
272
200
|
if (a.passRate < 80) {
|
|
@@ -295,7 +223,7 @@ export function loadSessionIntelligence(): string {
|
|
|
295
223
|
}
|
|
296
224
|
|
|
297
225
|
/** Load handoff state for the current project */
|
|
298
|
-
|
|
226
|
+
function loadHandoff(): string {
|
|
299
227
|
try {
|
|
300
228
|
const p = resolve(paths.state(), "last-handoff.json");
|
|
301
229
|
if (!existsSync(p)) return "";
|
|
@@ -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[] };
|
|
@@ -125,7 +126,7 @@ function toAnalysisEntries(
|
|
|
125
126
|
|
|
126
127
|
function isActionable(text: string): boolean {
|
|
127
128
|
const trimmed = text.trim();
|
|
128
|
-
if (
|
|
129
|
+
if (/\?\s*$/.test(trimmed)) return false;
|
|
129
130
|
if (extractKeywords(trimmed).size < 4) return false;
|
|
130
131
|
return true;
|
|
131
132
|
}
|
|
@@ -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> {
|
|
@@ -18,13 +18,13 @@ import { similarity } from "./text-similarity";
|
|
|
18
18
|
export type EvidenceType = "supporting" | "counter" | "confirmation" | "contradiction";
|
|
19
19
|
export type OpinionCategory = "communication" | "technical" | "workflow" | "general";
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
interface Evidence {
|
|
22
22
|
date: string;
|
|
23
23
|
type: EvidenceType;
|
|
24
24
|
source: string;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
interface Opinion {
|
|
28
28
|
id: string;
|
|
29
29
|
statement: string;
|
|
30
30
|
confidence: number;
|
|
@@ -145,7 +145,7 @@ export function createOpinion(statement: string, source: string): Opinion {
|
|
|
145
145
|
}
|
|
146
146
|
|
|
147
147
|
/** Check if an opinion already has evidence with this exact source text. */
|
|
148
|
-
|
|
148
|
+
function hasEvidence(opinion: Opinion, source: string): boolean {
|
|
149
149
|
return opinion.evidence.some((e) => e.source === source);
|
|
150
150
|
}
|
|
151
151
|
|
package/src/hooks/lib/paths.ts
CHANGED
|
@@ -54,6 +54,7 @@ export const paths = {
|
|
|
54
54
|
projectHistory: () => ensureDir(home("memory", "projects")),
|
|
55
55
|
sessionLearning: () => ensureDir(home("memory", "learning", "session")),
|
|
56
56
|
synthesis: () => ensureDir(home("memory", "learning", "synthesis")),
|
|
57
|
+
work: () => ensureDir(home("memory", "work")),
|
|
57
58
|
backups: () => ensureDir(home("backups")),
|
|
58
59
|
} as const;
|
|
59
60
|
|
|
@@ -78,6 +79,7 @@ export const assets = {
|
|
|
78
79
|
claudeSettingsTemplate: () => pkg("assets", "templates", "settings.claude.json"),
|
|
79
80
|
cursorHooksTemplate: () => pkg("assets", "templates", "hooks.cursor.json"),
|
|
80
81
|
copilotHooksTemplate: () => pkg("assets", "templates", "hooks.copilot.json"),
|
|
82
|
+
codexHooksTemplate: () => pkg("assets", "templates", "hooks.codex.json"),
|
|
81
83
|
agentTools: () => pkg("src", "tools", "agent"),
|
|
82
84
|
palDocs: () => pkg("assets", "templates", "PAL"),
|
|
83
85
|
} as const;
|