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.
Files changed (108) hide show
  1. package/README.md +2 -1
  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 -21
  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/SKILL.md +7 -52
  20. package/assets/skills/telos/tools/update-telos.ts +0 -1
  21. package/assets/templates/PAL/ALGORITHM.md +54 -5
  22. package/assets/templates/PAL/PROJECT_LIFECYCLE.md +48 -0
  23. package/assets/templates/PAL/README.md +1 -1
  24. package/assets/templates/PAL/STEERING_RULES.md +4 -0
  25. package/assets/templates/PAL/SYSTEM_ARCHITECTURE.md +32 -17
  26. package/assets/templates/PAL/WORK_TRACKING.md +1 -1
  27. package/assets/templates/hooks.codex.json +44 -0
  28. package/assets/templates/hooks.cursor.json +11 -5
  29. package/assets/templates/pal-settings.json +1 -3
  30. package/assets/templates/settings.claude.json +2 -1
  31. package/package.json +2 -1
  32. package/src/cli/index.ts +112 -14
  33. package/src/cli/migrate.ts +299 -0
  34. package/src/cli/setup-identity.ts +3 -3
  35. package/src/cli/setup-telos.ts +12 -80
  36. package/src/hooks/CompactRecover.ts +11 -5
  37. package/src/hooks/LoadContext.ts +35 -11
  38. package/src/hooks/PreCompactPersist.ts +26 -34
  39. package/src/hooks/SecurityValidator.ts +43 -21
  40. package/src/hooks/StopOrchestrator.ts +4 -1
  41. package/src/hooks/UserPromptOrchestrator.ts +4 -2
  42. package/src/hooks/handlers/auto-graduate.ts +2 -2
  43. package/src/hooks/handlers/backup.ts +3 -3
  44. package/src/hooks/handlers/context-digests.ts +74 -0
  45. package/src/hooks/handlers/failure.ts +5 -3
  46. package/src/hooks/handlers/inject-retrieval.ts +29 -6
  47. package/src/hooks/handlers/persist-last-exchange.ts +76 -0
  48. package/src/hooks/handlers/rating.ts +2 -1
  49. package/src/hooks/handlers/readme-sync.ts +3 -2
  50. package/src/hooks/handlers/session-intelligence.ts +17 -93
  51. package/src/hooks/handlers/session-name.ts +2 -2
  52. package/src/hooks/handlers/synthesis.ts +5 -2
  53. package/src/hooks/handlers/update-counts.ts +3 -2
  54. package/src/hooks/lib/agent.ts +20 -18
  55. package/src/hooks/lib/claude-md.ts +69 -14
  56. package/src/hooks/lib/context.ts +92 -246
  57. package/src/hooks/lib/entities.ts +7 -7
  58. package/src/hooks/lib/frontmatter.ts +4 -4
  59. package/src/hooks/lib/graduation.ts +7 -6
  60. package/src/hooks/lib/inference.ts +6 -2
  61. package/src/hooks/lib/learning-category.ts +1 -1
  62. package/src/hooks/lib/learning-store.ts +6 -1
  63. package/src/hooks/lib/notify.ts +2 -2
  64. package/src/hooks/lib/opinions.ts +3 -3
  65. package/src/hooks/lib/paths.ts +2 -0
  66. package/src/hooks/lib/projects.ts +142 -74
  67. package/src/hooks/lib/readme-sync.ts +1 -1
  68. package/src/hooks/lib/relationship.ts +4 -16
  69. package/src/hooks/lib/retrieval-index.ts +5 -3
  70. package/src/hooks/lib/retrieval.ts +11 -12
  71. package/src/hooks/lib/security.ts +24 -18
  72. package/src/hooks/lib/semi-static.ts +188 -0
  73. package/src/hooks/lib/session-names.ts +1 -1
  74. package/src/hooks/lib/settings.ts +1 -1
  75. package/src/hooks/lib/setup.ts +2 -65
  76. package/src/hooks/lib/signals.ts +2 -2
  77. package/src/hooks/lib/stdin.ts +1 -1
  78. package/src/hooks/lib/stop.ts +16 -6
  79. package/src/hooks/lib/token-usage.ts +1 -2
  80. package/src/hooks/lib/transcript.ts +1 -1
  81. package/src/hooks/lib/wisdom.ts +5 -5
  82. package/src/hooks/lib/work-tracking.ts +8 -14
  83. package/src/targets/claude/uninstall.ts +1 -1
  84. package/src/targets/codex/install.ts +95 -0
  85. package/src/targets/codex/uninstall.ts +70 -0
  86. package/src/targets/copilot/install.ts +39 -8
  87. package/src/targets/copilot/uninstall.ts +58 -17
  88. package/src/targets/cursor/install.ts +8 -0
  89. package/src/targets/cursor/uninstall.ts +18 -1
  90. package/src/targets/lib.ts +166 -14
  91. package/src/targets/opencode/install.ts +29 -1
  92. package/src/targets/opencode/plugin.ts +23 -12
  93. package/src/targets/opencode/uninstall.ts +30 -3
  94. package/src/tools/agent/algorithm-reflect.ts +1 -1
  95. package/src/tools/agent/analyze.ts +18 -18
  96. package/src/tools/agent/handoff-note.ts +116 -0
  97. package/src/tools/agent/project.ts +375 -75
  98. package/src/tools/agent/relationship-note.ts +51 -0
  99. package/src/tools/agent/synthesize.ts +6 -42
  100. package/src/tools/agent/thread.ts +15 -14
  101. package/src/tools/agent/wisdom-frame.ts +9 -3
  102. package/src/tools/import.ts +1 -1
  103. package/src/tools/relationship-reflect.ts +15 -13
  104. package/src/tools/self-model.ts +23 -19
  105. package/src/tools/session-summary.ts +3 -3
  106. package/src/tools/token-cost.ts +15 -16
  107. package/assets/skills/telos/tools/update-projects.ts +0 -106
  108. package/assets/templates/telos/PROJECTS.md +0 -7
