/**
 * IdentityRenderer — render framework-specific identity files from the
 * canonical AGENT.md source.
 *
 * Provider-portability v1.0.0: AGENT.md is the single source of truth
 * for an agent's identity (who I am, what I'm working on, who I'm
 * working with). Frameworks have their own conventions for the file
 * they auto-load at session start:
 *
 *   - Claude Code → CLAUDE.md at the project root
 *   - OpenAI Codex → AGENTS.md at the project root
 *   - Gemini CLI → GEMINI.md at the project root
 *   - (extend the renderer when new frameworks land)
 *
 * The shadow files are deterministically rendered from AGENT.md. They
 * are NOT the source of truth and should NOT be hand-edited — edits get
 * overwritten on the next render. Each shadow file ships with a header
 * banner saying so.
 *
 * This file does not write anything until you call render(); it's safe
 * to import without side effects.
 */
import fs from 'node:fs';
import path from 'node:path';
// ---------------------------------------------------------------------------
// Framework conventions
// ---------------------------------------------------------------------------
/**
 * Known framework conventions. Maps a framework id to the shadow filename
 * it expects at the project root.
 *
 * Extending: add a new entry here. The renderer picks it up automatically.
 */
export const FRAMEWORK_SHADOW_FILES = {
    'claude-code': 'CLAUDE.md',
    'codex-cli': 'AGENTS.md',
    'gemini-cli': 'GEMINI.md',
};
// ---------------------------------------------------------------------------
// Banner that prefixes every rendered shadow
// ---------------------------------------------------------------------------
function bannerFor(framework, sourceRelPath) {
    return [
        `<!--`,
        `  AUTO-GENERATED by Instar's IdentityRenderer.`,
        `  Source of truth: ${sourceRelPath}`,
        `  Framework: ${framework}`,
        ``,
        `  Do NOT hand-edit this file. Edits will be overwritten on the next`,
        `  identity render. Edit the source file instead.`,
        `-->`,
        ``,
    ].join('\n');
}
export class IdentityRendererError extends Error {
    constructor(message) {
        super(message);
        this.name = 'IdentityRendererError';
    }
}
/**
 * Render framework-specific shadow files from the canonical AGENT.md.
 *
 * Workflow:
 *   1. Locate source (preferred: `.instar/AGENT.md`; fallback: root `AGENT.md`).
 *   2. Read source content.
 *   3. For each requested framework, write `<projectDir>/<shadowFilename>`
 *      with the source content prefixed by the auto-generation banner.
 *
 * Throws IdentityRendererError if source can't be located or read.
 */
export function renderIdentity(options) {
    const source = resolveSource(options.projectDir, options.sourcePath);
    if (source === null) {
        throw new IdentityRendererError(`No canonical identity source found. Expected `
            + `${path.join(options.projectDir, '.instar', 'AGENT.md')} or `
            + `${path.join(options.projectDir, 'AGENT.md')}`);
    }
    const content = fs.readFileSync(source, 'utf-8');
    const sourceRel = path.relative(options.projectDir, source);
    const requestedFrameworks = options.frameworks ?? Object.keys(FRAMEWORK_SHADOW_FILES);
    const shadowsWritten = [];
    const skipped = [];
    for (const framework of requestedFrameworks) {
        const filename = FRAMEWORK_SHADOW_FILES[framework];
        if (!filename) {
            skipped.push(framework);
            continue;
        }
        const target = path.join(options.projectDir, filename);
        const banner = bannerFor(framework, sourceRel);
        const relayAppendix = options.appendTelegramRelayBlock
            ? '\n\n' + buildPersistentRelayAppendix(framework)
            : '';
        fs.writeFileSync(target, banner + content + relayAppendix, 'utf-8');
        shadowsWritten.push(target);
    }
    return { source, shadowsWritten, skipped };
}
/**
 * Markdown appendix that lives at the bottom of AGENTS.md / CLAUDE.md when
 * Telegram is configured. The wording mirrors the bootstrap-message helper
 * (`buildTelegramRelayBlock`) but is phrased for the system-prompt context
 * rather than the per-message turn.
 *
 * For Claude Code, the SessionStart shell hook already injects "MANDATORY:
 * After EVERY response, relay…" — but rendering it into CLAUDE.md too is
 * harmless redundancy that survives hook removal or misconfiguration.
 *
 * For Codex CLI, this is the ONLY persistent mechanism — Codex has no
 * SessionStart hook system, so the AGENTS.md content has to carry the
 * relay convention across every turn.
 */
