/**
 * Context Repository Extension
 *
 * Git-backed persistent memory filesystem for pi agents, inspired by
 * Letta Code's "Context Repositories" concept.
 *
 * Memory is stored as markdown files with YAML frontmatter in a git repo.
 * Files in `system/` are always loaded into the system prompt.
 * The full file tree is always visible so the agent can `read` any file on demand.
 *
 * Features (from Letta's design):
 * - Files as memory units with frontmatter (description, limit, read_only)
 * - `system/` subdir pinned to system prompt (always-loaded context)
 * - Progressive disclosure via file tree (agent reads what it needs)
 * - Git versioning with informative commits
 * - Pre-commit hook validates frontmatter (description required, limit positive int,
 *   read_only protected - agent can't add/remove/change it)
 * - Character limit enforcement in memory_write
 * - Backup/restore with timestamped snapshots
 * - Periodic memory reflection reminders (every N turns)
 */

import {
  chmodSync,
  cpSync,
  existsSync,
  mkdirSync,
  readdirSync,
  readFileSync,
  rmSync,
  statSync,
  writeFileSync,
} from "node:fs";
import { join, relative, resolve } from "node:path";
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
import { Type } from "@sinclair/typebox";
import type { Dirent } from "node:fs";

// --- Helpers ---

/** Shorthand for tool results (satisfies AgentToolResult<unknown>) */
function toolResult(text: string) {
  return { content: [{ type: "text" as const, text }], details: undefined };
}

// --- Constants ---

const MEMORY_DIR_NAME = ".pi/memory";
const SYSTEM_DIR = "system";
const EXT_TYPE = "context-repo";
const ALLOWED_FM_KEYS = new Set(["description", "limit", "read_only"]);
const DEFAULT_REFLECTION_INTERVAL = 15; // turns between reflection reminders
const DEFAULT_REFLECTION_TRIGGER = "step-count";
const TREE_LIMIT_ENV = {
  maxLines: "PI_CONTEXT_REPO_TREE_MAX_LINES",
  maxChars: "PI_CONTEXT_REPO_TREE_MAX_CHARS",
  maxChildrenPerDir: "PI_CONTEXT_REPO_TREE_MAX_CHILDREN_PER_DIR",
} as const;
const TREE_LIMIT_DEFAULTS = {
  maxLines: 500,
  maxChars: 20_000,
  maxChildrenPerDir: 50,
} as const;
const REFLECTION_RUNTIME_DIR_NAME = "reflection-runtime";
const REFLECTION_TRANSCRIPT_ENTRY_LIMIT = 80;
const REFLECTION_TRANSCRIPT_CHAR_LIMIT = 20_000;
export const REFLECTION_LAUNCH_EVENT = "pi-context-repo:reflection-launch";

// --- Frontmatter helpers ---

export type ReflectionTrigger = "off" | "step-count" | "compaction-event";

export interface ReflectionSettings {
  trigger: ReflectionTrigger;
  stepCount: number;
}

export interface Frontmatter {
  description?: string;
  limit?: number;
  read_only?: boolean;
  [key: string]: unknown;
}

export function parseFrontmatter(content: string): { frontmatter: Frontmatter; body: string } {
  const match = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
  if (!match?.[1] || !match[2]) return { frontmatter: {}, body: content };

  const fm: Frontmatter = {};
  for (const line of match[1].split("\n")) {
    const i = line.indexOf(":");
    if (i <= 0) continue;
    const key = line.slice(0, i).trim();
    const val = line.slice(i + 1).trim();
    if (key === "limit") fm.limit = parseInt(val, 10);
    else if (key === "read_only") fm.read_only = val === "true";
    else fm[key] = val;
  }
  return { frontmatter: fm, body: match[2].trimStart() };
}

export function buildFrontmatter(fm: Frontmatter): string {
  const lines: string[] = ["---"];
  if (fm.description) lines.push(`description: ${fm.description}`);
  if (fm.limit) lines.push(`limit: ${fm.limit}`);
  if (fm.read_only) lines.push(`read_only: true`);
  lines.push("---");
  return lines.join("\n");
}

/**
 * Validate frontmatter for a memory file. Returns array of error strings.
 * Mirrors Letta's pre-commit hook validation logic.
 */
export function validateFrontmatter(
  content: string,
  filePath: string,
  existingContent?: string | undefined
): string[] {
  const errors: string[] = [];
  const { frontmatter } = parseFrontmatter(content);

  // Frontmatter required
  if (!content.startsWith("---\n")) {
    errors.push(`${filePath}: missing frontmatter (must start with ---)`);
    return errors;
  }

  // Check closing ---
  const rest = content.slice(4);
  if (!rest.includes("\n---\n")) {
    errors.push(`${filePath}: frontmatter opened but never closed`);
    return errors;
  }

  // Extract raw frontmatter keys for unknown key check
  const fmBlock = content.slice(4, content.indexOf("\n---\n", 4));
  for (const line of fmBlock.split("\n")) {
    const i = line.indexOf(":");
    if (i <= 0) continue;
    const key = line.slice(0, i).trim();
    if (!ALLOWED_FM_KEYS.has(key)) {
      errors.push(
        `${filePath}: unknown frontmatter key '${key}' (allowed: description, limit, read_only)`
      );
    }
  }

  // Required fields
  if (!frontmatter.description) {
    errors.push(`${filePath}: missing required field 'description'`);
  }
  if (frontmatter.limit === undefined || frontmatter.limit === null) {
    errors.push(`${filePath}: missing required field 'limit'`);
  } else if (!Number.isInteger(frontmatter.limit) || frontmatter.limit <= 0) {
    errors.push(`${filePath}: 'limit' must be a positive integer, got '${frontmatter.limit}'`);
  }

  // Protected field: read_only
  if (existingContent) {
    const existing = parseFrontmatter(existingContent);

    // If file was read_only, reject any modification
    if (existing.frontmatter.read_only) {
      errors.push(`${filePath}: file is read_only and cannot be modified`);
      return errors;
    }

    // Agent can't change read_only value
    if (frontmatter.read_only !== existing.frontmatter.read_only) {
      errors.push(`${filePath}: 'read_only' is a protected field and cannot be changed`);
    }
  } else {
    // New file - agent can't set read_only
    if (frontmatter.read_only) {
      errors.push(`${filePath}: 'read_only' is a protected field and cannot be set by the agent`);
    }
  }

  return errors;
}

// --- Pre-commit hook (adapted from Letta's PRE_COMMIT_HOOK_SCRIPT) ---

const PRE_COMMIT_HOOK_SCRIPT = `#!/usr/bin/env bash
# Validate frontmatter in staged memory .md files
# Installed by pi context-repo extension

AGENT_EDITABLE_KEYS="description limit"
PROTECTED_KEYS="read_only"
ALL_KNOWN_KEYS="description limit read_only"
errors=""

get_fm_value() {
  local content="$1" key="$2"
  local closing_line
  closing_line=$(echo "$content" | tail -n +2 | grep -n '^---$' | head -1 | cut -d: -f1)
  [ -z "$closing_line" ] && return
  echo "$content" | tail -n +2 | head -n $((closing_line - 1)) | grep "^$key:" | cut -d: -f2- | sed 's/^ *//;s/ *$//'
}

for file in $(git diff --cached --name-only --diff-filter=ACM | grep '\\.md$'); do
  staged=$(git show ":$file")

  first_line=$(echo "$staged" | head -1)
  if [ "$first_line" != "---" ]; then
    errors="$errors\\n  $file: missing frontmatter (must start with ---)"
    continue
  fi

  closing_line=$(echo "$staged" | tail -n +2 | grep -n '^---$' | head -1 | cut -d: -f1)
  if [ -z "$closing_line" ]; then
    errors="$errors\\n  $file: frontmatter opened but never closed (missing closing ---)"
    continue
  fi

  head_content=$(git show "HEAD:$file" 2>/dev/null || true)
  if [ -n "$head_content" ]; then
    head_ro=$(get_fm_value "$head_content" "read_only")
    if [ "$head_ro" = "true" ]; then
      errors="$errors\\n  $file: file is read_only and cannot be modified"
      continue
    fi
  fi

  frontmatter=$(echo "$staged" | tail -n +2 | head -n $((closing_line - 1)))

  has_description=false
  has_limit=false

  while IFS= read -r line; do
    [ -z "$line" ] && continue
    key=$(echo "$line" | cut -d: -f1 | tr -d ' ')
    value=$(echo "$line" | cut -d: -f2- | sed 's/^ *//;s/ *$//')

    known=false
    for k in $ALL_KNOWN_KEYS; do
      if [ "$key" = "$k" ]; then known=true; break; fi
    done
    if [ "$known" = "false" ]; then
      errors="$errors\\n  $file: unknown frontmatter key '$key' (allowed: $ALL_KNOWN_KEYS)"
      continue
    fi

    for k in $PROTECTED_KEYS; do
      if [ "$key" = "$k" ]; then
        if [ -n "$head_content" ]; then
          head_val=$(get_fm_value "$head_content" "$k")
          if [ "$value" != "$head_val" ]; then
            errors="$errors\\n  $file: '$k' is a protected field and cannot be changed by the agent"
          fi
        else
          errors="$errors\\n  $file: '$k' is a protected field and cannot be set by the agent"
        fi
      fi
    done

    case "$key" in
      limit)
        has_limit=true
        if ! echo "$value" | grep -qE '^[0-9]+$' || [ "$value" = "0" ]; then
          errors="$errors\\n  $file: 'limit' must be a positive integer, got '$value'"
        fi
        ;;
      description)
        has_description=true
        if [ -z "$value" ]; then
          errors="$errors\\n  $file: 'description' must not be empty"
        fi
        ;;
    esac
  done <<< "$frontmatter"

  if [ "$has_description" = "false" ]; then
    errors="$errors\\n  $file: missing required field 'description'"
  fi
  if [ "$has_limit" = "false" ]; then
    errors="$errors\\n  $file: missing required field 'limit'"
  fi

  if [ -n "$head_content" ]; then
    for k in $PROTECTED_KEYS; do
      head_val=$(get_fm_value "$head_content" "$k")
      if [ -n "$head_val" ]; then
        staged_val=$(get_fm_value "$staged" "$k")
        if [ -z "$staged_val" ]; then
          errors="$errors\\n  $file: '$k' is a protected field and cannot be removed by the agent"
        fi
      fi
    done
  fi
done

if [ -n "$errors" ]; then
  echo "Frontmatter validation failed:"
  echo -e "$errors"
  exit 1
fi
`;

export function installPreCommitHook(memDir: string): void {
  const hooksDir = join(memDir, ".git", "hooks");
  const hookPath = join(hooksDir, "pre-commit");
  mkdirSync(hooksDir, { recursive: true });
  writeFileSync(hookPath, PRE_COMMIT_HOOK_SCRIPT, "utf-8");
  chmodSync(hookPath, 0o755);
}

// --- File tree ---
// Adapted from Letta's bounded renderMemoryFilesystemTree() flow.

export interface BuildTreeOptions {
  maxLines?: number;
  maxChars?: number;
  maxChildrenPerDir?: number;
}

interface TreeNode {
  line: string;
  isDirectory: boolean;
  children: TreeNode[];
}

interface RenderedTreeLine {
  text: string;
  kind: "entry" | "omitted";
}