@@ -0,0 +1,188 @@
1
+ /**
2
+ * Semi-static context source registry.
3
+ *
4
+ * One entry here = the only change needed to add a new source across all consumers:
5
+ * CLAUDE.md @imports, opencode instructions[], Cursor .mdc, Copilot .instructions.md,
6
+ * and the session-stop digest writer.
7
+ */
8
+
9
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
10
+ import { resolve } from "node:path";
11
+ import { parse } from "./frontmatter";
12
+ import { readFailures } from "./learning-store";
13
+ import { loadOpinionContext } from "./opinions";
14
+ import { palHome, paths } from "./paths";
15
+ import { readFramePrinciples } from "./wisdom";
16
+
17
+ /** A single semi-static context source — built at session stop, loaded natively at session start. */
18
+ interface SemiStaticSource {
19
+ /** Absolute path used in @imports (CLAUDE.md), instructions[] (opencode), and digest writes. */
20
+ readonly path: string;
21
+ /** When true, session-stop handler writes build result to path. */
22
+ readonly writesDigest: boolean;
23
+ /** Returns current content — builds fresh when writesDigest is true, reads the file otherwise. */
24
+ load(): string;
25
+ /** Slug for ~/.cursor/rules/pal-${slug}.mdc and ~/.copilot/instructions/pal-${slug}.instructions.md */
26
+ readonly slug: string;
27
+ /** Human-readable description for Cursor .mdc frontmatter. */
28
+ readonly description: string;
29
+ }
30
+
31
+ /** Returns the Cursor rules filename for a source. */
32
+ export function cursorFilename(src: SemiStaticSource): string {
33
+ return `pal-${src.slug}.mdc`;
34
+ }
35
+
36
+ /** Returns the Copilot instructions filename for a source. */
37
+ export function copilotFilename(src: SemiStaticSource): string {
38
+ return `pal-${src.slug}.instructions.md`;
39
+ }
40
+
41
+ function readFileSafe(path: string): string {
42
+ try {
43
+ if (!existsSync(path)) return "";
44
+ return readFileSync(path, "utf-8").trim();
45
+ } catch {
46
+ return "";
47
+ }
48
+ }
49
+
50
+ /** Build recommendations from the most recent synthesis report. */
51
+ export function loadSynthesisRecommendations(): string {
52
+ try {
53
+ const synthDir = paths.synthesis();
54
+ if (!existsSync(synthDir)) return "";
55
+
56
+ const months = readdirSync(synthDir).sort().reverse();
57
+ for (const month of months) {
58
+ const monthDir = resolve(synthDir, month);
59
+ try {
60
+ const files = readdirSync(monthDir)
61
+ .filter((f) => f.endsWith(".md"))
62
+ .sort()
63
+ .reverse();
64
+ if (files.length === 0) continue;
65
+
66
+ const content = readFileSync(resolve(monthDir, files[0]), "utf-8");
67
+
68
+ const recMatch = new RegExp(
69
+ /## Recommendations\n\n([\s\S]*?)(?:\n##|\n$|$)/
70
+ ).exec(content);
71
+ if (!recMatch?.[1]?.trim()) continue;
72
+
73
+ const recs = recMatch[1]
74
+ .trim()
75
+ .split("\n")
76
+ .filter((l) => l.trim())
77
+ .slice(0, 4);
78
+
79
+ if (recs.length === 0) continue;
80
+
81
+ const { meta } = parse<{ period?: string; average_rating?: string }>(content);
82
+ const period = meta.period ?? "";
83
+ const avgRating = meta.average_rating ? `${meta.average_rating}/10` : "";
84
+
85
+ const header = [
86
+ "## Pattern Synthesis",
87
+ period ? `*${period} — ${avgRating}*` : "",
88
+ ]
89
+ .filter(Boolean)
90
+ .join("\n");
91
+
92
+ return [header, ...recs].join("\n");
93
+ } catch {
94
+ /* try next month */
95
+ }
96
+ }
97
+ return "";
98
+ } catch {
99
+ return "";
100
+ }
101
+ }
102
+
103
+ /** Build the 5 most recent failure lessons as an avoid-list. */
104
+ export function loadFailurePatterns(): string {
105
+ try {
106
+ const entries = readFailures(paths.failures(), 5);
107
+ if (entries.length === 0) return "";
108
+
109
+ const lines = entries.map((e) => {
110
+ const label = e.rating ? `[${e.rating}/10]` : "";
111
+ const text = e.principle || e.context;
112
+ return `- ${label} ${text}`.trim();
113
+ });
114
+
115
+ return ["## Lessons from Recent Failures — Apply These Now", ...lines].join("\n");
116
+ } catch {
117
+ return "";
118
+ }
119
+ }
120
+
121
+ /**
122
+ * All semi-static context sources in load order.
123
+ * Adding one entry here is the only change needed to extend coverage to all consumers.
124
+ */
125
+ export function getSemiStaticSources(): SemiStaticSource[] {
126
+ const memory = paths.memory();
127
+ const home = palHome();
128
+ return [
129
+ {
130
+ path: resolve(memory, "self-model", "current.md"),
131
+ writesDigest: false,
132
+ load: () => readFileSafe(resolve(memory, "self-model", "current.md")),
133
+ slug: "self-model",
134
+ description: "PAL self-model",
135
+ },
136
+ {
137
+ path: resolve(memory, "wisdom", "context.md"),
138
+ writesDigest: true,
139
+ load: () => {
140
+ try {
141
+ const principles = readFramePrinciples();
142
+ if (principles.length === 0) return "";
143
+ return ["## Crystallized Principles", ...principles.map((p) => `- ${p}`)].join(
144
+ "\n"
145
+ );
146
+ } catch {
147
+ return "";
148
+ }
149
+ },
150
+ slug: "wisdom",
151
+ description: "PAL wisdom",
152
+ },
153
+ {
154
+ path: resolve(memory, "relationship", "opinions-context.md"),
155
+ writesDigest: true,
156
+ load: () => {
157
+ try {
158
+ return loadOpinionContext();
159
+ } catch {
160
+ return "";
161
+ }
162
+ },
163
+ slug: "opinions",
164
+ description: "PAL opinions",
165
+ },
166
+ {
167
+ path: resolve(memory, "learning", "synthesis-digest.md"),
168
+ writesDigest: true,
169
+ load: loadSynthesisRecommendations,
170
+ slug: "synthesis",
171
+ description: "PAL pattern synthesis",
172
+ },
173
+ {
174
+ path: resolve(memory, "learning", "failures-digest.md"),
175
+ writesDigest: true,
176
+ load: loadFailurePatterns,
177
+ slug: "failures",
178
+ description: "PAL recent failure lessons",
179
+ },
180
+ {
181
+ path: resolve(home, "docs", "STEERING_RULES.md"),
182
+ writesDigest: false,
183
+ load: () => readFileSafe(resolve(home, "docs", "STEERING_RULES.md")),
184
+ slug: "steering",
185
+ description: "PAL steering rules",
186
+ },
187
+ ];
188
+ }
@@ -6,7 +6,7 @@ import { existsSync, readFileSync, writeFileSync } from "node:fs";
6
6
  import { resolve } from "node:path";
