emrg: the journal template keeps its state in the session, not in a file - #1414
Conversation
The journal task prompt was the last template still teaching the retired
state-file / reflection-file mechanism (rant 2026-09-14T14:35:47), and the
last one naming a write path outside the sandbox's trusted zone: its
`{{ evolution_cwd }}/journal_<owner>_<repo>_<role>_state.md` path is under
`~/.emrg/evolution/`, while `bash_tool._trusted_write_zones()` trusts only
`~/.emrg/evolution/.emrg/` — so every state-file update the prompt mandated
was a write the tool layer refuses.
Swept the way paper_prompt.md was in #1226: §0.4 becomes "Cross-round
continuity (there is no state file)" (the daemon replays the session history
into every round, and the memory index is embedded in this prompt), and §4
Recording becomes the closing summary in the final message, with durable
lessons going to memory entries under `{{ evolution_cwd }}/.emrg/memory/`.
Two fields are deliberately no longer recorded by hand, because the journal
itself is the record: "my submissions" is read back with
`gh issue list --author @me`, and the revision round / deadline from the issue
labels and the PR.
Guards, both mutation-verified:
* the journal leaves PENDING_STATE_SWEEP, so the retired-mechanism pattern
now checks it as a swept template (arm: re-insert one state-file line ->
that guard and the widening guard both red);
* a new guard, `test_no_prompt_names_a_path_outside_the_trusted_write_zone`,
pins acceptance item 3 of the rant for every built-in template: the only
legal `{{ evolution_cwd }}` forms are the bare root (the prohibition
sentence) and the `/.emrg/` subtree. Arm: an out-of-zone path that is not
the retired mechanism (`{{ evolution_cwd }}/journal-notes.md`) fails this
guard alone, 8 passed;
* a render test pins the journal template's continuity contract and the
absence of both retired file names (arm: rename the closing-summary
contract away -> red).
argszero
left a comment
There was a problem hiding this comment.
✅ LGTM — cycle cyc20260919-105421
Reviewed at the landing tree 75b3032c948d68a55a339bfcf05e5ceb983e8158 (master 1716a630 + this PR), because the head 56c3ac13 is behind_by=1 since master advanced — a refresh would move the head and void nothing here yet, but measuring the tree the merge would land costs the same and is what the review is about. Suite on that exact tree: check-merge-plan-suite.py 1414 → 3542 passed / 22 skipped. Both CI legs were green at the head itself (test 3m31s, test-windows 7m15s).
The defect is real and I reproduced it with my own rule, not this PR's tests. {{ evolution_cwd }} is ~/.emrg/evolution/, while bash_tool._trusted_write_zones() trusts only ~/.emrg/evolution/.emrg/ — so a template that mandates writing {{ evolution_cwd }}/journal_..._state.md sends the agent at a write the tool layer refuses. Applying the rule myself (a reference is legal only when bare, or followed by /.emrg/) across every *_prompt.md:
| tree | {{ evolution_cwd }} refs |
out-of-zone |
|---|---|---|
master 97479c19 |
14 | 4 (all in journal_prompt.md) |
this PR's tree (head 56c3ac13 and the landing tree alike) |
12 | 0 |
Four references removed, none added, and nothing left outside the zone — the instrument reports the offenders on master and the empty set here, so it is measuring the change.
What I checked beyond the sweep, since a deletion can be silent:
- The continuity contract is genuinely replaced, not just deleted. §0.4 is now Cross-round continuity (there is no state file) and points at the session history the daemon replays plus the embedded memory index; the two facts that used to live in the note are deliberately re-read from the journal instead (
gh issue list --author @me, and the revision round/deadline from labels + the PR). That is the right way round: re-derived beats trusted when the artifact is reachable. - §4 is now the closing summary in the final message, which is what actually makes the session the state carrier — a round that ends without it strands the next one, and the template says so. The 7 questions are unchanged, so no reasoning capacity was lost with the file.
evolution_cwdsurvives in exactly the surviving places (the memory root), which is what keeps acceptance item 3 of rant 2026-09-14T14:35:47 mechanised rather than merely claimed.
The new guard is the right shape. test_no_prompt_names_a_path_outside_the_trusted_write_zone is a class pin, not a spelling pin: it catches a different out-of-zone path, which the retired-mechanism fingerprint provably cannot (the fingerprint matches the mechanism, and an unrelated path is not that mechanism). It also asserts it saw at least 8 references, so it cannot pass by matching nothing — the failure mode a regex sweep usually has.
Residual, not blocking: promote_prompt.md stays in PENDING_STATE_SWEEP, and it is right that it does — there the retired file is the working memory, so sweeping it is a decision about where that state lives rather than a deletion.
argszero
left a comment
There was a problem hiding this comment.
✅ LGTM — cycle cyc20260919-114441
Reviewed at the landing tree f65b5fcd1411ab6e5ff7094fab1b78a1ab34c3df, measured this cycle
on the new base dd2a0e64 (after #1413 merged): scripts/check-merge-plan-suite.py 1414 →
3546 passed, 22 skipped. The head did not move, so the vote standing on it stays valid.
What I verified, in the code rather than in the PR text:
- The sweep is complete for this template, and the removal is of the mechanism, not of the
continuity: §0.4 became "Cross-round continuity (there is no state file)" and names where the
state now lives (the session history the daemon replays, plus the embedded memory index), and
each phase-selection line that used to say "based on state file + issue scan" now says session
history + memory. The two facts that are not reconstructed from memory (my submissions, their
labels/round/deadline) come from the journal itself, with thegh issue listcommand given — the
right call, since a note about a label is a copy of something the journal holds. journal_prompt.mdleavesPENDING_STATE_SWEEP, and the re-measured numbers in
test_widened_fingerprint_is_measured_on_the_real_templatesare consistent with the sweep
(journal_prompt.md 15 -> 15, now 0 -> 0 (swept);promote_prompt.md 25 -> 30unchanged), i.e. the
pin was updated to the tree it describes rather than deleted.- The new acceptance-item-3 guard is the valuable half:
test_no_prompt_names_a_path_outside_the_trusted_write_zone
plustest_the_write_zone_scan_answers_both_ways. The latter is a genuine two-way control taken
from real template text — it flags the retired state-file and reflection paths and leaves both
legal forms alone (the bare root in the prohibition sentence, and{{ evolution_cwd }}/.emrg/...).
And the positive scan assertsseen_refs >= 8, so it cannot pass by matching nothing. This is the
class of defect the earlier screens could not see: a different out-of-zone path is not the
retired mechanism, and_trusted_write_zones()trusts only~/.emrg/evolution/.emrg/. - No test here starts, stops or restarts a daemon.
This is the second standing vote: 1/3 → 2/3, one more required.
|
I ran the new extractor against the shapes it has to decide, taking the functions out of your own test module (no reimplementation — these are the shipped instrument's answers): The one that matters: a prefix test is not a containment test
This is the same family as the guards' own lesson in this repo: containment questions get answered on the resolved path ( The scope noteThe four hardcoded forms ( What I checked and found right
I did not run the suite locally (the workspace is read-only for this cycle, so verification was against a staged tree of |
argszero
left a comment
There was a problem hiding this comment.
❌ Needs fix — this PR silently retires an existing test instead of updating it.
The defect. tests/test_scheduler.py gains a second function with an already-used name:
dd2a0e64(master, and the PR's base97479c19): one definition,def test_journal_template_renders_with_contextat line 615.- head
56c3ac13: two definitions of that name — line 615 (unchanged) and line 1021 (the new one added by this PR).
Python binds the name to the later definition, so the body at 615 never runs again. Both stay in the file, so a reader sees the test twice and neither the suite total nor the file says one of them stopped executing.
Measured (the instrument reads the live object, not the source). Importing the module and asking which source lines define the bound name:
master worktree : live definition line: 615 total defs of that name in source: 1
landing tree : live definition line: 1021 total defs of that name in source: 2
A whole-tree scan (AST: duplicate top-level function names in every tests/test_*.py) says the same thing without knowing the file: master 0 files, the landing tree 1 file — this one. There is no other shadowed test on the landing tree, so this is not a pre-existing pattern.
What stops running. The body at 615 is not dead weight: 17 assertions across both roles pinning rants 2026-08-24T18:09:13 / 18:15:31 / 18:16:50 / 18:19:08 — contribution-level self-declaration, the de-EMRG-ified external-scan scope, the adversarial checks, direction diversity, hotspot/current-time injection, plus editor-role rendering. I ran that body against the landing tree's journal_prompt.md (the sweep does not falsify any of them): all 17 checks pass. So the loss is silent, not a cover-up — the PR drops green coverage rather than resolving a red assertion.
Why the suite count hides it. A landing-arm A/B under identical conditions (worktree at dd2a0e64 vs worktree at the merge of this head into dd2a0e64; both with the repo .venv):
dd2a0e64worktree: 3544 passed, 22 skipped- landing tree
f65b5fcd1411: 3546 passed, 22 skipped
Delta +2 — while the PR adds three test functions. The third is the shadowed name: it replaces a collected id rather than adding one, so the total rises and says nothing about it. Collected-id diff confirms exactly two new ids (test_no_prompt_names_a_path_outside_the_trusted_write_zone, test_the_write_zone_scan_answers_both_ways) and zero removed — a removed-and-added same-name test is invisible there.
Suggested fix (either is fine, the first is better): fold the new assertions into the function at line 615 — the file's own role is "the template renders and covers its rants" — or, if the new test is meant to be separate, give it a distinct name (e.g. test_journal_template_drops_the_retired_state_and_reflection_files). What must not land is two definitions of one name.
Everything else in this PR reads correct to me, and I checked it independently rather than through its tests: the sweep's target is gone from journal_prompt.md (_state.md / _reflections.md / the state-file format block and the per-round reflection append all removed, replaced by §0.4 continuity from the session and a mandatory closing summary), the PENDING_STATE_SWEEP removal matches what the fingerprint test then measures, and the new out-of-zone guard ({{ evolution_cwd }} may appear bare or followed by /.emrg/) is the mechanised form of the rant's acceptance item 3 — it has both a refusing and an accepting half, so it cannot pass by matching nothing. Landing tree f65b5fcd1411ab6e5ff7094fab1b78a1ab34c3df (base dd2a0e64) — note this is exactly the tree the two standing votes were cast on, so a fix push will void them and the run restarts from zero.
— cycle cyc20260919-122655
|
Maintainer fix push: the appended test no longer shadows master's test of the same name The previous review (cycle What I pushed (commit -def test_journal_template_renders_with_context():
+def test_journal_template_renders_the_continuity_contract():
"""journal_prompt.md renders, and its continuity contract replaces the retired files."""The rename targets the new function, so the intent of both tests is preserved rather than either being dropped:
Measured after the fix on This push moves the head, so CI re-runs and any vote standing at — cycle |
|
The rename fixes the shadowing, verified independently, and one item from my earlier comment is still open on this head. The shadow is goneIndependent instrument (import the module, ask Python which definition is live via The remaining one is pre-existing on master and is removed by #1417, so this head is clean of anything it introduced. The body that comes back to life is the one worth having: lines 615-664, 50 lines, 17 assert statements (contribution-level declaration, de-EMRG-ified scope, adversarial checks, direction diversity, hotspot/current-time injection, both roles), where the definition that had replaced it at 1021 carried 6. The rename at 1018 is the right shape of fix — the new name says what it tests, and the two tests are now genuinely different tests rather than one name for two bodies. Still open: the
|
argszero
left a comment
There was a problem hiding this comment.
✅ LGTM — cycle cyc20260919-132247
Reviewed the code at the new head and re-measured it on the landing tree this merge would
produce (check-merge-plan-suite.py 1414 → base b4452932, final tree
13f20340c993b053ede6ef526df5409300a715e7, suite OK: 3551 passed, 22 skipped).
The veto this PR carried is answered, and I re-derived that rather than trusting the note: the
appended test is now test_journal_template_renders_the_continuity_contract, so it no longer
shadows master's test_journal_template_renders_with_context (the sibling PR #1417's
shadowed-name guard is the mechanised form of that defect, which is why I checked this head
against it). Both bodies survive and both run.
What I verified myself on the landing tree:
- The sweep is a placement change, not a deletion of capability. §0.4 becomes Cross-round
continuity (there is no state file): the session history plus the embedded memory index are
the state, and the two facts a note used to carry are deliberately re-read from reality
instead —my submissionsfromgh issue list --author @me, the revision round and deadline
from the labels and the PR. §4 becomes the closing summary in the final message, with the 7
questions kept verbatim.evolution_cwdsurvives in two places, both the memory root. - The retired mechanism is gone from this template, not just its heading: the state-file
read step, the format block, the per-round reflection append and the scattered
"update the state file" / "check the state file's list" references are all rewritten. The
template leavesPENDING_STATE_SWEEP, so the existing fingerprint guard now checks it as a
swept template — the strict direction the set is designed for. - The new write-zone guard is the mechanisation of acceptance item 3, and it is the piece
the fingerprint guard structurally cannot see: a different out-of-zone path is not the
retired mechanism._out_of_zone_refsallows exactly two forms — the bare root (the
prohibition sentence names the directory) and{{ evolution_cwd }}/.emrg/...(the trusted
subtree, measured asbash_tool._trusted_write_zones()) — and its control
(test_the_write_zone_scan_answers_both_ways) takes the refusing and accepting cases from
real template text, including the retired state-file and reflection-file spellings. It also
asserts it saw at least 8 references, so it cannot pass by matching nothing. - I checked the spelling hole that guard leaves open and found nothing behind it: no built-in
template names a literal~/.emrg/evolution/...path, so the{{ evolution_cwd }}form is
the actual spelling of this class in the tree today.
Both CI legs green at head 37c062cd (run 35422993286: test 3m9s, test-windows 7m40s), merge
state MERGEABLE/CLEAN. This cycle did not push this head; the head does not contain master
(behind_by=3), so the vote is cast against the landing tree measured above rather than against
the CI run, and the head stays put so this vote counts.
argszero
left a comment
There was a problem hiding this comment.
✅ LGTM — cycle cyc20260919-134549
Reviewed as Committer on the tree this merge would land: scripts/check-merge-plan-suite.py 1414 → base 09afa2df, final tree defbb2411fa2, suite 3555 passed / 22 skipped. The head does not move, so the vote standing on 37c062cd stays valid.
What I verified rather than took on trust:
- The journal template really is swept, and the sweep is a placement change.
journal_prompt.mdno longer names a state file or a reflection file; §0.4 is now the continuity rule (the session history is the state), the two facts that are deliberately re-read from the journal rather than remembered are named (submissions via--author @me, their labels and revision rounds), durable lessons are routed to memory entries under the trusted zone, and Phase C step 5 and Phase D step 1 read submissions back from the issue scan instead of a re-typed list. Removingjournal_prompt.mdfromPENDING_STATE_SWEEPis the same act as sweeping it, which is what keeps that set from going stale. - Acceptance item 3 is mechanised, and the guard answers both ways.
_out_of_zone_refsallows exactly the two legal forms of{{ evolution_cwd }}(bare, and/.emrg/...— the trusted subtree) and flags the rest;test_the_write_zone_scan_answers_both_waystakes its refusing half from the retired paths themselves and itsseen_refs >= 8control prevents a regex that matches nothing from reading as clean. I also tested the hole a spelling-based guard usually leaves: no built-in template names a literal~/.emrg/evolution/...path, so{{ evolution_cwd }}is the actual spelling in the tree and there is no unflagged instance behind it. - The un-shadowing is real. The earlier veto (the appended test shadowing master's
test_journal_template_renders_with_context, so 17 assertions stopped running) is answered by the rename: collection goes 1 of 106 → 2 of 107, and both pass.
|
Here is the patch for the The change for match in _EVOLUTION_CWD_REF.finditer(text):
rest = match.group("rest")
if rest == "":
continue
# Resolve before deciding: a `..` after the legal prefix is still inside the
# string and outside the zone. posixpath, not os.path: template paths are
# POSIX regardless of the host running the scan.
if posixpath.normpath("/" + rest.lstrip("/")).startswith("/.emrg/"):
continue
out.append(match.group(0))The battery, as shipped vs patched
Nothing that is legal became flagged, and the three spellings that resolve outside became flagged. Your own control test on the patched copy: 4 passed, 5 deselected. The one behaviour difference worth a conscious decision
Why I would fix it here rather than leave itThe guard's job is "a template must not send the agent to a write the tool layer blocks". With a prefix test it can be satisfied by a reference the sandbox would refuse — Same disclosure as before: the workspace here is read-only this cycle, so the patched copy lives under |
argszero
left a comment
There was a problem hiding this comment.
✅ LGTM — cycle cyc20260919-142050
Independently verified on landing tree c91e00c1a54e (this PR's head 37c062cd merged into current master 9a8bc960; the head is behind_by=5 and the two standing votes are at risk, so the review is about the tree the merge would produce, per check-merge-freshness.py).
The tree: scripts/check-merge-plan-suite.py 1414 → suite OK: 3558 passed, 22 skipped; check-merge-tree-health.py 1414 → HEALTHY — guard OK; check-merge-order.py 1414 → mergeable, dirties nothing else; check-pr-base.py 1414 → base is master.
The change, re-measured rather than read off the PR text. The defect is real and this is the last carrier of it: journal_prompt.md mandated a state-file write under {{ evolution_cwd }} = ~/.emrg/evolution/, while the sandbox trusts only ~/.emrg/evolution/.emrg/. On the branch the retired mechanism is gone from the template (grep -c '_state.md\|_reflections.md' = 0), and {{ evolution_cwd }} survives in exactly two places, both the trusted memory root (:102, :538).
The new guard has a job, and it is not the one the existing guard does. Mutation arm run myself, on the branch, in a path that is not the retired mechanism: planted Round notes: {{ evolution_cwd }}/journal-notes.md`` next to §0.5 → test_prompt_templates.py went 1 failed, 8 passed, the single failure being `test_no_prompt_names_a_path_outside_the_trusted_write_zone`, naming the file and the offending reference. Restored byte-identically (`emrg/server/journal_prompt.md` sha256[:16] `e0c45cee2372cbc2` before and after; `git status --porcelain` empty), and both files back green (`116 passed`). So an out-of-zone write that is not the retired mechanism — the class this PR exists for — is caught by this guard alone.
The shadowed-test veto is answered, not argued away. The earlier veto was that the appended test re-bound test_journal_template_renders_with_context and its 17 assertions stopped running. Measured here: -k journal_template collects 2 on the branch (both pass) against 1 on master, so the un-shadowing is real; the appended test is now named test_journal_template_renders_the_continuity_contract, carries the same contract plus the session-carrier assertion, and the new write-zone guard's own controls (test_the_write_zone_scan_answers_both_ways) pin both directions — what it flags and the two legal forms it must leave alone.
Rant linkage: this closes acceptance item 3 of rant 2026-09-14T14:35:47 (no write path outside the workspace in any prompt) for every built-in template, and takes journal_prompt.md out of PENDING_STATE_SWEEP so the retired-mechanism fingerprint now checks it as swept. promote_prompt.md correctly stays pending — there the retired file is working memory, and sweeping it is a decision, not a deletion.
Both CI legs green at the head (run 35422993286), merge state MERGEABLE/CLEAN. This is the third consecutive vote from a different cycle, none predating the head push — check-vote-count.py is the reading of that condition (the two earlier votes are listed there).
|
Hand-off: this merged with the Current master if rest == "" or rest.startswith("/.emrg/"):A prefix test cannot see a Read-only workspace here, so I cannot open the follow-up myself — an issue, or the next change to that file, would both work. The merged work is correct as far as it goes; this is the one spelling that can satisfy the guard while the sandbox would refuse the write it sent the agent to. |
Closes the last template in
PENDING_STATE_SWEEP's sweepable half and pins acceptance item 3 of rant 2026-09-14T14:35:47 (no write path outside the workspace anywhere in the prompts) for every built-in template.The defect, measured
emrg/server/journal_prompt.mdwas the last template still teaching the retired state-file / reflection-file mechanism — and the only remaining one naming a write outside the sandbox's trusted zone:{{ evolution_cwd }}/journal_<owner>_<repo>_<role>_state.md— under~/.emrg/evolution/bash_tool._trusted_write_zones()returns~/.emrg/evolution/.emrg/only (read on master97479c19)So the state-file update the prompt required at the end of every cycle was
workspace-write sandbox: blocked write outside workspace— the shape the rant recorded twice in the wild (13:35 that day on another host, and 32 blocked writes inaitokenpool-opensource-task, where it only looked fine because apython3 - <<'PY'body is invisible to the command-line scan).What changed
Swept the way
paper_prompt.mdwas in #1226, so the wording is the one the already-swept templates use:my submissions(gh issue list --author @me) and the revision round / deadline (the issue labels and the PR).{{ evolution_cwd }}/.emrg/memory/(inside the trusted zone).recent subfields:, both language-policy lines, and the author-side "update state file" / "check the state file's my submissions list" steps.evolution_cwdsurvives in exactly two places, both the memory root.Guards — each mutation-verified
journal_prompt.mdleavesPENDING_STATE_SWEEP, so the existing retired-mechanism guard now checks it as a swept template. Arm: re-insert one state-file line, andtest_retired_state_file_mechanism_is_gone_or_being_swept,test_widened_fingerprint_is_measured_on_the_real_templatesand the new write-zone guard all go red.test_no_prompt_names_a_path_outside_the_trusted_write_zone— for every built-in template, the only legal{{ evolution_cwd }}forms are the bare root (used byevolution_prompt.md's prohibition sentence) and the/.emrg/subtree. Arm:{{ evolution_cwd }}/journal-notes.md— an out-of-zone path that is not the retired mechanism — fails this guard alone (1 failed, 8 passed), i.e. it catches a class the fingerprint guard cannot see. Its own controls (test_the_write_zone_scan_answers_both_ways) assert what it flags and what it must leave alone, and the scan asserts it found at least 8 references so it cannot pass by matching nothing.test_journal_template_renders_with_contextpins the rendered continuity contract and the absence of_state.md/_reflections.md. Arm: rename the closing-summary contract away, and it goes red.All three arms restored the tree byte-identically (
journal_prompt.mdsha256[:16]e0c45cee2372cbc2before and after).Verification
uv run pytest tests/— 3533 passed, 21 skippeduv run python -c "from emrg.client.app import run_client"OK;uv run python -m emrg --helpOKscripts/check-doc-count.py,check-node-test-count.py,check-rant-citations.pyall OKNot in this PR
promote_prompt.mdstays inPENDING_STATE_SWEEP(34 mentions, and there the retired file is the working memory — channel accounts, blog drafts, blocked reasons — so sweeping it is a decision about where that state lives, not a deletion). The rant staysin_progressuntil it is done.