Repository navigation
Releases: Chestnuts-Sisyphus/Infinigrow
Releases · Chestnuts-Sisyphus/Infinigrow
Release list
v2.2.28 — attribution reads the executor's exit code, and two published verdicts are corrected (2026-09-19)
v2.2.28 — attribution reads the executor's exit code, and two published verdicts are corrected (2026-09-19)
Latest
v2.2.28 — attribution reads the executor's exit code, and two published verdicts are corrected (2026-09-19)
- Cold-start assets now read their facts instead of memorising them, and the tick's timeout
budget got a gate (AGENTS.md+.qoder/rules/ig-marker-triage.md+
src/infinigrow/engine/tick.py+tests/test_tick_timeout_budget.py+ the handoff redaction
plan). The repo's own onboarding docs pinned a HEAD tag and a "no open work" verdict — and they
were already stale on arrival (they saidv2.2.25; HEAD was two releases ahead). Anything that
moves every commit is now a command to run (git describe --tags,git status --short,
infinigrow version --check), not a number copied into prose. A new constant
MAX_EXECUTOR_CALLS_PER_TICK(= 2: the tick call plus the org-session call) feeds a budget gate:
executor_timeout_s × callsmust stay under the scheduled task's-ExecutionTimeLimit, or
Windows kills a run mid-flight and leaves a half-written ledger — a relation the local
IG_EXECUTOR_TIMEOUT_S=600(~20 min against a 30 min wall) makes real but nothing watched. The
gate reads the config default, not the machine env, so it is deterministic. Separately, the
handoff notes carry hundreds of machine-specific absolute paths; they are not edited here
(changing another session's handoff assets is out of scope) — a redaction plan is filed as a
pending decision instead. - The observation surface became a live reading, and the trigger it documents has fired
(src/infinigrow/engine/subject.py+engine/tick.py+tests/test_subject_composition.py
+ bothgrowth-subject.md). The doc promised that the day evidence files crowded the
observation surface, a "movable" criterion would be designed first. That day is measured, not
guessed:observation_compositiongroups the 20-slot file window by top-level directory and the
tick trace now prints it, so "who is eating the slots" is a computed reading instead of a manual
read of a trace file. At tick 745 the live subject readsapp18 /journal2 (app evidence
files, ever-newest by mtime and deliberately never rotated, crowd the growing journal out). A
copy-based comparison confirms the cause: pruningapp/to its 4 newest files flips the same
window tojournal16 /app4 — it is app's mtime ordering, not the size 20. Conclusion:
needs a decision, not a bigger constant. The observation caps (10 / 20) are untouched this
round; widening them would let still more evidence files crowd outjournal/without adding one
accountable quantity.tests/test_subject_composition.pypins grouping, that the archived subtree
never counts, and the reverse case where app squeezes journal out of the window entirely. A latent
locale bug surfaced on the way:tests/test_release_tooling.pyran the extraction tool as a
subprocess and decoded its output with the machine locale, so the em dash in a CHANGELOG heading
crashed the Windows GBK reader thread (stdoutcame backNone); the call now pinsencoding="utf-8". - The write surface got direct tests, the weak assertions became real, and five dead
definitions were retired (tests/test_store_gates.py+tests/test_rules.py+
tests/test_version.py+tests/test_release_tooling.py+.github/workflows/ci.yml+
src/infinigrow/rules/static_scan.py+core/paths.py/engine/sprout_queue.py/
ledger/rotation.py+ bothsuperseded.md+ bothmarkers.md).ledger/store.pyis the
only place allowed to touch disk, yet its eight write gates had no test of their own — they
were only covered indirectly when the engine happened to pass through them, so a broken
marker gate or a clobbered exclusive-create could sit unnoticed.tests/test_store_gates.py
pins each gate (append/guard, bad-line tolerance surfaced not hidden,require_within
rejecting escapes and symlink hops,create_exclusivelosing the race without clobbering,
write_work_file's marker gate refusing a shrink, atomic replace leaving no.part,
move_filewriting the archive before unlinking the source). Three assertions that had
drifted weak are now strong: every rule must actually PASS on the clean repo (not just
"return without raising"); the current version must appear as a real## v<version>section
heading with the top section the newest (a mere cross-reference no longer counts); the
release tool is checked by exit code (rc 0 present, rc 1 missing tag, rc 2 bad usage),
not just by an exception's message. A new check enforces every git tag has a CHANGELOG
section — which needs CI to fetch tags, soci.ymladdsfetch-depth: 0(a shallow
checkout lists no tags and would make the check vacuous). The R2 prompt↔code rule no longer
skips when the prompts are empty: empty/missing prompt text is now itself a failure (the old
prompt_text and ...guard let an empty prompts dir pass silently), with a reverse case in
selftestand a floor test pinningMIN_SYNC_TERMSat 17 terms ⊆ the coverage table. Five
never-called definitions (ENV_SUBJECT_ROOT,all_dirs,to_records,ledger_sizes,
total_archived_lines) — measured 2026-09-19 to have zero callers repo-wide — were retired
through the superseded four-step (a new rowS10, old rows kept,scan+pytestgreen).
No limit constant moved. - S9's retired exemption stopped masquerading as live in the Chinese comments
(src/infinigrow/cli.py+engine/sprout_queue.py+engine/domain_saturation.py+
tests/test_sprout_queue.py+ bothsuperseded.md). The S9 retirement guard watched only
the English identifierlong_task, so five comments could still describe the exemption as
current behaviour in Chinese —cli.py("长任务芽豁免连领上限"), the queue docstring
("连领上限 3 拍(长任务芽豁免)"), and three indomain_saturation.py("…且非长任务") —
right under a superseded row that says there is no exemption. Measured 2026-09-19: the word
长任务 appeared nowhere in the mechanism itself, only in prose, and the code has no such
condition. The five comments now say plainly there is no exemption; the return guard was
widened to reject the Chinese name 长任务 anywhere insrc//prompts/(not just the English
usage), with a reverse case proving both spellings are caught while a backtick prose mention
oflong_taskas history stays allowed. Bothsuperseded.mdS9 rows record the widened
guard. No constant or behaviour changed — this is wording, not a reopening of K16/S9. - The read side of the CLI entered CI, and so did Python 3.13 (
.github/workflows/ci.yml+
tests/test_cli_surface.py+ both READMEs +CONTRIBUTING.md). Until now CI exercised only the
write path (dry-run/tick/gardener/org-status);status,redemption --json,rotate,
rotate --searchandversion --checkhad never run on any CI, even though the docs publish them
as the reading surface — androtate --search, the documented answer to "moved but still
findable", had zero cases. The new step reuses the cold-start temp state root (${{ runner.temp }})
so it runs against ledgers that have content, on both ubuntu and windows;tests/test_cli_surface.py
pins the hit and miss directions of--search(hit must reportfile:line, miss must report 0 and
not crash), the exit-code contract ofversion --checkoffline (behind → 3, equal → 0), clean
stderr on the success path, and that the Python versions the docs list are exactly the CI matrix.
3.13 joins the matrix on measurement, not hope: this machine's interpreter is 3.13.7 and the
whole gate battery (pytest / ruff / scan R1–R10 / selftest / prompt-sync / privacy / abs-path) is
green on it. End-to-end on a copy of the live state root (the live one untouched):rotate --max-bytes 4000moved 1,290 + 3,462 + 1,233 lines intoarchive/, thenrotate --search
reported 20 hits in the archived files (rc=0) and 0 hits for an absent needle (rc=0); the config
source line printeddefaults、callerwithIG_EXECUTOR/IG_EXECUTOR_TIMEOUT_Scleared. - The project privacy layer is now provable on CI instead of permanently skipped
(tests/data/privacy-deny-sample.txt+tests/data/privacy-sentinel/leaky-example.md+
tests/test_privacy_deny_layer.py+.github/workflows/ci.yml+ bothprivacy.mdfiles).
The real deny list is machine-specific and stays out of the repository — that is by design and was
not changed — but as a consequencetest_repo_is_free_of_project_deny_hitsskipped on every CI
run, so nothing anywhere proved that--denystill bites; a broken loader would have read as
green. The fix is not to delete the skip (it is genuine environment adaptation): a committed
deny sample made of invented terms, plus a sentinel carrying the payload it must catch, now prove
both directions on ubuntu and windows — deny given → the three rules report hits and exit 1;
payload removed → zero hits and exit 0 — and the sentinel is itself checked to contribute no
generic-layer hits, so shipping it cannot dirty the repo-wide gate. - Bilingual parity moved from headings to body text, and the marker numbering got a registry
(tests/test_docs_bilingual.py+docs/markers.md+docs/zh/markers.md+
tools/check_markers.py+ the four English sections that were empty). The old check compared
only the heading skeleton, so a section could exist in English and be almost hollow: measured
today,growth-subject.md§4 was 18 Chinese lines against 3 English ones, §7 11 against 5, and
running.md§1/§2 13 against 3 and 16 against 5 — which is exactly how the N48 series, the
"19 characters" measurement,SUBJECT_DIR_LIMITand the K12/A17 timeout relation ended up
readable in one language only. The new checks are positional and mechanical: every section with
≥8 Chinese lines must have at least half that body in English, and number-carrying anchors must
...
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-checkgot its own exit code (core/exit_codes.py+cli.py+ bothrunning.md
files +tests/test_cli.py+.github/workflows/ci.yml). "Not this tick" used to answer1,
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" (|| truein CI swallowed both).
It is nowNOT_THIS_TICK = 5,1means usage errors again, and the CI cold-start step calls
the command on an empty state root where the answer isshould_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_taskexemption is retired (S9 indocs/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_recordignores unknown keys) but
the flag buys nothing; the new case fails if the name returns tosrc/orprompts/, 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 whilejournal/rotates atjournal_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 dropsapp/'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, andsubject_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 scanssrc/andprompts/too. - Last round's correction of the org-segment unit was itself wrong, and is corrected here
(engine/org_trigger.py+core/config.py+ bothmechanism.mdoriginals +
tests/test_org_trigger.py). v2.2.26 stated that a state root running only mechanical ticks
already writes anorg-llm.jsonlrow at tick 1. It does not:run_tickenters the org segment
only when a runner exists, and measured withIG_EXECUTOR=""over three ticks the file is never
created andorg-checkstill reports "never attempted". The run that produced the claim had
inheritedIG_EXECUTORfrom 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.mdand its Chinese original +
prompts/tick.md·prompts/org-session.md+tests/test_subject.py·tests/test_rotation.py).
rotate_journalmovedjournal/overflow into<subject>/archive/journal/, and the observation
surface went on looking at it:move_filewrites 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 atjournal_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 alwaysarchive/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, whilerotate_journal's own
docstring claimed the opposite ("does not affect the visibility of new content").
SUBJECT_ARCHIVE_DIRexcludes the subject root'sarchive/from all three readings and from the
object-name gate, proposals included; the rule is scoped to the root, sojournal/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.mdand 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" (predictedapp/0676-…, present0677-app_0637-…). Reading it row by row
here says it does not hold: all 174 files in the subject'sapp/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, sostatusand 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 noapp/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.mdand 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_nameis 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 ofjournal/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.27keeps its number), and the criterion is pinned against
the read side rather than the prose:Sprout.from_recordtolerating 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+ bothmechanism.mdoriginals +
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 mar...
v2.2.26 — the documents say what the code, the tree and the Release page actually do (2026-09-18)
v2.2.26 — the documents say what the code, the tree and the Release page actually do (2026-09-18)
- The published size claim was stale (README, both languages): "~4,400 lines of Python plus
~2,600 lines of tests" measured 7,083 / 5,617 on this machine — the engine grew ~60% past its own
description. Corrected to the measured figures, rounded the same way the sentence rounds.
tests/test_mechanism_docs.pynow counts the tree itself and fails if either README drifts more
than the rounding band — the figure was only able to rot because nothing was checking it. - The mechanism document now says how far a long run carries a redemption-rate study
(both languages, §5). Written from a live reading rather than an expectation: the on-read buckets
are object domain × predicted edge × actual edge, so maturity step is not one of them — joined
against the maturity ledger it degenerates here (every checkable sample sits at the capped step,
steps 1–3 have no samples, which is not computable, not 0). The capability-library sprout
origin is likewise still no-samples. And of the misses, none is a mis-chosen edge, so this data
answers "did the announced action happen", not "was the wrong edge picked" — 0 cases cannot be
read as 1.00. - The privacy scanner's scope is now the publish boundary (
tools/privacy_scan.py): it skipped
a hard-coded directory list, so every local-only folder (handover notes, machine config) needed a
code edit and forgetting one pitted the local scan against the release gate. It now honours the
top-level names in.gitignore, the same semantics as static rule R1. Not a free pass — pinned
with both a positive and a negative case intests/test_privacy.py, and documented in
docs/privacy.md/docs/zh/privacy.md. - The reserved
long_taskbranch is now guarded (K16, both languages). It stays unwired —
wiring it needs a mechanical test for "what counts as a long task", a design decision — but
"reserved" was costing more than a comment: the flag is the only bypass of the lead limit, so a
quiet assignment somewhere would turn that incident-borne constraint into a suggestion.
tests/test_sprout_queue.pynow fails if any engine module assigns it, and passes when the
flag (read from an old ledger row) does grant the exemption. - The bucket axes in the README and the org prompt now match the code (README both languages
+prompts/org-session.md). All three claimed "object domain × edge type × maturity step",
whilereconcile._bucketcomputes (object domain, predicted edge, actual edge) — so the org
session was told to cite a bucket that cannot be computed, and the public README promised an axis
the ledgers do not carry.tests/test_mechanism_docs.pypins the correct phrase and fails if the
old wording returns. - N58-① is closed: trigger ④ keeps its row reading (settled, no behaviour change). It was the
repository's only explicit open decision, and measuring it here settled the question rather than
deferring it: over ticks 556–628 only 14 of 73 ticks were quiet, the longest run of consecutive
quiet ticks was 1, and a run of ≥ 10 never occurred — so the literal "ten consecutive quiet
ticks" reading is not a slower org cadence but an unreachable one. The verdict is recorded in
both originals and inengine/org_trigger.py, andtests/test_mechanism_docs.pynow refuses
the old "open decision" wording on its way back. - A subject-state sentence in the Chinese original is now a dated snapshot
(docs/zh/growth-subject.md). It enumerated the subject's current top-level directories as
"journal / archive"; measured here (tick 633) they arejournalandapp, andarchive/only
appears at the first journal rotation. The rule next to it (directory limit 10, and raising it is
a judgement change) was never wrong — the observation masquerading as a fact was. It now carries
its measurement date and points atinfinigrow statusfor the live reading, and
tests/test_mechanism_docs.pyrejects the old enumeration on its way back. - A live knob that no document mentioned is now documented (
frozen_review_every, default 20;
config table + both mechanism originals +tests/test_mechanism_docs.py). The engine sweeps the
frozen zone every N ticks so a frozen sprout can be re-lit into the active queue — it has been
doing that since T4/A5, but the setting appeared in neither the config table nor the mechanism
document, which madeIG_FROZEN_REVIEW_EVERYundiscoverable. The new case fails if any integer
setting is missing from that table, or if a stated default or env-var name disagrees with code:
a missing row raises no error, it just becomes a switch nobody knows about. org-check's exit code is now written down, and now has a test (bothrunning.mdfiles +
core/exit_codes.py+tests/test_cli.py). It answers "not this tick" with rc=1 — the
usage-error slot, borrowed — so a caller that treats rc as pass/fail reads it exactly
backwards (the|| truein the CI cold-start step is that workaround). The command had no test
at all before; it now has both directions pinned (empty ledger →should_run=true/0, just-ran →
should_run=false/1, rc and JSON forced to agree). Behaviour is unchanged: a dedicated code is
an interface change, undecided.- Release titles have one source on each side of a measured boundary (
tools/sync_release_notes.py
+CONTRIBUTING.md+tests/test_release_tooling.py). The tool kept a hand-copied title for every
version and--applywrites whatever that table says — but the table was copied from an older
reading of the CHANGELOG. Comparing all 28 live Releases (gh release view --json name, 2026-09-18):
through v2.2.15 the published title is the recorded string, from v2.2.16 it is theCHANGELOG.md
heading verbatim, date included (that is what the workflow feedsgh release create). So the
copies were a second source, and applying them would have stripped the date off ten published
titles and reworded eleven more.title_for()now reads the boundary (TITLES_UNTIL = 2.2.15):
recorded before it, derived from the CHANGELOG after. Tests pin both halves, the exact set of 11
pre-boundary wordings that legitimately differ from the CHANGELOG (so nobody "aligns" a published
Release away), and a negative case proving a hand-copied title can never win after the boundary. - The observation-surface limits are now pinned where they are actually written
(tests/test_mechanism_docs.py; the limits themselves are unchanged). The existing check asked only
whether"10"and"20"appear anywhere in the mechanism document — and both do, for unrelated
reasons (stall alert 12 ticks, org cooldown 30 minutes, queue cap 50), so raising
SUBJECT_DIR_LIMITorSUBJECT_FILE_LIMITcould never redden it. The check now reads the sentences
that state the boundary (docs/zh/growth-subject.mdtable and rule line,docs/growth-subject.md
"Directory limit 10, file limit 20") against the constants, and carries its own negative case:
the same phrase with the digits swapped must not be found. Measured: simulateSUBJECT_DIR_LIMIT=12
and only 1 of the 4 phrases still matches (2 of 4 withSUBJECT_FILE_LIMIT=25) — the check fails,
which is the point:docs/growth-subject.mdpromises that changing a limit means changing document,
code and test together, and until now the third leg was decorative. - The org segment's cooldown window measures attempts, not LLM calls — the text now says so
(engine/org_trigger.py+core/config.py+ bothmechanism.mdoriginals +
tests/test_mechanism_docs.py). The reason string read "since the last LLM segment" and the config
table called the knob "LLM-segment gap", but the ledger behind both is written by any org
segment: measured here, a state root that only ever ran mechanical ticks already had an
org-llm.jsonlrow at tick 1. The wording was not harmless — it implies a manual debugging tick
costs nothing, while in fact it consumes the window and delays the real LLM segment by up to
org_cooldown_minminutes. Behaviour is unchanged (only a mechanism change would alter it: gate the
write on an actual LLM call — undecided); the four places that state the unit now agree, and the
new case rejects the old wording on its way back. SECURITY.md's scanner-scope sentence was describing a run that no longer happens
(SECURITY.md+tests/test_mechanism_docs.py). It said the scanner "also reports ~48 findings
underarchive/legacy-20260914/_venvtest/". After this version moved the skip list to
.gitignore, the default pass does not enterarchive/at all (measured: 126 files, no hits),
and the number was never the whole story: pointing the scanner at that tree explicitly
(--root archive) reports 667 files with 2,017 / 90 / 16 hits by rule. Both cases are now stated
separately and a case forbids the conflated wording from returning. Part of a full read of the
remaining public docs against code:upgrading.md's exit codes do match
core/exit_codes.py(BEHIND=3,REFUSE=4), and theversioning.md/privacy.md/
SECURITY.mdclaims that were checked held.
v2.2.25 — log rotation moves to the launcher's handle gap, where it can actually work (2026-09-18)
v2.2.25 — log rotation moves to the launcher's handle gap, where it can actually work (2026-09-18)
logs/tick.logrotation now runs in the launcher, before any handle opens (v2.2.25):
the v2.2.24 in-process approach turned out to be impossible — the scheduler's append
handle shares neither delete nor write (Windows), so bothunlinkand an in-place
truncate failed withPermissionErroron the live machine, keepingrc=1and a
path-carrying traceback in the log.tools/run_tick.batnow calls
tools/rotate_journal.pyat the very start, in the handle gap between runs: over the
threshold it archives the full original — redacted of local paths, since archived
logs are portable artifacts — and clears the main file for this run's appends.
Move-only, data lands in the archive first. The gardener no longer rotates the journal
(rotate_filesdrops the log section;ledger.store.truncate_fileis removed as an
unusable primitive). Test-pinned end to end.- The mechanism document states where rotation happens and why (both languages).
v2.2.24 — the log rotation fails gracefully on a busy journal, and the README names every status reading (2026-09-18)
v2.2.24 — the log rotation fails gracefully on a busy journal, and the README names every status reading (2026-09-18)
- Log rotation no longer crashes the tick when the journal is busy (new, 2026-09-18):
the scheduler holdslogs/tick.logwith an append handle; Windows (no delete-sharing)
madeunlinkfail withWinError 32on every rotation, so the tick exitedrc=1and a
traceback containing the machine's absolute paths was archived every run. Rotation now
writes the full original into the archive first, then truncates the main file in place
when it cannot be removed — the append side keeps writing, no error, no data loss
(ledger.store.truncate_file, test-pinned with a held handle). - The README quick-start
statusline carries all five readings (S3/D1): bilingual
description now lists the capacity and re-ask gates, evidence compliance, executor-side
loss and the usage split — matchingdocs/running.mdone line for one line. - The mechanism document notes the busy-handle tolerance on rotation (both languages),
so the failure semantics are written down where the mechanism is.
v2.2.23 — the ledger split no longer drops ticks, and the report carries the gate readings (2026-09-18)
v2.2.23 — the ledger split no longer drops ticks, and the report carries the gate readings (2026-09-18)
- The usage split now covers every call made today (S1/A4): org-session calls
(kind=org-session) used to be filtered out, so the bucket sums fell short of the
"calls today" total — a report that silently drops a call. They now form their own
组织会话bucket; the bucket sum always equals the day's call total (test-pinned). - The reconcile report carries the two time-gate readings (S1/A5): the frozen-capacity
line count / remaining rows, and the earliest re-ask tick — rendered by the same
functionsstatususes (frozen_requestion_eta, the frozen ledger row count), so the
archived report and the live status can never disagree about how far away the gates are. - The bilingual running guide's
statusline names all five new readings (S2/D1):
docs/running.mdanddocs/zh/running.mdnow list capacity gate, re-ask gate,
compliance, executor-side loss and the usage split in the one-line summary.
v2.2.22 — executor-side loss and token spend are both visible (2026-09-17)
v2.2.22 — executor-side loss and token spend are both visible (2026-09-17)
statusand the reconcile report report executor-side loss (R2/N69): of the last 10
ticks' executor traces, how many carry the "output was not parsed" marker — the same literal
marker and the same trace reader as the failure-attribution bucket, just windowed and
independent of which sprout the action was lost on. The adapter lives outside this
repository; until its fix gets a decision, the loss has to stay visible instead of being
remembered.- Token spend is split by sprout source (R9/E5):
statusnow answers "where did today's
tokens go" per source prefix (cap/sp/lib), joiningexecutor.jsonlcalls to the
tick's led sprout by tick number; calls with no topic and calls with unreported usage are
listed separately instead of being apportioned. Readings only — no budget gate, no
automatic downgrade. - The generic text-write primitive is hardened (R6/E1 follow-through):
encoding.write_text
now normalises the target path (resolve), refuses any input carrying a..segment, and
takes an optionalrootfor a containment check — the last escape hatch the scanner kept
flagging after the join-level guard (subject_path) landed.
v2.2.21 — name-to-path joins are guarded, and the release-note tool is public (2026-09-17)
v2.2.21 — name-to-path joins are guarded, and the release-note tool is public (2026-09-17)
- Object names now resolve through a single guarded path function (R6/E1). A scanner had
flagged a path-traversal primitive: joins likesubject_root / item.name(names come from
observation and ledgers) carried no containment check.engine.subject.subject_pathis now
the only resolver for name→path joins and rejects anything that resolves outside the subject
root — empty names, absolute paths, drive letters,..segments — sharing the single
core.paths.within_rootpredicate with the existing write guard; the org-session renderer
reads through it.SECURITY.mdgains a declared exceptions table for the two
reviewed-and-accepted scanner findings: the deterministic cold-start shuffle (reproducible
by design from the tick number; nothing security-bearing) and the genericwrite_text
primitive (its real call sites are guarded). - The release-note tool is now in
tools/and test-pinned (R7/D2):
tools/sync_release_notes.pybatch-realigns historical Release titles and bodies with the
repository sources (documented English notes first, theCHANGELOG.mdsection otherwise;
an empty body is skipped with a warning — never an empty shell on the public page). It is a
dry run by default and only touches GitHub with--apply.CONTRIBUTING.mdlists it with
the other maintenance tools, and a test asserts the title table covers the current version,
so a release cannot silently fall off the list.
v2.2.20 — the read and availability edges are now accountable (2026-09-17)
v2.2.20 — the read and availability edges are now accountable (2026-09-17)
- Read (
判读) and capability-library availability edges are now mechanically accountable
(R1/A1, A2). A difference-source read sprout points at a journal file that appeared
unpredicted; by the tick the sprout is led, that object has already rolled out of the
"most recent N by mtime" observation window (SUBJECT_FILE_LIMIT), so its
(object, dimension)key was never inobservable_keysand the row stayed
verifiable=falseforever — measured 8 of the last 9 read sprouts, while still holding
topic slots. The verdict now reads the literal mention of the source object in that
tick's executor output (full name, or the subject-relative form; directory objects need
the full name) — the same output text and the same matching predicate as "a trace hit
means the library entry was used" (entry_mentioned). A miss is mechanically decidable:
these edges reportverifiable=true, and a missing mention is recorded as a failure
(readable yet not written down = "not done", isomorphic to the solidify-edge evidence
file, which was built the same way in v2.2.17). The tick prompt states this verdict, so
the executor never has to guess. statusshows both time gates (R5/A3, A4): the frozen-area capacity gate as rows
left (frozen_cap − current rows; live:4103 行/上限 5000(剩 897 行)) and the
requestion gate's earliest requestion tick computed in the engine's own caliber
(candidate-pool entries only, latest freeze per object; live:主体/journal/0247-20260916.md
frozen at tick 325 → requestion tick 625). Previously neither gate had a reading, so
"how far away is it" was not visible anywhere.statusand the reconcile report show the evidence-file compliance rate (R8/A9):
of the cap leads in the last 30 ticks, the share whose evidence file really exists
(live:20/21 = 0.95, the missing one listed by path). The rate is computed through the
same path function the engine hands to the executor (app_evidence_path), so the ledger
row and the file on disk are reconciled on one predicate instead of being counted by hand.
v2.2.19 — a failed redemption is attributed to whoever actually failed (2026-09-17)
v2.2.19 — a failed redemption is attributed to whoever actually failed (2026-09-17)
- The "failed to redeem" attribution has a third bucket, and it takes precedence: when the
tick's executor trace carries the literal "output was not parsed" marker, the row is filed as
dropped on the executor side instead of being blamed on the proposal's age. Live case that
forced it (tick 449): the model replied with a JSON object containingactions, but the
opening{"was missing, so the adapter's parser failed and the whole reply — including a
valid write action — was dropped; the engine honestly recorded a failure to redeem, and the
age rule (59 ticks ≥ 30) would have filed it as "proposal went stale", which is the wrong
account.infinigrow redemptionnow prints all three buckets
(执行者侧未落地 / 提议过期 / 真没做); passtraces_dir(the CLI does) to enable the split —
without it the behaviour is exactly as before. - Also recorded, not fixed (the adapter is out of this repository's scope): 62 of 397 executor
traces (15.6%) hit that parse fallback, 23 of them on capped-sprout topics — so roughly one in
seven ticks an intended action never lands. The engine's bookkeeping is honest throughout; the
loss is on the executor side, and it is the main real cause behind "failed to redeem" rows on
the application edge. - Docs: both mechanism documents describe the three-way split and its proof levels.
- Tests:
+1(the unparsed-marker bucket, including the "notraces_dir→ old behaviour" path).