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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
287 changes: 61 additions & 226 deletions emrg/server/prompts/system.j2

Large diffs are not rendered by default.

111 changes: 45 additions & 66 deletions tests/test_daemon.py
Original file line number Diff line number Diff line change
Expand Up @@ -218,37 +218,41 @@ def test_system_prompt_environment_without_session(tmp_path):


def test_system_prompt_rant_handling_section(tmp_path):
"""Rant Handling section renders (rant 2026-08-17T11:51:59): agent must
know to detect rants in normal conversation, confirm with the user, and
only then call submit_rant."""
"""The rants section renders (rant 2026-08-17T11:51:59): detect a rant in ordinary
conversation, ask what is unclear, and call `submit_rant` only with consent.

The section is `## Rants` in the compacted prompt and states the same three things
without the `/rant` prefix: a rant arrives as ordinary conversation, so there is no
mode to enter — which is why the prefix is no longer the carrier of consent. The
client expands `/rant` before the model sees it, and the consent rule here is the
one that governs every other route.
"""
server = _make_server()
rendered = server._build_system_prompt()
assert "## Rant Handling" in rendered
assert "## Rants" in rendered
assert "submit_rant" in rendered
assert "explicit" in rendered.lower() # consent-before-call contract
assert "/rant" in rendered
assert "only once the host agrees" in rendered # consent-before-call contract
assert "the call is the confirmation" in rendered
assert "If the host says never mind, do not call it" in rendered


def test_system_prompt_says_a_task_cannot_write_its_own_prompt():
"""A task's only channel to its own prompt text is a rant (host ruling
2026-10-09T14:34:54; PR #1986).
"""A task's only channel to its own prompt is a rant (host ruling 2026-10-09T14:34:54).

The loop this closes, measured on 2026-10-09: the `competition` task wanted rules added
to `emrg/server/competition_prompt.md`, and it could do nothing about it from its own
The loop this closes, measured on 2026-10-09: the `competition` task wanted rules added to
`emrg/server/competition_prompt.md`, and it could do nothing about it from its own
workspace. Two copies exist — the repository's, which nothing reads at runtime, and the
install tree's, which is the one `_resolve_task_template` returns and which sits outside
every task's workspace and is replaced whole by the next upgrade — so a task that writes
the file it can reach is writing something no round reads, and a task that writes the one
that matters cannot. The host ruled it directly (2026-10-09T14:34:54, session
`emrg-evolution-competition-task`): 「你不能直接修改competition_prompt.md,丢宇emrg项目,
你只能提rant」.
that matters cannot.

Both halves of the citation are asserted, for the reason `check-rant-citations.py` gives:
the timestamp is **host-local** — that session exists only on the host that recorded it,
so the message does not resolve on a second host (issue #1252 measured 0 of 24) — while
`PR #1986` stays resolvable forever. A reader who cannot see the local store still has
the anchor, so asserting the timestamp alone would pin a reference half of whose
provenance can rot.
The ruling's citation (its timestamp, the pull request beside it, the command that re-finds
the message) is recorded in this project's memory and in the pull request that landed it;
the session prompt states the rule, the reason and the channel, and spends no prefix on the
provenance — which is the property the compacted prompt is built on and
`tests/test_system_prompt_structure.py::test_neither_the_file_nor_the_render_carries_provenance`
holds.

