Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions .pai-protected.json
Original file line number Diff line number Diff line change
Expand Up @@ -398,8 +398,6 @@
"USER/",
"USER directory",
"USER tier",
"~/.claude/skills",
"~/.claude/hooks",
"$PAI_DIR/skills",
"$PAI_DIR/hooks",
"ANTHROPIC_API_KEY=your",
Expand All @@ -417,7 +415,7 @@
"kai_to_pai_workflow": {
"description": "How to safely sync Kai improvements to PAI",
"steps": [
"1. Make changes in Kai (~/.claude/)",
"1. Make changes in Kai (~/.claude/ + ~/.pai/ under the two-root split)",
"2. Test thoroughly in Kai environment",
"3. Identify which changes should go to public PAI",
"4. Copy changes to PAI repo (~/Projects/PAI/)",
Expand Down
8 changes: 4 additions & 4 deletions Releases/v4.0.3+/.claude/PAI/SYSTEM_USER_EXTENDABILITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,10 +164,10 @@ When creating a new configurable component:
3. **Implement cascading lookup**
```typescript
function getConfigPath(): string | null {
const userPath = paiPath('USER', 'ComponentName', 'config.yaml');
const userPath = codePath('USER', 'ComponentName', 'config.yaml');
if (existsSync(userPath)) return userPath;

const systemPath = paiPath('ComponentName', 'config.example.yaml');
const systemPath = codePath('ComponentName', 'config.example.yaml');
if (existsSync(systemPath)) return systemPath;

return null; // Will use hardcoded defaults
Expand Down Expand Up @@ -196,8 +196,8 @@ To add USER extensibility to an existing component:

```typescript
// From SecurityValidator.hook.ts
const USER_PATTERNS_PATH = paiPath('PAI', 'USER', 'PAISECURITYSYSTEM', 'patterns.yaml');
const SYSTEM_PATTERNS_PATH = paiPath('PAI', 'PAISECURITYSYSTEM', 'patterns.example.yaml');
const USER_PATTERNS_PATH = codePath('PAI', 'USER', 'PAISECURITYSYSTEM', 'patterns.yaml');
const SYSTEM_PATTERNS_PATH = codePath('PAI', 'PAISECURITYSYSTEM', 'patterns.example.yaml');

function getPatternsPath(): string | null {
// USER first
Expand Down
6 changes: 2 additions & 4 deletions Releases/v4.0.3+/.claude/hooks/KittyEnvPersist.hook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,10 @@

import { writeFileSync, mkdirSync, existsSync } from 'fs';
import { join } from 'path';
import { getConfigDir } from './lib/paths';
import { codePath } from './lib/paths';
import { setTabState, readTabState } from './lib/tab-setter';
import { getDAName } from './lib/identity';

const configDir = getConfigDir();

// Skip for subagents
const claudeProjectDir = process.env.CLAUDE_PROJECT_DIR || '';
const isSubagent = claudeProjectDir.includes('/.claude/Agents/') ||
Expand All @@ -28,7 +26,7 @@ if (isSubagent) process.exit(0);
const kittyListenOn = process.env.KITTY_LISTEN_ON;
const kittyWindowId = process.env.KITTY_WINDOW_ID;
if (kittyListenOn && kittyWindowId) {
const stateDir = join(configDir, 'MEMORY', 'STATE');
const stateDir = codePath('MEMORY', 'STATE');
if (!existsSync(stateDir)) mkdirSync(stateDir, { recursive: true });
writeFileSync(
join(stateDir, 'kitty-env.json'),
Expand Down
9 changes: 4 additions & 5 deletions Releases/v4.0.3+/.claude/hooks/LastResponseCache.hook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,8 @@
*/

import { readHookInput, parseTranscriptFromInput } from './lib/hook-io';
import { writeFileSync } from 'fs';
import { join } from 'path';
import { homedir } from 'os';
import { writeFileSync, mkdirSync } from 'fs';
import { codePath } from './lib/paths';

async function main() {
const input = await readHookInput();
Expand All @@ -29,8 +28,8 @@ async function main() {

if (lastResponse) {
try {
const paiDir = process.env.PAI_DIR || join(homedir(), '.claude');
const cachePath = join(paiDir, 'MEMORY', 'STATE', 'last-response.txt');
const cachePath = codePath('MEMORY', 'STATE', 'last-response.txt');
mkdirSync(codePath('MEMORY', 'STATE'), { recursive: true });
writeFileSync(cachePath, lastResponse.slice(0, 2000), 'utf-8');
} catch (err) {
console.error('[LastResponseCache] Failed to write:', err);
Expand Down
58 changes: 29 additions & 29 deletions Releases/v4.0.3+/.claude/hooks/LoadContext.hook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@

import { readFileSync, existsSync, readdirSync } from 'fs';
import { join } from 'path';
import { getConfigDir, getPaiDir } from './lib/paths';
import { getConfigDir, codePath } from './lib/paths';
import { recordSessionStart } from './lib/notifications';
import { loadLearningDigest, loadWisdomFrames, loadFailurePatterns, loadSignalTrends } from './lib/learning-readback';

Expand Down Expand Up @@ -67,9 +67,10 @@ function isDynamicEnabled(settings: Settings, key: keyof DynamicContextConfig):

/**
* Load settings.json and return the settings object.
* settings.json lives in CONFIG root, not PAI root.
*/
function loadSettings(paiDir: string): Settings {
const settingsPath = join(paiDir, 'settings.json');
function loadSettings(configDir: string): Settings {
const settingsPath = join(configDir, 'settings.json');
if (existsSync(settingsPath)) {
try {
return JSON.parse(readFileSync(settingsPath, 'utf-8'));
Expand All @@ -84,13 +85,13 @@ function loadSettings(paiDir: string): Settings {
* Load files listed in settings.json → loadAtStartup.files
* Reads each file and injects as a system-reminder block.
*/
function loadStartupFiles(paiDir: string, settings: Settings): string | null {
function loadStartupFiles(settings: Settings): string | null {
const config = settings.loadAtStartup;
if (!config?.files || config.files.length === 0) return null;

const parts: string[] = [];
for (const relPath of config.files) {
const fullPath = join(paiDir, relPath);
const fullPath = codePath(relPath);
if (!existsSync(fullPath)) {
console.error(`⚠️ loadAtStartup: file not found: ${relPath}`);
continue;
Expand All @@ -112,11 +113,11 @@ function loadStartupFiles(paiDir: string, settings: Settings): string | null {
* Load relationship context for session startup.
* Returns a lightweight summary of key opinions and recent notes.
*/
function loadRelationshipContext(configDir: string, paiDir: string): string | null {
function loadRelationshipContext(): string | null {
const parts: string[] = [];

// Load high-confidence opinions (>0.85) from OPINIONS.md (PAI code root)
const opinionsPath = join(paiDir, 'PAI/USER/OPINIONS.md');
const opinionsPath = codePath('PAI', 'USER', 'OPINIONS.md');
if (existsSync(opinionsPath)) {
try {
const content = readFileSync(opinionsPath, 'utf-8');
Expand Down Expand Up @@ -153,9 +154,9 @@ function loadRelationshipContext(configDir: string, paiDir: string): string | nu

const recentNotes: string[] = [];
for (const date of [today, yesterday]) {
const notePath = join(
configDir,
'MEMORY/RELATIONSHIP',
const notePath = codePath(
'MEMORY',
'RELATIONSHIP',
formatMonth(date),
`${formatDate(date)}.md`
);
Expand Down Expand Up @@ -207,12 +208,12 @@ interface WorkSession {
/**
* Scan recent WORK/ directories (last 48h) for active sessions.
*/
function getRecentWorkSessions(paiDir: string): WorkSession[] {
const workDir = join(paiDir, 'MEMORY', 'WORK');
function getRecentWorkSessions(): WorkSession[] {
const workDir = codePath('MEMORY', 'WORK');
if (!existsSync(workDir)) return [];

let sessionNames: Record<string, string> = {};
const namesPath = join(paiDir, 'MEMORY', 'STATE', 'session-names.json');
const namesPath = codePath('MEMORY', 'STATE', 'session-names.json');
try {
if (existsSync(namesPath)) {
sessionNames = JSON.parse(readFileSync(namesPath, 'utf-8'));
Expand Down Expand Up @@ -331,8 +332,8 @@ function getRecentWorkSessions(paiDir: string): WorkSession[] {
/**
* Load persistent project progress files, flagging stale ones (>14 days).
*/
function getProjectProgress(paiDir: string): WorkSession[] {
const progressDir = join(paiDir, 'MEMORY', 'STATE', 'progress');
function getProjectProgress(): WorkSession[] {
const progressDir = codePath('MEMORY', 'STATE', 'progress');
if (!existsSync(progressDir)) return [];

const sessions: WorkSession[] = [];
Expand Down Expand Up @@ -384,9 +385,9 @@ function getProjectProgress(paiDir: string): WorkSession[] {
/**
* Unified activity dashboard — merges recent WORK sessions + persistent projects.
*/
async function checkActiveProgress(paiDir: string): Promise<string | null> {
const recentSessions = getRecentWorkSessions(paiDir);
const projects = getProjectProgress(paiDir);
async function checkActiveProgress(): Promise<string | null> {
const recentSessions = getRecentWorkSessions();
const projects = getProjectProgress();

if (recentSessions.length === 0 && projects.length === 0) {
return null;
Expand Down Expand Up @@ -427,9 +428,9 @@ async function checkActiveProgress(paiDir: string): Promise<string | null> {
}
}

const pDir = getPaiDir();
summary += `\n💡 To resume project: \`bun run ${pDir}/PAI/Tools/SessionProgress.ts resume <project>\`\n`;
summary += `💡 To complete project: \`bun run ${pDir}/PAI/Tools/SessionProgress.ts complete <project>\`\n`;
const sessionProgressPath = codePath('PAI', 'Tools', 'SessionProgress.ts');
summary += `\n💡 To resume project: \`bun run ${sessionProgressPath} resume <project>\`\n`;
summary += `💡 To complete project: \`bun run ${sessionProgressPath} complete <project>\`\n`;

return summary;
}
Expand All @@ -447,7 +448,6 @@ async function main() {
}

const configDir = getConfigDir();
const paiDir = getPaiDir();

// Tab reset is handled by KittyEnvPersist.hook.ts (runs before this hook)

Expand All @@ -460,15 +460,15 @@ async function main() {
console.error('✅ Loaded settings.json');

// Force-load startup files from settings.json → loadAtStartup (paths relative to PAI code root)
const startupContent = loadStartupFiles(paiDir, settings);
const startupContent = loadStartupFiles(settings);
if (startupContent) {
console.log(`<system-reminder>\n${startupContent}\n</system-reminder>`);
}

// Load relationship context (lightweight summary)
let relationshipContext: string | null = null;
if (isDynamicEnabled(settings, 'relationshipContext')) {
relationshipContext = loadRelationshipContext(configDir, paiDir);
relationshipContext = loadRelationshipContext();
if (relationshipContext) {
console.error(`💕 Loaded relationship context (${relationshipContext.length} chars)`);
}
Expand All @@ -479,10 +479,10 @@ async function main() {
// Load learning readback context
let learningContext = '';
if (isDynamicEnabled(settings, 'learningReadback')) {
const learningDigest = loadLearningDigest(configDir);
const wisdomFrames = loadWisdomFrames(configDir);
const failurePatterns = loadFailurePatterns(configDir);
const signalTrends = loadSignalTrends(configDir);
const learningDigest = loadLearningDigest();
const wisdomFrames = loadWisdomFrames();
const failurePatterns = loadFailurePatterns();
const signalTrends = loadSignalTrends();

const learningParts: string[] = [];
if (signalTrends) learningParts.push(signalTrends);
Expand Down Expand Up @@ -518,7 +518,7 @@ Dynamic context loaded. Core identity, rules, and format are in CLAUDE.md.

// Active work summary
if (isDynamicEnabled(settings, 'activeWorkSummary')) {
const activeProgress = await checkActiveProgress(configDir);
const activeProgress = await checkActiveProgress();
if (activeProgress) {
console.log(activeProgress);
console.error(`📋 Active work summary loaded (${activeProgress.length} chars)`);
Expand Down
5 changes: 2 additions & 3 deletions Releases/v4.0.3+/.claude/hooks/RelationshipMemory.hook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -265,9 +265,8 @@ async function main() {
process.exit(0);
}

// Write to daily relationship file
const configDir = getConfigDir();
const filepath = ensureRelationshipDir(configDir);
// Write to daily relationship file (RELATIONSHIP lives under PAI root)
const filepath = ensureRelationshipDir(getPaiDir());
initDailyFile(filepath);

const formatted = formatNotes(notes);
Expand Down
7 changes: 3 additions & 4 deletions Releases/v4.0.3+/.claude/hooks/SessionCleanup.hook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,10 @@ import { writeFileSync, existsSync, readFileSync, unlinkSync } from 'fs';
import { join } from 'path';
import { getISOTimestamp } from './lib/time';
import { setTabState, cleanupKittySession } from './lib/tab-setter';
import { codePath } from './lib/paths';

const BASE_DIR = process.env.PAI_DIR || join(process.env.HOME!, '.claude');
const MEMORY_DIR = join(BASE_DIR, 'MEMORY');
const STATE_DIR = join(MEMORY_DIR, 'STATE');
const WORK_DIR = join(MEMORY_DIR, 'WORK');
const STATE_DIR = codePath('MEMORY', 'STATE');
const WORK_DIR = codePath('MEMORY', 'WORK');

// Session-scoped state file lookup with legacy fallback
function findStateFile(sessionId?: string): string | null {
Expand Down
5 changes: 2 additions & 3 deletions Releases/v4.0.3+/.claude/hooks/VoiceCompletion.hook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,9 @@
*/

import { readFileSync, existsSync } from 'fs';
import { join } from 'path';
import { homedir } from 'os';
import { readHookInput, parseTranscriptFromInput } from './lib/hook-io';
import { handleVoice } from './handlers/VoiceNotification';
import { getSettingsPath } from './lib/paths';

/**
* Voice gate: only main terminal sessions get voice.
Expand All @@ -39,7 +38,7 @@ function isMainSession(): boolean {
*/
function isVoiceEnabled(): boolean {
try {
const settingsPath = join(homedir(), '.claude', 'settings.json');
const settingsPath = getSettingsPath();
if (!existsSync(settingsPath)) return true;
const settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
return settings.notifications?.voice?.enabled !== false;
Expand Down
9 changes: 4 additions & 5 deletions Releases/v4.0.3+/.claude/hooks/WorkCompletionLearning.hook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,11 @@ import { writeFileSync, existsSync, readFileSync, readdirSync, mkdirSync } from
import { join, dirname } from 'path';
import { getISOTimestamp, getPSTDate } from './lib/time';
import { getLearningCategory } from './lib/learning-utils';
import { codePath } from './lib/paths';

const BASE_DIR = process.env.PAI_DIR || join(process.env.HOME!, '.claude');
const MEMORY_DIR = join(BASE_DIR, 'MEMORY');
const STATE_DIR = join(MEMORY_DIR, 'STATE');
const WORK_DIR = join(MEMORY_DIR, 'WORK');
const LEARNING_DIR = join(MEMORY_DIR, 'LEARNING');
const STATE_DIR = codePath('MEMORY', 'STATE');
const WORK_DIR = codePath('MEMORY', 'WORK');
const LEARNING_DIR = codePath('MEMORY', 'LEARNING');

// Session-scoped state file lookup with legacy fallback
function findStateFile(sessionId?: string): string | null {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ interface DriftReport {
// ============================================================================

const SYSTEM_DIR = codePath('PAI');
const HOOKS_DIR = configPath('hooks');
const HOOKS_DIR = codePath('hooks');
const HANDLERS_DIR = join(HOOKS_DIR, 'handlers');
const LIB_DIR = join(HOOKS_DIR, 'lib');
const DRIFT_STATE_FILE = codePath('MEMORY', 'STATE', 'doc-drift-state.json');
Expand Down
24 changes: 12 additions & 12 deletions Releases/v4.0.3+/.claude/hooks/handlers/UpdateCounts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
import { readFileSync, writeFileSync, readdirSync, existsSync, statSync } from 'fs';
import { join } from 'path';
import { execSync, spawn } from 'child_process';
import { getConfigDir, getPaiDir, getSettingsPath } from '../lib/paths';
import { getConfigDir, getPaiDir, getSettingsPath, codePath } from '../lib/paths';


interface Counts {
Expand Down Expand Up @@ -148,17 +148,17 @@ function countSubdirs(dir: string): number {
* Get all counts
*/
function getCounts(configDir: string, paiDir: string): Counts {
const ratingsPath = join(configDir, 'MEMORY/LEARNING/SIGNALS/ratings.jsonl');
const ratingsPath = codePath('MEMORY', 'LEARNING', 'SIGNALS', 'ratings.jsonl');
return {
skills: countSkills(paiDir),
workflows: countWorkflowFiles(join(paiDir, 'skills')),
hooks: countHooks(configDir),
signals: countFilesRecursive(join(configDir, 'MEMORY/LEARNING'), '.md'),
hooks: countHooks(paiDir),
signals: countFilesRecursive(codePath('MEMORY', 'LEARNING'), '.md'),
files: countFilesRecursive(join(paiDir, 'PAI/USER')),
work: countSubdirs(join(configDir, 'MEMORY/WORK')),
sessions: countFilesRecursive(join(configDir, 'MEMORY'), '.jsonl'),
research: countFilesRecursive(join(configDir, 'MEMORY/RESEARCH'), '.md') +
countFilesRecursive(join(configDir, 'MEMORY/RESEARCH'), '.json'),
work: countSubdirs(codePath('MEMORY', 'WORK')),
sessions: countFilesRecursive(codePath('MEMORY'), '.jsonl'),
research: countFilesRecursive(codePath('MEMORY', 'RESEARCH'), '.md') +
countFilesRecursive(codePath('MEMORY', 'RESEARCH'), '.json'),
ratings: countRatingsLines(ratingsPath),
updatedAt: new Date().toISOString(),
};
Expand All @@ -168,8 +168,8 @@ function getCounts(configDir: string, paiDir: string): Counts {
* Refresh usage cache from Anthropic OAuth API.
* Called by stop hook so status line never needs to make this 700ms API call.
*/
async function refreshUsageCache(paiDir: string): Promise<void> {
const usageCachePath = join(paiDir, 'MEMORY/STATE/usage-cache.json');
async function refreshUsageCache(): Promise<void> {
const usageCachePath = codePath('MEMORY', 'STATE', 'usage-cache.json');

try {
// Extract OAuth token — macOS Keychain or Linux credentials file
Expand Down Expand Up @@ -281,7 +281,7 @@ export async function handleUpdateCounts(): Promise<void> {
// signal aborting it ("Hook cancelled"). A detached process runs independently
// and isn't killed when the hook exits.
try {
const scriptPath = join(configDir, 'hooks', 'handlers', 'UpdateCounts.ts');
const scriptPath = join(paiDir, 'hooks', 'handlers', 'UpdateCounts.ts');
const child = spawn('bun', ['run', scriptPath], {
detached: true,
stdio: 'ignore',
Expand All @@ -302,7 +302,7 @@ export async function handleUpdateCounts(): Promise<void> {
// - UPDATE_COUNTS_REFRESH_ONLY=1: only refresh usage cache — spawned as detached bg process by the hook
if (import.meta.main) {
if (process.env.UPDATE_COUNTS_REFRESH_ONLY === '1') {
refreshUsageCache(getConfigDir()).then(() => process.exit(0)).catch(() => process.exit(0));
refreshUsageCache().then(() => process.exit(0)).catch(() => process.exit(0));
} else {
handleUpdateCounts().then(() => process.exit(0));
}
Expand Down
Loading
Loading