# PAL System Architecture

<!--
SYSTEM ARCHITECTURE TEMPLATE
=============================
This file defines the GENERIC architecture patterns for any PAL installation.
These are the foundational patterns that apply to ALL PAL implementations.

WHAT GOES HERE:
- The Founding Principles (universal)
- Generic system patterns (skill structure, hook lifecycle, memory layout)
- Architecture diagrams showing how components interact
- Design philosophy and constraints

WHAT DOES NOT GO HERE:
- User-specific skill counts, configurations, or API keys
- Personal projects or deployment details
- Ephemeral state or session-specific information
-->

**The Founding Principles and Universal Architecture Patterns for the Portable Agent Layer**

---

## Core Philosophy

**PAL is scaffolding for AI, not a replacement for human intelligence.**

AI systems need structure to be reliable. Like physical scaffolding supports construction work, PAL provides the architectural framework that makes AI assistance dependable, maintainable, and effective — regardless of which AI agent runs underneath.

---

## The Founding Principles

### 1. Portability First

**PAL exists so your AI context, memory, and workflows survive any single tool.**

AI agents come and go. Your accumulated knowledge, preferences, and workflows should not be locked into one vendor's tool. PAL abstracts the agent-specific layer so the same skills, hooks, memory, and configuration work across Claude Code, opencode, and future agents.