Asserted on the **render**: `system.j2` is the single render site every session receives,
so a rule placed here reaches a `competition` or `paper` session that opens no other
Expand All @@ -257,75 +261,50 @@ def test_system_prompt_says_a_task_cannot_write_its_own_prompt():
"""
server = _make_server()
rendered = server._build_system_prompt()
assert "A task cannot change its own prompt text" in rendered, (
assert "A task cannot change its own prompt" in rendered, (
"the rendered system prompt does not say who owns a task's prompt file — the rule "
"is then carried by no artifact a task session receives"
)
assert "`project: emrg`" in rendered, (
assert "File a rant against `emrg`" in rendered, (
"the rule must name the channel (a rant against `emrg`), because 'you may not write "
"it' without a route leaves the task with nowhere to go"
)
assert "install tree" in rendered, (
assert "replaced whole by the next upgrade" in rendered, (
"the reason the write does not work is missing: without it the rule reads as a "
"policy preference rather than a description of two copies"
)
# A rule recorded as the host's is a message that can be pointed at: the quote is
# verbatim, the command that re-finds it is right beside it, and the reference carries
# the resolvable half too — the timestamp is host-local (issue #1252).
assert "2026-10-09T14:34:54" in rendered, (
"the host message the rule rests on is gone, so the rule rests on this instance's "
"inference instead"
)
assert "PR #1986" in rendered, (
"the citation keeps only its host-local half: a timestamp indexes one machine's "
"store, so a reader on a second host has nothing to resolve — the public record "
"spelled beside it is the anchor that survives"
)
# The scope is the half a reader on another host acts on. Without it the paragraph
# reads as a command that works everywhere, and a session that runs it and reads
# `NOT FOUND` (rc 1) concludes the host never said it — the failure mode this
# paragraph exists to close (reviewer finding on #1986, reproduced on two hosts).
assert "host-local" in rendered and "NOT FOUND" in rendered, (
"the paragraph no longer says the citation is host-local: that reading was taken "
"on the machine that recorded the message, and a reader elsewhere can only "
"reproduce it as `NOT FOUND` — stated, that is a scope; unstated, it is a "
"contradiction the reader cannot resolve"
)
assert "You cannot modify competition_prompt.md directly" in rendered, (
"the quotation from another language ships untranslated, against the Language "
"Policy block seven lines below that these paragraphs cite for their placement"
)
assert "丢宇emrg项目" in rendered, (
"the host's own words are not quoted verbatim — the message is re-findable only by "
"the exact string, and a paraphrase cannot be looked up"
)
assert "find-host-message.py --pattern '不能直接修改competition_prompt.md'" in rendered, (
"the render does not say how to re-find the message, so the next reader has to "
"trust this file"
"policy preference rather than a description of two copies — the install tree's copy "
"sits outside every workspace and is replaced whole by the next upgrade"
)


def test_system_prompt_temp_file_rules_section(tmp_path):
"""Temp File Rules section renders with the session tmp dir (rant
2026-08-25T18:10:57): throwaway scripts must go under the session
directory's tmp/ subdir, not the project root / workdir / ~/.emrg root."""
"""The temp-file rule renders with the session tmp dir (rant 2026-08-25T18:10:57):
throwaway scripts go under the session directory's tmp/ subdir, not the project root /
workdir / ~/.emrg root.

The rule is a bullet of `## Session and history` in the compacted prompt rather than a
`## Temp File Rules` section of its own — one rule, one heading, next to the topic it
serves (`tests/test_system_prompt_structure.py` holds the placement). What this measures
is unchanged: the rendered rule exists, it names the session's own path, and it names the
places the scripts must not go.
"""
server = _make_server()
session = Session.create_with_id("tmpfile-test", tmp_path)
rendered = server._build_system_prompt(session)
assert "## Temp File Rules" in rendered
assert "Throwaway scripts and payloads go under" in rendered
assert "tmp/" in rendered
# the rendered path must point into the session directory
assert str(session.dir_path) in rendered
assert "never in the project root" in rendered or "project root" in rendered
assert "project root" in rendered
assert "the config root" in rendered


def test_system_prompt_temp_file_rules_absent_without_session():
"""Without a session the temp-file rules section is skipped (no path to
anchor it to), same as Session & History."""
"""Without a session the session section is skipped (no path to anchor it to), and the
temp-file rule goes with it — it lives inside that section."""
server = _make_server()
rendered = server._build_system_prompt()
assert "## Temp File Rules" not in rendered
assert "## Session & History" not in rendered
assert "## Session and history" not in rendered
assert "Throwaway scripts and payloads go under" not in rendered


def test_submit_rant_tool_registered():
Expand Down
20 changes: 15 additions & 5 deletions tests/test_evolution_prompt_round_cost.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,11 +72,20 @@
),
}

#: The rule's unit and its criterion, as shipped in **both** carriers. Editing either one alone
#: is the defect this pair catches, so `test_both_carriers_state_the_same_unit_and_criterion`
#: The rule's unit, its criterion and its boundary, as shipped in **both** carriers. Editing either
#: one alone is the defect this pair catches, so `test_both_carriers_state_the_same_unit_and_criterion`
#: reads the live text of each rather than one file's expectations about the other.
SHARED_UNIT = "several tool calls in one round"
SHARED_CRITERION = "that is a judgement, and a judgement gets its own round"
#:
#: These are the longest phrases the two carriers genuinely share, and that is a measurement rather
#: than a preference: R10 is a rulebook entry a cycle reads and `system.j2` is one bullet in every
#: session's prefix, so the two state the same rule at different lengths — `several tool calls in one
#: round, of any tool the cycle has (…)` against `**Several calls in one round** when no call's
#: arguments depend on another's answer`. The phrases below are where they agree word for word (the
#: concrete three-call case, the criterion's consequence, the boundary that decides it), and a drift
#: in any of them still moves both — which is the property the comparison exists for.
SHARED_UNIT = "three calls in one round"
SHARED_CRITERION = "gets its own round"
SHARED_BOUNDARY = "no call's arguments depend on"