function buildPersistentRelayAppendix(framework) {
    // Codex AND Gemini have no SessionStart hook system, so the shadow file
    // (AGENTS.md / GEMINI.md) is the ONLY persistent carrier of the relay
    // convention across every turn — they get the "every turn" wording.
    const isHooklessFramework = framework === 'codex-cli' || framework === 'gemini-cli';
    return [
        `## Telegram Relay (MANDATORY)`,
        ``,
        `Every Telegram message you receive arrives prefixed \`[telegram:N "topic name" from User]\`. After responding, you MUST run this exact bash command to send the user your reply — without it, the user only sees sentinel messages and thinks you ignored them:`,
        ``,
        '```bash',
        `cat <<'EOF' | .instar/scripts/telegram-reply.sh N`,
        `Your conversational response text here.`,
        `EOF`,
        '```',
        ``,
        `(Replace \`N\` with the topic id from the prefix. If \`.instar/scripts/telegram-reply.sh\` is missing on an older install, fall back to \`.claude/scripts/telegram-reply.sh\`.)`,
        `If the response itself contains a literal \`EOF\` line or shell-sensitive content that could break the heredoc wrapper, base64-encode the response text and pipe that encoded text to \`telegram-reply.sh --stdin-base64 N\` instead.`,
        ``,
        `Rules:`,
        `- Strip the \`[telegram:N]\` prefix before interpreting the message.`,
        `- Relay ONLY conversational text — not tool output, raw logs, or internal reasoning.`,
        `- Send a short ACK first ("Got it, looking into this…"), then do the work, then send the full reply.`,
        isHooklessFramework
            ? `- ${framework === 'gemini-cli' ? 'Gemini CLI' : 'Codex CLI'} specifically: this convention applies to EVERY turn in a Telegram-spawned session, not just the first one. There's no SessionStart hook reminder beyond this shadow-file section.`
            : `- Claude Code specifically: the SessionStart hook reinforces this on session start; this section is the durable backup.`,
    ].join('\n');
}
/**
 * Resolve the canonical AGENT.md path. Searches the explicit override
 * first, then `.instar/AGENT.md`, then root-level `AGENT.md`. Returns
 * null if none is found.
 */
function resolveSource(projectDir, override) {
    const candidates = [
        override,
        path.join(projectDir, '.instar', 'AGENT.md'),
        path.join(projectDir, 'AGENT.md'),
    ].filter((p) => p !== undefined);
    for (const candidate of candidates) {
        if (fs.existsSync(candidate))
            return candidate;
    }
    return null;
}
/**
 * Detect framework from an existing project layout. Used for legacy
 * installs that have CLAUDE.md but no AGENT.md — we can infer which
 * framework was configured and seed AGENT.md from the shadow content.
 *
 * Returns null when no framework-specific file is found.
 */
export function detectFrameworkFromShadowFiles(projectDir) {
    for (const [framework, filename] of Object.entries(FRAMEWORK_SHADOW_FILES)) {
        if (fs.existsSync(path.join(projectDir, filename)))
            return framework;
    }
    return null;
}
/**
 * Ensure the shadow file a framework expects exists at the project root,
 * bootstrapping the canonical AGENT.md from any legacy shadow first if
 * needed.
 *
 * Lifecycle:
 *   - If `<projectDir>/<framework-shadow>` already exists, no-op.
 *   - If `.instar/AGENT.md` exists, render the requested framework's
 *     shadow from it.
 *   - If neither exists but a different shadow does (e.g., legacy
 *     CLAUDE.md without AGENT.md), bootstrap AGENT.md from that shadow,
 *     then render the requested shadow.
 *   - If nothing exists, no-op — the project has no identity content
 *     to seed from.
 *
 * Returns the path of the written shadow file, or null when the operation
 * was a no-op.
 *
 * Idempotent. Safe to call before every spawn — checks file existence
 * first and only writes when there's something useful to do.
 */
