From 9d9f7ec20564ccc52bfb95aaaa5a663831f3581a Mon Sep 17 00:00:00 2001 From: EMRG Evolution Date: Mon, 26 Oct 2026 15:27:17 +0800 Subject: [PATCH 1/3] emrg: the Feishu prompt tells its round that the cadence is not its to lower MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A Feishu task's frequency is decided by its record and by nothing else, but every round may answer the vibe check's recommend_slowdown: true, and the scheduler then waits the heartbeat interval instead of the record's own. For a watcher that inverts the task: a round that found nothing is exactly the round that invites the recommendation, and the quieter the inbox, the stronger the case for slowing the thing that watches it. The template said nothing, so the only defence was a block in one installation's tasks.yml, which no other installation can read. competition_prompt.md already carries the rule for the same reason. §0. Cadence now states it before §1, in the file's own voice and with no host attribution: no message states it for this task, and scripts/check-rant-citations.py already enforces that anything attributed carries a public record. tests/test_feishu_prompt.py guards it at both levels — the file that ships and the prompt a round is sent — because a Jinja construct hides text from the render while the file keeps every character. --- emrg/server/feishu_prompt.md | 18 +++ tests/test_feishu_prompt.py | 253 +++++++++++++++++++++++++++++++++++ 2 files changed, 271 insertions(+) create mode 100644 tests/test_feishu_prompt.py diff --git a/emrg/server/feishu_prompt.md b/emrg/server/feishu_prompt.md index c968d955..6bcea17b 100644 --- a/emrg/server/feishu_prompt.md +++ b/emrg/server/feishu_prompt.md @@ -20,6 +20,24 @@ invented, never defaulted. --- +### 0. Cadence + +How often this task runs is decided by its record — `interval`, and any `schedule` window that +overrides it — and by nothing else. The cadence is not this round's to lower. + +- Answer the vibe check's `recommend_slowdown` with `false` on every round, and leave + `slowdown_reason` empty. The check asks whether this task is still worth what it costs, and a + task whose value is that it keeps looking is worth a round that found nothing. +- A round that read nothing is a correct round: §1's "a round that reads nothing must say so" is + the whole of what it owes. Finding nothing is never itself a reason for a longer interval. +- Quiet is not evidence either. A conversation that has been silent for days, a message that + arrived and needed nothing, a reply still outstanding — none of them is a ground for waiting + longer, and the longer the host has been silent, the more one arriving message matters. +- Neither is cost: a round that found little was cheap because there was little, which is what + watching costs, not a sign the watching can be done less often. +- Only the host changes the cadence, by changing the record. A round that finds the interval + shorter than it thinks this task needs reports that; it does not act on it. + ### 1. Read **The round's first step is to call `boss`** — a separate session that owns this task's direction while this one does the work — and to treat its answer as the round's stance; whatever the answer does not mention is still this session's to decide. A session that is not offered `boss` has no owner to ask, and carries on without one. diff --git a/tests/test_feishu_prompt.py b/tests/test_feishu_prompt.py new file mode 100644 index 00000000..0a1f4a70 --- /dev/null +++ b/tests/test_feishu_prompt.py @@ -0,0 +1,253 @@ +"""Content guards for `emrg/server/feishu_prompt.md` — the cadence rule (issue #2594). + +The Feishu task is a watcher: its value is that it reads the host's conversations +often enough that a message which arrived is answered in the round after it arrived. +That value is decided entirely by the task record — `interval`, and any `schedule` +window that overrides it. + +The vibe check sits across that decision. Every round an agent may answer +`recommend_slowdown: true`, and the scheduler then waits the heartbeat interval +instead of the record's own. For most task types that is a reasonable safeguard; for +a watcher it inverts the task, because a round that found nothing is exactly the +round that invites the recommendation — the quieter the inbox, the stronger the case +for slowing the thing that watches the inbox. Until now the template said nothing +about it, so a Feishu task was free to slide and the only defence was a block in one +installation's `tasks.yml`, which no other installation can read. + +`competition_prompt.md` already carries a version of this rule for the same kind of +reason, and the two must agree rather than offer a round two answers. + +Two levels, two questions +------------------------- +`feishu_prompt.md` is a Jinja2 template: `TaskHandler._build_evolution_prompt` +renders it and sends the **result**. So the file-level assertions below answer "does +the source that ships carry this?", and the render-level ones answer "is this what a +round is sent?" — neither implies the other. A Jinja construct (`{# … #}`, +`{% if false %}`) removes text from the prompt while the file keeps every character, +a failure measured on `competition_prompt.md` on 2026-10-08 (issue #1904) where all +five load-bearing phrases stayed in the file and none reached the render. + +A citation this rule does not carry +---------------------------------- +The rule is written as the template's own reasoning, with no host attribution, +because no message states it for this task: `find-host-message.py --pattern +'禁止降频'` answers `NOT FOUND` over the span its logs cover (2026-10-23 → +2026-10-26). `scripts/check-rant-citations.py` already scans this file, so a +citation added later without a public record reddens there — the attribution rule is +mechanised, and nothing here restates it. +""" + +from __future__ import annotations + +import re +from pathlib import Path + +import pytest +import yaml + +from emrg.protocol import InstanceIdentity +from emrg.server import scheduler as mod +from tests.task_handler_factory import make_handler + +PROMPT = Path(__file__).resolve().parent.parent / "emrg" / "server" / "feishu_prompt.md" +COMPETITION_PROMPT = PROMPT.with_name("competition_prompt.md") + +#: The heading of the section this file exists to keep. Its number is not decoration: +#: `0` states that it is read before the round's first step, which is §1. +HEADING = "### 0. Cadence" + +#: The first step of the round — the thing the rule has to precede to bind it. +FIRST_STEP = "### 1. Read" + +#: The agent name the render fixture asks for — a name in no template, so "the render +#: carries it" proves a placeholder resolved rather than a string being copied through. +PROMPT_PROJECT = "demoproj" + +#: What the block must say, and why each term is load-bearing rather than decorative. +#: `recommend_slowdown` is the field the round answers; `false` is the answer; the two +#: record fields are what the block must name as the cadence's owner, so a round knows +#: both what to reply and what not to touch. +BOUND_FIELD = "recommend_slowdown" +CADENCE_OWNERS = ("interval", "schedule") + +#: The misreading the block exists to name: a round that read nothing is a normal round. +EMPTY_ROUND_MARK = "a correct round" + + +def _cadence_block(text: str) -> str | None: + """The `### 0. Cadence` block as a reader meets it, or `None` when it is absent. + + Deliberately answers rather than asserting, so the both-ways control below can show + that a missing block is reported as missing instead of as nothing to check. + """ + marker = f"\n{HEADING}" + start = text.find(marker) + if start < 0: + return None + rest = text[start + len(marker) :] + end = re.search(r"\n#{2,3} ", rest) + return rest[: end.start()] if end else rest + + +def _require_block(text: str, subject: str) -> str: + block = _cadence_block(text) + assert block is not None, ( + f"{subject} carries no `{HEADING}` block — the no-slowdown rule is gone, so this " + "task is free to answer `recommend_slowdown: true` and wait up to eight times its " + "record's interval (issue #2594)" + ) + assert block.strip(), ( + f"the `{HEADING}` heading is in {subject} but its body is empty — a template " + "construct kept the title and sent nothing underneath it" + ) + return block + + +def _missing_terms(block: str) -> list[str]: + return [term for term in (BOUND_FIELD, "false", "slowdown_reason", *CADENCE_OWNERS) if term not in block] + + +@pytest.fixture(scope="module") +def rendered(tmp_path_factory) -> str: + """The Feishu prompt as a round receives it, rendered by the real builder. + + `TaskHandler._build_evolution_prompt` is the only thing that turns this template into + a prompt, so it is what these tests render through: a fresh `jinja2.Environment` here + could differ in `trim_blocks`, `lstrip_blocks` or the loader, and the artifact under + test would then be a prompt no round is ever sent. + + `config_dir` is redirected at a tmp tree — the shape + `tests/test_evolution_prompt_index_rule.py` uses — so the `agents.yml` read lands on + a file this test made, never on the host's `~/.emrg`. + """ + tmp_path = tmp_path_factory.mktemp("feishu-prompt") + agent_dir = tmp_path / PROMPT_PROJECT + agent_dir.mkdir(exist_ok=True) + (tmp_path / "agents.yml").write_text( + yaml.safe_dump([{"name": PROMPT_PROJECT, "path": str(agent_dir)}]), + encoding="utf-8", + ) + original = mod.config_dir + mod.config_dir = lambda: tmp_path + try: + handler = make_handler( + name="feishu-task", + type="feishu", + config={"agent": PROMPT_PROJECT}, + identity=InstanceIdentity(), + template_path=PROMPT, + ) + return handler._build_evolution_prompt() + finally: + mod.config_dir = original + + +def test_the_template_states_the_cadence_rule(): + """The rule exists in the source that ships, in its own section.""" + text = PROMPT.read_text(encoding="utf-8") + block = _require_block(text, "feishu_prompt.md") + missing = _missing_terms(block) + assert not missing, ( + f"the `{HEADING}` block no longer names {missing}: an agent reading it cannot tell " + "which field it answers, or which fields decide the cadence it must not lower" + ) + + +def test_the_cadence_block_is_read_before_the_first_step(): + """Placement, not just presence: the rule sits above the step it constrains. + + A rule parked below §1 is met by a round that has already started reading, and the + round is what it binds — the same defect `competition_prompt.md` §0.0 guards against. + """ + text = PROMPT.read_text(encoding="utf-8") + _require_block(text, "feishu_prompt.md") + assert text.index(HEADING) < text.index(FIRST_STEP), ( + f"the `{HEADING}` block drifted below `{FIRST_STEP}` — the rule that governs the " + "round is read after the round has begun" + ) + + +def test_the_block_says_an_empty_round_is_a_correct_round(): + """The specific misreading, named: finding nothing is not a reason to wait longer. + + Without this sentence the block only forbids an answer, and a round that read nothing + has no other way to justify itself than the recommendation it is being denied. + """ + block = _require_block(PROMPT.read_text(encoding="utf-8"), "feishu_prompt.md") + assert EMPTY_ROUND_MARK in block, ( + f"the `{HEADING}` block no longer says a round that read nothing is {EMPTY_ROUND_MARK} " + "— that is the one reading which produces the recommendation" + ) + assert "### 1. Read" not in block, ( + "the block has swallowed §1 — the answer to an empty round belongs to §1, and a " + "second copy here would be two instructions where one is enough" + ) + + +def test_the_cadence_rule_reaches_the_round_and_not_merely_the_file(rendered): + """The same requirement, asserted on the prompt the agent is actually sent. + + The file-level test above cannot see this failure: a Jinja comment around the block + leaves every phrase in `feishu_prompt.md` and drops all of them from the render. + """ + block = _require_block(rendered, "the rendered Feishu prompt") + missing = _missing_terms(block) + assert not missing, ( + f"the rendered `{HEADING}` block does not carry {missing}: the rule is in the file " + "and in no render, which is the one failure a file-level guard cannot see" + ) + assert EMPTY_ROUND_MARK in block, ( + "the rendered block lost the empty-round sentence — a round is told what to answer " + "and not why the round it just had was fine" + ) + + +def test_the_rendered_cadence_block_still_precedes_the_round_it_binds(rendered): + """Placement has to survive rendering, which is where the reader meets it.""" + assert HEADING in rendered, ( + "the rendered Feishu prompt has no `### 0. Cadence` block at all — whether or not " + "the file still carries it" + ) + assert rendered.index(HEADING) < rendered.index(FIRST_STEP), ( + "the rendered cadence block drifted below §1, so a round that is already reading " + "has not been told the rule that binds it" + ) + + +def test_both_templates_that_forbid_a_slowdown_give_the_same_answer(): + """One rule, two carriers, compared — not one carrier trusted. + + `competition_prompt.md` forbids the same recommendation for the same reason. A round + reading two templates that disagree would follow whichever it met first, so the two + are read together and both must name the field and the answer. + """ + competitors = COMPETITION_PROMPT.read_text(encoding="utf-8") + assert "### 0.0 " in competitors, ( + "the competition template lost its own cadence block — this comparison would then " + "have one side and prove nothing" + ) + for name, path in (("feishu_prompt.md", PROMPT), ("competition_prompt.md", COMPETITION_PROMPT)): + body = path.read_text(encoding="utf-8") + assert "recommend_slowdown" in body, f"{name} no longer names the field the round answers" + assert re.search(r"`recommend_slowdown`[^.\n]{0,40}`false`", body), ( + f"{name} no longer states that `recommend_slowdown` is `false` — the two task " + "templates must give a round one answer, not two" + ) + + +def test_the_cadence_scanner_answers_both_ways(): + """The device earns its verdict: a block is found, and its absence is not silence. + + A scanner that returned the whole text for a missing marker would make every + assertion above pass on a template that had lost the section. + """ + present = "\n### 0. Cadence\n\nrule text\n\n### 1. Read\n\nbody\n" + assert _cadence_block(present) is not None + assert "rule text" in _cadence_block(present) + assert "### 1. Read" not in _cadence_block(present), ( + "the scanner ran past the next heading, so it would keep reading a deleted block's " + "neighbours as if they were part of it" + ) + assert _cadence_block("\n### 1. Read\n") is None, ( + "a template without the block was reported as carrying one" + ) From 2b118a3fb225c885ec7f80714428fdb6ec000230 Mon Sep 17 00:00:00 2001 From: EMRG Evolution Date: Mon, 26 Oct 2026 15:28:56 +0800 Subject: [PATCH 2/3] emrg: the Feishu guard's attribution reading names what the instrument measured MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit '禁止降频' answering NOT FOUND is a fact about the corpus — the log keeps a message's first 60 characters and the sessions reach back to 2026-07-15 — not about what the host said. '降频' finds 25 messages, none on the date competition_prompt.md cites, and the host's own question about exempting a specific task (2026-10-23 20:20:51) is findable. Recorded as the occasion for the rule rather than as its authority. --- tests/test_feishu_prompt.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/tests/test_feishu_prompt.py b/tests/test_feishu_prompt.py index 0a1f4a70..2fd267ea 100644 --- a/tests/test_feishu_prompt.py +++ b/tests/test_feishu_prompt.py @@ -30,10 +30,21 @@ A citation this rule does not carry ---------------------------------- The rule is written as the template's own reasoning, with no host attribution, -because no message states it for this task: `find-host-message.py --pattern -'禁止降频'` answers `NOT FOUND` over the span its logs cover (2026-10-23 → -2026-10-26). `scripts/check-rant-citations.py` already scans this file, so a -citation added later without a public record reddens there — the attribution rule is +because no message states it as a directive for this task. Two readings, both taken +on this tree on 2026-10-26: `--pattern '禁止降频'` answers `NOT FOUND`, while +`--pattern '降频'` finds 25 messages and none of them on 2026-10-06 — the date +`competition_prompt.md` cites. So the competition file's own citation cannot be +resolved from here, and the reason is the corpus rather than the host: the daemon log +keeps only a message's first 60 characters, and the sessions reach back to 2026-07-15. +A `NOT FOUND` is therefore a statement about what the record holds, never about what +the host said. + +What the host did say, and findably, is the question this section answers — +`2026-10-23 20:20:51`, session `s_260727_1103_866d`, 「支持特定任务不降频吗?」, whether +a specific task can be exempt from the slowdown. A question is not a directive, so it +is the occasion for the rule and not its authority; the authority is the task's own +purpose. `scripts/check-rant-citations.py` already scans this file, so an attribution +added later without a public record reddens there — the attribution rule is mechanised, and nothing here restates it. """ From e14ae6afc7ffe9b14845521bed4fd7a27848f0a4 Mon Sep 17 00:00:00 2001 From: EMRG Evolution Date: Mon, 26 Oct 2026 17:00:02 +0800 Subject: [PATCH 3/3] emrg: the citation reading names its span, and the guard names its template --- tests/test_feishu_prompt.py | 38 ++++++++++++++++++++++--------------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/tests/test_feishu_prompt.py b/tests/test_feishu_prompt.py index 2fd267ea..dc282b9e 100644 --- a/tests/test_feishu_prompt.py +++ b/tests/test_feishu_prompt.py @@ -31,21 +31,29 @@ ---------------------------------- The rule is written as the template's own reasoning, with no host attribution, because no message states it as a directive for this task. Two readings, both taken -on this tree on 2026-10-26: `--pattern '禁止降频'` answers `NOT FOUND`, while -`--pattern '降频'` finds 25 messages and none of them on 2026-10-06 — the date -`competition_prompt.md` cites. So the competition file's own citation cannot be -resolved from here, and the reason is the corpus rather than the host: the daemon log -keeps only a message's first 60 characters, and the sessions reach back to 2026-07-15. -A `NOT FOUND` is therefore a statement about what the record holds, never about what -the host said. - -What the host did say, and findably, is the question this section answers — -`2026-10-23 20:20:51`, session `s_260727_1103_866d`, 「支持特定任务不降频吗?」, whether -a specific task can be exempt from the slowdown. A question is not a directive, so it -is the occasion for the rule and not its authority; the authority is the task's own -purpose. `scripts/check-rant-citations.py` already scans this file, so an attribution -added later without a public record reddens there — the attribution rule is -mechanised, and nothing here restates it. +with `scripts/find-host-message.py` on 2026-10-26: `--pattern '禁止降频'` answers +`NOT FOUND`, while `--pattern '降频'` finds 25 messages and none of them on +2026-10-06 — the date `competition_prompt.md` cites. So the competition file's own +citation cannot be resolved from here, and the reason is the corpus rather than the +host: the daemon log keeps only a message's first 60 characters. + +Those counts are readings over the span that run reported, not a property of this +tree. The instrument states that span in its own header — how many log files it read, +and how many session directories hold a history, with the first and last instant of +each — and both numbers are the installation's, so a reader elsewhere reproduces the +reading's shape rather than its digits. A `NOT FOUND` is therefore a statement about +what the record holds, never about what the host said. + +What the host did say — `2026-10-23 20:20:51`, 「支持特定任务不降频吗?」, whether a +specific task can be exempt from the slowdown — is the question this section answers. +A question is not a directive, so it is the occasion for the rule and not its +authority; the authority is the task's own purpose. The quotation is carried for what +it asks and is not offered as evidence another installation could open, because the +session id holding it resolves only in the record where it was spoken. +`scripts/check-rant-citations.py` scans `emrg/server/feishu_prompt.md` — the template +is in that guard's `INSTRUCTION_FILES` — so an attribution added to it without a +public record reddens there. The attribution rule is mechanised, and nothing here +restates it. """ from __future__ import annotations