#: The unit this rule may no longer be stated in. The first version of R10 taught exactly
#: these; the host retired the merge-several-calls-into-one reading explicitly at
Expand Down Expand Up @@ -174,7 +183,8 @@ def test_both_carriers_state_the_same_unit_and_criterion() -> None:
),
"emrg/server/prompts/system.j2": SYSTEM_PROMPT.read_text(encoding="utf-8"),
}
for marker, label in ((SHARED_UNIT, "unit"), (SHARED_CRITERION, "criterion")):
for marker, label in ((SHARED_UNIT, "unit"), (SHARED_CRITERION, "criterion"),
(SHARED_BOUNDARY, "boundary")):
missing = [name for name, text in carriers.items() if marker not in text]
assert not missing, (
f"the {label} `{marker}` is not in {missing}: R10 and system.j2 carry one rule, so "
Expand Down
17 changes: 9 additions & 8 deletions tests/test_language_policy_reach.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,20 +48,21 @@ class over: a rule that governs every actor, stated in carriers that some actors
REPO_ROOT = Path(__file__).resolve().parent.parent
SYSTEM_PROMPT = REPO_ROOT / "emrg" / "server" / "prompts" / "system.j2"

#: The block's identity in `system.j2`. Written as the heading line, so a second copy
#: appended anywhere in the template is visible as a count of 2.
POLICY_HEADING = "## Language Policy — outward-facing output"
#: The block's identity in `system.j2`. The policy now sits in `## Writing`, beside the other
#: rules about how the reply reads; written as the heading line, so a second copy appended
#: anywhere in the template is visible as a count of 2.
POLICY_HEADING = "## Writing"

#: Load-bearing terms of the statement. Each is a verbatim substring of the shipped
#: wording, so the list cannot drift away from the sentence it checks without this file
#: saying so. They cover both halves: what must be English, and what is exempt.
POLICY_TERMS = (
"Outward-facing GitHub output is written in English", # the rule itself
"PR titles and PR bodies", # the outward artifacts
"Outward-facing GitHub text is English", # the rule itself
"PR titles and bodies", # the outward artifacts
"review comments", # the case a review cycle writes
"A quotation from another language is carried in English", # the quotation rule
"Internal artifacts are exempt", # the half that keeps memory Chinese
"`.emrg/memory/`", # where the exempt artifacts live
"A quotation from another language is carried in English with its source named", # the quotation rule
"Internal records keep their own language", # the half that keeps memory Chinese
"memory files, session notes, cycle records, run logs, closing summaries", # where they live
)

#: The clause this rule replaced, and the reason there is a test for its absence: a
Expand Down
40 changes: 17 additions & 23 deletions tests/test_memory_row_bound_reach.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,16 @@
REPO_ROOT = Path(__file__).resolve().parents[1]
SYSTEM_PROMPT = REPO_ROOT / "emrg" / "server" / "prompts" / "system.j2"

#: The hygiene section's identity in `system.j2`, written as its heading line so a second
#: copy would be visible here rather than silently deepening the block.
HYGIENE_HEADING = "**Memory Hygiene**"
#: The section's identity in `system.j2`, written as its heading line so a second copy would be
#: visible here rather than silently deepening the block. One `## Memory` section holds the format,
#: the two numbers and the organisation rules; a second heading starting with `Memory` is the split
#: `tests/test_system_prompt_structure.py` refuses.
HYGIENE_HEADING = "## Memory"

#: The row-bound sentence's identity: the line that states what a row is. Chosen as a phrase
#: that survives rewording of the number, so this file keeps measuring the *bound's* carrier
#: rather than one spelling of the sentence.
BOUND_MARKER = "pure index"
BOUND_MARKER = "The index is a pointer list"

#: The variable the template must use. Named here so the assertion and the template cannot
#: disagree about which name is the one.
Expand All @@ -51,34 +53,26 @@
#: The line cap's identity: the sentence that asks for compaction. Chosen as a phrase that
#: survives rewording of the number, so this file keeps measuring the *cap's* carrier rather
#: than one spelling of the sentence.
LINE_CAP_MARKER = "consolidate: merge redundant memories"
LINE_CAP_MARKER = "it is consolidated"

#: The variable the line-cap sentence must use — the same name/value pairing as `BOUND_VAR`.
LINE_CAP_VAR = "{{ memory_index_row_cap }}"


def _hygiene_block(text: str) -> str:
"""The lines of the memory-hygiene section, heading through the last bullet.
"""The `## Memory` region, heading through the bullet list, excluding the next `## ` section.

Read as a *block* rather than line by line because the nesting is the point: a bound
stated under `{% if false %}` is in the file and in no render, and a check that scanned
the whole file for a phrase would pass it.
Read as a *region* rather than searched file-wide because the boundaries are the point: a
bound stated under `{% if false %}` is in the file and in no render, and a rewrite that moved
one of the two numbers into an unrelated section would still pass a scan of the whole file.
Both numbers must be inside this one section — they belong to the same list.
"""
lines = text.splitlines()
try:
# Prefix rather than equality: the heading line carries its rant citations after
# the marker (`**Memory Hygiene** (PR #941, …)`), and an exact match would miss the
# block it is looking for — the shape `test_language_policy_reach.py` records for
# the same reason.
start = next(i for i, line in enumerate(lines) if line.startswith(HYGIENE_HEADING))
except StopIteration:
start = text.find(HYGIENE_HEADING)
if start == -1:
return ""
block: list[str] = []
for line in lines[start + 1:]:
if line.startswith("{%") or line.startswith("#") or not line.strip():
break
block.append(line)
return "\n".join(block)
rest = text[start + len(HYGIENE_HEADING):]
end = rest.find("\n## ")
return rest if end == -1 else rest[:end]


def _bound_line(text: str) -> str:
Expand Down
Loading
Loading