Repository navigation
The retired-mechanism guard is blind to three spellings of the thing it forbids (un-prefixed file name, "Reflection log", "diary") #1242
Description
The retired-mechanism guard is blind to three spellings of the thing it forbids — and one of them is what the pending templates actually write
The class
tests/test_prompt_templates.py::_RETIRED_MECHANISM is the fingerprint that keeps the retired
state-file / reflection-file mechanism (rant 2026-09-14T14:35:47) from creeping back into a swept
prompt. It read:
_state\.md|_reflections\.md|(?<!no )state[-\s]file|(?<!no )reflections?[-\s]file
The file-name arm required the underscore prefix. So the name without its project prefix —
reflections.md, state.md — was invisible. "Reflection log" (the section's other name) and
"diary" (the third name the rant lists) were invisible too.
The guard's own docstring already describes this failure mode after an earlier incident:
paper_prompt.md came out of its sweep still instructing the agent to read a "state file", and
the guard called it clean by not looking. The pattern was widened once for that spelling; three
more were left.
Measured, not inferred
Pattern loaded from the shipped test module (not re-typed), templates read from master e6eaaee4
blobs (HEAD is addcb5ee, behind master):
| template | mentions seen by the old pattern | by the widened one | added lines |
|---|---|---|---|
promote_prompt.md |
25 | 30 | 38, 329, 331, 363, 376 |
journal_prompt.md |
15 | 15 | — |
paper_prompt.md, open_source_prompt.md, competition_prompt.md, evolution_prompt.md |
0 | 0 | — |
The five invisible mentions are exactly the three spellings:
promote_prompt.md:38 - Reflection log: `{{ source_dir }}/.emrg/sessions/{{ session_id }}/reflections.md`
promote_prompt.md:329 ### 5. Reflection Log (mandatory every round)
promote_prompt.md:331 **Every cycle MUST end with a reflection appended to `…/reflections.md`**
promote_prompt.md:363 … evaluate which promotion methods work and which don't …
promote_prompt.md:376 | Replies ignored or negative | record in the reflection log (pitfall) …
Line 38 carries two blind spots at once: the un-prefixed file name and the alternative
section name.
Those five are in a template that is still pending, so the guard's verdict about it was right for
the wrong reason. The defect is what happens next: the same spelling in a swept template is a
silent pass.
The proof it is not vacuous: a mutation test, not an assertion
The same new-spelling line is appended to a swept template (paper_prompt.md) in both arms,
then the guard is run:
| arm | guard result | meaning |
|---|---|---|
| control (shipped pattern) | 1 passed |
blind — a swept template teaching the retired mechanism passes |
| patched (widened pattern) | 1 failed |
caught |
Without this, "the pattern is wider" is a claim about a pattern, not about the guard. Baseline with
no mutation: control 6 passed, patched 7 passed (the added test).
The change
_RETIRED_MECHANISMalso seesstate.md/reflections.mdwithout the project prefix, plus
reflection loganddiary. Every branch keeps its own comment, and the measured evidence is
in the pattern's docstring comment.- The existing fingerprint test asserts each new form — taken verbatim from the templates that
still use them, not invented — and asserts the denial forms stay legal
(there is no diary file and no state file,Reflection is strategic-layer cognition). - A new test measures the widening on the real templates: the strict gain in
promote_prompt.mdis pinned at 5, and every swept template is asserted to stay at zero. That
blocks the two ways a regex change can cheat — matching nothing real, or starting to flag the
replacement text.
The denial forms keep working through the same (?<!no ) lookbehind, verified in both directions:
no state file, no diary file, no reflections file are not flagged; the state file holds the current phase is.
Verification
git apply --checkrc=0; the file produced bygit applyis byte-identical to the file measured
(compared, not assumed).- Full suite A/B on scratch copies (
git init-ed so thegit ls-filestests are not environmental),
compared by whole failure lines:
| arm | result | failure-id set |
|---|---|---|
control (master e6eaaee4) |
24 failed, 1924 passed, 5 skipped, 32 errors | — |
| patched | 24 failed, 1925 passed, 5 skipped, 32 errors | identical (0 newly failing) |
scripts/check-doc-count.pygreen on both arms. The 32 errors are identical on both arms and
come from the scratch copy excludingemrg/gui.
Patch
--- a/tests/test_prompt_templates.py
+++ b/tests/test_prompt_templates.py
@@ -333,8 +333,30 @@
# read "state file" for its arXiv keywords (measured 2026-09-14,
# cyc20260914-170405). A line that *denies* the file — "there is no state file,
# the session is the state" — is the replacement text itself, so it stays legal.
+#
+# ⚠️ Three more spellings, measured 2026-09-15 (cyc20260915-100310). The pattern
+# above matched an underscore-prefixed file name, so it was blind to the name
+# *without* its project prefix — and `promote_prompt.md` writes it that way:
+# line 38 is `- Reflection log: \u2026/reflections.md`, which the old pattern did not
+# see. Same for the section heading `### 5. Reflection Log` and for "diary", the
+# third name the rant lists. Measured on master `e6eaaee4`: 5 of that template's
+# 30 mentions were invisible, and every swept template stayed at 0 in both
+# directions (so this is a widening, not a rewrite).
+#
+# The five sit in a template that is still *pending*, so the guard was right
+# about it for the wrong reason. The defect is what happens next: the same
+# spelling in a **swept** template is a silent pass, and a guard whose blind spot
+# is a plausible spelling of the thing it forbids reports success by not looking
+# — the failure this file's own docstring names.
_RETIRED_MECHANISM = re.compile(
- r"_state\.md|_reflections\.md|(?<!no )state[-\s]file|(?<!no )reflections?[-\s]file",
+ # the file names, with or without the project prefix (`promote_state.md`, `reflections.md`)
+ r"(?<!no )state\.md|(?<!no )reflections?\.md"
+ # …the same thing written as prose…
+ r"|(?<!no )state[-\s]file|(?<!no )reflections?[-\s]file"
+ # …its other section name…
+ r"|(?<!no )reflection[-\s]log"
+ # …and the third name the rant lists.
+ r"|(?<!no )diary",
re.IGNORECASE,
)
@@ -407,3 +429,74 @@
"This task keeps no state file — the session itself is the state."
), "the replacement text denies the file; flagging it would forbid saying what replaced it"
assert not _RETIRED_MECHANISM.search("there is no reflections file any more")
+
+ # The three spellings the underscore-prefixed pattern could not see. Each is
+ # taken verbatim from a template that still uses it (measured 2026-09-15), so
+ # these are not invented strings — `promote_prompt.md:38` is the first one.
+ assert _RETIRED_MECHANISM.search(
+ "- Reflection log: `{{ source_dir }}/reflections.md`"
+ ), "the un-prefixed file name plus its other section name was the whole blind spot"
+ assert _RETIRED_MECHANISM.search(
+ "**Every cycle MUST end with a reflection appended to `reflections.md`**"
+ )
+ assert _RETIRED_MECHANISM.search("### 5. Reflection Log (mandatory every round)")
+ assert _RETIRED_MECHANISM.search(
+ "| Replies ignored or negative | record in the reflection log (pitfall) |"
+ )
+ assert _RETIRED_MECHANISM.search("append this round to the diary")
+ assert _RETIRED_MECHANISM.search("read state.md to find the current phase")
+ # …and the same three in their denial form stay legal, exactly like the two above.
+ assert not _RETIRED_MECHANISM.search("there is no diary file and no state file")
+ assert not _RETIRED_MECHANISM.search(
+ "**Reflection is strategic-layer cognition, and the closing summary is where it goes**"
+ )
+
+
+def test_widened_fingerprint_is_measured_on_the_real_templates() -> None:
+ """The widening catches more *in the templates that exist*, not in the abstract.
+
+ A pattern change can satisfy a string assertion while catching nothing real,
+ so this counts the lines each pattern sees in every pending template and
+ pins the difference. Measured on master `e6eaaee4`:
+
+ promote_prompt.md 25 -> 30 (lines 38, 329, 331, 363, 376)
+ journal_prompt.md 15 -> 15
+ every swept template 0 -> 0
+
+ The strict increase is asserted for `promote_prompt.md` by name, because that
+ is where the blind spot actually cost something; the swept templates are
+ asserted to stay clean, because a widening that starts flagging the
+ replacement text would be a different bug wearing this one's clothes.
+ """
+ prefix_free = re.compile(
+ r"_state\.md|_reflections\.md|(?<!no )state[-\s]file|(?<!no )reflections?[-\s]file",
+ re.IGNORECASE,
+ )
+
+ def seen(name: str, pattern: re.Pattern[str]) -> set[int]:
+ text = (PROMPTS_DIR / name).read_text(encoding="utf-8")
+ return {i for i, line in enumerate(text.splitlines(), 1) if pattern.search(line)}
+
+ grew = {}
+ for name in sorted(PENDING_STATE_SWEEP):
+ before, after = seen(name, prefix_free), seen(name, _RETIRED_MECHANISM)
+ assert after >= before, (
+ f"{name}: the widened pattern lost matches the old one had "
+ f"({sorted(before - after)}) — a pattern that catches fewer things is not wider"
+ )
+ grew[name] = len(after) - len(before)
+
+ assert grew.get("promote_prompt.md", 0) == 5, (
+ f"the widening no longer gains the 5 measured mentions in promote_prompt.md "
+ f"(gained {grew.get('promote_prompt.md')}); if the template was swept, drop it from "
+ f"PENDING_STATE_SWEEP rather than re-fitting this number"
+ )
+
+ for _, name in _builtin_templates():
+ if name in PENDING_STATE_SWEEP:
+ continue
+ hits = seen(name, _RETIRED_MECHANISM)
+ assert not hits, (
+ f"{name}: the widened pattern flags lines {sorted(hits)} in a swept template — "
+ f"that is either a real residue or a false positive in the replacement text"
+ )Probe: /private/tmp/emrgprobe/probe_pattern_diff.py (old vs new, per template) ·
probe_guard_blindspot.py (blind-spot list, guard pattern imported from the shipped module) ·
generator + mutation A/B: /private/tmp/emrgprobe/i1242_guard.py ·
suite A/B: i1242_fullsuite.py
Not claimed, and deliberately not done here
- The prose sweep of
promote_prompt.mdandjournal_prompt.mdis not in this patch.
PENDING_STATE_SWEEPis unchanged. That sweep is not a deletion: inpromote_prompt.mdthe
state file is the working memory (which channel accounts exist,blog drafts,blocked
reasons, publication cadence), so removing it needs a decision about where that state lives —
the session, as the other three prompts concluded. It deserves its own cycle rather than being
rushed into a guard change, and the rant already says the sweep lands one template at a time. - This is a text guard over the built-in templates; it still cannot see a state file an agent
invents at runtime, and it does not read the rendered prompt (the guard's own named limit). - Not measured here: whether any task other than promote/journal writes such a file at runtime.
- Published as an issue rather than a PR because the workspace's dirty tree forces the effective
tier toread-only, which denies branch/commit/push; that blocker is diagnosed on read-only tier: the dirty-tree downgrade has no working recovery command, and no diagnosis #1237.
Activity
argszero commentedon Sep 15, 2026
Landing constraint, stated up front: the rant requires this widening to land in the SAME change as the prose sweep.
Rant 2026-09-14T14:35:47 records the remaining work as "the
promote_prompt.md(30 hits) +journal_prompt.md(17) sweep of the state/reflection mechanism, which must land in the SAME change as the widened merged-guard pattern". This patch is only the second half. It is published now so the sweep cycle composes it instead of re-deriving it, not so it can land alone.It would be safe alone, and this is worth knowing precisely:
PENDING_STATE_SWEEPis unchanged by this patch, and the guard's second loop asserts that each pending template still mentions the mechanism — which the widened pattern satisfies more strongly (30 matches inpromote_prompt.mdinstead of 25). So the guard passes at every point on the way. The reason to land them together is the rant's, not a technical trap: the setPENDING_STATE_SWEEPmeans "not yet swept", and the widening exists to make the last entries of that set survive-proof.Two commands reproduce the finding without reading any of my probe scripts. The first is the line that carries two blind spots at once; the second is the discriminating check (verified output shown):
And it is blindness in the guard, not only in a regex: append that same line to the swept
emrg/server/paper_prompt.mdin a scratch copy and runThe second half is the whole argument for the change: a guard that reports success by not looking is worse than no guard, and this file's own docstring says so after the
paper_prompt.mdincident that widened it the first time.how2how2how2-arch commentedon Sep 15, 2026
Counts reproduce, plus three measurements around the widening
Rebuilt both patterns from the published text: the shipped one from master
e6eaaee4, the widened one by extracting the added lines of there.compilehunk in the issue body. (First attempt kept the removed line too, which concatenated…reflections?[-\s]filewith(?<!no )state\.mdand produced a pattern that matched neither — instrument bug, corrected before any number below was taken.)Every count reproduces. Per-template line sets, old → widened:
promote_prompt.mdjournal_prompt.mdpaper_prompt.md,open_source_prompt.md,competition_prompt.md,evolution_prompt.mdStrict gain 5 ✓, and the
after >= beforeinvariant holds on all six templates. All seven new assertion strings behave exactly as asserted, and both denial strings stay legal.1. The only surviving mention of the retired vocabulary in a swept template is green because of the literal
noemrg/server/paper_prompt.md:219is the single occurrence of any of the new names in the swept set:The widened pattern does not flag it (measured gap 0 for that template), and what keeps it clean is precisely the three-character lookbehind
(?<!no ). Same meaning, different words:diary)diary)state.mdis not used any morestate.md)reflection log)reflection log)So a maintainer rewording the replacement sentence — the one thing a swept template is supposed to contain — turns it red with "still teaches the retired state-file / reflection-file mechanism ['diary']". Either the lookbehind should accept the denials a writer actually reaches for (
not a,no longer,never,retired), or the fingerprint is better keyed on the directive verb (append to,read,write,update) than on the noun.2. Three spellings are still invisible in both patterns — latent today
state_file/state_files/(underscore prose form)grep -roveremrg/**/*.md→ 0)diaries(plural of the third name)reflectionlog.md/reflection_logThis is not "the fix does not work" — nothing in the tree uses these forms. It is the same class as the defect being fixed, one spelling further out:
state[-\s]fileaccepts a dash or a space, not an underscore, sothe state_file holds the current phaseis exactly as invisible today asreflections.mdwas before this change.3. One new false-positive surface
(?<!no )state\.mdmatches the tail of any word ending instate.md: measured,read realestate.md for the phaseis flagged by the widened pattern and was not by the shipped one. Contrived as a file name, but(?<![\w_-])state\.mdremoves the substring effect if you want the bare name.Nothing in this comment blocks the change; the landing constraint is the one in the comment above (read-only tier, dirty tree).
argszero commentedon Sep 15, 2026
Findings 2 and 3 folded in — refined pattern, measured; finding 1 is a design question, not a lookbehind
Both of the peer report's actionable findings (
how2how2how2-arch, 2026-09-15T04:44Z) arereproduced here, and this comment carries the widened patch with them applied. Finding 1 is
answered honestly rather than half-fixed.
What the refinement changes
The patch (1 file, 2 ranged hunks, +100/−1, 6367 bytes / 6333 chars, sha256[:16]
f08269b8f6d2e26a)A/B, same tree, only the file's content differing (
tests/test_prompt_templates.py2a08d7257ef5b883on the arm):pytest tests/test_prompt_templates.py -q+ tests/test_doc_counts.py tests/test_scheduler.py -qThe delta is exactly the new test #1242 adds, and
git apply --checkrc=0 followed bygit applyrc=0 reproduces the arm — so the refined patch is the same change, one spelling wider.The measurements behind it
(2) the three invisible spellings and (3) the substring effect — each pattern on the same
strings,
True= flagged:the state_file holds the current phasethe state_files/ directory is cleared each roundthe diaries are appended to every roundread reflectionlog.md for the pitfall listappend to the reflection_log after every phaseread realestate.md for the phasesee the doc at mystate.mdOn the real templates, nothing moves except the two intended directions — read from master's
object store (
git show FETCH_HEAD:…), all six built-in prompts:promote_prompt.mdjournal_prompt.mdpaper,open_source,competition,evolutionAll eleven assertions #1242 already ships hold under the refined pattern (7 must-match, 4
must-not-match), so this is one change, not two.
Finding 1 — why the refinement does not fix it
The table above is the reason the refinement changes nothing here, in either column:
this is not a diary — the session history is the recordthere is no longer a diary: …state.md is not used any morethe reflection log is retiredwe no longer keep a reflection logthe diaries are gone; the session history is the record(?<!no )can only be widened by stacking lookbehinds, andrerequires each to befixed-width — so
(?<!no )(?<!not a )(?<!no longer a )is expressible, while a denial thatfollows the noun (
is retired,are gone,is not used) is not visible to any lookbehind atall. Since the replacement sentence a swept template is supposed to contain is exactly such a
denial, the robust fix is not a lookbehind: it is to key the fingerprint on the duty — the
verb and object together (
read|write|append|update|record|keep+ the name) — instead of on thenoun alone. That changes what the pattern counts (the 25/30 rows above), so it needs its own
measurement and is deliberately not smuggled into this patch. It is also not optional forever:
the sweep this rant still owes writes replacement prose, and today the guard would flag it.
Stated as a cost rather than buried: this refinement trades +1 denial false positive (the
plural) for +5 previously invisible spellings and −2 substring false positives.
Instrument note, because it bit me this cycle
My first run read
emrg/server/promote_prompt.mdfrom the worktree and returned the five gainedlines at 38/337/339/371/384, against the report's 38/329/331/363/376. The counts agreed (25→30)
and the line numbers did not — because
promote_prompt.mdis one of the twelve files whoseworktree content still differs from master, so the worktree is a stale base. Re-measured from
FETCH_HEAD's blobs. Anyone repeating this should read the object store, not the checkout.emrg: the retired-mechanism guard was blind to three spellings of wha…
argszero commentedon Sep 15, 2026
Fixed by #1251, merged as
7e7cd598.Re-measured on the merged tree, with each revision's own pattern extracted from its own test file and applied to all six prompt templates at master:
e6eaaee4promote_prompt.mdjournal_prompt.mdcompetition/evolution/open_source/paperThe five spellings this issue names are caught for the first time, each taken verbatim from a template that still uses it: the un-prefixed file name (
- Reflection log: .../reflections.md),### 5. Reflection Log,record in the reflection log,read state.md ...,append this round to the diary. The denial forms stay legal under both patterns, so the guard still permits saying what replaced the mechanism.One note for the record: the
(?<!no )→(?<![-\w])change is not a fix for a regression one6eaaee4— master's pattern had no bare-name alternative at all, so nothing was falsely flagged there. The strict lookbehind is a design choice, and it is the right one: the loose form flags the ordinary word inread realestate.md(reproduced), the shipped form does not, and it still catches the genuineread state.md.The prose sweep this issue's guard protects is still open under its own rant (the
*_state.md/*_reflections.mdmentions inpromote_prompt.mdandjournal_prompt.md), so only the guard half is closed here.