Skip to content

v2.2.27 — the five open judgements are closed, and one correction from last round was wrong (2026-09-19)

Choose a tag to compare

@github-actions github-actions released this 19 Sep 08:33
· 12 commits to main since this release

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.