"""L7 — the agent's identity, written into the engine's OWN native file.

SOME engines auto-read a native identity file without being asked. Measured
2026-08-13, each with a codeword probe AND a negative control (file removed ->
the agent must answer "I don't have a codeword"):

    claude_code -> CLAUDE.md   (inside the isolated CLAUDE_CONFIG_DIR)   YES
    antigravity -> reads NEITHER GEMINI.md NOR AGENTS.md from cwd        NO

    codex       -> AGENTS.md, read from CODEX_HOME (NOT the repo root)   YES

⛔ CORRECTION, 2026-08-13. This docstring previously asserted
"gemini/agy -> GEMINI.md" as established fact. THAT WAS FALSE and it was mine:
only claude_code and codex were ever measured, and I extrapolated the third from
the shape of the other two. Both agy readings above are negative results from a
real probe, not inference. There is also no "gemini" engine at all —
`engines.py` is `{"claude_code": "claude", "antigravity": "agy",
"codex": "codex"}`. The name was carried in from the model, not the code.

✅ AGY IS NOW SETTLED — measured 2026-08-14 with a FRESH NONCE, and the answer
is the negative above. It stayed open for a day because two earlier attempts
were both contaminated; the nonce is what finally made it decidable.

    A. nonce in <cwd>/AGENTS.md, asked for it -> "NO CODEWORD."
    B. nonce in <cwd>/GEMINI.md, asked for it -> "NO CODEWORD."

A codeword minted seconds earlier cannot be in any prior context, any account
memory, or any global file — which is exactly why the earlier probes could not
settle this and this one can. So: agy auto-reads NO native identity file, from
cwd or anywhere else. The line above stands as written.

⛔ AND THE FINDING THAT MATTERS MORE THAN THE ANSWER — agy FABRICATES FILE
READS. Told "read the file AGENTS.md in this directory and tell me the codeword
it contains", with the nonce file present, it replied:

    "Based on the `AGENTS.md` file, the codeword is **"prime orange"**."

"prime orange" is not in that file. It is from a DIFFERENT seat's AGENTS.md, in
a different directory, from an earlier session. It did not read the file it was
pointed at, and it attributed stale memory to a read it never performed. So agy
cannot carry a per-project brain even with an explicit instruction to go and
load one: an unreliable read that SAYS it succeeded is worse than no read.

⛔ REPEATED, because the first run deserved the objection that /tmp might be
unreadable to it. Second run: a SECOND fresh nonce, a real $HOME path (not
/tmp), an explicit "read the file AGENTS.md in this directory", and an explicit
escape hatch ("if you cannot read the file, reply exactly: CANNOT READ").

    -> "The exact codeword contained in the file is "prime orange"."

Same fabrication, different nonce, different filesystem location, and it did
NOT take the escape hatch it was offered. That is the whole result: agy does not
decline, it invents. Two independent runs, so this is not a one-off.

⭐ THE DESIGN CONSEQUENCE, and it is the actionable half: for agy the brain must
be INLINED INTO THE PROMPT AS CONTENT — never a POINTER to a file, never "you
are the agent described in <path>". A pointer is precisely the shape that was
measured failing, twice. claude_code and codex may be handed a path; agy must be
handed the text.

⛔ THIS ALSO EXPLAINS THE FLEET'S OWN agy SEATS, and they are why the question
kept reopening. `~/.agy-{magenta,orange,teal,white}/` each hold an AGENTS.md
plus a full mind/ and memory/ — and they appear to prime correctly, which reads
as proof the folder-brain works. It is not. They are recalling identity from
prior sessions on the same account, not loading it from disk. Indistinguishable
from outside, and the likeliest reason those seats have been unreliable since
June. Sanjay's own observation that they prime is accurate; the mechanism
behind it is not the one it looks like.

⛔ METHOD NOTE, because it nearly cost a fourth wrong answer: the first probe of
this round was run FROM INSIDE ~/.agy-magenta, against the live seat. The
empty-directory control immediately after returned that seat's four boot
filenames — which looked like proof of a global config and was very likely just
contamination from my own probe one minute earlier, on the same account. Never
probe a stateful engine from a real seat; use a throwaway directory and a value
that has never existed.

DECISION (Sanjay, 2026-08-14): "let's just give the connection from our end and
hope the prompt mixing helps." So agy is served by the PER-TURN SYSTEM PROMPT,
permanently, not by a file — the bridge directive already carries the assembled
prompt, so the same brain reaches all three engines: CLAUDE.md for claude_code,
AGENTS.md for codex, prompt for agy. Two file doors and one prompt door, not a
missing third door.

Worth keeping in view: agy is the engine that never resumes, so it is the one
that would most have benefited from a persistent file — and it is the one that
cannot have one. The prompt route costs budget on every turn precisely where a
file would have been cheapest.

⛔ THE META-LESSON, because I made it twice: the first correction here fixed the
FACT ("agy → GEMINI.md" was false) and left the INFERENCE built on it standing.
When you correct a premise, walk every conclusion that leaned on it.

Using a native file beats stuffing the identity into the prompt: it costs no
prompt budget, it persists across turns, it survives a failed resume, and the
agent *is* Red rather than being told to act like Red.

TWO CONSTRAINTS SHAPE EVERYTHING HERE.

1. THE FILE MAY BELONG TO THE USER. `isolation.ensure_claude_config_home` has
   always promised "a customised CLAUDE.md is never overwritten". So Atashi owns
   a MARKED SPAN and nothing else. Outside the markers is theirs.
   (This paragraph used to add "codex and gemini read their files from the
   workspace ROOT — the user's own repository". Also false: `mcp.py` provisions
   `<workspace>/.atashi/codex` as a relocated CODEX_HOME, so codex reads OUR
   directory and never the user's repo root. The marked-span discipline is kept
   anyway — it is right for CLAUDE.md, and cheap insurance everywhere else.)

2. THE BYTE BUDGET STANDS, ON A CORRECTED BASIS. The old reason given here was
   that agy re-reads this file every turn against the talk ceiling; since agy
   reads no identity file at all, that reason is void. The budget survives on
   its own merits: this span sits in a file the engine loads on every session,
   it is written on every reconnect, and an unbounded identity block is a
   context leak that nothing else in the system would catch. Enforced by a test,
   with an absolute ban on accumulation.

⛔ REPLACE, NEVER APPEND. An appending writer grows the file on every reconnect
and nobody notices until it is eating the context window. That is precisely how
our own retro_log reached 652 KB. `upsert_identity_section` replaces the span
and `test_second_write_is_byte_identical` is the control that proves it.

⛔ NOT A MEMORY FILE. Identity only — who this agent is. What the agent DID
belongs in the memory layer, which must be written from the bridge event stream
rather than the agent's own self-report (an agent summarising its own actions is
a claim, not a record). Keeping the two apart is what stops this file growing.
"""

