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.
Files changed (89) hide show
  1. package/README.md +1 -0
  2. package/assets/skills/analyze-pdf/tools/pdf-download.ts +1 -1
  3. package/assets/skills/analyze-youtube/tools/youtube-analyze.ts +1 -1
  4. package/assets/skills/consulting-report/tools/dev.ts +2 -2
  5. package/assets/skills/consulting-report/tools/generate-pdf.ts +9 -9
  6. package/assets/skills/consulting-report/tools/scaffold.ts +2 -2
  7. package/assets/skills/create-pdf/tools/md-to-html-pdf.ts +2 -2
  8. package/assets/skills/opinion/tools/opinion.ts +3 -2
  9. package/assets/skills/presentation/SKILL.md +1 -1
  10. package/assets/skills/presentation/tools/doctor.ts +2 -5
  11. package/assets/skills/presentation/tools/lib/inline.ts +6 -11
  12. package/assets/skills/presentation/tools/lib/lint-helpers.ts +2 -2
  13. package/assets/skills/presentation/tools/lib/lint-rules.ts +5 -2
  14. package/assets/skills/presentation/tools/setup-template.ts +10 -7
  15. package/assets/skills/projects/SKILL.md +44 -20
  16. package/assets/skills/research/tools/gemini-search.ts +2 -2
  17. package/assets/skills/research/tools/grok-search.ts +2 -2
  18. package/assets/skills/research/tools/perplexity-search.ts +2 -2
  19. package/assets/skills/telos/tools/update-telos.ts +0 -1
  20. package/assets/templates/PAL/ALGORITHM.md +27 -3
  21. package/assets/templates/hooks.codex.json +44 -0
  22. package/assets/templates/hooks.cursor.json +11 -5
  23. package/package.json +5 -2
  24. package/src/cli/index.ts +113 -17
  25. package/src/cli/migrate.ts +299 -0
  26. package/src/cli/setup-identity.ts +3 -3
  27. package/src/cli/setup-telos.ts +0 -1
  28. package/src/hooks/CompactRecover.ts +11 -5
  29. package/src/hooks/LoadContext.ts +14 -2
  30. package/src/hooks/PreCompactPersist.ts +26 -34
  31. package/src/hooks/SecurityValidator.ts +43 -21
  32. package/src/hooks/StopOrchestrator.ts +4 -1
  33. package/src/hooks/UserPromptOrchestrator.ts +4 -2
  34. package/src/hooks/handlers/auto-graduate.ts +2 -2
  35. package/src/hooks/handlers/backup.ts +3 -3
  36. package/src/hooks/handlers/failure.ts +5 -3
  37. package/src/hooks/handlers/inject-retrieval.ts +29 -6
  38. package/src/hooks/handlers/persist-last-exchange.ts +76 -0
  39. package/src/hooks/handlers/rating.ts +2 -1
  40. package/src/hooks/handlers/readme-sync.ts +3 -2
  41. package/src/hooks/handlers/session-intelligence.ts +9 -8
  42. package/src/hooks/handlers/session-name.ts +2 -2
  43. package/src/hooks/handlers/synthesis.ts +5 -2
  44. package/src/hooks/handlers/update-counts.ts +3 -2
  45. package/src/hooks/lib/agent.ts +20 -18
  46. package/src/hooks/lib/context.ts +45 -117
  47. package/src/hooks/lib/entities.ts +7 -7
  48. package/src/hooks/lib/frontmatter.ts +4 -4
  49. package/src/hooks/lib/graduation.ts +8 -7
  50. package/src/hooks/lib/inference.ts +6 -2
  51. package/src/hooks/lib/learning-category.ts +1 -1
  52. package/src/hooks/lib/learning-store.ts +6 -1
  53. package/src/hooks/lib/notify.ts +2 -2
  54. package/src/hooks/lib/opinions.ts +3 -3
  55. package/src/hooks/lib/paths.ts +2 -0
  56. package/src/hooks/lib/projects.ts +142 -74
  57. package/src/hooks/lib/readme-sync.ts +1 -1
  58. package/src/hooks/lib/relationship.ts +3 -15
  59. package/src/hooks/lib/retrieval-index.ts +5 -3
  60. package/src/hooks/lib/retrieval.ts +11 -12
  61. package/src/hooks/lib/security.ts +22 -18
  62. package/src/hooks/lib/semi-static.ts +4 -2
  63. package/src/hooks/lib/session-names.ts +1 -1
  64. package/src/hooks/lib/settings.ts +1 -1
  65. package/src/hooks/lib/setup.ts +2 -60
  66. package/src/hooks/lib/signals.ts +2 -2
  67. package/src/hooks/lib/stdin.ts +1 -1
  68. package/src/hooks/lib/stop.ts +13 -6
  69. package/src/hooks/lib/token-usage.ts +1 -2
  70. package/src/hooks/lib/transcript.ts +1 -1
  71. package/src/hooks/lib/wisdom.ts +5 -5
  72. package/src/hooks/lib/work-tracking.ts +13 -18
  73. package/src/targets/codex/install.ts +95 -0
  74. package/src/targets/codex/uninstall.ts +70 -0
  75. package/src/targets/lib.ts +140 -14
  76. package/src/targets/opencode/plugin.ts +22 -11
  77. package/src/tools/agent/algorithm-reflect.ts +1 -1
  78. package/src/tools/agent/analyze.ts +18 -18
  79. package/src/tools/agent/handoff-note.ts +1 -1
  80. package/src/tools/agent/project.ts +375 -75
  81. package/src/tools/agent/synthesize.ts +6 -42
  82. package/src/tools/agent/thread.ts +15 -14
  83. package/src/tools/agent/wisdom-frame.ts +9 -3
  84. package/src/tools/import.ts +1 -1
  85. package/src/tools/relationship-reflect.ts +13 -11
  86. package/src/tools/self-model.ts +20 -16
  87. package/src/tools/session-summary.ts +3 -3
  88. package/src/tools/token-cost.ts +15 -16
  89. package/assets/skills/telos/tools/update-projects.ts +0 -106