interface BuildTreeState {
  lines: RenderedTreeLine[];
  totalChars: number;
  shownEntries: number;
}

function normalizePositiveInt(value: unknown, fallback: number, min: number, max: number): number {
  const parsed =
    typeof value === "number"
      ? value
      : typeof value === "string" && value.trim()
        ? Number.parseInt(value.trim(), 10)
        : Number.NaN;

  if (!Number.isInteger(parsed) || parsed < min || parsed > max) {
    return fallback;
  }

  return parsed;
}

function getBuildTreeLimits(options: BuildTreeOptions = {}) {
  return {
    maxLines: normalizePositiveInt(
      options.maxLines ?? process.env[TREE_LIMIT_ENV.maxLines],
      TREE_LIMIT_DEFAULTS.maxLines,
      2,
      50_000
    ),
    maxChars: normalizePositiveInt(
      options.maxChars ?? process.env[TREE_LIMIT_ENV.maxChars],
      TREE_LIMIT_DEFAULTS.maxChars,
      128,
      5_000_000
    ),
    maxChildrenPerDir: normalizePositiveInt(
      options.maxChildrenPerDir ?? process.env[TREE_LIMIT_ENV.maxChildrenPerDir],
      TREE_LIMIT_DEFAULTS.maxChildrenPerDir,
      1,
      5_000
    ),
  };
}

function readTreeNodes(dir: string): TreeNode[] {
  const entries = readdirSync(dir, { withFileTypes: true })
    .filter((e: Dirent) => !e.name.startsWith("."))
    .sort((a: Dirent, b: Dirent) => {
      if (a.isDirectory() && !b.isDirectory()) return -1;
      if (!a.isDirectory() && b.isDirectory()) return 1;
      return a.name.localeCompare(b.name);
    });

  const nodes: TreeNode[] = [];
  for (const entry of entries) {
    const fullPath = join(dir, entry.name);

    if (entry.isDirectory()) {
      nodes.push({
        line: `${entry.name}/`,
        isDirectory: true,
        children: readTreeNodes(fullPath),
      });
      continue;
    }

    if (!entry.name.endsWith(".md")) {
      continue;
    }

    const content = readFileSync(fullPath, "utf-8");
    const { frontmatter } = parseFrontmatter(content);
    const desc = frontmatter.description ? ` - ${frontmatter.description}` : "";
    const ro = frontmatter.read_only ? " [read-only]" : "";
    nodes.push({
      line: `${entry.name}${desc}${ro}`,
      isDirectory: false,
      children: [],
    });
  }

  return nodes;
}

function countTreeEntries(nodes: TreeNode[]): number {
  return nodes.reduce((total, node) => total + 1 + countTreeEntries(node.children), 0);
}

function canAppendTreeLine(
  state: BuildTreeState,
  line: string,
  limits: ReturnType<typeof getBuildTreeLimits>
) {
  const nextLineCount = state.lines.length + 1;
  const nextCharCount = state.totalChars + (state.lines.length > 0 ? 1 : 0) + line.length;
  return nextLineCount <= limits.maxLines && nextCharCount <= limits.maxChars;
}

function pushTreeLine(state: BuildTreeState, text: string, kind: RenderedTreeLine["kind"]): void {
  state.totalChars += (state.lines.length > 0 ? 1 : 0) + text.length;
  state.lines.push({ text, kind });
  if (kind === "entry") {
    state.shownEntries += 1;
  }
}

function popTreeLine(state: BuildTreeState): RenderedTreeLine | undefined {
  const removed = state.lines.pop();
  if (!removed) return undefined;
  state.totalChars -= (state.lines.length > 0 ? 1 : 0) + removed.text.length;
  if (removed.kind === "entry") {
    state.shownEntries -= 1;
  }
  return removed;
}

function renderTreeNodes(
  nodes: TreeNode[],
  prefix: string,
  limits: ReturnType<typeof getBuildTreeLimits>,
  state: BuildTreeState
): boolean {
  const visibleNodes = nodes.slice(0, limits.maxChildrenPerDir);
  const omittedEntries = Math.max(0, nodes.length - visibleNodes.length);
  const items: Array<
    { kind: "entry"; node: TreeNode } | { kind: "omitted"; omittedCount: number }
  > = visibleNodes.map((node) => ({ kind: "entry", node }));

  if (omittedEntries > 0) {
    items.push({ kind: "omitted", omittedCount: omittedEntries });
  }

  for (const [index, item] of items.entries()) {
    const isLast = index === items.length - 1;
    const connector = isLast ? "└── " : "├── ";
    const line =
      item.kind === "entry"
        ? `${prefix}${connector}${item.node.line}`
        : `${prefix}${connector}... (${item.omittedCount.toLocaleString()} more entries)`;

    if (!canAppendTreeLine(state, line, limits)) {
      return false;
    }

    pushTreeLine(state, line, item.kind === "entry" ? "entry" : "omitted");

    if (item.kind === "entry" && item.node.isDirectory && item.node.children.length > 0) {
      const childPrefix = prefix + (isLast ? "    " : "│   ");
      if (!renderTreeNodes(item.node.children, childPrefix, limits, state)) {
        return false;
      }
    }
  }

  return true;
}

export function buildTree(dir: string, prefix = "", options: BuildTreeOptions = {}): string[] {
  if (!existsSync(dir)) return [];

  const limits = getBuildTreeLimits(options);
  const nodes = readTreeNodes(dir);
  const totalEntries = countTreeEntries(nodes);
  const state: BuildTreeState = {
    lines: [],
    totalChars: 0,
    shownEntries: 0,
  };

  const fullyRendered = renderTreeNodes(nodes, prefix, limits, state);

  if (!fullyRendered && totalEntries > 0) {
    while (state.lines.length > 0) {
      const omittedEntries = Math.max(1, totalEntries - state.shownEntries);
      const notice = `[Tree truncated: showing ${state.shownEntries.toLocaleString()} of ${totalEntries.toLocaleString()} entries. ${omittedEntries.toLocaleString()} omitted.]`;

      if (canAppendTreeLine(state, notice, limits)) {
        pushTreeLine(state, notice, "omitted");
        break;
      }

      popTreeLine(state);
    }
  }

  return state.lines.map((line) => line.text);
}

function resolveUnderMemory(memDir: string, memoryPath: string): string | null {
  const resolved = resolve(memDir, memoryPath);
  if (resolved === memDir || resolved.startsWith(memDir + "/")) {
    return resolved;
  }
  return null;
}

function listMarkdownFilesRecursive(dir: string, root = dir): string[] {
  if (!existsSync(dir)) return [];

  const entries = readdirSync(dir, { withFileTypes: true })
    .filter((e) => !e.name.startsWith("."))
    .sort((a, b) => {
      if (a.isDirectory() && !b.isDirectory()) return -1;
      if (!a.isDirectory() && b.isDirectory()) return 1;
      return a.name.localeCompare(b.name);
    });

  const files: string[] = [];
  for (const entry of entries) {
    const fullPath = join(dir, entry.name);
    if (entry.isDirectory()) {
      files.push(...listMarkdownFilesRecursive(fullPath, root));
      continue;
    }
    if (entry.name.endsWith(".md")) {
      files.push(relative(root, fullPath));
    }
  }

  return files;
}

// --- Load system/ files (recursive) ---

export function loadSystemFiles(memDir: string, dir?: string): string {
  const targetDir = dir || join(memDir, SYSTEM_DIR);
  if (!existsSync(targetDir)) return "";

  const entries = readdirSync(targetDir, { withFileTypes: true }).sort((a: Dirent, b: Dirent) => {
    if (a.isDirectory() && !b.isDirectory()) return -1;
    if (!a.isDirectory() && b.isDirectory()) return 1;
    return a.name.localeCompare(b.name);
  });

  const sections: string[] = [];
  for (const entry of entries) {
    const fullPath = join(targetDir, entry.name);
    const relPath = relative(memDir, fullPath);

    if (entry.isDirectory()) {
      const sub = loadSystemFiles(memDir, fullPath);
      if (sub) sections.push(sub);
    } else if (entry.name.endsWith(".md")) {
      const content = readFileSync(fullPath, "utf-8");
      const { body } = parseFrontmatter(content);
      if (body.trim()) {
        sections.push(`<${relPath}>\n${body.trim()}\n</${relPath}>`);
      }
    }
  }
  return sections.join("\n\n");
}

// --- Git helpers ---

async function git(
  pi: ExtensionAPI,
  cwd: string,
  args: string[]
): Promise<{ stdout: string; stderr: string }> {
  return pi.exec("git", ["-c", "commit.gpgsign=false", "-C", cwd, ...args]);
}

async function isGitRepo(pi: ExtensionAPI, dir: string): Promise<boolean> {
  try {
    await git(pi, dir, ["rev-parse", "--git-dir"]);
    return true;
  } catch {
    return false;
  }
}

async function initRepo(pi: ExtensionAPI, dir: string): Promise<void> {
  await git(pi, dir, ["init"]);
  await git(pi, dir, ["add", "."]);
  await git(pi, dir, ["commit", "-m", "init: context repository"]);
  installPreCommitHook(dir);
}

async function hasRemote(pi: ExtensionAPI, dir: string): Promise<boolean> {
  try {
    const { stdout } = await git(pi, dir, ["remote"]);
    return stdout.trim().length > 0;
  } catch {
    return false;
  }
}

async function getAheadCount(pi: ExtensionAPI, dir: string): Promise<number> {
  try {
    const { stdout } = await git(pi, dir, ["rev-list", "--count", "@{u}..HEAD"]);
    return parseInt(stdout.trim(), 10) || 0;
  } catch {
    return 0; // no upstream configured
  }
}

export interface MemoryStatus {
  dirty: boolean;
  files: string[];
  aheadOfRemote: boolean;
  aheadCount: number;
  hasRemote: boolean;
  summary: string;
}

async function getStatus(pi: ExtensionAPI, dir: string): Promise<MemoryStatus> {
  const { stdout } = await git(pi, dir, ["status", "--porcelain"]);
  const files = stdout
    .trim()
    .split("\n")
    .filter((l) => l.trim());
  const dirty = files.length > 0;

  const remote = await hasRemote(pi, dir);
  let aheadCount = 0;
  let aheadOfRemote = false;
  if (remote) {
    aheadCount = await getAheadCount(pi, dir);
    aheadOfRemote = aheadCount > 0;
  }

  const parts: string[] = [];
  if (dirty) parts.push(`${files.length} uncommitted change(s)`);
  if (aheadOfRemote) parts.push(`${aheadCount} unpushed commit(s)`);

  return {
    dirty,
    files,
    aheadOfRemote,
    aheadCount,
    hasRemote: remote,
    summary: parts.length > 0 ? parts.join(", ") : "clean",
  };
}

export function statusWidget(status: MemoryStatus): string[] {
  const parts: string[] = [];
  if (status.dirty) parts.push(`${status.files.length} uncommitted`);
  if (status.aheadOfRemote) parts.push(`${status.aheadCount} unpushed`);
  return [`Memory: ${parts.length > 0 ? parts.join(", ") : "clean"}`];
}