**What portability means in practice:**
- No agent-specific assumptions in core logic
- Agent-specific code isolated in `src/targets/`
- Skills, memory, and TELOS (Greek: τέλος — end/purpose/goal; the user's persistent life context) are agent-agnostic
- A single `pal cli install --<agent>` registers everything

### 2. Cross-Platform by Default

**Every feature must work identically on macOS, Linux, and Windows.**

No platform-specific code without a portable fallback. Path resolution, shell commands, and file operations use cross-platform abstractions. Environment overrides (`PAL_HOME`, `PAL_PKG`, etc.) let users customize paths without touching code.

### 3. The Continuously Upgrading Algorithm

**The Algorithm is the gravitational center — everything else exists to serve it.**

PAL is built around a universal pattern for accomplishing any task: **Current State → Ideal State** via verifiable criteria. This pattern applies at every scale.

Everything feeds back into improving The Algorithm:
- The **Memory System** captures signals from every interaction
- The **Hook System** detects sentiment, ratings, and behavioral patterns
- The **Learning System** organizes evidence by session
- The **Wisdom System** crystallizes recurring patterns into principles
- The **Relationship System** tracks how the user works and what they prefer

### 4. Scaffolding > Model

**The system architecture matters more than the underlying AI model.**

A well-structured system with good scaffolding will outperform a more powerful model with poor structure. PAL's value comes from:
- Organized workflows that guide AI execution
- Routing systems that activate the right context
- Quality gates that verify outputs
- Memory systems that enable learning
- Feedback systems that provide awareness

### 5. As Deterministic as Possible

**Favor predictable, repeatable outcomes over flexibility.**

- Same input → Same output
- Behavior defined by code and hooks, not prompt variations
- Version control tracks explicit changes
- Hooks run deterministic TypeScript, not prompt-driven logic

### 6. Code Before Prompts

**Write code to solve problems, use prompts to orchestrate code.**

Prompts should never replicate functionality that code can provide:
- Hooks are TypeScript, not prompt instructions
- Tools are CLI programs, not natural language procedures
- Memory is structured JSON/JSONL, not freeform text
- Configuration is `pal-settings.json`, not prose in CLAUDE.md

### 7. Clear Thinking + Prompting is King

**The quality of outcomes depends on the quality of thinking and prompts.**

Before any code, before any architecture — there must be clear thinking:
- Understand the problem deeply before solving it
- Define success criteria before building
- Challenge assumptions before accepting them
- Simplify before optimizing

### 8. UNIX Philosophy

**Do one thing well. Compose tools through standard interfaces.**

- Each hook handler does one thing
- Each tool is a standalone CLI program
- Each skill is a self-contained capability
- Hooks compose via `Promise.allSettled` — independent, parallel, isolated failures
- Tools compose via `pal cli <name>` — standard I/O, exit codes

### 9. CLI as Interface

**Every operation should be accessible via command line.**

- `pal cli <command>` for system management and user-facing utilities
- Skills triggered via agent slash commands
- No hidden operations — everything is scriptable and testable

### 10. Spec / Test First

**Define expected behavior before writing implementation.**

- Write tests before implementation
- Tests should fail initially
- Implement until tests pass
- Run `check-write`, `type-check`, and `test` after every edit

### 11. Custom Skill Management

**Skills are the organizational unit for all domain expertise.**

Skills are active orchestrators, not passive documentation:
- **Self-activating:** Trigger automatically based on user request
- **Self-contained:** Package all context, workflows, and tools
- **Composable:** Can invoke other skills and agents
- **Evolvable:** Easy to add, modify, or deprecate

### 12. Custom Memory System

**Automatic capture and preservation of valuable work.**

Every session, every insight, every decision — captured automatically:
- Relationship notes extracted via inference
- Learnings captured per session
- Ratings tracked (explicit and implicit)
- Wisdom crystallized from recurring patterns
- Failures logged for pattern avoidance

### 13. Permission to Fail

**Explicit permission to say "I don't know" prevents hallucinations.**

The AI has explicit permission to say "I don't know" when:
- Information isn't available in context
- Multiple conflicting answers seem equally valid
- Verification isn't possible

Fabricating an answer is far worse than admitting uncertainty.

---

## Skill System Architecture

### Canonical Skill Structure

```
assets/skills/<name>/
├── SKILL.md              # Main skill file (REQUIRED)
└── tools/                # CLI tools for automation (optional)
    └── tool-name.ts      # TypeScript CLI tool
```

### SKILL.md Format

```markdown
---
name: skill-name
description: What it does. Use when [triggers]. Capabilities.
---

# Skill Name

Brief description.

## Workflow
[Steps, decision trees, or procedures]
```

### Key Rules

- **Description**: Used by the agent for skill matching — be specific about triggers
- **One SKILL.md per skill**: The single entry point
- **Tools are optional**: Only add when the skill needs CLI automation
- **Skills are agent-agnostic**: No agent-specific assumptions in SKILL.md

---

## Hook System Architecture

### Lifecycle

```
┌─────────────────────┐
│   Session Start     │──► LoadContext.ts
│                     │    - Regenerate CLAUDE.md if stale
│                     │    - Inject dynamic context (system-reminder)
└─────────────────────┘

┌─────────────────────┐
│  User Prompt Submit │──► UserPromptOrchestrator.ts
│                     │    - Prompt context (one merged system-reminder):
│                     │      contextual steering, skill trigger matches,
│                     │      prior-lesson retrieval
│                     │    - Rating capture (explicit/implicit)
│                     │    - Session naming (first prompt)
└─────────────────────┘

┌─────────────────────┐
│   Pre Tool Use      │──► SecurityValidator.ts
│                     │    - Bash command validation
│                     │    - File path validation
│                     │──► SkillGuard.ts
│                     │    - Block false-positive skill matches
└─────────────────────┘

┌─────────────────────┐
│       Stop          │──► StopOrchestrator.ts → runStopHandlers()
│                     │    Detached (inference-bearing, fire-and-forget):
│                     │    - Session intelligence (title, summary, insights, handoff)
│                     │    - Failure principle extraction (low ratings)
│                     │    - Auto-graduate wisdom (24h TTL, idempotent)
│                     │    Concurrent (Promise.allSettled):
│                     │    - Work session capture
│                     │    - Project touch / history
│                     │    - Persist last exchange (handoff seed)
│                     │    - Cache last response (rating correlation)
│                     │    - Reflect trigger check
│                     │    - Self-model trigger check
│                     │    - Synthesis pipeline
│                     │    - Desktop notify
│                     │    - Write context digests (semi-static sources)
│                     │    - Auto-backup
│                     │    - Count updates
│                     │    - Tab reset
└─────────────────────┘
```

### Per-Agent Turn Contract

Every agent's turns are measured and its replies filed under the session id the agent sent. What differs is whether the prompt hook can hand context back, and where the final reply comes from.

| Agent | Prompt event → context per turn | Final reply from |
|-------|---------------------------------|------------------|
| Claude Code | `UserPromptSubmit` → plain text | `Stop` transcript |
| Codex | `UserPromptSubmit` → `hookSpecificOutput.additionalContext` | `Stop` `last_assistant_message` |
| VS Code | `UserPromptSubmit` → `hookSpecificOutput.additionalContext` | `Stop` transcript |
| opencode | `chat.message` → synthetic part | `session.idle` session messages |
| Cursor | `beforeSubmitPrompt` → `additional_context`, undocumented | `afterAgentResponse` `text` (`AgentResponse.ts`) |
| Copilot CLI | `userPromptTransformed` → `modifiedTransformedPrompt` (`PromptTransformed.ts`) | `agentStop` transcript |

The Copilot CLI drops what its `userPromptSubmitted` hook returns, so that hook only measures the turn and parks the context; `userPromptTransformed` fires next and appends it to the prompt the model receives. It also writes the reply to the `agentStop` transcript only after that hook returns, so a stop whose transcript still ends on the user's message hands off to a detached `StopDeferred.ts`, which waits for the reply before running the stop handlers. Cursor documents no context field on `beforeSubmitPrompt`, so PAL offers `additional_context` there but logs no interaction hint as sent until it is shown to reach the model; its documented context channel is session start. cursor-agent also runs every hook in `~/.claude/settings.json`, so when PAL's own Cursor hooks are installed, the copy registered with `--agent=claude` exits at once (`lib/cursor-shadow.ts`) and each Cursor event runs PAL once. `test/agent-turns.test.ts` holds each agent to this table.

### Design Principles

- **Fail-open**: Hook errors never block the user's session
- **Detached inference**: Long inference calls (session intelligence, failure principle, auto-graduate) spawn detached child processes that survive the hook returning. The parent hook exits in milliseconds — cold-start latency for the subscription CLI no longer blocks the next prompt.
- **Parallel execution**: Non-inference handlers run via `Promise.allSettled` — one failure doesn't block others
- **Idempotent**: Handlers check for existing state before writing (auto-graduate uses 24h TTL + content dedup; session intelligence dedups by message count)
- **Timeout-aware**: Inference calls have hard timeouts (60s default; longer because they run detached and don't block the user)
- **Race-safe**: Detached claim files use atomic `rename()` with ENOENT treated as a benign "already claimed" signal (opencode fires `session.idle` + `session.diff` concurrently)
- **Subagent-aware**: `LoadContext.ts` skips heavy context loading for subagent sessions

### Configuration

Hooks are registered in the agent's settings file during `pal cli install`:

```json
{
  "hooks": {
    "SessionStart": [{ "type": "command", "command": "bun run <path>/LoadContext.ts" }],
    "UserPromptSubmit": [{ "type": "command", "command": "bun run <path>/UserPromptOrchestrator.ts" }],
    "PreToolUse": [
      { "type": "command", "command": "bun run <path>/SecurityValidator.ts", "matcher": "Bash" },
      { "type": "command", "command": "bun run <path>/SkillGuard.ts", "matcher": "Skill" }
    ],
    "Stop": [{ "type": "command", "command": "bun run <path>/StopOrchestrator.ts" }]
  }
}
```

---

## Memory System Architecture

### Directory Structure

```
memory/
├── state/                         # Runtime state
│   ├── sessions.json              # Session registry (name, cwd, status, summary)
│   ├── projects.json              # Multi-session project tracking
│   ├── counts.json                # Cached counts for greeting
│   ├── last-responses.json        # Cached responses for rating correlation
│   ├── pending-failure.json       # Deferred failure capture
│   └── debug.log                  # Hook execution logs
│
├── signals/                       # User feedback
│   └── ratings.jsonl              # Explicit + implicit ratings
│
├── relationship/                  # Interaction tracking
│   ├── YYYY-MM/
│   │   └── YYYY-MM-DD.md         # Daily notes (W/O/B format)
│   ├── opinions.json              # Confidence-tracked opinions
│   └── reflections/               # Periodic reflection reports
│
├── session-learning/              # Per-session learnings
│   └── YYYY-MM/
│       └── YYYY-MM-DD_title.md    # Session learning with frontmatter
│
├── failures/                      # Low-rating context dumps
│   └── YYYY-MM/
│       └── YYYY-MM-DD_context.md  # Failed session context for avoidance
│
├── wisdom/                        # Crystallized principles
│   └── frames/
│       └── domain.md              # Domain-specific principles with confidence
│
└── synthesis/                     # Pattern aggregation
    └── YYYY-MM/
        └── YYYY-MM-DD_period.md   # Weekly/monthly synthesis reports
```

### Data Flow

```
Session interaction
  │
  ├─► [Stop] Rating capture ──► signals/ratings.jsonl
  │
  ├─► [Stop] Relationship extraction (Haiku) ──► relationship/YYYY-MM/YYYY-MM-DD.md
  │
  ├─► [Stop] Work learning capture ──► session-learning/YYYY-MM/...
  │
  ├─► [Stop] Failure capture (low ratings) ──► failures/YYYY-MM/...
  │
  ├─► [Periodic] Reflect trigger ──► relationship/opinions.json
  │                                  relationship/reflections/...
  │
  ├─► [Periodic] Synthesis ──► synthesis/YYYY-MM/...
  │
  └─► [Periodic] Wisdom graduation ──► wisdom/frames/...
```

### Relationship Note Format

Three note types, captured via Haiku inference at session end:

```
W — World facts (user's situation, projects, tools)
O(c=0.85) — Opinions/preferences with confidence
B(c=0.75) — Beliefs/behavioral patterns with confidence
```

### Opinion Lifecycle

```
Relationship notes (O/B types)
  │
  ├─► Reflect tool groups similar notes
  │   ├─► 2+ occurrences → new opinion at 50% confidence
  │   └─► Matches existing → +2% supporting evidence
  │
  ├─► AI confirmation → +10% (via tool:opinion)
  ├─► AI contradiction → -20% (via tool:opinion)
  │
  └─► At ≥85% confidence → auto-injected into session context
```

---

## Context Loading Architecture

### Three-Tier Design

**Tier 1 — Operational** (loaded natively at agent startup):
- CLAUDE.md / AGENTS.md — identity, modes, context routing table
- Loaded once per session, always available, never re-fetched

**Tier 2 — Semi-static** (pre-compiled at previous session stop, loaded natively):
- Self-model, wisdom, opinions, synthesis, failures, steering rules
- Written to disk by `writeContextDigests()` at Stop time
- Loaded natively per-agent: `@imports` in CLAUDE.md (Claude Code), `instructions[]` in opencode config, `.mdc` rules in `~/.cursor/rules/` (Cursor), `.instructions.md` in `~/.copilot/instructions/` (Copilot), always-on rules in `~/.gemini/config/plugins/pal/rules/` (Antigravity)
- Content is global/user-level — safe to pre-compile (not project-scoped)

**Tier 3 — Dynamic** (injected fresh each session by LoadContext hook):
- Handoff notes, session intelligence, relationship notes, project history
- Changes per-session or is project-scoped — can't be pre-compiled
- Injected as `<system-reminder>` block via stdout
- Each section independently toggleable in `pal-settings.json → dynamicContext`

### Semi-Static Registry

All semi-static sources are defined in `src/hooks/lib/semi-static.ts` via `getSemiStaticSources()`. Adding one entry there propagates automatically to every consumer — no other files need to change.

### Dynamic Injection Order

```
LoadContext.ts
  │
  ├─► Regenerate CLAUDE.md if template/telos changed
  │
  └─► Build system-reminder (dynamic sections only):
      1. loadAtStartup files (user-configured)
      2. Handoff note (in-progress work from last session)
      3. Due nudges (algorithm review, relationship reflect, learning analysis)
      4. Session intelligence (rating trend, algorithm performance)
      5. Recent interaction notes (last 2 days)
      6. Active projects
      7. Project session history (this project)
      8. Other recent learnings (other projects, titles only)
```

### Injection Budget

Claude Code caps each string a hook injects at **10,000 characters**: plain stdout as a whole, or each JSON field (`additionalContext`, `systemMessage`) on its own. Over the cap, the text is saved to a file and replaced with the file path plus a preview of the first 2,000 characters, and the model is never told to read the file. No setting raises the cap.

Design rules for anything injected by a hook (Tier 3 and per-prompt reminders):
- Stay under 10,000 characters per hook output.
- Put what matters most in the first 2,000 characters, in case a future change crosses the cap.
- Measure before adding a section: pipe a SessionStart payload into `LoadContext.ts` and count the bytes.

Tier 1 and Tier 2 load as memory files, not hook output, so this cap does not apply to them. Other agents' limits are not documented here; check them before relying on the same budget.

### On-Demand Context

Everything else loads via the routing table in CLAUDE.md. The AI reads files only when the current task requires that context — no upfront loading of the full system.

---

## Inference Routing Architecture

### Subscription-First Model

PAL's background inference (session naming, summaries, failure capture, wisdom graduation, etc.) routes through whichever subscription CLI is currently hosting the session. No API key is required by default — the user already pays for the subscription that comes with their agent.

### Routing Table

The active agent is detected via `getActiveAgent()` in `src/hooks/lib/agent.ts` (driven by the `PAL_AGENT` env var set per-target). The dispatcher in `src/hooks/lib/inference.ts` then routes to the matching CLI:

| Active agent | CLI invoked | Notes |
|--------------|-------------|-------|
| `claude` | `claude --print` | Inherits the user's Claude Code subscription |
| `opencode` | `opencode run` | Output parsed via `extractOpencodeText()` |
| `cursor` | `cursor-agent` | argv-only, no stdin |
| `copilot` | `copilot` | GitHub Copilot CLI |
| `codex` | `codex exec` | Falls back to `PAL_OPENAI_API_KEY` if the `codex` binary is missing |
| `antigravity` | `agy --input-format stream-json` | Runs in an empty temp workspace under a tool-less `pal-inference` agent, which keeps PAL's plugin rules out of the reply; Gemini Flash low (small) / high (medium). The run's conversation files are deleted afterwards |

If no CLI binary is available, the dispatcher falls back to `PAL_ANTHROPIC_API_KEY` (Haiku via the Anthropic API) or `PAL_OPENAI_API_KEY` (OpenAI API). `canInfer()` returns `false` only when both routes are unavailable — handlers then skip silently.

### Recursion Defense (Three Layers)

Spawning a CLI from inside a hook risks the child re-triggering the same hooks, leading to an infinite spawn loop. Three defenses, all active at once:

1. **CLI flags** — Mirrors PAI's pattern: `--setting-sources ''` and `--tools ''` strip the child of its hook config and tool access entirely (claude, opencode equivalents).
2. **Sentinel env var** — `PAL_SPAWNED_INFERENCE=1` is set on every spawn. Hook entry points call `exitIfSpawnedInference()` and bail before doing any work.
3. **Depth counter** — `PAL_SPAWN_DEPTH` increments per hop; the circuit breaks at `MAX_DEPTH=1`. Belt-and-suspenders for the above.

The `CLAUDECODE` env var is unset *scoped to the child process only* (not deleted from the parent's environment) so the spawned `claude` CLI doesn't think it's nested inside another Claude session.

### Detached Inference Pattern

Long inference calls (session intelligence, failure principle extraction, auto-graduate) run via `spawnDetachedInference()`. The parent hook writes the transcript to a tmp file, spawns a detached child with `--run <args>`, and returns immediately. The child reads the tmp file, runs inference, writes results to disk, and unlinks the tmp file.

```
StopOrchestrator
  │
  ├─► detachSessionIntelligence(transcript, sessionId)
  │     └─► [child] inference → session-learning/YYYY-MM/...
  │
  ├─► detachFailurePrinciple(transcript)
  │     ├─► Atomic claim: rename pending-failure.json → tmp/pending.json
  │     │   (ENOENT means another concurrent Stop hook claimed it — benign)
  │     └─► [child] inference → failures/YYYY-MM/...
  │
  └─► detachAutoGraduate()
        └─► [child] 24h TTL + content dedup → wisdom/frames/...
```

The orchestrator returns in milliseconds; subscription-CLI cold-start latency no longer blocks the user's next prompt.

### Resilience

- **Empty-abort retry** — If a CLI returns an empty stdout (burst-concurrency race), retry once with 500–1500ms jitter
- **Test kill-switch** — `PAL_INFERENCE_DISABLED=1` (preloaded by `test/setup.ts`) short-circuits all inference so tests never spawn real CLIs
- **Per-call attribution** — Every inference call logs `caller=X sessionId=Y` so debug.log makes routing trivially auditable
- **Doctor probes** — `pal cli doctor` actually invokes each route and reports which agents are correctly wired

---

## Security Architecture

### Fail-Open Design

`SecurityValidator.ts` runs on PreToolUse for Bash commands. It blocks known-dangerous patterns but never prevents legitimate work:

- **Blocked**: `rm -rf /`, `chmod 777`, known secret file paths, command injection patterns
- **Allowed**: Everything else passes through
- **On error**: The hook exits silently — a broken security hook never blocks the user

### What's Protected

- Dangerous shell commands (recursive deletes, permission changes)
- Known secret file paths (.env, credentials, key files)
- Command injection patterns in arguments

### What's NOT Protected

PAL does not implement:
- Network-level security (no firewall, no proxy)
- File encryption (memory is plaintext on disk)
- Access control (anyone with filesystem access can read memory)

Users are responsible for securing their own machine and API keys.

---

## Cross-Platform Architecture

### Agent Abstraction

```
src/targets/
├── claude/          # Claude Code specific
│   ├── install.ts   # Register hooks + skills in ~/.claude/settings.json
│   └── uninstall.ts
├── opencode/        # opencode specific
│   ├── install.ts   # Register hooks + skills in opencode config
│   ├── uninstall.ts
│   └── plugin.ts    # opencode plugin interface
├── cursor/          # Cursor specific
│   ├── install.ts   # Register hooks + skills in ~/.cursor/
│   └── uninstall.ts
├── copilot/         # GitHub Copilot specific
│   ├── install.ts   # Write instruction files + update VS Code settings
│   └── uninstall.ts
├── codex/           # Codex specific
│   ├── install.ts   # Merge hooks into ~/.codex/hooks.json + skills + AGENTS.md
│   └── uninstall.ts
└── lib.ts           # Shared: JSON read/write, settings merge, TELOS scaffold
```

All five targets install hooks, skills, and AGENTS.md. Codex does not yet support subagents.

### Path Resolution

All paths resolve through `src/hooks/lib/paths.ts`:

| Path | Default | Override |
|------|---------|----------|
| PAL home | `~/.pal` | `PAL_HOME` |
| PAL package | Auto-detected from source | `PAL_PKG` |
| Claude config | `~/.claude` | `PAL_CLAUDE_DIR` |
| opencode config | `~/.config/opencode` | `PAL_OPENCODE_DIR` |
| Cursor config | `~/.cursor` | `PAL_CURSOR_DIR` |
| Copilot config | `~/.copilot` | `PAL_COPILOT_DIR` |
| Codex config | `~/.codex` | `PAL_CODEX_DIR` |
| Antigravity CLI plugin | `~/.gemini/config/plugins/pal` | `PAL_GEMINI_DIR` (replaces `~/.gemini`) |
| Antigravity CLI settings (PAL's command allowlist only) | `~/.gemini/antigravity-cli/settings.json` | `PAL_GEMINI_DIR` |
| Agents dir | `~/.agents` | `PAL_AGENTS_DIR` |

### Portability Contract

- Core logic (`src/hooks/lib/`, `src/tools/`) has zero agent-specific imports
- Agent-specific code lives only in `src/targets/`
- Skills reference no agent-specific APIs
- Memory format is plain JSON/JSONL/Markdown — readable by any tool

---

## File Naming Conventions

| Type | Convention | Example |
|------|------------|---------|
| Skill directory | kebab-case | `extract-wisdom/`, `first-principles/` |
| SKILL.md | Uppercase | `SKILL.md` |
| Hook files | PascalCase | `LoadContext.ts`, `StopOrchestrator.ts` |
| Handler files | kebab-case | `session-name.ts`, `work-learning.ts` |
| Library files | kebab-case | `text-similarity.ts`, `signal-trends.ts` |
| Tool files | kebab-case | `relationship-reflect.ts`, `token-cost.ts` |
| Memory files | date-prefixed | `2026-03-24.md`, `2026-03-24_weekly.md` |
| Template files | UPPER_SNAKE | `ALGORITHM.md`, `MEMORY_SYSTEM.md` |