@@ -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
- export type AgentType = "claude" | "cursor";
8
+ type AgentType = "claude" | "cursor" | "codex";
9
9
 
10
10
  /** Detect which agent is running via environment variables */
11
- export function detectAgent(): AgentType {
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: { decision: "block", reason }
21
- * Cursor: { permission: "deny", user_message }
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
- return JSON.stringify({ decision: "block", reason });
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 context;
41
+ return JSON.stringify({ decision: "block", reason });
40
42
  }
@@ -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, readSessions } from "./work-tracking";
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
- export function loadSelfModel(): string {
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
- export function loadProjectHistoryContext(): string {
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
- /** Load recent relationship notes (today + yesterday) */
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
- // 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");
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
- export function loadSessionIntelligence(): string {
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
- lines.push("");
175
+ const lowNote = r.lowCount > 0 ? ` ${r.lowCount} low ratings.` : "";
249
176
  lines.push(
250
- `**Rating trend:** ${r.avg}/10 avg (last 10: ${r.recentAvg}/10, ${r.trend}).${r.lowCount > 0 ? ` ${r.lowCount} low ratings.` : ""}`
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
- export function loadHandoff(): string {
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
- export interface PersonEntity {
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
- export interface CompanyEntity {
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
- export interface LinkEntity {
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
- export interface SourceEntity {
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
- export interface EntityIndex {
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
- if (!index.links) index.links = {};
104
- if (!index.sources) index.sources = {};
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
- export interface Parsed<T = Record<string, string>> {
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 = line.match(/^(\w[\w-]*)\s*:\s*(.*)$/);
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).replace(/\\"/g, '"');
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.replace(/"/g, '\\"')}"`);
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
- export interface AnalysisEntry {
28
+ interface AnalysisEntry {
28
29
  source: string;
29
30
  path: string;
30
31
  text: string;
31
32
  date: string;
32
33
  }
33
34
 
34
- export interface PatternGroup {
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
- export interface RatingsSummary {
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 (/\?[\s]*$/.test(trimmed)) return false;
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 (!process.env.PAL_ANTHROPIC_API_KEY) {
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 = principle.match(/^[^.!?]+[.!?]?/);
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
- export interface AnalyzeOptions {
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 interface InferenceOptions {
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
- export interface InferenceResult {
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
- export type LearningCategory = "system" | "algorithm";
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 = body.match(/## Insights\n([\s\S]*?)(?=\n##|$)/);
134
+ const insightsMatch = new RegExp(/## Insights\n([\s\S]*?)(?=\n##|$)/).exec(
135
+ body
136
+ );
132
137
 
133
138
  entries.push({
134
139
  filename: file,
@@ -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.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
23
+ return s.replaceAll("\\", "\\\\").replaceAll('"', '\\"');
24
24
  }
25
25
 
26
26
  function escapePowerShellSingle(s: string): string {
27
- return s.replace(/'/g, "''");
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
- export interface Evidence {
21
+ interface Evidence {
22
22
  date: string;
23
23
  type: EvidenceType;
24
24
  source: string;
25
25
  }
26
26
 
27
- export interface Opinion {
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
- export function hasEvidence(opinion: Opinion, source: string): boolean {
148
+ function hasEvidence(opinion: Opinion, source: string): boolean {
149
149
  return opinion.evidence.some((e) => e.source === source);
150
150
  }
151
151
 
@@ -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;