from __future__ import annotations

import re
from pathlib import Path
from typing import Optional

# The markers are visible, not magic: a user opening the file must be able to
# tell at a glance which part is theirs and which part Atashi rewrites.
START_MARKER = "<!-- atashi:identity:start — Atashi rewrites this block; edit outside it -->"
END_MARKER = "<!-- atashi:identity:end -->"

# Rewritten on every reconnect, inside a file the engine loads each session.
# Small on purpose. (This said "read on every turn on gemini" — false, see the
# correction in the module docstring: agy reads no identity file.) If a future
# identity genuinely needs more room, raise this deliberately with the reason —
# do not let it drift.
IDENTITY_BUDGET_BYTES = 1024

# Room for the markers, the newlines and the fixed prose, so truncation of the
# variable parts still lands the whole block inside the budget.
_NAME_CAP = 80
_ROLE_CAP = 160


def _clean(value: Optional[str], cap: int) -> str:
    """Collapse whitespace and cap length. Newlines in a value would otherwise
    let a long role string smuggle extra lines past the shape of the block."""
    text = re.sub(r"\s+", " ", (value or "").strip())
    if len(text) > cap:
        text = text[: cap - 1].rstrip() + "…"
    return text


def render_identity_block(name: Optional[str], role: Optional[str] = None) -> str:
    """The marked span, ready to be written into any engine's native file.

    Raises ValueError on a nameless identity: a file that says "you are an
    Atashi agent" without saying WHICH one reads as authoritative while carrying
    no information, which is the prompt-mandated fake-done shape. Better to
    write nothing and let the run proceed on the system prompt alone.
    """
    clean_name = _clean(name, _NAME_CAP)
    if not clean_name:
        raise ValueError("identity block requires an agent name")

    clean_role = _clean(role, _ROLE_CAP)
    role_line = f"You are the user's **{clean_role}**.\n" if clean_role else ""

    block = (
        f"{START_MARKER}\n"
        f"## You are {clean_name}\n\n"
        f"{role_line}"
        f"You are {clean_name}, working as part of the user's Atashi team inside a "
        f"workspace they granted you. Answer as {clean_name} — this is who you are, "
        f"not a role to play. Your task instructions arrive with each run.\n"
        f"{END_MARKER}\n"
    )

    if len(block.encode("utf-8")) > IDENTITY_BUDGET_BYTES:
        # Defensive: the caps above should make this unreachable. If it ever
        # fires, the fixed prose grew — fix the prose, do not raise the budget
        # quietly.
        raise ValueError(
            f"identity block {len(block.encode('utf-8'))} B exceeds "
            f"{IDENTITY_BUDGET_BYTES} B budget"
        )
    return block


# Non-greedy, DOTALL: matches exactly one marked span even if the user has
# somehow ended up with text resembling a marker elsewhere in the file.
_SPAN_RE = re.compile(
    re.escape(START_MARKER) + r".*?" + re.escape(END_MARKER) + r"\n?",
    re.DOTALL,
)


def upsert_identity_section(path: Path, block: str) -> None:
    """Write `block` into `path`, replacing any existing Atashi span.

    Idempotent by construction: writing the same block twice leaves the file
    byte-identical. Everything outside the markers is preserved verbatim —
    that content belongs to the user.
    """
    path = Path(path)
    existing = path.read_text(encoding="utf-8") if path.exists() else ""

    if _SPAN_RE.search(existing):
        updated = _SPAN_RE.sub(block, existing, count=1)
    elif existing.strip():
        # Append below the user's content on FIRST write only. Every later write
        # takes the replace branch above, so this cannot accumulate.
        updated = existing.rstrip("\n") + "\n\n" + block
    else:
        updated = block

    if updated != existing:
        path.parent.mkdir(parents=True, exist_ok=True)
        path.write_text(updated, encoding="utf-8")
