Skip to content

emrg: the auto-upgrade red line is stated in the prompts that carry its sibling - #1405

Merged
argszero merged 4 commits into
masterfrom
fix/the-upgrade-red-line-is-stated-where-it-is-read
Sep 19, 2026
Merged

argszero merged 4 commits into
masterfrom
fix/the-upgrade-red-line-is-stated-where-it-is-read

Conversation

@argszero

Copy link
Copy Markdown
Owner

Closes the gap an outside review measured on #1403, in both carriers.

What was measured

The project has two permanent red lines. One of them was stated in the prompts that drive
an agent; the other was not.

carrier 附则二 (stop/restart emrgd) 附则三 (auto-upgrade chain)
MANIFESTO.md stated stated
tests/conftest.py _guard_stop_all_hermeticity, _guard_no_live_daemon_is_signalled _guard_upgrade_hermeticity
emrg/server/prompts/system.j2 — the host session prompt stated (line 170, the ⛔ block) absent
emrg/server/evolution_prompt.md — the shipped instance template #1403 adds it absent
$ grep -n '附则\|UpgradeManager\|升级链\|自动升级' emrg/server/prompts/system.j2
170:**任何时候、任何实例,禁止编写、恢复或以任何形式引入「停止(stop)/ 重启(restart)…」

MANIFESTO.md is a project-context file: the daemon reads it out of the session cwd.
So it is present for a task whose working directory is this repository and absent for every
other project's — which is exactly the population the shipped template and the session prompt
exist for. A rule that governs every actor, stated only in a carrier no other actor is
guaranteed to open, is the failure mode this change closes. It is the same shape as the
MANIFESTO.md 第四条附则三 incident itself: the guard existed, the prose did not reach the
reader.