export function ensureFrameworkIdentityFile(projectDir, framework, options = {}) {
    const filename = FRAMEWORK_SHADOW_FILES[framework];
    if (!filename)
        return null;
    const targetPath = path.join(projectDir, filename);
    // When the shadow exists, check whether the Telegram-relay appendix is
    // missing. If callers asked for it (Telegram is configured) and the
    // existing file predates the appendix, re-render so Codex sessions get
    // the durable relay convention without requiring a manual edit.
    if (fs.existsSync(targetPath)) {
        if (options.appendTelegramRelayBlock) {
            try {
                const existing = fs.readFileSync(targetPath, 'utf-8');
                if (existing.includes('## Telegram Relay (MANDATORY)')) {
                    return null; // already up to date
                }
            }
            catch {
                // unreadable — fall through to re-render
            }
            // Fall through to render below
        }
        else {
            return null;
        }
    }
    // Ensure AGENT.md exists as the canonical source. If it doesn't,
    // try to bootstrap from a legacy shadow file (e.g., CLAUDE.md).
    const sourcePath = resolveSource(projectDir, options.stateDir
        ? path.join(options.stateDir, 'AGENT.md')
        : undefined);
    if (sourcePath === null) {
        const bootstrapped = bootstrapAgentMdFromShadow(projectDir, options);
        if (bootstrapped === null)
            return null;
    }
    const result = renderIdentity({
        projectDir,
        frameworks: [framework],
        sourcePath: options.stateDir
            ? path.join(options.stateDir, 'AGENT.md')
            : undefined,
        appendTelegramRelayBlock: options.appendTelegramRelayBlock,
    });
    return result.shadowsWritten[0] ?? null;
}
/**
 * Render every known framework identity shadow EXCEPT claude-code.
 *
 * `instar init` writes a rich, Claude-Code-specific CLAUDE.md via
 * `generateClaudeMd()` (the capability/instructions document — not an
 * identity render). That file legitimately owns the CLAUDE.md filename.
 * But before this helper, init wrote NOTHING for non-Claude runtimes: a
 * Codex install got a CLAUDE.md (which Codex ignores) and no AGENTS.md,
 * so identity wasn't auto-loaded until the first server spawn called
 * `ensureFrameworkIdentityFile`. This closes that init-time gap
 * additively: it produces AGENTS.md / GEMINI.md (and any future
 * non-Claude shadow) from the canonical `.instar/AGENT.md`, and
 * deliberately skips `claude-code` so the rich CLAUDE.md is never
 * clobbered.
 *
 * Idempotent and best-effort: no-ops when AGENT.md is absent, never
 * throws into the init flow (init must not fail because a shadow could
 * not be written). Returns the shadow paths written.
 */
export function renderNonClaudeIdentityShadows(projectDir, options = {}) {
    const nonClaude = Object.keys(FRAMEWORK_SHADOW_FILES).filter((fw) => fw !== 'claude-code');
    const sourcePath = options.stateDir
        ? path.join(options.stateDir, 'AGENT.md')
        : undefined;
    // No canonical source → nothing to render. Not an error at init time.
    if (resolveSource(projectDir, sourcePath) === null)
        return [];
    try {
        const result = renderIdentity({
            projectDir,
            frameworks: nonClaude,
            sourcePath,
            appendTelegramRelayBlock: options.appendTelegramRelayBlock,
        });
        return result.shadowsWritten;
    }
    catch {
        // Best-effort: a shadow render failure must never break `instar init`.
        return [];
    }
}
/**
 * Bootstrap AGENT.md from a legacy CLAUDE.md (or other shadow file).
 *
 * Used during migration when a project has a CLAUDE.md but no AGENT.md.
 * Reads the shadow, strips the auto-generation banner if present, and
 * writes the canonical AGENT.md.
 *
 * Returns the absolute path of the written AGENT.md, or null if no
 * shadow file could be found.
 */
export function bootstrapAgentMdFromShadow(projectDir, options = {}) {
    const framework = detectFrameworkFromShadowFiles(projectDir);
    if (!framework)
        return null;
    const shadowFilename = FRAMEWORK_SHADOW_FILES[framework];
    const shadowPath = path.join(projectDir, shadowFilename);
    let content = fs.readFileSync(shadowPath, 'utf-8');
    // Strip an existing banner if present so we don't end up with nested
    // "AUTO-GENERATED" notices.
    content = content.replace(/^<!--[\s\S]*?-->\n+/, '');
    const stateDir = options.stateDir ?? path.join(projectDir, '.instar');
    if (!fs.existsSync(stateDir)) {
        fs.mkdirSync(stateDir, { recursive: true });
    }
    const agentMdPath = path.join(stateDir, 'AGENT.md');
    fs.writeFileSync(agentMdPath, content, 'utf-8');
    return agentMdPath;
}
//# sourceMappingURL=IdentityRenderer.js.map