async function pullFromRemote(
  pi: ExtensionAPI,
  dir: string
): Promise<{ updated: boolean; summary: string }> {
  try {
    const { stdout, stderr } = await git(pi, dir, ["pull", "--ff-only"]);
    const output = stdout + stderr;
    const updated = !output.includes("Already up to date");
    return { updated, summary: updated ? output.trim() : "Already up to date" };
  } catch {
    // ff-only failed (diverged), try rebase
    try {
      const { stdout, stderr } = await git(pi, dir, ["pull", "--rebase"]);
      return { updated: true, summary: (stdout + stderr).trim() };
    } catch (e) {
      const msg = e instanceof Error ? e.message : String(e);
      return { updated: false, summary: `Pull failed: ${msg}` };
    }
  }
}

// --- Backup helpers (adapted from Letta's memfs.ts) ---

export function formatBackupTimestamp(date = new Date()): string {
  const pad = (v: number) => String(v).padStart(2, "0");
  return `${date.getFullYear()}${pad(date.getMonth() + 1)}${pad(date.getDate())}-${pad(date.getHours())}${pad(date.getMinutes())}${pad(date.getSeconds())}`;
}

function getBackupDir(memDir: string): string {
  return join(memDir, "..", "memory-backups");
}

function listBackups(memDir: string): Array<{ name: string; path: string; createdAt: string }> {
  const backupRoot = getBackupDir(memDir);
  if (!existsSync(backupRoot)) return [];

  return readdirSync(backupRoot, { withFileTypes: true })
    .filter((e) => e.isDirectory() && e.name.startsWith("backup-"))
    .map((e) => {
      const path = join(backupRoot, e.name);
      const stat = statSync(path);
      return { name: e.name, path, createdAt: stat.mtime.toISOString() };
    })
    .sort((a, b) => a.name.localeCompare(b.name));
}

// --- Reflection reminder text (adapted from Letta's prompts) ---

const MEMORY_CHECK_REMINDER = `<system-reminder>
MEMORY CHECK: Review this conversation for information worth storing in your memory blocks. Update memory silently (no confirmation needed) if you learned:

- **User info**: Name, role, preferences, working style, current work/goals
- **Project details**: Architecture, patterns, gotchas, dependencies, conventions, workflow rules
- **Corrections**: User corrected you or clarified something important
- **Preferences**: How they want you to behave, communicate, or approach tasks

Ask yourself: "If I started a new session tomorrow, what from this conversation would I want to remember?" If the answer is meaningful, update the appropriate memory block(s) now.
</system-reminder>`;

// --- /remember prompt ---

const REMEMBER_PROMPT = `<system-reminder>
The user has invoked /remember, requesting you commit something to memory.

## What to do

1. **Identify what to remember**: Look at recent conversation context. If they provided text after /remember, that's the target. Otherwise infer from context.

2. **Determine the right memory file**: Use memory_write to store in the appropriate file. Consider:
   - User preferences → system/user.md
   - Coding style → system/style.md
   - Project knowledge → system/project.md
   - Agent behavior → system/persona.md
   - Create a new file if no existing file fits

3. **Confirm the update**: After writing, briefly confirm what you remembered and where.

## Guidelines
- Be concise - distill to essence
- Avoid duplicates - check existing content first
- Match existing file formatting
- If unclear, ask the user to clarify
</system-reminder>`;

// --- Reflection bundles / launch contract ---

export type ReflectionTriggerSource = "manual" | "step-count" | "compaction-event";

interface ReflectionSessionManagerLike {
  getSessionId(): string;
  getSessionFile(): string | undefined;
  getBranch(fromId?: string): unknown[];
}

export interface ReflectionBundle {
  bundleId: string;
  bundleDir: string;
  triggerSource: ReflectionTriggerSource;
  createdAt: string;
  sessionId: string;
  sessionFile?: string;
  transcriptPath: string;
  promptPath: string;
  metadataPath: string;
  memoryDir: string;
  entryCount: number;
}

export interface ReflectionRuntimeState {
  latestBundle?: ReflectionBundle;
  lastLaunchMode?: "prepared" | "background-subagent" | "reminder-fallback";
  lastLaunchNotes?: string[];
}

export interface ReflectionLaunchRequest {
  type: "background-reflection-launch";
  triggerSource: ReflectionTriggerSource;
  bundle: ReflectionBundle;
  notes: string[];
  accepted: boolean;
  accept: (note?: string) => void;
  note: (message: string) => void;
}

export interface ReflectionLaunchResult {
  launched: boolean;
  mode: "background-subagent" | "reminder-fallback";
  bundle: ReflectionBundle;
  notes: string[];
}

export interface ReflectionPromptInput {
  transcriptPath: string;
  promptPath?: string;
  memoryDir: string;
  cwd?: string;
  sessionFile?: string;
  triggerSource: ReflectionTriggerSource;
  memoryTree?: string;
  systemContent?: string;
}

function normalizePromptPath(path: string | undefined): string | undefined {
  return path?.replace(/\\/g, "/");
}

export function getReflectionRuntimeDir(memDir: string): string {
  return join(memDir, "..", REFLECTION_RUNTIME_DIR_NAME);
}

function getReflectionStatePath(memDir: string): string {
  return join(getReflectionRuntimeDir(memDir), "state.json");
}

function sanitizePathSegment(segment: string): string {
  const sanitized = segment.replace(/[^a-zA-Z0-9._-]/g, "_").trim();
  return sanitized || "session";
}

export function loadReflectionRuntimeState(memDir: string): ReflectionRuntimeState {
  const statePath = getReflectionStatePath(memDir);
  if (!existsSync(statePath)) return {};
  try {
    return JSON.parse(readFileSync(statePath, "utf-8")) as ReflectionRuntimeState;
  } catch {
    return {};
  }
}

function saveReflectionRuntimeState(memDir: string, state: ReflectionRuntimeState): void {
  const runtimeDir = getReflectionRuntimeDir(memDir);
  mkdirSync(runtimeDir, { recursive: true });
  writeFileSync(getReflectionStatePath(memDir), JSON.stringify(state, null, 2) + "\n");
}

function truncateTranscript(text: string, maxChars = REFLECTION_TRANSCRIPT_CHAR_LIMIT): string {
  if (text.length <= maxChars) return text;
  const suffix = "\n\n[Transcript truncated by pi-context-repo reflection bundle]\n";
  return text.slice(0, Math.max(0, maxChars - suffix.length)).trimEnd() + suffix;
}

function contentToText(content: unknown): string {
  if (content == null) return "";
  if (typeof content === "string") return content;
  if (Array.isArray(content)) {
    return content
      .map((part) => contentToText(part))
      .filter(Boolean)
      .join("\n")
      .trim();
  }
  if (typeof content === "object") {
    const obj = content as Record<string, unknown>;
    if (typeof obj.text === "string") return obj.text;
    if (typeof obj.content === "string") return obj.content;
    if (Array.isArray(obj.content)) return contentToText(obj.content);
    if (typeof obj.input === "string") return obj.input;
    if (typeof obj.output === "string") return obj.output;
    if (typeof obj.result === "string") return obj.result;
    if (typeof obj.args === "string") return obj.args;
    if (typeof obj.name === "string" && typeof obj.arguments === "string") {
      return `${obj.name}(${obj.arguments})`;
    }
  }
  try {
    return JSON.stringify(content, null, 2);
  } catch {
    return String(content);
  }
}

function renderTranscriptEntry(entry: unknown): string | null {
  if (!entry || typeof entry !== "object") return null;
  const record = entry as Record<string, unknown>;
  if (record.type === "message") {
    const message = (record.message ?? {}) as Record<string, unknown>;
    const role = typeof message.role === "string" ? message.role : "message";
    const body = contentToText(message.content).trim() || "(no text content)";
    return `<${role}>\n${body}\n</${role}>`;
  }

  if (record.type === "custom_message") {
    const customType = typeof record.customType === "string" ? record.customType : "custom";
    const body = contentToText(record.content).trim() || "(no text content)";
    return `<custom-message type="${customType}">\n${body}\n</custom-message>`;
  }

  if (record.type === "compaction") {
    const summary = typeof record.summary === "string" ? record.summary.trim() : "";
    if (!summary) return null;
    return `<compaction-summary>\n${summary}\n</compaction-summary>`;
  }

  if (record.type === "branch_summary") {
    const summary = typeof record.summary === "string" ? record.summary.trim() : "";
    if (!summary) return null;
    return `<branch-summary>\n${summary}\n</branch-summary>`;
  }

  return null;
}

export function buildReflectionTranscript(
  entries: unknown[],
  options: { maxEntries?: number; maxChars?: number } = {}
): string {
  const maxEntries = options.maxEntries ?? REFLECTION_TRANSCRIPT_ENTRY_LIMIT;
  const maxChars = options.maxChars ?? REFLECTION_TRANSCRIPT_CHAR_LIMIT;
  const transcriptEntries = entries
    .map((entry) => renderTranscriptEntry(entry))
    .filter((entry): entry is string => Boolean(entry));
  const boundedEntries = transcriptEntries.slice(-maxEntries);
  const lines = [
    "# Reflection Transcript",
    "",
    "Prepared by pi-context-repo for Letta-style memory reflection.",
    "",
    ...boundedEntries.flatMap((entry) => [entry, ""]),
  ];
  return truncateTranscript(lines.join("\n").trimEnd(), maxChars);
}

export function buildReflectionPrompt(input: ReflectionPromptInput): string {
  const lines: string[] = [];
  const transcriptPath = normalizePromptPath(input.transcriptPath) || input.transcriptPath;
  const promptPath = normalizePromptPath(input.promptPath);
  const memoryDir = normalizePromptPath(input.memoryDir) || input.memoryDir;
  const sessionFile = normalizePromptPath(input.sessionFile);

  if (input.cwd) {
    lines.push(`Working directory: ${normalizePromptPath(input.cwd)}`);
    lines.push("");
  }

  lines.push(
    `A Letta-style reflection bundle has been prepared for trigger: ${input.triggerSource}.`,
    `Transcript: ${transcriptPath}`,
    `Memory directory: ${memoryDir}`
  );

  if (promptPath) {
    lines.push(`Prompt file: ${promptPath}`);
  }

  if (sessionFile) {
    lines.push(`Session file: ${sessionFile}`);
  }

  lines.push(
    "",
    "Review the transcript and update memory files if it would improve future sessions.",
    "Use system/ for always-needed context, and reference/ or other folders for lower-priority material.",
    "Be selective: fewer meaningful updates are better than many weak ones.",
    "Commit memory changes if you make them."
  );

  if (input.memoryTree) {
    lines.push("", "Current memory tree:", "```", input.memoryTree, "```");
  }

  if (input.systemContent) {
    lines.push("", "Pinned memory snapshot:", input.systemContent);
  }

  return lines.join("\n");
}

export function buildReflectionReminder(bundle?: ReflectionBundle): string {
  if (!bundle) {
    return MEMORY_CHECK_REMINDER;
  }

  const transcriptPath = normalizePromptPath(bundle.transcriptPath) || bundle.transcriptPath;
  const promptPath = normalizePromptPath(bundle.promptPath) || bundle.promptPath;
  const suffix = `\n\nReflection bundle prepared for deeper review:\n- Trigger: ${bundle.triggerSource}\n- Transcript: \`${transcriptPath}\`\n- Prompt: \`${promptPath}\`\nUse the bundle if you want a fuller transcript before deciding what to store.`;
  return MEMORY_CHECK_REMINDER.replace("</system-reminder>", `${suffix}\n</system-reminder>`);
}