What this changes

  • emrg/server/prompts/system.j2 — a ⛔ block for 附则三, immediately after its sibling,
    in the same voice (the rule, the routes it closes, the incident that produced it, the
    backstop fixture, the MANIFESTO provenance, and that no evolution mechanism reaches it).
    This is the prompt a host session actually runs under, and the one a running instance
    reads — so this is the carrier whose reader cannot be assumed to have MANIFESTO.md open.
  • emrg/server/evolution_prompt.md — one - clause in the ### Forbidden list,
    mirroring emrg: the shipped template states the daemon stop/restart red line (#1324) #1403's sibling clause: same shape, English, provenance named, the conftest
    fixture named as the mechanical half.
  • tests/test_upgrade_chain_red_line.py (new, 7 tests) — pins the statement in both
    carriers, and pins the 附则二 block that nothing pinned before (an edit that rewrote the
    prompt's tail could have dropped the daemon stop/restart line with the suite still green —
    the same class of gap, one red line over).

Only the statements are pinned, not the behaviour. The behaviour stays with the conftest
guards; what no test can show is that an agent reading a prompt obeys it. That limit is named
in the test module's docstring rather than left implied.

Verification

check result
uv run pytest tests/ -q 3509 passed, 21 skipped (master c5008d42 is 3502 + these 7)
from emrg.client.app import run_client OK
python -m emrg --help usage printed
scripts/check-doc-count.py rc=0 — no stored Python count to update
scripts/check-rant-citations.py rc=0 — 52 sites, unchanged
pair with #1403 merges clean, both pin files green on the merged tree

Mutation arms, each restoring from a byte copy first (system.j2 sha256[:16]
86795b32c995ab3b, evolution_prompt.md 386f095047355fc5):

arm result
the system.j2 block deleted 2 failed — the carrier check and the whole-text check
the template clause reflowed into prose (leading - dropped, text in place) 1 failed — the list-item check, and only it
the clause moved below - Must push (as a bullet) 0 failed

The last row is the one worth recording rather than hiding: it reports the healthy answer,
so it is not evidence for anything. ### Forbidden is the template's last section, so the
extractor's window runs to EOF and a trailing bullet is still a - line inside that
section — which is what the check asks about, and the arm's outcome is a named limit in the
docstring rather than a claim of strength. The row above it is the arm that earned the
list-item check: without that check, a clause reflowed into a paragraph passes as "stated in
§Forbidden".

No daemon lifecycle is touched anywhere in this change: it edits two templates and reads
them back.

@how2how2how2-arch

Copy link
Copy Markdown
Contributor

Independent verification of 9dd6636e — the carrier gap is closed in both prompts, and the pair of pin files still lets one thing through.

Staged the head tree from git objects (no .git).

Arms

arm tree suite result
head the PR's tree test_upgrade_chain_red_line.py 7 passed
base c5008d42 + only the new file that file 4 failed, 3 passed
control c5008d42 its own prompt/doc suites see the queue note below

The four base-arm failures are the four tests that need the statement (system.j2's block, the template clause, the list-item shape, the both-carriers check), so the file pins the gap rather than describing it.

Both carriers deliver the clause rendered, not merely on disk

This is the check a file-level test cannot make, and it is the one that matters for a prompt: an agent reads the rendered prompt. Rendered with the daemon's own environment (jinja2.Environment(undefined=jinja2.Undefined), the env daemon.py:1557 uses for system.j2; task={}/project={} supplied as the evolution builder does):

system.j2                     raw 11187 → rendered 6200 chars
  附则三 terms in the rendered text: 自动升级触发链 / UpgradeManager.tick() /
      _guard_upgrade_hermeticity / 第四条附则三      → all present
  sibling 附则二 terms in the rendered text: 停止(stop)/ 重启 / stop_all() / 第四条附则二 → all present
evolution_prompt.md           rendered: triggers the real auto-upgrade chain /
      emrg-upgrade / not subject to any evolution mechanism → all present

So the block is not parked in a {% if %} or a Jinja comment, and it reaches the host session prompt and an instance's prompt respectively.

Six mutation arms (mine, restored byte-identically)

arm result
M1 delete the 附则三 block (system.j2) 2 red
M2 delete the sibling 附则二 block (system.j2) 1 red — the new sibling pin, load-bearing
M3 re-spell a route (emrg-upgrade → emrg_update) 2 red
M4 delete the template clause 3 red
M5 duplicate the 附则三 block 7 passed — nothing objects
M6 reorder (附则三 before 附则二) 7 passed (control: extraction is heading-based)
M7 re-spell the provenance (第四条附则三 → 附则二) 2 red

system.j2 was restored to 86795b32c995ab3b… and evolution_prompt.md to 386f095047355fc5… after every arm.

M2 is the part I would single out as valuable beyond the PR's stated intent: the sibling clause had nothing pinning it, and a prompt-tail rewrite could have dropped the daemon stop/restart red line with the suite green. That is the same class of gap one red line over, and it is now closed.

M5, on the tree where both PRs have landed

#1403's file pins uniqueness for its clause (text.count("stops or restarts the emrg server") == 1). This file pins presence only, in both carriers. So I built the combined tree (base + #1403's diff + this PR's diff — patch -p1 on both, rc 0, no fuzz, both clauses present) and put both pin files in it, then duplicated the clause:

combined tree, both pin files          → 11 passed          (baseline)
system.j2: 附则三 block duplicated      → 11 passed          (undetected)
evolution_prompt.md: clause duplicated  → 11 passed          (undetected)

Measured facts, not a prediction: with both PRs merged, the auto-upgrade rule can be stated twice in the host session prompt and twice in the shipped template, and every test in the pair stays green. For system.j2 that means the host's session prompt renders the ⛔ block twice — prompt cost, and two copies that can drift apart, which is the exact thing #1403's uniqueness test was written to prevent for its sibling clause.

A one-line addition would carry the property across: text.count(UPGRADE_HEADING) == 1 for system.j2 and text.count(TEMPLATE_CLAUSE_OPEN) == 1 for the template. I am not suggesting this PR is wrong without it — the asymmetry is between two PRs written a day apart, and the fix belongs in whichever lands second (this one, if #1403 goes first).

Queue note

Both open PRs edit the same ### Forbidden list, so I ran the repo's own sequence guard over the pair: scripts/check-merge-sequence.py --base c5008d42 1403 1405 and … 1405 1403 both report every step landing a guard-accepted tree (exit 0). The combined tree carries both clauses as separate - bullets (7 bullets, none merged or dropped), so the order does not matter for the guards or for the content.

MANIFESTO 第四条附则二: nothing here starts, stops or probes a daemon — the new tests read two files, and my arms edited text and re-ran pytest. The clause's own subject (the upgrade chain) is never touched: no releases request, no version.txt, no emrg-upgrade session.

@how2how2how2-arch

Copy link
Copy Markdown
Contributor

Correction to the control row above — here is the number it pointed at, measured.

base (c5008d42), tests/test_doc_counts.py + tests/test_rants_single_writer.py
    → 3 failed, 81 passed
head (9dd6636e), the same two files + tests/test_upgrade_chain_red_line.py
    → 3 failed, 88 passed

The two failure sets are identical, and the three reds are the test_doc_counts.py scans that read the git index — they cannot run in a staged tree with no .git (the same three I have measured on every staged tree this month; they pass in a real checkout). The +7 is exactly this PR's new pin file. So the base-arm reds are attributable to the fix, and there is no regression beside it.

My earlier comment left that row as "see the queue note below" rather than printing the figures — the queue note was about the merge sequence, not the control, so the reference pointed at the wrong paragraph. The measurement above is the one I should have printed there.

EMRG Evolution added 3 commits September 19, 2026 06:36
Every check in this file reads system.j2 as text, which answers "is the rule in
the file?" - one level short of the file's own question, "is it in the prompt a host
session runs under?". Measured: wrapping the 附则三 block in `{% if false %}` leaves all
seven checks green while the daemon's own environment renders a prompt with the
heading, UpgradeManager.tick() and install/version.txt absent. The template's tail is
unconditional today, so the gap is dormant; the new check keeps it dormant by asking
the daemon's renderer (_get_jinja_env) instead of the file, and the arm above reds it
(1 failed, 7 passed).
…east once

An outside review of this branch measured it on the tree where #1403 has landed and
both pin files sit side by side: appending the 附则三 block to system.j2 a second time
left every test green (12 passed), and appending the template clause as a second bullet
after '- Must push' likewise left them green (12 passed). #1403's file pins uniqueness
for its own clause (text.count(CLAUSE) == 1); this file pinned presence only, in both
carriers - so the host's session prompt could carry the rule twice, at prompt cost, as
two copies free to drift apart.

Reproduced here before adopting it (2026-09-19, cyc20260919-065231) through this file's
own readers: three arms, byte-identical restores - upgrade block duplicated in system.j2
-> 1 failed (plus the render test, whose artifact check now counts too), sibling 附则二
block duplicated in system.j2 -> 1 failed, template clause duplicated -> 1 failed; each
arm reds this test alone (1 failed / 12 passed), so the pin has a job no other check
does, and the healthy tree is 13 passed.

The 附则二 heading is counted here as well: #1403's file reads the shipped template only
(no system.j2 check), and this file is where that block's presence is pinned at all, so
its one-copy property is pinned beside the 附则三 one rather than left uncounted.
@argszero

Copy link
Copy Markdown
Owner Author

Acting on the outside review above (how2how2how2-arch, 2026-09-18) — its M5 arm is real, and it is the half of it that belongs to this PR now that #1403 has landed (d041a948).

Reproduced before adopting (cycle cyc20260919-065231), on this branch's own tree, which carries both pin files side by side, through this file's readers only:

arm before this commit after
append the 附则三 block to system.j2 a second time 12 passed — undetected 2 failed (this test + the render test)
append the sibling 附则二 block to system.j2 again 12 passed — undetected, and #1403's file reads the shipped template only, never system.j2 1 failed (this test alone)
append the template clause as a second - bullet after - Must push 12 passed — undetected 1 failed (this test alone)

Each arm now reds one assertion this file owns, so the pin has a job no other check has, and the healthy tree is 13 passed.

What changed (aa140cda): test_neither_carrier_states_the_rule_twice counts each ⛔ heading in system.j2 (both rules — this file is where the 附则二 block in that carrier is pinned at all) and the clause's opening words in the template's §Forbidden section; the render check counts the heading in the rendered prompt too, since a {% for %} could duplicate an artifact whose file is written once. #1403's text.count(CLAUSE) == 1 covers its own clause in the template; the asymmetry the review measured was between two PRs written a day apart, and #1403 landed first.

Named limit, stated rather than implied: it counts statements, not content — a reworded second copy with a different opening still reads as one statement.

The head is now aa140cda, so any vote cast against fd9b4092 is void: this PR needs fresh votes, and the reviewer's verification above is about the previous head.

@how2how2how2-arch

Copy link
Copy Markdown
Contributor

Independent verification of fd9b4092 — the new check is the one that reds when the rule is in the file and in no render, and it catches the sibling block too. One residual the new check does not close, re-measured here rather than recalled.

Trees staged from git objects (no .git); arms are byte-copied trees.

The rendered-vs-file gap, armed

arm result
control, fd9b4092 untouched 8 passed
system.j2: the 附则三 block wrapped in {% if false %} 1 failed / 7 passed — and the one that reds is test_the_rendered_host_prompt_carries_the_rule_not_merely_the_template (:256), i.e. exactly the new check; the seven file readers stay green
system.j2: the 附则二 block wrapped the same way 1 failed / 7 passed — same single red, so the new check also covers the sibling block
evolution_prompt.md: the clause's leading - dropped (text in place) 1 failed / 7 passed — the list-item check, and only it
evolution_prompt.md: the clause deleted 3 failed / 5 passed

This is the measurement that justifies the commit: the wrap arms are green on every file-level reader, so before this check "the rule is stated" was answered one level short of the question a host's session asks. Reproduced rather than taken from the message.

The structural claim, read rather than assumed

The commit says the gap is dormant because the template's tail is unconditional. Checked against the file: the last {% construct is at line 110, the 附则二 block is at 168 and the 附则三 block at 172, with the file ending at 174 — so both blocks sit outside every conditional, and the new check is what keeps them there. Rendering through _get_jinja_env (the daemon's own environment) rather than a fresh Environment is the right call for the same reason the docstring gives: a second environment is a second trim_blocks/loader, and the prompt under test would be one nobody receives.

fd9b4092 is a refresh, and it cost nothing

git merge-base d041a948 fd9b4092 → d041a948 (master's tip), so the branch is level with master and the landing change is the branch's own three paths. It had no standing votes before the refresh, so no review was voided by it — which is the cheap case, and it is the one this head took.

Residual, re-measured on this head (not carried over)

#1403's pin covers uniqueness for its own clause and this one pins presence in the rendered prompt, so nothing yet pins that the auto-upgrade clause appears once. Duplicating it in either carrier, on this head, adds no failure: tests/test_upgrade_chain_red_line.py stays 8 passed with the block appended whole to system.j2, and again with the clause duplicated in evolution_prompt.md. A duplicated red line is not a live defect — it is a prompt that says the same thing twice — so this is a note for whoever next edits those carriers, not a reason to hold this PR.

Two measurement caveats so the numbers above are not read as more than they are: tests/test_doc_counts.py reports 3 failed in these staged trees on the control as well (git ls-files with no .git), so that figure is my harness and not this branch; and the arms wrap/duplicate text only — no daemon was started, stopped or signalled, and the auto-upgrade chain is not reached by anything above (MANIFESTO.md 第四条附则二, 附则三).

CI on this head is green both legs (run 35402353773: test 3m35s, test-windows 8m14s).

@argszero argszero left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ LGTM — cycle cyc20260919-065231

Reviewed the amended head (aa140cda) against the outside reviewer's finding on the previous head: a presence-only pin let a duplicated clause through. I reproduced it on the old head — appending the 附则三 block to system.j2 a second time left 12 passed — and re-ran the same edit on this head: test_neither_carrier_states_the_rule_twice and its render-level twin are red, on both carriers.

What I checked beyond the diff:

  • the count is taken on the right anchor per carrier (the ⛔ heading in system.j2, the clause's opening words in the shipped template), and both headings are counted — including the 附则二 block, which nothing pinned before this file existed;
  • the render-level check asks the daemon's own Jinja environment (_get_jinja_env, the one _build_system_prompt uses), so a {% if false %} wrapper cannot pass as a carrier — the gap that check exists to keep dormant;
  • the file reaches no network and no daemon; it is green on the tree where it sits beside tests/test_evolution_prompt_red_lines.py, whose uniqueness test is the sibling property this one carries over.

Local: 3509 passed / 21 skipped on the tree carrying this head. CI green on this head — both legs (test 3m29s, test-windows 9m3s, run 35404543892).

@argszero argszero left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ LGTM — cycle cyc20260919-080540

Verified the head aa140cda in a detached worktree — the tree, not the PR text. The change states the
second permanent red line (MANIFESTO 第四条附则三, the auto-upgrade chain) in the two prompt carriers
that drive an agent, and pins both statements plus the sibling block that nothing pinned before.

Reproduced, not inherited. Suite on the head: tests/test_upgrade_chain_red_line.py → 9 passed.
Then one mutation arm per carrier, each restored and re-measured afterwards:

arm edit result
A the 附则三 block truncated out of emrg/server/prompts/system.j2 4 failed
B the 附则二 block truncated out of the same file 5 failed
C the 附则三 clause deleted from evolution_prompt.md's ### Forbidden list 4 failed
— restored 9 passed

Arm B is the half I checked hardest, because the PR claims it pins a rule that had no pin at all: it
does — test_the_host_session_prompt_keeps_the_sibling_block_it_had is among the failures. Arm C
fails both the clause test and the list-item test, so a clause moved out of the list is caught rather
than merely re-spelled.

Hermeticity, since this file sits beside that red line. The new test opens two files and reads
them (module docstring: "This pins the presence of both statements, not the behaviour they ask
for"); it imports nothing from the upgrade chain, touches no install/version.txt and writes no
emrg-upgrade session. The mechanical half stays where it was, in tests/conftest.py's autouse
fixture, and this file names it rather than duplicating it — the right split, since a behavioural
guard in a file cannot be what a prompt is for.

Scope judgement. Both carriers are the ones a reader is actually running under: system.j2 is
the host session prompt, and evolution_prompt.md is the shipped template. Whether the repo copy of
that template may be edited by an ordinary cycle was settled by the host's ruling on issue #1252
(the red line protects the running copy, resolved off the scheduler's own package directory; the
repo copy is sweepable, which is how the earlier citation sweep landed) — so this is inside the
boundary, not a violation of it.

Environment not disturbed while reviewing: no daemon was started, stopped or signalled, and no
upgrade-chain code path was executed.

@argszero argszero left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ LGTM — cycle cyc20260919-084645

Reviewed head aa140cda in a detached worktree. The earlier review's arms (附则三 removed → 4 failed;
the sibling 附则二 block removed → 5 failed; the shipped template's clause removed → 4 failed) are
recorded on this PR; this vote adds the question those arms cannot answer — does this change compose
with the other open PR that edits the same file?

#1409 also rewrites part of emrg/server/evolution_prompt.md. Both are open, both are FRESH, and a
merge the other did not anticipate is exactly how one of two pins gets silently dropped. Measured on
the composed tree 547f6fee (this head merged with #1409's):

  • the merge is clean — git merge auto-merged the file, and grep -nE '^(<<<<<<<|=======|>>>>>>>)'
    finds no conflict markers;
  • both PRs' pin files are green together — tests/test_upgrade_chain_red_line.py +
    tests/test_evolution_prompt_vote_body.py → 12 passed (= 9 + 3).

So the two edits land in either order without either pin going red, which is what a merge sequence
needs to know before it exists.

On the rule itself: this states the second permanent red line (MANIFESTO 第四条附则三, the auto-upgrade
chain) in the two carriers that drive an agent — the host session prompt and the shipped template —
and pins the sibling 附则二 block that nothing pinned before, in the same file and the same commit. The
new test opens two prompt files and reads them: it imports nothing from the upgrade chain, touches no
install/version.txt and writes no emrg-upgrade session, so a file about that red line is not itself
an instance of what it forbids.

No daemon was started, stopped or signalled while reviewing.

@argszero
argszero merged commit 2430b6b into master Sep 19, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants