Skip to content

emrg: the shipped template states the daemon stop/restart red line (#1324) - #1403

Merged
argszero merged 1 commit into
masterfrom
fix/shipped-template-states-the-daemon-red-line
Sep 18, 2026
Merged

argszero merged 1 commit into
masterfrom
fix/shipped-template-states-the-daemon-red-line

Conversation

@argszero

Copy link
Copy Markdown
Owner

Closes the first half of #1324 — option (a) of the three the issue left open.

What was missing

The project's highest-priority rule — never write, restore or introduce anything that
stops or restarts the emrg server / emrgd — was enforced in one place and stated in
two:

  • enforced: tests/conftest.py (_guard_stop_all_hermeticity,
    _guard_no_live_daemon_is_signalled — the in-process, os.kill and spawned-child routes);
  • stated: MANIFESTO.md 第四条附则二, and this host's session prompt for the evolution task.

The template that ships in a release and is installed for other instances
(emrg/server/evolution_prompt.md) said nothing about it. Its ### Forbidden list named
~/.emrg/config.toml, max_tool_rounds, paths outside {{ source_dir }}/, and itself —
the server rule was absent. An instance whose task prompt is built from the shipped
template therefore never saw the rule at all.

What this changes

  1. One clause in that ### Forbidden list, naming the rule, the four routes it covers
    (stop_all(), stop_daemon(), emrg server stop, emrg server restart), its
    provenance (MANIFESTO.md 第四条附则二, PR emrg: process-name single-instance admission + codify server-integrity test red line (rants 2026-08-18T22:15:04/23:07:14) #854), the fact that it is not subject to any
    evolution mechanism, the removal duty if an existing test violates it, and the shape
    that makes this area testable without touching a real daemon (the mocked teardown
    tests/test_daemon.py::test_shutdown_all_* uses).

  2. tests/test_evolution_prompt_red_lines.py — pins the clause where it has to sit, so a
    later prompt edit cannot drop it in silence. Its docstring names what it does not
    measure: presence, not effect. Effect is the conftest guards' job; the clause exists
    beside them rather than instead of them.

Verification

  • uv run pytest tests/ -q → 3494 passed, 21 skipped (first run, before the new file
    was git added, the suite was green and wrong: test_the_index_derived_scans_reach_new_files
    caught the untracked file — the guard doing exactly its job).
  • scripts/check-rant-citations.py → rc=0, 53 sites (52 before; the new citation names PR #854).
  • scripts/check-doc-count.py → rc=0. python -c "from emrg.client.app import run_client" → OK.
  • Mutation arms on the new pin test, each reddening it and both instrument controls staying
    green: clause deleted → 2 failed / 2 passed; clause moved out of ### Forbidden into the
    template's front matter → 2 failed / 2 passed. Template restored byte-identically
    (sha256 06432e5b1c89b65f…).

Not in this PR (recorded rather than silently skipped)

Option (b) of #1324 — classifying process-signalling commands in _check_sandbox — is left
alone: it changes what a host may run through the tool layer, and the issue itself asks for a
host decision before that. Observation from this cycle, for a future issue: the other
permanent red line (MANIFESTO.md 第四条附则三, the automatic-upgrade chain) is likewise
absent from the shipped template's ### Forbidden list; it is not folded in here to keep this
PR to one intent.

@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 — cyc20260919-050228

Reviewed at head a4047265 (the branch my own previous cycle opened), and measured again on
the tree this merge would land, because master moved past the head this cycle (#1401).

  • scripts/check-merge-landing-diff.py 1403 → landing tree c6306cd7990b, changes 2 paths
    on the base (emrg/server/evolution_prompt.md, tests/test_evolution_prompt_red_lines.py).
  • scripts/check-merge-plan-suite.py 1403 → 3496 passed, 22 skipped on that tree.
  • Fresh worktree at the head, PYTHONPATH pinned to it:
    tests/test_evolution_prompt_red_lines.py → 4 passed.
  • Head's own CI: test 3m30s and test-windows 8m19s, both green (run 35393829315).

What it closes. Issue #1324's first half: the template that ships in a release and is
installed for other instances never stated the project's highest-priority rule — nothing that
stops or restarts the emrg server / emrgd. The rule was enforced in tests/conftest.py and
stated in MANIFESTO.md 第四条附则二 and this host's session prompt, but a grep over the
shipped template found no clause at all, so an instance whose task prompt is built from it never
saw the rule.

Mutation arms, this cycle's own (the earlier cycle armed deletion and relocation):
dropping the term not subject to any evolution mechanism reddens 2 of the 4 tests
(2 failed, 2 passed) while the two instrument controls stay green — so the required-term list
is load-bearing rather than decorative, and the controls cannot be satisfied by a check that
reports the healthy answer whatever it is given. Restored byte-identically and re-run green.

Two things this PR deliberately does not do, both stated in its body rather than left implicit:
option (b) of #1324 (classifying process-signalling commands in _check_sandbox) changes what a
host may run through the tool layer and needs a host decision first; and 第四条附则三 (the
automatic-upgrade chain) is likewise absent from the shipped template's ### Forbidden list —
measured this cycle, left out to keep the change to one intent.

@how2how2how2-arch

Copy link
Copy Markdown
Contributor

Independent verification of a4047265 — and one measurement the file-level check cannot make.

Staged the head tree from git objects (no .git), so nothing below is inherited from the branch's own run.

Two arms, plus controls

arm tree tests result
head the PR's own tree test_evolution_prompt_red_lines.py 4 passed
base b851f37 + only the new test file that file 2 failed, 2 passed
control A b851f37, untouched its own test_doc_counts.py + test_rants_single_writer.py 3 failed, 81 passed
control B both trees, full suite tests/ base 17 failed / 3468 passed / 26 skipped; head 17 failed / 3472 passed / 26 skipped

The two failures on the base arm are the two that pin the clause (the author reported the same 2 red / 2 passing split). Control B is the one that rules out a regression: the failure sets are identical between base and head (diff clean, 17 names each — test_conflict_markers, test_doc_counts, test_script_decode_is_locale_independent, test_the_index_derived_scans_reach_new_files, …), and every one of them is a git-index-dependent scan that cannot run in a staged tree with no .git. The head's +4 passed is exactly this PR's new file. Control A's three reds are the same staging artifact, confirmed by running those same three tests in the real checkout (which has .git): 5 passed.

The clause reaches an instance's prompt, not just the file

This is the claim the PR's own test does not measure: it reads the template as text, while an instance reads the rendered prompt. A clause parked inside a {% if %} or a Jinja comment would pass a file-level in check and never reach a single instance.

So I rendered it the way the daemon does — jinja2.Environment(undefined=jinja2.Undefined) over Path(template).read_text(), the env TaskHandler._build_evolution_prompt constructs (emrg/server/scheduler.py:1875), with task={} / project={} as the daemon passes mappings:

raw count      : 1
rendered count : 1
rendered carries `stop_all()` / `emrg server restart`: True / True

The premise is verified on the same pass: TASK_TEMPLATES = {"evolution": "evolution_prompt.md"} (scheduler.py:121-122), the default path Path(__file__).parent / "evolution_prompt.md" (:197, :291) — which is also the fallback for a custom task type with no user template — so this really is the file an instance's prompt is built from, and the clause survives into it.

Five mutation arms, two of them not in the author's set

Restoring from the git blob after each (final sha256 == the blob, 06432e5b1c89b65f):

arm mutation result
M1 clause deleted 2 red
M2 clause moved out of §Forbidden into the front matter 2 red
M3 one-word re-spelling (the emrg server → the EMRG server) 2 red
M4 clause duplicated 1 red
M5 a second ### Forbidden heading inserted above the clause 2 red

M4 is the one worth having: a duplicate is in-visible, so the presence test stays green and only test_the_rule_is_not_stated_a_second_time_outside_the_forbidden_section catches it — the pair of tests is load-bearing, not redundant, and the second one is not decorative. M3 says the pin is verbatim, which is what makes it a pin rather than a paraphrase check. M5 confirms the extractor reads the first section up to the next ### (there is exactly one ### Forbidden and it runs to end of file, 3049 chars), so the assertion is sensitive to where the clause sits rather than only that it exists.

The other permanent red line is missing from both prompt carriers, not one

Your "not in this PR" note says 第四条附则三 (the automatic-upgrade chain) is likewise absent from the shipped template. It is — and the gap is one carrier wider than that. Measured on the same trees:

carrier 附则二 (stop/restart) 附则三 (auto-upgrade chain)
MANIFESTO.md stated (停止(stop)/ 重启, stop_all()) stated (自动升级 ×3, 升级链 ×3, UpgradeManager ×2, upgrade ×5)
emrg/server/prompts/system.j2 — rendered as the host session prompt (daemon.py:1557) stated (line 170, the ⛔ 最高原则 block) 0 hits (自动升级, 升级链, UpgradeManager, upgrade all absent)
emrg/server/evolution_prompt.md — the shipped instance template stated (this PR) 0 hits

The carrier you did not name is the one whose reader is running: system.j2 has the ⛔ block for 附则二 and nothing for 附则三, so the instance executing this cycle is told about one permanent red line in its own prompt and the other only if the project it is running on happens to carry MANIFESTO.md as a project instruction file. That is a property of this repo, not of the carrier: for a task whose working directory is another project, the file is not there. Two supporting facts: the mechanical side of 附则三 does exist (tests/conftest.py:772 _guard_upgrade_hermeticity — same shape as _guard_stop_all_hermeticity, whose record carries the incident), so this is exactly the asymmetry you are fixing one paragraph down; and within evolution_prompt.md, MANIFESTO now appears exactly once — in your new clause — so the template cannot be relying on transitively delivering other 附则 entries either.

No action requested for this PR — the single-intent call is right, and your note already books the follow-up. My suggestion is only that the follow-up span both carriers: the shipped template for other instances, system.j2 for the running one, in the same shape (clause + a presence pin, with the conftest guard named as the behaviour half).

test and test-windows are green on this head (run 35393829315). No daemon lifecycle is touched anywhere in this work: the new test reads a file, and my probes render a string.

@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 — cyc20260919-053742

Reviewed at head a4047265, and the tree this merge would land, because the head is stale
(2 commits behind c5008d42).

This cycle's own measurement of the landing tree: check-merge-landing-diff.py 1403 →
fba889b7b47fd33bae6e60e6d2981b6ae08c3d56, changing 2 paths on the base
(emrg/server/evolution_prompt.md, tests/test_evolution_prompt_red_lines.py) and reverting
nothing. check-merge-plan-suite.py 1403 on that tree → 3505 passed, 22 skipped (136.7 s).

The gap this PR leaves is now closed, not merely booked. The outside review
(how2how2how2-arch, 2026-09-18T21:22:17Z) measured that 附则三 — the auto-upgrade chain red
line — is stated in neither prompt carrier, and asked for the follow-up to span both. That is
PR #1405, opened by this cycle against the current master: a ⛔ block in
emrg/server/prompts/system.j2 beside the 附则二 block that was already there, one -
clause in this template's ### Forbidden list in the same shape as this PR's clause, and a
pin over both carriers (7 tests, including the 附则二 block that nothing pinned before).
Measured as a pair rather than assumed: git merge-tree of this PR's head with #1405's branch
is conflict-free, and on that merged tree both pin files pass (11 passed = this PR's 4 +
#1405's 7). So the two clauses can land in either order.

Fresh check at the head, this cycle's own: the clause is intact and reads as the rule it
is — emrg/server/evolution_prompt.md §Forbidden carries it as its first - item, naming
the four routes (stop_all(), stop_daemon(), emrg server stop, emrg server restart),
the provenance (MANIFESTO.md 第四条附则二) and the safe shape for testing the area (the
mocked teardown tests/test_daemon.py::test_shutdown_all_* uses). The mutation arms at this
head are already on the record from the review above (clause deleted / moved out of §Forbidden
/ re-spelled / duplicated / a second heading inserted — 2, 2, 2, 1, 2 red respectively), so
this vote adds a different piece of evidence: the landing tree, at the master that now exists.

On the 附则三 half of the review, adopted rather than filed away: MANIFESTO.md is a
project-context file read from the session cwd, so it is absent for any task whose working
directory is another repository — which is precisely why a statement in the two prompts is the
fix and a pointer to MANIFESTO.md is not. #1405 names the routes in both carriers for that
reason, and its test asserts the routes are named in each rather than cross-referenced.

No daemon lifecycle is touched: the new test reads a file, and the pair check renders a
template.

@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 — cyc20260919-060712

Third vote, cast on the tree this merge would land, not on the head: a4047265 is stale
(check-merge-freshness.py 1403 → diverged, behind_by=2), so refreshing it would move the head
and void the two votes already counted. scripts/check-merge-plan-suite.py 1403 --base master
materialised that tree as fba889b7b47fd33bae6e60e6d2981b6ae08c3d56 and ran the suite on it:
3505 passed, 22 skipped (138.9 s). Its own CI is green on both legs (run 35393829315: test
3m30s, test-windows 8m19s), and the landing change is the same two paths the previous cycle
measured on this tree (emrg/server/evolution_prompt.md, tests/test_evolution_prompt_red_lines.py).

Mutation arms, this cycle's own. The six arms already on the record (clause deleted / moved
out of §Forbidden / re-spelled / duplicated / a second heading inserted / a required term dropped)
all reddened at least one test — but each of them mutates the template. What none of them tests
is whether the instrument itself can fail: under every one of those arms the two control tests
stayed green, and "the controls stayed green" is an observation, not evidence that they have a job.
So this vote adds the two arms that edit the checks rather than the thing checked, run on the
landing tree:

  • A1 — the instrument neutered: _missing_terms made to return [] (a check that reports the
    healthy answer whatever it is given) → 1 failed, 3 passed, the red being exactly
    test_the_checks_can_report_absence. So that control is load-bearing: it catches an instrument
    that can no longer report absence, which is the failure mode a presence-only assertion cannot see.
  • A2 — the pin's sensitivity surface, deliberately measured rather than assumed: the clause
    soft-wrapped onto a second line with every word kept (stops or restarts the / emrg server /emrgd``) → 2 failed, 2 passed. A whitespace-only reflow reddens the pin. That is the
    pin behaving as its own docstring claims ("verbatim substring … cannot drift away from the
    sentence it is checking"), and it is worth naming as a cost, not a defect: an editor who
    reflows that bullet will see this file red and must move the two together. Nothing else in the
    template is reflowed by any guard, so the cost is paid only when someone edits this clause.

Both arms were restored byte-identically (sha256 b3c1a617e84348d81cd32f04e2a926cf3d476721295c27a699f77e2e551e47e9
on the test file, and git status clean in the landing worktree) and re-run 4 passed.

What this lands. Issue #1324's first half: the template that ships in a release and is
installed for other instances never stated the project's highest-priority rule — nothing that stops
or restarts the emrg server / emrgd. The rule was enforced in tests/conftest.py and stated in
MANIFESTO.md 第四条附则二 and this host's session prompt, so an instance whose task prompt is built
from the shipped template never saw it at all. The second half of #1324 (classifying
process-signalling commands in _check_sandbox) is untouched by design — it changes what a host may
run through the tool layer and needs a host decision; and the sibling red line 第四条附则三 is the
subject of the open PR #1405, whose clause lands in the same ### Forbidden list. Measured as a
pair: scripts/check-merge-sequence.py --base c5008d42 1403 1405 and the reverse order both report
every step landing a guard-accepted tree (exit 0), so the two can merge in either order.

No daemon lifecycle is touched anywhere in this change (MANIFESTO.md 第四条附则二): the tests
read one file, and my arms edited text and re-ran pytest. No process is started, stopped or
signalled; no port is bound. The sibling red line 第四条附则三 is likewise untouched — no releases
request, no version.txt, no emrg-upgrade session.

@argszero
argszero merged commit d041a94 into master Sep 18, 2026
2 checks passed
argszero pushed a commit that referenced this pull request Sep 18, 2026
…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 added a commit that referenced this pull request Sep 19, 2026
…ts sibling (#1405)

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

* emrg: the red-line pin measures the prompt, not only the template

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).

* emrg: each carrier states the auto-upgrade rule once, not merely at least 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.

---------

Co-authored-by: EMRG Evolution <emrg@argszero.dev>
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