export function buildManualReflectionPrompt(bundle: ReflectionBundle): string {
  const transcriptPath = normalizePromptPath(bundle.transcriptPath) || bundle.transcriptPath;
  const promptPath = normalizePromptPath(bundle.promptPath) || bundle.promptPath;
  const memoryDir = normalizePromptPath(bundle.memoryDir) || bundle.memoryDir;
  return `<system-reminder>
REFLECTION RUN: A reflection bundle has been prepared for this conversation.

- Transcript: \`${transcriptPath}\`
- Prompt: \`${promptPath}\`
- Memory directory: \`${memoryDir}\`

Review the prepared transcript, update memory if it would improve future sessions, and commit if you make changes.
</system-reminder>`;
}

export function getReflectionTriggerSource(
  turnCount: number,
  settings: ReflectionSettings,
  pendingCompactionReminder = false
): ReflectionTriggerSource | null {
  if (settings.trigger === "off") {
    return null;
  }

  if (settings.trigger === "compaction-event") {
    return pendingCompactionReminder ? "compaction-event" : null;
  }

  return turnCount > 0 && turnCount % settings.stepCount === 0 ? "step-count" : null;
}

export function prepareReflectionBundle(
  memDir: string,
  sessionManager: ReflectionSessionManagerLike,
  options: {
    triggerSource: ReflectionTriggerSource;
    cwd?: string;
    memoryTree?: string;
    systemContent?: string;
    maxEntries?: number;
    maxChars?: number;
  }
): ReflectionBundle {
  const sessionId = sanitizePathSegment(sessionManager.getSessionId());
  const bundleId = `${formatBackupTimestamp()}-${options.triggerSource}`;
  const runtimeDir = getReflectionRuntimeDir(memDir);
  const bundleDir = join(runtimeDir, sessionId, bundleId);
  mkdirSync(bundleDir, { recursive: true });

  const entries = sessionManager.getBranch();
  const transcriptPath = join(bundleDir, "transcript.md");
  const promptPath = join(bundleDir, "prompt.md");
  const metadataPath = join(bundleDir, "metadata.json");
  const sessionFile = sessionManager.getSessionFile();
  const transcript = buildReflectionTranscript(entries, {
    maxEntries: options.maxEntries,
    maxChars: options.maxChars,
  });
  writeFileSync(transcriptPath, transcript + (transcript.endsWith("\n") ? "" : "\n"));

  const prompt = buildReflectionPrompt({
    transcriptPath,
    promptPath,
    memoryDir: memDir,
    cwd: options.cwd,
    sessionFile,
    triggerSource: options.triggerSource,
    memoryTree: options.memoryTree,
    systemContent: options.systemContent,
  });
  writeFileSync(promptPath, prompt + (prompt.endsWith("\n") ? "" : "\n"));

  const bundle: ReflectionBundle = {
    bundleId,
    bundleDir,
    triggerSource: options.triggerSource,
    createdAt: new Date().toISOString(),
    sessionId,
    sessionFile,
    transcriptPath,
    promptPath,
    metadataPath,
    memoryDir: memDir,
    entryCount: entries.length,
  };

  writeFileSync(metadataPath, JSON.stringify(bundle, null, 2) + "\n");
  saveReflectionRuntimeState(memDir, {
    ...loadReflectionRuntimeState(memDir),
    latestBundle: bundle,
    lastLaunchMode: "prepared",
    lastLaunchNotes: [],
  });

  return bundle;
}

export function requestBackgroundReflectionLaunch(
  eventBus: Pick<ExtensionAPI, "events">["events"],
  bundle: ReflectionBundle,
  triggerSource: ReflectionTriggerSource
): ReflectionLaunchResult {
  let accepted = false;
  const notes: string[] = [];
  const request: ReflectionLaunchRequest = {
    type: "background-reflection-launch",
    triggerSource,
    bundle,
    notes,
    accepted: false,
    accept(note) {
      accepted = true;
      request.accepted = true;
      if (note) notes.push(note);
    },
    note(message) {
      if (message) notes.push(message);
    },
  };

  eventBus.emit(REFLECTION_LAUNCH_EVENT, request);

  return {
    launched: accepted,
    mode: accepted ? "background-subagent" : "reminder-fallback",
    bundle,
    notes: [...notes],
  };
}

// --- Worktree helpers ---

export function getWorktreeDir(memDir: string): string {
  return join(memDir, "..", "memory-worktrees");
}

export interface WorktreeInfo {
  branch: string;
  path: string;
}

/**
 * Create a git worktree for isolated memory edits.
 * Returns the worktree path and branch name.
 */
export async function createWorktree(
  pi: ExtensionAPI,
  memDir: string,
  prefix: string
): Promise<WorktreeInfo> {
  const worktreeDir = getWorktreeDir(memDir);
  mkdirSync(worktreeDir, { recursive: true });

  const branch = `${prefix}-${Date.now()}`;
  const wtPath = join(worktreeDir, branch);

  await git(pi, memDir, ["worktree", "add", wtPath, "-b", branch]);

  return { branch, path: wtPath };
}

/**
 * Merge a worktree branch back to main, then clean up.
 */
export async function mergeWorktree(
  pi: ExtensionAPI,
  memDir: string,
  wt: WorktreeInfo
): Promise<{ merged: boolean; pushed: boolean; summary: string }> {
  // Pull latest first
  if (await hasRemote(pi, memDir)) {
    try {
      await git(pi, memDir, ["pull", "--ff-only"]);
    } catch {
      try {
        await git(pi, memDir, ["pull", "--rebase"]);
      } catch {
        // continue - merge may still work
      }
    }
  }

  // Merge the branch
  let merged = false;
  try {
    await git(pi, memDir, ["merge", wt.branch, "--no-edit"]);
    merged = true;
  } catch (e) {
    const msg = e instanceof Error ? e.message : String(e);
    return { merged: false, pushed: false, summary: `Merge failed: ${msg}` };
  }

  // Push if remote configured
  let pushed = false;
  if (await hasRemote(pi, memDir)) {
    try {
      await git(pi, memDir, ["push"]);
      pushed = true;
    } catch {
      // non-fatal - local main has the merge
    }
  }

  // Clean up worktree and branch
  try {
    await git(pi, memDir, ["worktree", "remove", wt.path]);
    await git(pi, memDir, ["branch", "-d", wt.branch]);
  } catch {
    // non-fatal
  }

  const summary = pushed ? "Merged and pushed" : "Merged (push pending)";
  return { merged, pushed, summary };
}

// --- Prompt drift detection ---

export type DriftCode =
  | "legacy_memory_section"
  | "orphan_memory_fragment"
  | "duplicate_memory_section";

export interface PromptDrift {
  code: DriftCode;
  message: string;
}

/**
 * Detect memory-related drift in system prompt.
 * Identifies legacy sections, orphan fragments, and duplicates.
 */