7
7
  import { paths } from "./paths";
8
8
 
9
- export interface SessionNames {
9
+ interface SessionNames {
10
10
  [sessionId: string]: string;
11
11
  }
12
12
 
@@ -11,7 +11,7 @@ import { paths } from "./paths";
11
11
 
12
12
  // ── Types ──
13
13
 
14
- export interface Identity {
14
+ interface Identity {
15
15
  ai: { name: string; fullName: string; displayName: string; catchphrase: string };
16
16
  principal: { name: string; timezone: string };
17
17
  }
@@ -5,23 +5,15 @@
5
5
  * The AI is instructed to mark steps done after writing each file.
6
6
  */
7
7
 
8
- import { existsSync, readFileSync, writeFileSync } from "node:fs";
9
- import { resolve } from "node:path";
10
- import { ensureDir, palHome, paths } from "./paths";
8
+ import { existsSync, readFileSync } from "node:fs";
11
9
 
12
- export interface SetupStep {
10
+ interface SetupStep {
13
11
  done: boolean;
14
12
  file: string;
15
13
  question: string;
16
14
  hint: string;
17
15
  }
18
16
 
19
- export interface SetupState {
20
- version: number;
21
- completed: boolean;
22
- steps: Record<string, SetupStep>;
23
- }
24
-
25
17
  /** Ordered setup steps — defines the wizard flow */
26
18
  export const SETUP_STEPS: Record<string, Omit<SetupStep, "done">> = {
27
19
  mission: {
@@ -36,11 +28,6 @@ export const SETUP_STEPS: Record<string, Omit<SetupStep, "done">> = {
36
28
  "What are your current goals? (short-term, medium-term, long-term) (~/.pal/telos/GOALS.md)",
37
29
  hint: "e.g. Ship v2 by Q3, learn Rust, get promoted to staff engineer",
38
30
  },
39
- projects: {
40
- file: "telos/PROJECTS.md",
41
- question: "What projects are you currently working on? (~/.pal/telos/PROJECTS.md)",
42
- hint: "e.g. PAL (active, high priority), personal blog (paused), side SaaS (early stage)",
43
- },
44
31
  beliefs: {
45
32
  file: "telos/BELIEFS.md",
46
33
  question: "What principles or values guide your work? (~/.pal/telos/BELIEFS.md)",
@@ -55,10 +42,6 @@ export const SETUP_STEPS: Record<string, Omit<SetupStep, "done">> = {
55
42
 
56
43
  export const STEP_ORDER = Object.keys(SETUP_STEPS);
57
44
 
58
- function setupPath(): string {
59
- return resolve(ensureDir(paths.state()), "setup.json");
60
- }
61
-
62
45
  /** Check if a TELOS file has real content (not just template scaffolding) */
63
46
  export function hasRealContent(filePath: string): boolean {
64
47
  if (!existsSync(filePath)) return false;
@@ -75,49 +58,3 @@ export function hasRealContent(filePath: string): boolean {
75
58
  return false;
76
59
  }
77
60
  }
78
-
79
- /** Create initial setup state, auto-detecting already-populated TELOS files */
80
- export function createInitialState(): SetupState {
81
- const steps: Record<string, SetupStep> = {};
82
- for (const [key, def] of Object.entries(SETUP_STEPS)) {
83
- const populated = hasRealContent(resolve(palHome(), def.file));
84
- steps[key] = { done: populated, ...def };
85
- }
86
- const allDone = Object.values(steps).every((s) => s.done);
87
- return { version: 1, completed: allDone, steps };
88
- }
89
-
90
- /** Read setup state, or return null if no setup.json exists */
91
- export function readSetupState(): SetupState | null {
92
- const p = setupPath();
93
- if (!existsSync(p)) return null;
94
- try {
95
- return JSON.parse(readFileSync(p, "utf-8"));
96
- } catch {
97
- return null;
98
- }
99
- }
100
-
101
- /** Write setup state to disk */
102
- export function writeSetupState(state: SetupState): void {
103
- writeFileSync(setupPath(), `${JSON.stringify(state, null, 2)}\n`);
104
- }
105
-
106
- /** Seed setup.json if it doesn't exist yet. Returns the state. */
107
- export function ensureSetupState(): SetupState {
108
- const existing = readSetupState();
109
- if (existing) return existing;
110
- const fresh = createInitialState();
111
- writeSetupState(fresh);
112
- return fresh;
113
- }
114
-
115
- /** Get the list of remaining (not done) step keys, in order */
116
- export function remainingSteps(state: SetupState): string[] {
117
- return STEP_ORDER.filter((k) => !state.steps[k]?.done);
118
- }
119
-
120
- /** Check if setup is fully completed */
121
- export function isSetupComplete(state: SetupState): boolean {
122
- return state.completed;
123
- }
@@ -3,14 +3,14 @@ import { resolve } from "node:path";
3
3
  import { paths } from "./paths";
4
4
  import { now } from "./time";
5
5
 
6
- export interface Signal {
6
+ interface Signal {
7
7
  ts: string;
8
8
  type: string;
9
9
  [key: string]: unknown;
10
10
  }
11
11
 
12
12
  /** Append a signal to a JSONL file in the signals directory */
13
- export function emitSignal(
13
+ function emitSignal(
14
14
  filename: string,
15
15
  data: { type: string; [key: string]: unknown }
16
16
  ): void {
@@ -1,5 +1,5 @@
1
1
  /** Read all of stdin as a string */
2
- export async function readStdin(): Promise<string> {
2
+ async function readStdin(): Promise<string> {
3
3
  const chunks: Buffer[] = [];
4
4
  for await (const chunk of Bun.stdin.stream()) {
5
5
  chunks.push(Buffer.from(chunk));
@@ -3,12 +3,15 @@
3
3
  * Used by StopOrchestrator.ts (Claude Code) and opencode plugin.
4
4
  */
5
5
 
6
- import { existsSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
6
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
7
+ import { readFile, unlink } from "node:fs/promises";
7
8
  import { resolve } from "node:path";
8
9
  import { autoGraduate } from "../handlers/auto-graduate";
9
10
  import { autoBackup } from "../handlers/backup";
11
+ import { writeContextDigests } from "../handlers/context-digests";
10
12
  import { notifyDesktop } from "../handlers/desktop-notify";
11
13
  import { captureFailure } from "../handlers/failure";
14
+ import { persistLastExchange } from "../handlers/persist-last-exchange";
12
15
  import { projectTouch } from "../handlers/project-touch";
13
16
  import { checkReflectTrigger } from "../handlers/reflect-trigger";
14
17
  import { checkSelfModelTrigger } from "../handlers/self-model-trigger";
@@ -22,7 +25,7 @@ import { logDebug, logError } from "./log";
22
25
  import { ensureDir, paths } from "./paths";
23
26
  import { extractContent, extractLastAssistant, parseMessages } from "./transcript";
24
27
 
25
- export interface RunStopHandlersOptions {
28
+ interface RunStopHandlersOptions {
26
29
  lastAssistantMessage?: string;
27
30
  sessionId?: string;
28
31
  }
@@ -40,6 +43,9 @@ export async function runStopHandlers(
40
43
  // Cache last assistant response (session-scoped)
41
44
  cacheLastResponse(messages, options.lastAssistantMessage, options.sessionId);
42
45
 
46
+ // Always persist last exchange — drives CompactRecover + "Pick Up Where You Left Off"
47
+ if (options.sessionId) persistLastExchange(messages, options.sessionId);
48
+
43
49
  // Run all handlers concurrently. Auto-graduate is idempotent (24h TTL +
44
50
  // state-dedup + content-dedup) so it's safe to fire on every Stop.
45
51
  // project-touch only fires when cwd resolves to an active registered project.
@@ -56,6 +62,7 @@ export async function runStopHandlers(
56
62
  autoGraduate(),
57
63
  projectTouch(options.lastAssistantMessage),
58
64
  notifyDesktop(options.sessionId),
65
+ Promise.resolve(writeContextDigests()),
59
66
  ]);
60
67
 
61
68
  const handlerNames = [
@@ -71,6 +78,7 @@ export async function runStopHandlers(
71
78
  "auto-graduate",
72
79
  "project-touch",
73
80
  "desktop-notify",
81
+ "context-digests",
74
82
  ];
75
83
  for (let i = 0; i < results.length; i++) {
76
84
  const r = results[i];
@@ -148,15 +156,16 @@ async function checkPendingFailure(transcript: string): Promise<void> {
148
156
  if (!existsSync(pendingPath)) return;
149
157
 
150
158
  try {
151
- const pending = JSON.parse(readFileSync(pendingPath, "utf-8")) as {
159
+ const pending = JSON.parse(await readFile(pendingPath, "utf-8")) as {
152
160
  rating: number;
153
161
  context: string;
154
162
  detailedContext?: string;
155
163
  principle?: string;
156
164
  responsePreview?: string;
157
165
  userPreview?: string;
166
+ cwd?: string;
158
167
  };
159
- unlinkSync(pendingPath);
168
+ await unlink(pendingPath);
160
169
 
161
170
  // Extract principle from full transcript if not already present
162
171
  let { principle, detailedContext } = pending;
@@ -196,7 +205,7 @@ Return JSON:
196
205
  detailed_context?: string;
197
206
  };
198
207
  principle = parsed.principle || undefined;
199
- if (!detailedContext) detailedContext = parsed.detailed_context || undefined;
208
+ detailedContext ??= parsed.detailed_context || undefined;
200
209
  }
201
210
  } catch {
202
211
  /* graceful fallback — capture without principle */
@@ -208,7 +217,8 @@ Return JSON:
208
217
  pending.context,
209
218
  transcript,
210
219
  detailedContext,
211
- principle
220
+ principle,
221
+ pending.cwd
212
222
  );
213
223
  } catch {
214
224
  // Non-critical
@@ -8,10 +8,9 @@ import { resolve } from "node:path";
8
8
  import { HAIKU_MODEL } from "./models";
9
9
  import { ensureDir, paths } from "./paths";
10
10
 
11
- export type TokenCaller =
11
+ type TokenCaller =
12
12
  | "rating"
13
13
  | "failure"
14
- | "work-learning"
15
14
  | "session-name"
16
15
  | "session-intelligence"
17
16
  | "relationship"
@@ -5,7 +5,7 @@
5
5
 
6
6
  import { readFileSync } from "node:fs";
7
7
 
8
- export interface Message {
8
+ interface Message {
9
9
  role: string;
10
10
  content: string | unknown;
11
11
  }
@@ -63,13 +63,13 @@ function existingCrystalPrinciples(content: string): string[] {
63
63
  if (m[1]) out.push(m[1].trim());
64
64
  }
65
65
  for (const line of content.split("\n")) {
66
- const m = line.match(/^\s*-\s*(.+?)\s*\[CRYSTAL:\s*\d+%\]\s*$/);
66
+ const m = new RegExp(/^\s*-\s*(.+?)\s*\[CRYSTAL:\s*\d+%\]\s*$/).exec(line);
67
67
  if (m?.[1]) out.push(m[1].trim());
68
68
  }
69
69
  return out;
70
70
  }
71
71
 
72
- export interface PromoteCrystalResult {
72
+ interface PromoteCrystalResult {
73
73
  domain: string;
74
74
  principle: string;
75
75
  confidence: number;
@@ -123,7 +123,7 @@ export function promoteCrystal(
123
123
  return { domain, principle, confidence, framePath, skipped: null };
124
124
  }
125
125
 
126
- export interface FrameDoc {
126
+ interface FrameDoc {
127
127
  domain: string;
128
128
  principle: string;
129
129
  body: string;
@@ -153,7 +153,7 @@ export function readFramesForRetrieval(): FrameDoc[] {
153
153
  }
154
154
 
155
155
  for (const line of content.split("\n")) {
156
- const m = line.match(/^\s*-\s*(.+?)\s*\[CRYSTAL:\s*(\d+)%\]\s*$/);
156
+ const m = new RegExp(/^\s*-\s*(.+?)\s*\[CRYSTAL:\s*(\d+)%\]\s*$/).exec(line);
157
157
  if (!m) continue;
158
158
  const name = m[1]?.trim();
159
159
  const pct = parseInt(m[2] ?? "", 10);
@@ -188,7 +188,7 @@ export function readFramePrinciples(): string[] {
188
188
 
189
189
  // legacy fallback: bullet lines "- X [CRYSTAL: N%]"
190
190
  for (const line of content.split("\n")) {
191
- const match = line.match(/^\s*-\s*(.+?)\s*\[CRYSTAL:\s*(\d+)%\]\s*$/);
191
+ const match = new RegExp(/^\s*-\s*(.+?)\s*\[CRYSTAL:\s*(\d+)%\]\s*$/).exec(line);
192
192
  if (!match) continue;
193
193
  const name = match[1]?.trim();
194
194
  const pct = parseInt(match[2] ?? "", 10);
@@ -29,7 +29,7 @@ function sessionsPath(): string {
29
29
  return resolve(ensureDir(paths.state()), "sessions.json");
30
30
  }
31
31
 
32
- export function readSessions(): SessionRecord[] {
32
+ function readSessions(): SessionRecord[] {
33
33
  const p = sessionsPath();
34
34
  if (!existsSync(p)) return [];
35
35
  try {
@@ -54,12 +54,6 @@ export function writeSession(record: SessionRecord): void {
54
54
  writeFileSync(sessionsPath(), JSON.stringify(pruned, null, 2), "utf-8");
55
55
  }
56
56
 
57
- /** Filter sessions within the last N hours */
58
- export function recentSessions(hours: number): SessionRecord[] {
59
- const cutoff = Date.now() - hours * 60 * 60 * 1000;
60
- return readSessions().filter((s) => new Date(s.ts).getTime() > cutoff);
61
- }
62
-
63
57
  /** Detect session completion status from last assistant message */
64
58
  export function detectStatus(lastAssistant: string): SessionRecord["status"] {
65
59
  const completionSignals =
@@ -118,15 +112,15 @@ function cleanForHandoff(text: string): string {
118
112
  /** Extract handoff notes from last assistant message */
119
113
  export function extractHandoff(lastAssistant: string): string {
120
114
  // Look for explicit next-steps / TODO / remaining sections
121
- const sectionMatch = lastAssistant.match(
115
+ const sectionMatch = new RegExp(
122
116
  /(?:next steps?|todo|remaining|what's left|still need|want me to)[:\s]*\n([\s\S]{10,300}?)(?:\n\n|\n(?=[A-Z#]))/i
123
- );
117
+ ).exec(lastAssistant);
124
118
  if (sectionMatch) return cleanForHandoff(sectionMatch[1]);
125
119
 
126
120
  // Look for closing question/offer (common assistant pattern)
127
- const closingMatch = lastAssistant.match(
121
+ const closingMatch = new RegExp(
128
122
  /(?:want (?:me to|to)|shall I|should I|ready to|anything else|let me know)[^\n]*$/im
129
- );
123
+ ).exec(lastAssistant);
130
124
 
131
125
  const cleaned = cleanForHandoff(lastAssistant);
132
126
 
@@ -144,7 +138,7 @@ export function extractHandoff(lastAssistant: string): string {
144
138
 
145
139
  // ── Per-Project History ──────────────────────────────────────────
146
140
 
147
- export interface ProjectHistoryEntry {
141
+ interface ProjectHistoryEntry {
148
142
  date: string;
149
143
  title: string;
150
144
  summary: string;
@@ -152,8 +146,8 @@ export interface ProjectHistoryEntry {
152
146
  }
153
147
 
154
148
  /** Convert a cwd path to a filesystem-safe slug (last directory segment) */
155
- export function cwdToSlug(cwd: string): string {
156
- const normalized = cwd.replace(/\\/g, "/").replace(/\/+$/, "");
149
+ function cwdToSlug(cwd: string): string {
150
+ const normalized = cwd.replaceAll("\\", "/").replace(/\/+$/, "");
157
151
  return normalized.split("/").pop() || "unknown";
158
152
  }
159
153
 
@@ -57,7 +57,7 @@ if (removedAgents.length > 0) {
57
57
  // --- Remove PAL system docs ---
58
58
  removePalDocs();
59
59
 
60
- // --- Remove AGENTS.md and CLAUDE.md symlink ---
60
+ // --- Remove AGENTS.md and CLAUDE.md ---
61
61
  const agentsMd = resolve(platform.opencodeDir(), "AGENTS.md");
62
62
  const claudeMd = resolve(CLAUDE_DIR, "CLAUDE.md");
63
63
  try {
@@ -0,0 +1,95 @@
1
+ /**
2
+ * PAL — Codex target installer
3
+ * Merges PAL hooks into ~/.codex/hooks.json (never overwrites user hooks).
4
+ * Symlinks skills. Ensures AGENTS.md symlink via regenerateIfNeeded().
5
+ */
6
+
7
+ import {
8
+ copyFileSync,
9
+ existsSync,
10
+ mkdirSync,
11
+ readFileSync,
12
+ writeFileSync,
13
+ } from "node:fs";
14
+ import { resolve } from "node:path";
15
+ import { regenerateIfNeeded } from "../../hooks/lib/claude-md";
16
+ import { assets, palPkg, platform } from "../../hooks/lib/paths";
17
+ import {
18
+ copySkills,
19
+ countSkills,
20
+ generateSkillIndex,
21
+ loadCodexHooksTemplate,
22
+ log,
23
+ mergeCodexHooks,
24
+ readJson,
25
+ scaffoldPalSettings,
26
+ writeJson,
27
+ } from "../lib";
28
+
29
+ /**
30
+ * Ensure `features.hooks = true` in config.toml without touching other content.
31
+ * Appends the setting if missing; skips if already present.
32
+ */
33
+ function enableCodexHooks(configPath: string): void {
34
+ let content = "";
35
+ if (existsSync(configPath)) {
36
+ content = readFileSync(configPath, "utf-8");
37
+ // Already enabled — nothing to do
38
+ if (/^\s*hooks\s*=\s*true/m.test(content)) {
39
+ log.info("Codex hooks already enabled in config.toml");
40
+ return;
41
+ }
42
+ // [features] section exists but no hooks line — insert after the header
43
+ if (/^\[features\]/m.test(content)) {
44
+ content = content.replace(/(\[features\][^\n]*\n)/, "$1hooks = true\n");
45
+ writeFileSync(configPath, content, "utf-8");
46
+ log.success("Added hooks = true to existing [features] section in config.toml");
47
+ return;
48
+ }
49
+ }
50
+ // No config.toml, or no [features] section — append the block
51
+ const block = `${content.endsWith("\n") || content === "" ? "" : "\n"}\n[features]\nhooks = true\n`;
52
+ writeFileSync(configPath, content + block, "utf-8");
53
+ log.success("Enabled hooks = true in ~/.codex/config.toml");
54
+ }
55
+
56
+ const PKG_ROOT = palPkg().replaceAll("\\", "/");
57
+ const CODEX_DIR = platform.codexDir();
58
+ const HOOKS_FILE = resolve(CODEX_DIR, "hooks.json");
59
+
60
+ // --- Ensure ~/.codex/ exists ---
61
+ mkdirSync(CODEX_DIR, { recursive: true });
62
+
63
+ // --- Merge hooks ---
64
+ if (existsSync(HOOKS_FILE)) {
65
+ copyFileSync(HOOKS_FILE, `${HOOKS_FILE}.bak.${Date.now()}`);
66
+ log.info("Backed up hooks.json");
67
+ }
68
+
69
+ const template = loadCodexHooksTemplate(assets.codexHooksTemplate(), PKG_ROOT);
70
+ const existing = readJson<Record<string, unknown>>(HOOKS_FILE, {});
71
+ const merged = mergeCodexHooks(existing, template);
72
+
73
+ writeJson(HOOKS_FILE, merged);
74
+ log.success("Merged PAL hooks into ~/.codex/hooks.json");
75
+
76
+ // --- Symlink skills to ~/.codex/skills/ ---
77
+ const codexSkillsDir = resolve(CODEX_DIR, "skills");
78
+ copySkills(codexSkillsDir);
79
+ generateSkillIndex();
80
+
81
+ // --- Scaffold PAL settings ---
82
+ scaffoldPalSettings();
83
+
84
+ // --- Generate / verify AGENTS.md symlink ---
85
+ regenerateIfNeeded();
86
+ log.success("Ensured AGENTS.md symlink at ~/.codex/AGENTS.md");
87
+
88
+ // --- Enable hooks in config.toml ---
89
+ const CONFIG_FILE = resolve(CODEX_DIR, "config.toml");
90
+ enableCodexHooks(CONFIG_FILE);
91
+
92
+ log.success("Codex installation complete");
93
+ console.log("");
94
+ log.info(`Skills: ${countSkills()}`);
95
+ log.info(`Hooks: ${HOOKS_FILE}`);