You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
v2.2.27 — the five open judgements are closed, and one correction from last round was wrong (2026-09-19)
v2.2.27 — the five open judgements are closed, and one correction from last round was wrong (2026-09-19)
org-check got its own exit code (core/exit_codes.py + cli.py + both running.md
files + tests/test_cli.py + .github/workflows/ci.yml). "Not this tick" used to answer 1,
which is the usage-error slot — a caller branching on rc read the answer backwards, and a
genuinely mistyped command hid behind a legitimate "no" (|| true in CI swallowed both).
It is now NOT_THIS_TICK = 5, 1 means usage errors again, and the CI cold-start step calls
the command on an empty state root where the answer is should_run=true/0, so the workaround
is gone rather than widened. This is an interface change; anything that keyed on rc=1 for
"not now" should key on 5, or better, on the JSON field. Behaviour of the decision itself is
unchanged.
The reserved long_task exemption is retired (S9 in docs/superseded.md, both languages
+ engine/model.py·sprout_queue.py·tick.py·org_session.py + tests/test_sprout_queue.py). Last round guarded it as "reserved, unwired, not deleted";
measuring it here showed what that was worth: at tick 647 the ledgers hold 4,370 sprout rows,
every one carrying the key, none set to true — the branch had never been used, yet a row
that did carry it would still have released a sprout past the lead limit. The limit came out of
the v1 "186 of 221 queued sprouts were one family" incident, so a bypass that no writer ever
needed was not worth its reach. Old rows still parse (from_record ignores unknown keys) but
the flag buys nothing; the new case fails if the name returns to src/ or prompts/, if the
S9 row disappears from either table, or if a marked legacy row is treated as exempt.
Two capacity questions were answered by measuring, not by adjusting (docs/growth-subject.md
and its Chinese original + tests/test_mechanism_docs.py). app/ holds the solidify edge's
evidence files and has no rotation rule while journal/ rotates at journal_keep_files: that
asymmetry is now written down as a decision, with the reason (moving an evidence file out makes
a named object read as missing and drops app/'s directory-object count — differences the
engine made for itself, not growth) and with the measurement that bounds it (145 files,
261,713 bytes, 102 added on the busiest day). The observation limits stay at 10 directories /
20 files: nothing presses either boundary, and subject_files' per-file window is deliberately
narrower than the counts, which use real totals. The verdict sentence is asserted against the
constants themselves, so raising a limit without changing the criterion goes red. The stale
directory enumeration that round five fixed in the documents was still sitting in engine/subject.py's own comment; it is gone and the ban now scans src/ and prompts/ too.
Last round's correction of the org-segment unit was itself wrong, and is corrected here
(engine/org_trigger.py + core/config.py + both mechanism.md originals + tests/test_org_trigger.py). v2.2.26 stated that a state root running only mechanical ticks
already writes an org-llm.jsonl row at tick 1. It does not: run_tick enters the org segment
only when a runner exists, and measured with IG_EXECUTOR="" over three ticks the file is never
created and org-check still reports "never attempted". The run that produced the claim had
inherited IG_EXECUTOR from the environment — it was an executor-backed debug tick, which does
consume the cooldown window, so the operational warning stands with a narrower condition. Rows
come from three paths after the segment starts: missing prompt (never touches the executor),
failed executor call, completed run. Settled: the unit stays "attempt". Narrowing it to
"the LLM call succeeded" would leave a broken executor being retried every tick, since ①②③④ all
pass again when nothing was recorded. The regression is now pinned by behaviour, not prose: a
case runs two executor-less ticks and asserts the ledger does not exist.
Journal rotation now moves content out of the growth surface (engine/subject.py + ledger/rotation.py + docs/growth-subject.md and its Chinese original + prompts/tick.md·prompts/org-session.md + tests/test_subject.py·tests/test_rotation.py). rotate_journal moved journal/ overflow into <subject>/archive/journal/, and the observation
surface went on looking at it: move_file writes the destination before unlinking the source, so
an archived entry carries the mtime of the move — newer than whatever is still growing. Measured
on a copy of the live subject at journal_keep_files=200, adding one entry per round and running
the gardener: past the limit every rotation took one more slot of the 20-file window (rounds
12/13/14 = 1/2/3), the first slot was always archive/journal/…md.<stamp>, archive/ and archive/journal/ became directory objects that could be proposed, and the subject's file count
never dropped (363→377). Rotation had moved nothing out of sight, while rotate_journal's own
docstring claimed the opposite ("does not affect the visibility of new content"). SUBJECT_ARCHIVE_DIR excludes the subject root's archive/ from all three readings and from the
object-name gate, proposals included; the rule is scoped to the root, so journal/archive/ stays
ordinary content. Re-running the same curve: archived slots 0 of 20 across 14 rounds, window head
back to the newest journal entry. The observation and capacity limits themselves are unchanged
(10 directories / 20 files / 200 entries).
A missing evidence file is now attributed, and last round's naming theory is refuted
(engine/reconcile.py·engine/sprout_sources.py + cli.py·engine/tick.py + docs/mechanism.md and its Chinese original + tests/test_app_edge.py·tests/test_mechanism_docs.py).
The theory on the table was "the predicted name and the written name disagree, so false failures
enter the denominator" (predicted app/0676-…, present 0677-app_0637-…). Reading it row by row
here says it does not hold: all 174 files in the subject's app/ are reproducible from some cap
row's (object, tick) through the engine's own path function — 0 orphans — and the two flagged
rows (ticks 676, 679) declared in their traces exactly the path the engine had written verbatim
into the prompt, returned rc=0, and still produced no file anywhere on disk. 0677-app_0637-… is
another tick's own evidence file, mis-paired with this one. What was actually missing was a
reading that separates the causes, so status and the reconciliation report now carry one:
自述已写未落地 (declared the agreed path, file absent) / 命名漂移 (declared a different name —
a tick off by one is caught here, not hidden) / 未自述写入 (no trace, or no app/ write claimed).
It reads only the trace's output section: the prompt already quotes the path, and scanning the
whole file would have the engine testify for itself. Measured over the last 30 / 40 / 60 cap
leads: 2 / 0 / 0 across the three buckets. The redemption algorithm, its numerator and its
denominator are untouched — this only says which point was lost and why.
The journal naming rule now has an enforcement point (engine/subject.py + prompts/org-session.md + docs/growth-subject.md and its Chinese original + tests/test_subject.py·tests/test_mechanism_docs.py). JOURNAL_NAME_RX/valid_journal_name
had no call site at all while the comment and the docs called it a mechanical judgement —
either the claim was decorative or the enforcement was missing; the fix was to wire it in, not to
soften the wording. subject.valid_journal_name is now called from the object-name gate's
proposal branch (valid_subject_object), so an org session that names a not-yet-existing journal/ file with a malformed name is refused on the spot instead of at reconciliation.
Scope is deliberately narrow and pinned by tests: direct children of journal/ only, directory
objects untouched, already-observable objects unaffected. The writing side stays a prompt
convention plus a test guard — the engine does not rename what the executor produced, because
rewriting reality to fit the expectation is the failure mode this whole mechanism exists to avoid.
The version policy now says incompatible where it used to say "ledger format"
(docs/versioning.md + Chinese original + tests/test_mechanism_docs.py). The rule listed any
ledger-format change as Major, while S9 deleted a serialized key and shipped it as a Patch
(2.2.27) — policy and fact disagreed, and the tag had to settle which one wins. Settled: Major
is for a change where the same ledger reads a different verdict under old vs new code, or old
code fails on a new row; dropping a key nobody wrote, or adding a nullable one, stays a Patch.
No version is reclassified here (2.2.27 keeps its number), and the criterion is pinned against
the read side rather than the prose: Sprout.from_record tolerating a retired key and as_record() no longer writing it are asserted in the same test, so if the reader ever turns
strict, the "compatible = Patch" sentence loses its support and that test goes red.
A mutation audit of the criteria, and the one that turned out decorative
(engine/subject.py + tests/test_mechanism_docs.py + both mechanism.md originals +
README size figures). 23 pinned constants and sentences were each flipped to a wrong value in a
clean clone of HEAD (PYTHONPATH pointing inside the clone, so the working tree and the live tick
were never at risk) and the suite re-run. 22 went red. The survivor was the org-session prompt's
journal-naming clause: the guard compared a string literal copying the rule instead of the
rule itself, so marring that half-sentence kept the suite green — a decorative criterion. It was
rewritten to point at its real landing place: subject.JOURNAL_NAME_SPEC is now the single
constant the gate quotes in its refusal message, and the test asserts the refusal carries it and
that org-session.md names the enforcement ("对象名机械闸" / "当场拒收"). Re-audited on the fixed
HEAD: dropping "当场拒收" from the prompt fails 1 test, stopping the gate from quoting the
constant fails 3, breaking the constant itself fails 3. One mutation still passes by design —
swapping the sentence's connective words for synonyms — because what is pinned is the spec string,
the gate's citation of it, and the named enforcer, not that sentence's phrasing.
Two findings recorded as measurement, not as fixes: the three quota defaults
(journal_keep_files, frozen_cap, frozen_keep_tail) did not survive a full-suite run —
they are caught by test_every_numeric_knob_is_listed; a narrow selector in round one had
reported them as decorative, which is the mistake of testing one view and calling it coverage.
Separately, the frozen-zone capacity was checked against the 300-tick re-ask window on a copy of
the state root (tick 713: 4,373 rows, 627 short of the cap, arriving 0.66–1.50 rows/tick →
418–950 ticks away, while a rotation keeps 4,000 rows ≥ 2,600 ticks of history): rotation cannot
outrun the window, written into both mechanism.md originals and pinned by arithmetic against
the constants, not against the prose.
The lib* no-samples gap got a dated ETA (both mechanism.md originals). At tick 713 the
state root reports 28 pending questions, all blocked (28 re-ask cooldown, 17 also short of the
idle threshold), earliest re-askable tick 892 — ~10 minutes per tick measured here, so due
the evening of 2026-09-20. What will be measured then is whether the whole
pending → sprout → pickup → closed/consumed chain flows, with the two named readings to re-run;
until then that origin stays reported as no samples, not as 0 or 1.00.