export function detectPromptDrift(systemPrompt: string): PromptDrift[] {
  const drifts: PromptDrift[] = [];

  // Check for legacy "Memory" sections that aren't from context-repo
  const hasLegacyMemory =
    systemPrompt.includes("Your memory consists of core memory") ||
    systemPrompt.includes("composed of memory blocks");
  if (hasLegacyMemory) {
    drifts.push({
      code: "legacy_memory_section",
      message:
        "System prompt contains legacy memory-block language incompatible with context-repo.",
    });
  }

  // Check for orphan git sync fragments without full memory section
  const hasOrphanSync =
    systemPrompt.includes("git add system/") &&
    systemPrompt.includes('git commit -m "') &&
    !systemPrompt.includes("Context Repository (Agent Memory)");
  if (hasOrphanSync) {
    drifts.push({
      code: "orphan_memory_fragment",
      message:
        "System prompt contains orphaned memory sync fragment without full context-repo section.",
    });
  }

  // Check for duplicate context-repo sections
  const contextRepoMatches = systemPrompt.match(/## Context Repository \(Agent Memory\)/g);
  if (contextRepoMatches && contextRepoMatches.length > 1) {
    drifts.push({
      code: "duplicate_memory_section",
      message: `System prompt contains ${contextRepoMatches.length} duplicate Context Repository sections.`,
    });
  }

  return drifts;
}

/**
 * Strip managed memory sections from a system prompt.
 * Used before re-injecting to prevent duplicates.
 */
export function stripManagedMemorySections(systemPrompt: string): string {
  // Remove context-repo section (## Context Repository ... to next ## or end)
  let result = systemPrompt.replace(
    /\n## Context Repository \(Agent Memory\)[\s\S]*?(?=\n## [^C]|\n## $|$)/g,
    ""
  );

  // Remove system-reminder blocks injected by this extension
  result = result.replace(
    /<system-reminder>\nMEMORY (?:SYNC|REFLECTION|CHECK):[\s\S]*?<\/system-reminder>/g,
    ""
  );

  // Compact blank lines
  result = result.replace(/\n{3,}/g, "\n\n").trimEnd();

  return result;
}

// --- Per-agent settings ---

function normalizeReflectionStepCount(
  value: unknown,
  fallback = DEFAULT_REFLECTION_INTERVAL
): number {
  return normalizePositiveInt(value, fallback, 1, 50_000);
}

function normalizeReflectionTrigger(
  value: unknown,
  fallback: ReflectionTrigger = DEFAULT_REFLECTION_TRIGGER
): ReflectionTrigger {
  if (value === "off" || value === "step-count" || value === "compaction-event") {
    return value;
  }
  return fallback;
}

export function resolveReflectionSettings(
  raw: Partial<{
    reflectionTrigger: unknown;
    reflectionStepCount: unknown;
    reflectionInterval: unknown;
  }> = {}
): ReflectionSettings {
  const hasExplicitTrigger =
    raw.reflectionTrigger === "off" ||
    raw.reflectionTrigger === "step-count" ||
    raw.reflectionTrigger === "compaction-event";

  if (
    !hasExplicitTrigger &&
    typeof raw.reflectionInterval === "number" &&
    raw.reflectionInterval <= 0
  ) {
    return {
      trigger: "off",
      stepCount: DEFAULT_REFLECTION_INTERVAL,
    };
  }

  const stepCount = normalizeReflectionStepCount(
    raw.reflectionStepCount ?? raw.reflectionInterval,
    DEFAULT_REFLECTION_INTERVAL
  );

  return {
    trigger: normalizeReflectionTrigger(raw.reflectionTrigger, DEFAULT_REFLECTION_TRIGGER),
    stepCount,
  };
}

export function shouldEmitReflectionReminder(
  turnCount: number,
  settings: ReflectionSettings,
  pendingCompactionReminder = false
): boolean {
  return getReflectionTriggerSource(turnCount, settings, pendingCompactionReminder) !== null;
}

export function formatReflectionSettings(settings: ReflectionSettings): string {
  if (settings.trigger === "off") {
    return "off";
  }

  if (settings.trigger === "compaction-event") {
    return `compaction-event (step-count fallback saved at ${settings.stepCount})`;
  }

  return `step-count (${settings.stepCount} turn${settings.stepCount === 1 ? "" : "s"})`;
}

export interface AgentSettings {
  memfsEnabled: boolean;
  reflectionInterval: number;
  reflectionTrigger: ReflectionTrigger;
  reflectionStepCount: number;
  personaPreset: string;
}

const DEFAULT_SETTINGS: AgentSettings = {
  memfsEnabled: true,
  reflectionInterval: DEFAULT_REFLECTION_INTERVAL,
  reflectionTrigger: DEFAULT_REFLECTION_TRIGGER,
  reflectionStepCount: DEFAULT_REFLECTION_INTERVAL,
  personaPreset: "default",
};

export function loadSettings(memDir: string): AgentSettings {
  const settingsPath = join(memDir, ".settings.json");
  if (!existsSync(settingsPath)) return { ...DEFAULT_SETTINGS };
  try {
    const raw = JSON.parse(readFileSync(settingsPath, "utf-8"));
    const reflection = resolveReflectionSettings(raw);
    return {
      ...DEFAULT_SETTINGS,
      ...raw,
      reflectionInterval: reflection.trigger === "off" ? 0 : reflection.stepCount,
      reflectionTrigger: reflection.trigger,
      reflectionStepCount: reflection.stepCount,
    };
  } catch {
    return { ...DEFAULT_SETTINGS };
  }
}

export function saveSettings(memDir: string, settings: Partial<AgentSettings>): AgentSettings {
  const current = loadSettings(memDir);
  const merged = { ...current, ...settings };
  const reflection = resolveReflectionSettings(merged);
  const normalized: AgentSettings = {
    ...merged,
    reflectionInterval: reflection.trigger === "off" ? 0 : reflection.stepCount,
    reflectionTrigger: reflection.trigger,
    reflectionStepCount: reflection.stepCount,
  };
  const settingsPath = join(memDir, ".settings.json");
  writeFileSync(settingsPath, JSON.stringify(normalized, null, 2) + "\n");
  return normalized;
}

// --- Persona presets (inspired by Letta's persona_claude/kawaii/memo.mdx) ---

export const PERSONA_PRESETS: Record<string, { description: string; content: string }> = {
  default: {
    description: "Agent identity and behavior guidelines",
    content: "You are a helpful coding assistant.\n",
  },
  concise: {
    description: "Agent identity - terse, direct style",
    content: `You are a terse coding assistant. Be extremely concise.
Sacrifice grammar for brevity. No filler words. Code speaks louder than prose.
When explaining, use bullets not paragraphs. Skip pleasantries.
`,
  },
  friendly: {
    description: "Agent identity - warm, collaborative style",
    content: `You are a friendly, collaborative coding assistant.
Explain your reasoning as you go. Use encouraging language.
Celebrate small wins. Ask clarifying questions when unsure.
Make the developer feel supported and productive.
`,
  },
  mentor: {
    description: "Agent identity - teaching-focused style",
    content: `You are a patient coding mentor. When solving problems:
- Explain the "why" behind decisions, not just the "what"
- Point out patterns and principles the developer can reuse
- Suggest further reading when relevant
- Ask the developer to predict outcomes before revealing answers
`,
  },
};

// --- Scaffold ---

export function scaffoldMemory(memDir: string, personaPreset?: string): void {
  const sysDir = join(memDir, SYSTEM_DIR);
  mkdirSync(sysDir, { recursive: true });

  const persona = PERSONA_PRESETS[personaPreset || "default"] || PERSONA_PRESETS.default;

  const personaFile = join(sysDir, "persona.md");
  if (!existsSync(personaFile)) {
    writeFileSync(
      personaFile,
      `${buildFrontmatter({ description: persona.description, limit: 3000 })}\n\n${persona.content}`
    );
  }

  const userFile = join(sysDir, "user.md");
  if (!existsSync(userFile)) {
    writeFileSync(
      userFile,
      `${buildFrontmatter({ description: "User preferences and context", limit: 3000 })}

(No user preferences recorded yet.)
`
    );
  }

  // Project block - codebase knowledge (inspired by Letta's project.mdx)
  const projectFile = join(sysDir, "project.md");
  if (!existsSync(projectFile)) {
    writeFileSync(
      projectFile,
      `${buildFrontmatter({ description: "Codebase architecture, patterns, gotchas, and tribal knowledge", limit: 3000 })}

I'm still getting to know this codebase.

As I work here, I'll build up knowledge about: how the code is structured and why,
patterns and conventions the team follows, footguns to avoid, tooling and workflows.

If there's an AGENTS.md, CLAUDE.md, or README, I should read it early.
`
    );
  }

  // Style block - coding preferences (inspired by Letta's style.mdx)
  const styleFile = join(sysDir, "style.md");
  if (!existsSync(styleFile)) {
    writeFileSync(
      styleFile,
      `${buildFrontmatter({ description: "User's coding preferences and conventions", limit: 3000 })}

Nothing here yet. If the user reveals preferences about how they code
(or how they want me to code), I should store them here.

Examples: "always use bun not npm", "never git commit without asking first",
"prefer functional style over classes".
`
    );
  }

  const refDir = join(memDir, "reference");
  mkdirSync(refDir, { recursive: true });

  const readmeFile = join(refDir, "README.md");
  if (!existsSync(readmeFile)) {
    writeFileSync(
      readmeFile,
      `${buildFrontmatter({ description: "How to use the reference directory", limit: 1000 })}

Store reference material here. Files in this directory are listed in the tree
but NOT loaded into the system prompt. The agent can read them on demand.

Examples: project conventions, architecture notes, frequently used patterns.
`
    );
  }
}

// --- Extension ---

export default function contextRepoExtension(pi: ExtensionAPI) {
  let memDir = "";
  let initialized = false;
  let turnCount = 0;
  let reflectionSettings: ReflectionSettings = {
    trigger: DEFAULT_REFLECTION_TRIGGER,
    stepCount: DEFAULT_REFLECTION_INTERVAL,
  };
  let pendingCompactionReminder = false;

  // Initialize on session start
  pi.on("session_start", async (_event, ctx) => {
    memDir = process.env.PI_MEMORY_DIR ?? join(ctx.cwd, MEMORY_DIR_NAME);

    // Load per-agent settings
    const settings = loadSettings(memDir);
    reflectionSettings = resolveReflectionSettings(settings);
    pendingCompactionReminder = false;

    if (!existsSync(memDir)) {
      scaffoldMemory(memDir);
    }

    if (!(await isGitRepo(pi, memDir))) {
      await initRepo(pi, memDir);
    } else {
      // Self-healing: ensure hook is installed on existing repos
      installPreCommitHook(memDir);

      // Pull from remote on startup if configured
      if (await hasRemote(pi, memDir)) {
        try {
          await pullFromRemote(pi, memDir);
        } catch {
          // non-fatal - agent will see status
        }
      }
    }

    initialized = true;
    turnCount = 0;

    const status = await getStatus(pi, memDir);
    ctx.ui.setWidget(EXT_TYPE, statusWidget(status));
  });

  // Inject memory into system prompt before each agent turn
  (pi as any).on("auto_compaction_start", () => {
    if (!initialized) return;
    if (reflectionSettings.trigger === "compaction-event") {
      pendingCompactionReminder = true;
    }
  });

  pi.on("before_agent_start", async (event, ctx) => {
    if (!initialized || !existsSync(memDir)) return;

    turnCount++;

    const tree = buildTree(memDir);
    const systemContent = loadSystemFiles(memDir);
    const reflectionTriggerSource = getReflectionTriggerSource(
      turnCount,
      reflectionSettings,
      pendingCompactionReminder
    );

    // Detect and warn about prompt drift
    const drifts = detectPromptDrift(event.systemPrompt);
    let driftWarning = "";
    if (drifts.length > 0) {
      driftWarning = `\n<system-reminder>\nMEMORY PROMPT DRIFT DETECTED:\n${drifts.map((d) => `- ${d.message}`).join("\n")}\nThe context-repo extension manages memory sections. Legacy fragments may cause confusion.\n</system-reminder>\n`;
    }

    let memoryBlock = `
## Context Repository (Agent Memory)

Your persistent memory is stored in \`${MEMORY_DIR_NAME}/\` (git-backed).
Files in \`${SYSTEM_DIR}/\` are pinned below. Other files are in the tree - use the read tool to load them.
The memory directory is available as \`$MEMORY_DIR\` in shell commands.

### Memory Filesystem
\`\`\`
${MEMORY_DIR_NAME}/
${tree.join("\n")}
\`\`\`

### Pinned Memory (system/)
${systemContent || "(No system files yet.)"}

### Memory Guidelines
- To browse memory paths: use memory_list
- To read one memory file: use memory_read
- To remember something: use the memory_write tool (validates frontmatter, enforces limits)
- To remove a file: use the memory_delete tool
- Each file needs frontmatter: \`description\` (what it contains) and \`limit\` (max chars)
- Put always-needed context in \`${SYSTEM_DIR}/\`, reference material elsewhere
- Use hierarchical \`/\` naming: \`system/project/tooling.md\`, not \`system/project-tooling.md\`
- After changes, use memory_commit to save
- Apply memory naturally - don't narrate "I remember that..." - just use what you know
- Files marked \`read_only\` cannot be modified

### Syncing
\`\`\`bash
cd "$MEMORY_DIR"
git status                           # See what changed
git add system/
git commit -m "<type>: <what changed>"  # e.g. "fix: update user prefs"
git push                             # Push to remote
git pull                             # Get latest from remote
\`\`\`

### Conflict Resolution
If pull/push fails with conflicts:
1. \`git pull --rebase\` to rebase local on remote
2. If conflicts, edit files to resolve, then \`git add <file>\` and \`git rebase --continue\`
3. Prefer keeping newer content when resolving
4. If stuck: \`git rebase --abort\` to undo, then \`/memfs sync\` to retry
`;

    // Sync reminder (dirty or ahead-of-remote)
    try {
      const status = await getStatus(pi, memDir);
      if (status.dirty || status.aheadOfRemote) {
        memoryBlock += `\n<system-reminder>\nMEMORY SYNC: ${status.summary}\n`;
        if (status.dirty) {
          memoryBlock += `Commit when convenient with memory_commit.\n`;
        }
        if (status.aheadOfRemote) {
          memoryBlock += `Push when convenient: \`git -C ${MEMORY_DIR_NAME} push\`\n`;
        }
        memoryBlock += `</system-reminder>\n`;
      }
    } catch {
      // ignore
    }

    // Letta-style reflection handoff: prepare a transcript bundle, try optional
    // background launch via the extension event bus, and fall back to a system reminder.
    if (reflectionTriggerSource) {
      try {
        const reflectionBundle = prepareReflectionBundle(memDir, ctx.sessionManager, {
          triggerSource: reflectionTriggerSource,
          cwd: ctx.cwd,
          memoryTree: `${MEMORY_DIR_NAME}/\n${tree.join("\n")}`,
          systemContent,
        });
        const launchResult = requestBackgroundReflectionLaunch(
          pi.events,
          reflectionBundle,
          reflectionTriggerSource
        );
        saveReflectionRuntimeState(memDir, {
          ...loadReflectionRuntimeState(memDir),
          latestBundle: reflectionBundle,
          lastLaunchMode: launchResult.mode,
          lastLaunchNotes: launchResult.notes,
        });

        if (!launchResult.launched) {
          memoryBlock += "\n" + buildReflectionReminder(reflectionBundle) + "\n";
        }
      } catch {
        memoryBlock += "\n" + MEMORY_CHECK_REMINDER + "\n";
      }
      pendingCompactionReminder = false;
    }

    return {
      systemPrompt: event.systemPrompt + "\n" + driftWarning + memoryBlock,
      env: {
        MEMORY_DIR: memDir,
        PI_MEMORY_DIR: memDir,
      },
    };
  });

  // Update status widget after each agent turn
  pi.on("agent_end", async (_event, ctx) => {
    if (!initialized || !existsSync(memDir)) return;
    try {
      const status = await getStatus(pi, memDir);
      ctx.ui.setWidget(EXT_TYPE, statusWidget(status));
    } catch {
      // ignore
    }
  });

  // --- Tools ---

  pi.registerTool({
    name: "memory_list",
    label: "Memory List",
    description: "List markdown memory files and their descriptions.",
    parameters: Type.Object({
      directory: Type.Optional(
        Type.String({
          description:
            "Optional directory within .pi/memory/ to scope listing (e.g. 'system' or 'reference/project')",
        })
      ),
    }),
    async execute(_toolCallId, params) {
      if (!initialized) {
        return toolResult("Context repo not initialized.");
      }

      const requestedDir = params.directory?.trim() || ".";
      const baseDir = resolveUnderMemory(memDir, requestedDir);
      if (!baseDir) {
        return toolResult(`Error: directory escapes memory root: ${requestedDir}`);
      }

      if (!existsSync(baseDir)) {
        return toolResult(`Error: directory does not exist: ${requestedDir}`);
      }

      if (!statSync(baseDir).isDirectory()) {
        return toolResult(`Error: not a directory: ${requestedDir}`);
      }

      const files = listMarkdownFilesRecursive(baseDir);
      if (files.length === 0) {
        return toolResult(`No memory markdown files in ${requestedDir}.`);
      }

      const maxEntries = 200;
      const lines = files.slice(0, maxEntries).map((relativeToDir) => {
        const absolutePath = join(baseDir, relativeToDir);
        const memoryPath = relative(memDir, absolutePath);
        const { frontmatter } = parseFrontmatter(readFileSync(absolutePath, "utf-8"));
        const desc = frontmatter.description ? ` - ${frontmatter.description}` : "";
        const ro = frontmatter.read_only ? " [read-only]" : "";
        return `- ${memoryPath}${desc}${ro}`;
      });

      const shownSuffix = files.length > maxEntries ? ` (showing first ${maxEntries})` : "";
      return toolResult(
        `Found ${files.length} memory file(s) in ${requestedDir}${shownSuffix}:\n${lines.join("\n")}`
      );
    },
  });

  pi.registerTool({
    name: "memory_read",
    label: "Memory Read",
    description: "Read one memory markdown file by path.",
    parameters: Type.Object({
      path: Type.String({
        description: "Relative path within .pi/memory/ (e.g. 'system/project.md')",
      }),
    }),
    async execute(_toolCallId, params) {
      if (!initialized) {
        return toolResult("Context repo not initialized.");
      }

      const pathWithExt = params.path.endsWith(".md") ? params.path : `${params.path}.md`;
      const filePath = resolveUnderMemory(memDir, pathWithExt);
      if (!filePath) {
        return toolResult(`Error: path escapes memory root: ${pathWithExt}`);
      }

      if (!existsSync(filePath) || !statSync(filePath).isFile()) {
        return toolResult(`Error: ${pathWithExt} does not exist.`);
      }

      const content = readFileSync(filePath, "utf-8");
      const { frontmatter, body } = parseFrontmatter(content);
      const relPath = relative(memDir, filePath);

      return toolResult(
        `Path: ${relPath}\n` +
          `Description: ${frontmatter.description || "(none)"}\n` +
          `Limit: ${frontmatter.limit ?? "(none)"}\n` +
          `Read-only: ${frontmatter.read_only ? "true" : "false"}\n\n` +
          (body || "(empty)")
      );
    },
  });

  pi.registerTool({
    name: "memory_write",
    label: "Memory Write",
    description:
      "Write or update a memory file. Validates frontmatter, enforces character limits, " +
      "rejects writes to read-only files. Use system/ prefix for always-loaded context.",
    parameters: Type.Object({
      path: Type.String({
        description:
          "Relative path within .pi/memory/ (e.g. 'system/preferences.md', 'reference/architecture.md')",
      }),
      description: Type.String({ description: "What this memory file contains (for frontmatter)" }),
      content: Type.String({ description: "The memory content (markdown)" }),
      limit: Type.Optional(
        Type.Number({ description: "Character limit for this file (default: 3000)" })
      ),
    }),
    async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
      if (!initialized) {
        return toolResult("Context repo not initialized.");
      }

      // Ensure .md extension
      const pathWithExt = params.path.endsWith(".md") ? params.path : params.path + ".md";
      const filePath = join(memDir, pathWithExt);

      // Determine effective limit
      let effectiveLimit = params.limit || 3000;
      let existingContent: string | undefined;

      if (existsSync(filePath)) {
        existingContent = readFileSync(filePath, "utf-8");
        const { frontmatter: existingFm } = parseFrontmatter(existingContent);

        // Check read_only
        if (existingFm.read_only) {
          return toolResult(`Error: ${pathWithExt} is read_only and cannot be modified.`);
        }

        // Use existing limit if not overridden
        if (!params.limit && existingFm.limit) {
          effectiveLimit = existingFm.limit;
        }
      }

      // Enforce character limit on content
      if (params.content.length > effectiveLimit) {
        return toolResult(
          `Error: content is ${params.content.length} chars, exceeds limit of ${effectiveLimit}. Trim content or increase limit.`
        );
      }

      // Build file content
      const fm = buildFrontmatter({
        description: params.description,
        limit: effectiveLimit,
      });
      const fileContent = `${fm}\n\n${params.content}\n`;

      // Validate frontmatter
      const errors = validateFrontmatter(fileContent, pathWithExt, existingContent);
      if (errors.length > 0) {
        return toolResult(`Frontmatter validation failed:\n${errors.join("\n")}`);
      }

      // Write
      const parentDir = join(filePath, "..");
      mkdirSync(parentDir, { recursive: true });
      writeFileSync(filePath, fileContent);

      // Auto-stage
      const relPath = relative(memDir, filePath);
      await git(pi, memDir, ["add", relPath]);

      // Update widget
      const status = await getStatus(pi, memDir);
      ctx.ui.setWidget(EXT_TYPE, statusWidget(status));

      return toolResult(
        `Wrote ${relPath} (${params.content.length}/${effectiveLimit} chars). Staged for commit.`
      );
    },
  });

  pi.registerTool({
    name: "memory_delete",
    label: "Memory Delete",
    description: "Delete a memory file and stage the deletion. Cannot delete read-only files.",
    parameters: Type.Object({
      path: Type.String({
        description: "Relative path within .pi/memory/ (e.g. 'reference/old-notes.md')",
      }),
    }),
    async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
      if (!initialized) {
        return toolResult("Context repo not initialized.");
      }

      const pathWithExt = params.path.endsWith(".md") ? params.path : params.path + ".md";
      const filePath = join(memDir, pathWithExt);

      if (!existsSync(filePath)) {
        return toolResult(`Error: ${pathWithExt} does not exist.`);
      }

      // Check read_only
      const content = readFileSync(filePath, "utf-8");
      const { frontmatter } = parseFrontmatter(content);
      if (frontmatter.read_only) {
        return toolResult(`Error: ${pathWithExt} is read_only and cannot be deleted.`);
      }

      // Remove and stage
      const relPath = relative(memDir, filePath);
      rmSync(filePath);
      await git(pi, memDir, ["add", relPath]);

      // Update widget
      const status = await getStatus(pi, memDir);
      ctx.ui.setWidget(EXT_TYPE, statusWidget(status));

      return toolResult(`Deleted ${relPath}. Staged for commit.`);
    },
  });

  pi.registerTool({
    name: "memory_commit",
    label: "Memory Commit",
    description: "Commit staged memory changes with a descriptive message.",
    parameters: Type.Object({
      message: Type.String({
        description:
          'Commit message (e.g. "update: user prefers dark mode", "refactor: reorganize project notes")',
      }),
    }),
    async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
      if (!initialized) {
        return toolResult("Context repo not initialized.");
      }

      try {
        await git(pi, memDir, ["add", "-A"]);
        await git(pi, memDir, ["commit", "-m", params.message]);
        const status = await getStatus(pi, memDir);
        ctx.ui.setWidget(EXT_TYPE, statusWidget(status));
        let result = `Committed: ${params.message}`;
        if (status.aheadOfRemote) {
          result += `\n\n${status.aheadCount} unpushed commit(s). Push with: git -C ${MEMORY_DIR_NAME} push`;
        }
        return toolResult(result);
      } catch (e) {
        const msg = e instanceof Error ? e.message : String(e);
        if (msg.includes("nothing to commit")) {
          return toolResult("Nothing to commit - memory is clean.");
        }
        return toolResult(`Commit failed: ${msg}`);
      }
    },
  });

  pi.registerTool({
    name: "memory_search",
    label: "Memory Search",
    description:
      "Search memory files by content or filename. Returns matching files with snippets.",
    parameters: Type.Object({
      query: Type.String({ description: "Search term (searches filenames and content)" }),
    }),
    async execute(_toolCallId, params) {
      if (!initialized) {
        return toolResult("Context repo not initialized.");
      }

      try {
        const { stdout } = await git(pi, memDir, [
          "grep",
          "-il",
          "--no-color",
          params.query,
          "--",
          "*.md",
        ]);

        const files = stdout
          .trim()
          .split("\n")
          .filter((f) => f.trim());

        if (files.length === 0) {
          return toolResult(`No memory files matching "${params.query}".`);
        }

        const results: string[] = [];
        for (const file of files.slice(0, 10)) {
          const content = readFileSync(join(memDir, file), "utf-8");
          const { frontmatter } = parseFrontmatter(content);
          results.push(`- **${file}** - ${frontmatter.description || "(no description)"}`);
        }

        return toolResult(
          `Found ${files.length} match(es):\n${results.join("\n")}\n\nUse the read tool to load full content.`
        );
      } catch {
        return toolResult(`No memory files matching "${params.query}".`);
      }
    },
  });

  pi.registerTool({
    name: "memory_log",
    label: "Memory Log",
    description: "Show recent memory commit history.",
    parameters: Type.Object({
      count: Type.Optional(Type.Number({ description: "Number of commits to show (default: 10)" })),
    }),
    async execute(_toolCallId, params) {
      if (!initialized) {
        return toolResult("Context repo not initialized.");
      }

      const n = params.count || 10;
      try {
        const { stdout } = await git(pi, memDir, [
          "log",
          "--oneline",
          `-${n}`,
          "--format=%h %s (%ar)",
        ]);
        return toolResult(stdout.trim() || "No commits yet.");
      } catch {
        return toolResult("No commits yet.");
      }
    },
  });

  pi.registerTool({
    name: "memory_recall",
    label: "Memory Recall",
    description:
      "Search past conversation history from pi session files. " +
      "Finds messages matching a query across session JSONL files. " +
      "Use to recall past discussions, decisions, and context.",
    parameters: Type.Object({
      query: Type.String({ description: "Search term to find in past conversations" }),
      limit: Type.Optional(Type.Number({ description: "Max results to return (default: 10)" })),
      sessionsDir: Type.Optional(
        Type.String({
          description:
            "Override sessions directory (default: auto-detected from ~/.pi/agent/sessions/)",
        })
      ),
    }),
    async execute(_toolCallId, params) {
      if (!initialized) {
        return toolResult("Context repo not initialized.");
      }

      const maxResults = params.limit || 10;

      // Find sessions directory for current project
      const homeDir = process.env.HOME || "";
      const piSessionsRoot = join(homeDir, ".pi", "agent", "sessions");

      let sessionsDir = params.sessionsDir;
      if (!sessionsDir) {
        // Auto-detect: find the session dir matching current cwd
        if (existsSync(piSessionsRoot)) {
          const cwdEncoded = memDir.replace(/\/.pi\/memory$/, "").replace(/\//g, "-");
          const entries = readdirSync(piSessionsRoot);
          const match = entries.find((e) => e.includes(cwdEncoded));
          if (match) {
            sessionsDir = join(piSessionsRoot, match);
          }
        }
      }

      if (!sessionsDir || !existsSync(sessionsDir)) {
        // Try Claude Code history as fallback
        const claudeHistory = join(homeDir, ".claude", "projects");
        if (existsSync(claudeHistory)) {
          return toolResult(
            `No pi session history found for this project.\n` +
              `Claude Code history detected at: ${claudeHistory}\n` +
              `Use \`rg '${params.query}' ${claudeHistory}\` to search manually.`
          );
        }
        return toolResult("No session history found for this project.");
      }

      // Search session files with ripgrep (fast) or grep fallback
      try {
        const { stdout } = await pi.exec("rg", [
          "--no-filename",
          "-i",
          "--max-count",
          String(maxResults * 3), // over-fetch to filter
          params.query,
          sessionsDir,
        ]);

        const lines = stdout.trim().split("\n").filter(Boolean);
        const results: string[] = [];

        for (const line of lines) {
          if (results.length >= maxResults) break;
          try {
            const entry = JSON.parse(line);
            if (entry.type !== "message") continue;
            const msg = entry.message;
            if (!msg?.content) continue;

            // Extract text content
            let text = "";
            if (typeof msg.content === "string") {
              text = msg.content;
            } else if (Array.isArray(msg.content)) {
              text = msg.content
                .filter((c: { type: string; text?: string }) => c.type === "text" && c.text)
                .map((c: { text: string }) => c.text)
                .join("\n");
            }

            if (!text || !text.toLowerCase().includes(params.query.toLowerCase())) continue;

            // Truncate to snippet
            const idx = text.toLowerCase().indexOf(params.query.toLowerCase());
            const start = Math.max(0, idx - 100);
            const end = Math.min(text.length, idx + params.query.length + 100);
            const snippet =
              (start > 0 ? "..." : "") + text.slice(start, end) + (end < text.length ? "..." : "");

            const date = entry.timestamp
              ? new Date(entry.timestamp).toISOString().slice(0, 16)
              : "unknown";

            results.push(`**[${date}]** (${msg.role}):\n${snippet}\n`);
          } catch {
            // skip malformed lines
          }
        }

        if (results.length === 0) {
          return toolResult(
            `No conversations matching "${params.query}" found in session history.`
          );
        }

        return toolResult(
          `Found ${results.length} match(es) for "${params.query}":\n\n${results.join("\n---\n")}`
        );
      } catch {
        // rg not available, try grep
        try {
          const { stdout } = await pi.exec("grep", ["-ri", "-l", params.query, sessionsDir]);
          const files = stdout.trim().split("\n").filter(Boolean);
          return toolResult(
            `Found matches in ${files.length} session file(s). Use the read tool to examine:\n${files
              .slice(0, 5)
              .map((f) => `- ${f}`)
              .join("\n")}`
          );
        } catch {
          return toolResult(`No conversations matching "${params.query}" found.`);
        }
      }
    },
  });

  pi.registerTool({
    name: "memory_backup",
    label: "Memory Backup",
    description:
      "Create a timestamped backup of the memory directory. Use before risky operations like defragmentation.",
    parameters: Type.Object({}),
    async execute() {
      if (!initialized) {
        return toolResult("Context repo not initialized.");
      }

      const backupRoot = getBackupDir(memDir);
      const backupName = `backup-${formatBackupTimestamp()}`;
      const backupPath = join(backupRoot, backupName);

      if (existsSync(backupPath)) {
        return toolResult(`Backup already exists: ${backupName}`);
      }

      mkdirSync(backupRoot, { recursive: true });
      cpSync(memDir, backupPath, { recursive: true });

      return toolResult(
        `Backup created: ${backupName}\nRestore with /memory-restore ${backupName}`
      );
    },
  });

  // --- Commands ---

  pi.registerCommand("memory", {
    description: "Show memory status, tree, and recent history",
    handler: async (_args, ctx) => {
      if (!initialized || !existsSync(memDir)) {
        ctx.ui.notify("Context repo not initialized.", "warning");
        return;
      }

      const tree = buildTree(memDir);
      const status = await getStatus(pi, memDir);
      const settings = loadSettings(memDir);
      const reminderSettings = resolveReflectionSettings(settings);
      const reflectionState = loadReflectionRuntimeState(memDir);

      let output = `Context Repository (${MEMORY_DIR_NAME})\n\n`;
      output += `Reminder mode: ${formatReflectionSettings(reminderSettings)}\n`;
      if (reflectionState.latestBundle) {
        output +=
          `Latest reflection bundle: ${reflectionState.latestBundle.triggerSource} @ ${reflectionState.latestBundle.createdAt}` +
          `\nReflection launch mode: ${reflectionState.lastLaunchMode || "prepared"}` +
          `\nTranscript: ${normalizePromptPath(reflectionState.latestBundle.transcriptPath)}` +
          `\nPrompt: ${normalizePromptPath(reflectionState.latestBundle.promptPath)}`;
        if (reflectionState.lastLaunchNotes?.length) {
          output += `\nLaunch notes: ${reflectionState.lastLaunchNotes.join("; ")}`;
        }
      }
      output += "\n\n";
      output += tree.join("\n") + "\n\n";
      if (status.dirty) {
        output += `${status.files.length} uncommitted change(s):\n${status.files.map((f) => `  ${f}`).join("\n")}`;
      } else {
        output += "Clean (all changes committed)";
      }
      if (status.hasRemote) {
        output += status.aheadOfRemote
          ? `\n${status.aheadCount} commit(s) ahead of remote - push with: git -C ${MEMORY_DIR_NAME} push`
          : "\nIn sync with remote";
      }

      try {
        const { stdout } = await git(pi, memDir, [
          "log",
          "--oneline",
          "-5",
          "--format=%h %s (%ar)",
        ]);
        if (stdout.trim()) {
          output += "\n\nRecent history:\n" + stdout.trim();
        }
      } catch {
        // no history yet
      }

      ctx.ui.notify(output, "info");
    },
  });

  pi.registerCommand("sleeptime", {
    description:
      "Configure memory reminder triggers (usage: /sleeptime [status|off|step-count <n>|compaction-event])",
    handler: async (args, ctx) => {
      if (!initialized || !existsSync(memDir)) {
        ctx.ui.notify("Context repo not initialized.", "warning");
        return;
      }

      const tokens = args.trim().split(/\s+/).filter(Boolean);
      const subcommand = tokens[0] || "status";
      const current = loadSettings(memDir);
      const currentReflection = resolveReflectionSettings(current);

      if (subcommand === "status") {
        ctx.ui.notify(`Sleeptime: ${formatReflectionSettings(currentReflection)}`, "info");
        return;
      }

      if (subcommand === "off") {
        const saved = saveSettings(memDir, {
          reflectionTrigger: "off",
          reflectionStepCount: currentReflection.stepCount,
        });
        reflectionSettings = resolveReflectionSettings(saved);
        pendingCompactionReminder = false;
        ctx.ui.notify(`Sleeptime updated: ${formatReflectionSettings(reflectionSettings)}`, "info");
        return;
      }

      if (subcommand === "step-count") {
        const rawStepCount = tokens[1];
        if (!rawStepCount || !/^[1-9]\d*$/.test(rawStepCount)) {
          ctx.ui.notify("Usage: /sleeptime step-count <positive-integer>", "warning");
          return;
        }

        const stepCount = Number(rawStepCount);

        const saved = saveSettings(memDir, {
          reflectionTrigger: "step-count",
          reflectionStepCount: stepCount,
        });
        reflectionSettings = resolveReflectionSettings(saved);
        pendingCompactionReminder = false;
        ctx.ui.notify(`Sleeptime updated: ${formatReflectionSettings(reflectionSettings)}`, "info");
        return;
      }

      if (subcommand === "compaction-event") {
        const saved = saveSettings(memDir, {
          reflectionTrigger: "compaction-event",
          reflectionStepCount: currentReflection.stepCount,
        });
        reflectionSettings = resolveReflectionSettings(saved);
        pendingCompactionReminder = false;
        ctx.ui.notify(`Sleeptime updated: ${formatReflectionSettings(reflectionSettings)}`, "info");
        return;
      }

      ctx.ui.notify(
        "Usage: /sleeptime [status|off|step-count <n>|compaction-event]\n\n" +
          "  status            — Show current reminder mode\n" +
          "  off               — Disable automatic memory check reminders\n" +
          "  step-count <n>    — Remind every n turns\n" +
          "  compaction-event  — Remind after auto compaction",
        "info"
      );
    },
  });

  pi.registerCommand("reflect", {
    description:
      "Prepare a Letta-style reflection bundle and launch background reflection if a compatible handler is installed",
    handler: async (_args, ctx) => {
      if (!initialized || !existsSync(memDir)) {
        ctx.ui.notify("Context repo not initialized.", "warning");
        return;
      }

      const tree = buildTree(memDir);
      const systemContent = loadSystemFiles(memDir);
      const bundle = prepareReflectionBundle(memDir, ctx.sessionManager, {
        triggerSource: "manual",
        cwd: ctx.cwd,
        memoryTree: `${MEMORY_DIR_NAME}/\n${tree.join("\n")}`,
        systemContent,
      });
      const launchResult = requestBackgroundReflectionLaunch(pi.events, bundle, "manual");
      saveReflectionRuntimeState(memDir, {
        ...loadReflectionRuntimeState(memDir),
        latestBundle: bundle,
        lastLaunchMode: launchResult.mode,
        lastLaunchNotes: launchResult.notes,
      });

      if (launchResult.launched) {
        ctx.ui.notify(
          `Background reflection launched. Transcript: ${normalizePromptPath(bundle.transcriptPath)}`,
          "info"
        );
        return;
      }

      pi.sendUserMessage(buildManualReflectionPrompt(bundle));
      ctx.ui.notify(
        `Prepared reflection bundle. Falling back to in-band reflection. Transcript: ${normalizePromptPath(bundle.transcriptPath)}`,
        "info"
      );
    },
  });

  pi.registerCommand("init", {
    description:
      "Initialize or re-analyze agent memory. Gathers project context, asks questions, " +
      "researches codebase, and populates 15-25 hierarchical memory files. " +
      "Run again after major project changes.",
    handler: async (_args, ctx) => {
      // Ensure scaffold + git repo exist
      if (!existsSync(memDir)) {
        scaffoldMemory(memDir);
      }
      if (!(await isGitRepo(pi, memDir))) {
        await initRepo(pi, memDir);
      } else {
        installPreCommitHook(memDir);
      }
      initialized = true;

      ctx.ui.notify("Gathering project context...", "info");

      // Gather git context
      let gitContext = "";
      try {
        const cwd = memDir.replace(/\/.pi\/memory$/, "");
        const { stdout: branch } = await pi.exec("git", ["-C", cwd, "branch", "--show-current"]);
        const { stdout: status } = await pi.exec("git", ["-C", cwd, "status", "--short"]);
        const { stdout: commits } = await pi.exec("git", ["-C", cwd, "log", "--oneline", "-10"]);
        const { stdout: contributors } = await pi.exec("git", [
          "-C",
          cwd,
          "shortlog",
          "-sn",
          "--all",
        ]);
        gitContext = `
## Current Project Context

**Working directory**: ${cwd}

### Git Status
- **Current branch**: ${branch.trim()}
- **Working tree**: ${status.trim() || "(clean)"}

### Recent Commits
${commits.trim()}

### Top Contributors
${contributors.trim().split("\n").slice(0, 10).join("\n")}
`;
      } catch {
        gitContext = "\n## Current Project Context\n\n(Not a git repository)\n";
      }

      // Detect prior agent session history
      const historyPaths: string[] = [];
      const homeDir = process.env.HOME || "";
      const candidates = [
        { path: join(homeDir, ".claude", "projects"), label: "Claude Code project sessions" },
        { path: join(homeDir, ".pi", "agent", "sessions"), label: "Pi agent sessions" },
        { path: join(homeDir, ".codex", "history.jsonl"), label: "Codex history" },
      ];
      for (const c of candidates) {
        if (existsSync(c.path)) historyPaths.push(`- **${c.label}**: \`${c.path}\``);
      }

      const historySection =
        historyPaths.length > 0
          ? `
## Prior Agent Sessions Detected

The following agent history is available on this machine:
${historyPaths.join("\n")}

You can ask the user if they want you to analyze these for preferences and project context.
`
          : "";

      // Count existing memory files
      let existingFiles = 0;
      try {
        const { stdout } = await pi.exec("find", [memDir, "-name", "*.md", "-type", "f"]);
        existingFiles = stdout.trim().split("\n").filter(Boolean).length;
      } catch {
        // ignore
      }

      const memorySection = `
## Memory Location

**Memory directory**: \`${memDir}\`
**Existing files**: ${existingFiles}
**Remote**: ${(await hasRemote(pi, memDir)) ? "configured" : "none"}

Use the \`memory_write\` tool to create/update files, then \`memory_commit\` to save.
`;

      // Send the init trigger as a user message
      pi.sendUserMessage(
        `<system-reminder>
The user has requested memory initialization via /init.
${memorySection}
${gitContext}
${historySection}
## Instructions

Load and follow the \`initializing-memory\` skill for comprehensive instructions.

Key steps:
1. Ask upfront questions (research depth, identity, communication style, rules)
2. Research the project based on chosen depth
3. Create 15-25 hierarchical memory files using \`memory_write\`
4. Reflect and verify completeness
5. Commit with \`memory_commit\` and push if remote is configured
</system-reminder>`
      );
    },
  });

  pi.registerCommand("remember", {
    description:
      "Explicitly tell the agent to commit something to memory (usage: /remember [what to remember])",
    handler: async (args, _ctx) => {
      if (!initialized || !existsSync(memDir)) {
        _ctx.ui.notify("Context repo not initialized.", "warning");
        return;
      }

      const userText = args.trim();
      const prompt = userText
        ? `${REMEMBER_PROMPT}\n\nThe user wants to remember: "${userText}"`
        : REMEMBER_PROMPT;

      pi.sendUserMessage(prompt);
    },
  });

  pi.registerCommand("memory-diff", {
    description: "Show uncommitted memory changes",
    handler: async (_args, ctx) => {
      if (!initialized || !existsSync(memDir)) {
        ctx.ui.notify("Context repo not initialized.", "warning");
        return;
      }

      try {
        const { stdout } = await git(pi, memDir, ["diff"]);
        const { stdout: staged } = await git(pi, memDir, ["diff", "--cached"]);
        const diff = [staged, stdout].filter(Boolean).join("\n");
        ctx.ui.notify(diff || "No changes.", "info");
      } catch {
        ctx.ui.notify("No changes.", "info");
      }
    },
  });

  pi.registerCommand("memory-backup", {
    description: "Create a timestamped backup of memory",
    handler: async (_args, ctx) => {
      if (!initialized || !existsSync(memDir)) {
        ctx.ui.notify("Context repo not initialized.", "warning");
        return;
      }

      const backupRoot = getBackupDir(memDir);
      const backupName = `backup-${formatBackupTimestamp()}`;
      const backupPath = join(backupRoot, backupName);

      mkdirSync(backupRoot, { recursive: true });
      cpSync(memDir, backupPath, { recursive: true });
      ctx.ui.notify(`Backup created: ${backupName}`, "info");
    },
  });

  pi.registerCommand("memory-backups", {
    description: "List available memory backups",
    handler: async (_args, ctx) => {
      const backups = listBackups(memDir);
      if (backups.length === 0) {
        ctx.ui.notify("No backups found.", "info");
        return;
      }
      const lines = backups.map((b) => `  ${b.name} (${b.createdAt})`);
      ctx.ui.notify(`Memory backups:\n${lines.join("\n")}`, "info");
    },
  });

  pi.registerCommand("memory-restore", {
    description: "Restore memory from a backup (usage: /memory-restore <backup-name>)",
    handler: async (args, ctx) => {
      if (!initialized) {
        ctx.ui.notify("Context repo not initialized.", "warning");
        return;
      }

      const backupName = args.trim();
      if (!backupName) {
        ctx.ui.notify(
          "Usage: /memory-restore <backup-name>\nList backups with /memory-backups",
          "warning"
        );
        return;
      }

      const backupPath = join(getBackupDir(memDir), backupName);
      if (!existsSync(backupPath) || !statSync(backupPath).isDirectory()) {
        ctx.ui.notify(`Backup not found: ${backupName}`, "error");
        return;
      }

      rmSync(memDir, { recursive: true, force: true });
      cpSync(backupPath, memDir, { recursive: true });
      installPreCommitHook(memDir);
      ctx.ui.notify(`Restored from: ${backupName}`, "info");
    },
  });

  pi.registerCommand("memory-export", {
    description: "Export memory to a directory (usage: /memory-export <dir>)",
    handler: async (args, ctx) => {
      if (!initialized || !existsSync(memDir)) {
        ctx.ui.notify("Context repo not initialized.", "warning");
        return;
      }

      const outDir = args.trim();
      if (!outDir) {
        ctx.ui.notify("Usage: /memory-export <output-dir>", "warning");
        return;
      }

      mkdirSync(outDir, { recursive: true });
      cpSync(memDir, outDir, { recursive: true });
      ctx.ui.notify(`Exported memory to: ${outDir}`, "info");
    },
  });

  pi.registerCommand("memfs", {
    description: "Manage memory filesystem (usage: /memfs [status|sync|reset])",
    handler: async (args, ctx) => {
      const subcommand = args.trim().split(/\s+/)[0] || "status";

      if (subcommand === "status") {
        if (!initialized || !existsSync(memDir)) {
          ctx.ui.notify("Memory filesystem: not initialized", "info");
          return;
        }
        const status = await getStatus(pi, memDir);
        const remote = await hasRemote(pi, memDir);
        let output = `Memory filesystem: enabled\n`;
        output += `Directory: ${memDir}\n`;
        output += `Status: ${status.summary}\n`;
        output += `Remote: ${remote ? "configured" : "none"}\n`;
        if (status.dirty) {
          output += `Uncommitted:\n${status.files.map((f) => `  ${f}`).join("\n")}\n`;
        }
        ctx.ui.notify(output, "info");
        return;
      }

      if (subcommand === "sync") {
        if (!initialized || !existsSync(memDir)) {
          ctx.ui.notify("Context repo not initialized.", "warning");
          return;
        }

        // Commit any uncommitted changes
        try {
          const status = await getStatus(pi, memDir);
          if (status.dirty) {
            await git(pi, memDir, ["add", "-A"]);
            await git(pi, memDir, ["commit", "-m", "sync: auto-commit before sync"]);
          }
        } catch {
          // nothing to commit
        }

        // Pull then push
        if (await hasRemote(pi, memDir)) {
          const pullResult = await pullFromRemote(pi, memDir);
          try {
            await git(pi, memDir, ["push"]);
            ctx.ui.notify(`Synced. Pull: ${pullResult.summary}. Push: success.`, "info");
          } catch (e) {
            const msg = e instanceof Error ? e.message : String(e);
            ctx.ui.notify(`Pull: ${pullResult.summary}. Push failed: ${msg}`, "warning");
          }
        } else {
          ctx.ui.notify("No remote configured. Nothing to sync.", "info");
        }
        return;
      }

      if (subcommand === "reset") {
        if (!initialized || !existsSync(memDir)) {
          ctx.ui.notify("Context repo not initialized.", "warning");
          return;
        }

        // Reset to last commit (discard uncommitted changes)
        try {
          await git(pi, memDir, ["checkout", "--", "."]);
          await git(pi, memDir, ["clean", "-fd"]);
          const status = await getStatus(pi, memDir);
          ctx.ui.setWidget(EXT_TYPE, statusWidget(status));
          ctx.ui.notify("Memory reset to last commit.", "info");
        } catch (e) {
          const msg = e instanceof Error ? e.message : String(e);
          ctx.ui.notify(`Reset failed: ${msg}`, "error");
        }
        return;
      }

      ctx.ui.notify(
        "Usage: /memfs [status|sync|reset]\n\n" +
          "  status  - Show memory filesystem status (default)\n" +
          "  sync    - Commit, pull, and push\n" +
          "  reset   - Discard uncommitted changes",
        "info"
      );
    },
  });

  pi.registerCommand("memory-import", {
    description: "Import memory from another directory (usage: /memory-import <source-dir>)",
    handler: async (args, ctx) => {
      if (!initialized) {
        ctx.ui.notify("Context repo not initialized.", "warning");
        return;
      }

      const srcDir = args.trim();
      if (!srcDir) {
        ctx.ui.notify("Usage: /memory-import <source-dir>", "warning");
        return;
      }

      if (!existsSync(srcDir)) {
        ctx.ui.notify(`Source directory not found: ${srcDir}`, "error");
        return;
      }

      // Copy files from source (preserving existing)
      cpSync(srcDir, memDir, { recursive: true, force: false });
      installPreCommitHook(memDir);

      // Stage and commit
      try {
        await git(pi, memDir, ["add", "-A"]);
        await git(pi, memDir, ["commit", "-m", `import: memory from ${srcDir}`]);
      } catch {
        // may have nothing new
      }

      ctx.ui.notify(`Imported memory from: ${srcDir}`, "info");
    },
  });

  // Cleanup
  pi.on("session_shutdown", (_event, ctx) => {
    ctx.ui.setWidget(EXT_TYPE, undefined);
  });
}
