Skip to content

Releases: Chestnuts-Sisyphus/Infinigrow

v2.2.28 — attribution reads the executor's exit code, and two published verdicts are corrected (2026-09-19)

Choose a tag to compare

@github-actions github-actions released this 19 Sep 12:46

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 said v2.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 × calls must 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
    + both growth-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_composition groups 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 reads app 18 / journal 2 (app evidence
    files, ever-newest by mtime and deliberately never rotated, crowd the growing journal out). A
    copy-based comparison confirms the cause: pruning app/ to its 4 newest files flips the same
    window to journal 16 / app 4 — 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 out journal/ without adding one
    accountable quantity. tests/test_subject_composition.py pins 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.py ran 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 (stdout came back None); the call now pins encoding="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 + both superseded.md + both markers.md). ledger/store.py is 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_exclusive losing the race without clobbering,
    write_work_file's marker gate refusing a shrink, atomic replace leaving no .part,
    move_file writing 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, so ci.yml adds fetch-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
    selftest and a floor test pinning MIN_SYNC_TERMS at 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 row S10, old rows kept, scan + pytest green).
    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 + both superseded.md). The S9 retirement guard watched only
    the English identifier long_task, so five comments could still describe the exemption as
    current behaviour in Chinese — cli.py ("长任务芽豁免连领上限"), the queue docstring
    ("连领上限 3 拍(长任务芽豁免)"), and three in domain_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 in src//prompts/ (not just the English
    usage), with a reverse case proving both spellings are caught while a backtick prose mention
    of long_task as history stays allowed. Both superseded.md S9 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 --search and version --check had never run on any CI, even though the docs publish them
    as the reading surface — and rotate --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 report file:line, miss must report 0 and
    not crash), the exit-code contract of version --check offline (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 4000 moved 1,290 + 3,462 + 1,233 lines into archive/, then rotate --search
    reported 20 hits in the archived files (rc=0) and 0 hits for an absent needle (rc=0); the config
    source line printed defaults、caller with IG_EXECUTOR/IG_EXECUTOR_TIMEOUT_S cleared.
  • 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 + both privacy.md files).
    The real deny list is machine-specific and stays out of the repository — that is by design and was
    not changed — but as a consequence test_repo_is_free_of_project_deny_hits skipped on every CI
    run, so nothing anywhere proved that --deny still 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_LIMIT and 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
    ...
Read more

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

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 mar...
Read more

v2.2.26 — the documents say what the code, the tree and the Release page actually do (2026-09-18)

Choose a tag to compare

@github-actions github-actions released this 18 Sep 19:17

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.py now 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 in tests/test_privacy.py, and documented in
    docs/privacy.md / docs/zh/privacy.md.
  • The reserved long_task branch 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.py now 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",
    while reconcile._bucket computes (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.py pins 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 in engine/org_trigger.py, and tests/test_mechanism_docs.py now 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 are journal and app, and archive/ 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 at infinigrow status for the live reading, and
    tests/test_mechanism_docs.py rejects 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 made IG_FROZEN_REVIEW_EVERY undiscoverable. 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 (both running.md files +
    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 || true in 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 --apply writes 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 the CHANGELOG.md
    heading verbatim, date included (that is what the workflow feeds gh 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_LIMIT or SUBJECT_FILE_LIMIT could never redden it. The check now reads the sentences
    that state the boundary (docs/zh/growth-subject.md table 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: simulate SUBJECT_DIR_LIMIT=12
    and only 1 of the 4 phrases still matches (2 of 4 with SUBJECT_FILE_LIMIT=25) — the check fails,
    which is the point: docs/growth-subject.md promises 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 + both mechanism.md originals +
    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.jsonl row 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_min minutes. 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
    under archive/legacy-20260914/_venvtest/". After this version moved the skip list to
    .gitignore, the default pass does not enter archive/ 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 the versioning.md / privacy.md /
    SECURITY.md claims that were checked held.

v2.2.25 — log rotation moves to the launcher's handle gap, where it can actually work (2026-09-18)

Choose a tag to compare

@github-actions github-actions released this 18 Sep 14:09

v2.2.25 — log rotation moves to the launcher's handle gap, where it can actually work (2026-09-18)

  • logs/tick.log rotation 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 both unlink and an in-place
    truncate failed with PermissionError on the live machine, keeping rc=1 and a
    path-carrying traceback in the log. tools/run_tick.bat now calls
    tools/rotate_journal.py at 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_files drops the log section; ledger.store.truncate_file is 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)

Choose a tag to compare

@github-actions github-actions released this 18 Sep 11:13

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 holds logs/tick.log with an append handle; Windows (no delete-sharing)
    made unlink fail with WinError 32 on every rotation, so the tick exited rc=1 and 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 status line 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 — matching docs/running.md one 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)

Choose a tag to compare

@github-actions github-actions released this 17 Sep 17:56

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
    functions status uses (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 status line names all five new readings (S2/D1):
    docs/running.md and docs/zh/running.md now 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)

Choose a tag to compare

@github-actions github-actions released this 17 Sep 16:19

v2.2.22 — executor-side loss and token spend are both visible (2026-09-17)

  • status and 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): status now answers "where did today's
    tokens go" per source prefix (cap/sp/lib), joining executor.jsonl calls 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 optional root for 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)

Choose a tag to compare

@github-actions github-actions released this 17 Sep 16:06

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 like subject_root / item.name (names come from
    observation and ledgers) carried no containment check. engine.subject.subject_path is 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_root predicate with the existing write guard; the org-session renderer
    reads through it. SECURITY.md gains 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 generic write_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.py batch-realigns historical Release titles and bodies with the
    repository sources (documented English notes first, the CHANGELOG.md section 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.md lists 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)

Choose a tag to compare

@github-actions github-actions released this 17 Sep 15:51

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 in observable_keys and the row stayed
    verifiable=false forever — 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 report verifiable=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.
  • status shows 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.
  • status and 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)

Choose a tag to compare

@github-actions github-actions released this 17 Sep 10:51

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 containing actions, 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 redemption now prints all three buckets
    (执行者侧未落地 / 提议过期 / 真没做); pass traces_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 "no traces_dir → old behaviour" path).