Repository navigation
SameWrite 1.4.2 — release notes
One sentence. v1.4.2 hardens the legacy optimizer's evidence integrity and keeps candidate
emission inside the requested output root, while leaving the SameWrite skill body, routing
description and hook behaviour unchanged.
This is a patch release. The skill body is byte-identical, its description is unchanged,
no hook behaviour changed, and nothing new is activated. Nothing a model reads changes; what
changes is how the offline optimizer (tools/optimize.py) weighs legacy evidence and where it is
allowed to write a candidate specification. It ships two merged pull requests:
#13 (evidence integrity) and
#16 (candidate-path containment, closing
#15). Both were accepted by an independent
review before merge.
1. What was wrong
- The legacy optimizer promoted findings from evidence it never checked. The six original
defects, R142_01–R142_06, were reproduced on77e3677before anything was repaired
(docs/V142_COUNTEREXAMPLES.md§1–§2):- a history built from bounded sweeps promoted a candidate, and
--accept-partialchanged
nothing because it was never read; - one newer ineligible record anchored the analysis and stranded six eligible ones;
HOST_BEHAVIOR_SHIFTrefused promotion while still writing the candidate file;- a record claiming
COMPLETEwith zero sessions, turns and carry promoted; - a live sweep that lost a torn transcript line reported
COMPLETE; - the carry sweep and the skill-listing scan selected different "newest N" transcripts.
- a history built from bounded sweeps promoted a candidate, and
- Candidate emission could leave the operator's output root. The candidate id carries the
analysed scope verbatim and was also used as the directory name, so a scope such as
x/../../upputHYPOTHESIS.mdabove the--emit-candidatedirectory. A NUL in a legacy
scope ended in a Python traceback with no--jsonresult, and a symlink already sitting at the
candidate's directory name was followed out of the root (#15).
2. What changed — legacy evidence integrity (PR #13)
- Evidence quality is derived, not trusted. One place decides what a legacy record is worth:
the worst of what it claims and what its own counters prove. ACOMPLETEclaim with impossible
numbers (zero sessions with files scanned, sessions without turns, carry without shares, more
sessions than files scanned) isINVALID. A schema older than the quality field, or a schema-2
record without the acquisition counters its writer always wrote, isUNKNOWN. - An intentional bound is not a loss.
PARTIALmeans a bound the caller chose
(skipped_by_limit).DEGRADEDmeans evidence that was selected and then lost (unreadable,
oversize, malformed, identity changed under the read, conflicting sources). The live sweep
derives the same distinction from its counters. --accept-partialadopts an intentional bounded corpus, and nothing else. It never adopts
DEGRADED,UNKNOWN,INVALIDorEMPTYevidence.- Lost evidence no longer reads as complete. A torn or malformed transcript line makes the
live sweepDEGRADED, and the run reportsPARTIAL_EVIDENCEinstead ofNO_ACTION. - Eligibility comes before anchoring. The newest record that can speak for a population
chooses the scope, workload class and corpus size. AnINVALIDorEMPTYrecord no longer
strands the eligible history behind it. - A refusing status writes nothing. The status gate lives inside
emit_candidates(), so no
caller can persist a specification underHOST_BEHAVIOR_SHIFT,PARTIAL_EVIDENCE,
INSUFFICIENT_DATAorNO_ACTION. A run that promised a candidate and wrote none reports
INTERNAL_ERROR. - One definition of a bounded sample.
carry.bounded_paths()selects the newest N sources
once; the carry sweep and the listing scan receive the same selection. - Physical evidence loss opens a recoverable history epoch, not permanent poisoning. A
rejected line classified as a loss cuts the history at its position. Rejections classified as
non-loss open no boundary: current-generation records refused by design, retry bookkeeping and
well-formed foreign entries. Evidence before a cut is never combined with evidence after it,
and enough clean later evidence promotes normally. The damage stays reported in
history.damage. - A rejected record is not trusted for its own scope. A rejected line that represents a loss
cuts every scope (file_global), because the line cannot be trusted to name the population it
damaged. A line that is not valid UTF-8 is a loss, not a label.scope_iditself is validated
before a record is accepted. - A valid record whose own counters prove a loss opens a trusted scope-local boundary. It
belongs to the epoch it closes; other scopes are not cut. - A
run_idis an identity claim, not proof. Two records are one run only when their scope,
file-global epoch,run_idand canonical persisted observation all match (TRUE_RETRY):
deduplicated and counted, opening no boundary. The same identity with a different observation
is aRUN_ID_CONFLICT: counted inhistory.run_id_conflicts, never reported as a retry, and
it cuts a scope-local boundary that later evidence recovers from. - Zero carry is legitimate evidence. A sweep that measured no carry (
shares: {},
carry_bytes: 0) is readable andEMPTY, not corruption. - Legacy optimizer
--jsonoutput schema is now v2. It addshistory.quality,
history.damage,history.run_id_conflictsandscope.records_in_epoch, and removes no key.
evidence_qualityis now derived and may readDEGRADEDorUNKNOWN. ACANDIDATEwhose own
evidence passed its gate now outranksPARTIAL_EVIDENCEfrom evidence it never used.
3. What changed — candidate-path containment (PR #16, closes #15)
- Logical identity and storage identity are separate values.
candidate_idstays the
logical identity, scope included, and is never rewritten.candidate_storage_component()is the
single mapping from that id to a directory name. - Ordinary candidate directories keep their names. An id that is already one ordinary path
component is its own directory name, byte for byte, so existing candidates are still found. - Unsafe ids are re-homed deterministically. An id holding
/or\, equal to.,..or
empty, or starting with the reserved prefix under upper-casing, is stored as
candidate-sha256-<sha256 of the complete id>: one component, deterministic, never a dot
segment. - Candidate-controlled identity cannot leave the resolved
--emit-candidateroot. The emitter
checks, whatever the caller hands it, that the candidate directory is a direct child of
realpath(OUTDIR)(path semantics, not a string prefix). Checked on the legacy-history scope
surface and on--scope-id, and with relative,..-spelled and symlinked output roots. - A pre-existing symlink at the candidate directory name is refused, not followed; so is a
Windows junction where the interpreter can detect one (Python 3.12+). - NUL and pathname-invalid identities fail through a controlled machine result. They are not
hashed into a name. The run prints its--json, reports the logical id incandidates_failed
with a static reason, reportsINTERNAL_ERRORwhen nothing else was written, and leaves no
traceback, no temporary file and no lock. - Machine fields still report logical ids.
candidate_ids,candidates_written,
candidates_existingandcandidates_failedare unchanged in meaning, andHYPOTHESIS.mdstill
states the logicalcandidate_idandscope_id. No output field was added for this. - Issue #15 is closed.
4. What did NOT change
canonical skill body ................. unchanged
skill description .................... unchanged
hook behavior ........................ unchanged
routing behavior ..................... unchanged
automatic promotion .................. NO
automatic candidate persistence ...... NO
automatic policy mutation ............ NO
schema-4 legacy-optimizer support .... NO
AI-VOS activation .................... none
new model-facing instructions ........ none
TOTAL_SAVINGS ................... NOT_PROVEN
WORLD_BEST_CLAIM ................ NOT_TESTED
The version bump activates nothing. tests/test_release_shadow_only.py proves that
mechanically: no promotion entry point ships, the shadow reporter creates no file and no
candidate, and no runtime module branches on the product version. The legacy optimizer still
refuses current-generation (schema-4) records by name.
5. Known open maintenance debt
These are tracked separately. Neither was introduced by 1.4.2, and neither is repaired here.
- #14. A legacy record with a non-string
run_idcan crash candidate rendering and may leave
a temporary candidate file behind. - #17. Under the current trust model, a co-writer that already has write access inside a
candidate directory can pre-place the predictable candidate temporary pathname
(HYPOTHESIS.md.tmp-<pid>) as a symlink, which the write then follows. This needs another
actor or process that already has write access inside the candidate directory. It is not
remote access, code execution or privilege escalation.
6. Other disclosed residuals
- Legacy
UNKNOWNevidence stays fail-closed. It cannot promote,--accept-partialdoes not
adopt it, and it opens no epoch. AnUNKNOWNrecord in the active epoch of a scope therefore
keeps history-based promotion refused for that scope. - Check-then-create race. The containment check and the directory creation are two system
calls. A separate actor that can already write inside the output root could swap a link in
between. - Case-insensitive volumes are modelled, not measured. The reserved-prefix rule uses
upper-casing as the model of NTFS and macOS name comparison; it can only over-match. It has not
been executed on a real case-insensitive filesystem in CI. - Windows junctions on Python below 3.12 are not detected, and Windows paths remain
UNTESTED, as the README already states. - A scope holding a lone UTF-16 surrogate still fails earlier, in the candidate id's own
digest. That is pre-existing and outside the path repair.
7. Compatibility
- Drop-in patch. No skill body change, so no model-facing behaviour changes, and no model
benchmark is re-run for this release. - Evidence history. Schema 4's format is unchanged, and existing schema-4 histories remain
readable. Newly written records are not guaranteed to be byte-identical to 1.4.1's: the
repaired bounded selection (--max-files) can choose a different set of sources. For example,
a source whose mtime cannot be read no longer sends the whole selection back to discovery
order, so sessions, turns and shares can differ. On matched runs that selected the same
sources, the observed record differed only in the writer-version field, which now reads
1.4.2. That is an observation, not a guarantee. Legacy histories (schemas 0–2) are read by
the stricter law above. - The optimizer can say no where 1.4.1 said yes. A history containing bounded, lost,
unattested or impossible evidence can now reportPARTIAL_EVIDENCEorINSUFFICIENT_DATA
instead ofCANDIDATE. That is the repair, not a regression. - Candidate directories. Ordinary ids keep their directory names. An id containing
\or
/, or one that is itself.,..or empty, is re-homed. On POSIX an id containing\was a
single component before, so after upgrading it is written once more under its new name. - Cost. Canonical observation hashing adds a constant-factor cost to reading very large
legacy histories; the reader remains linear in history length. - Python 3.9 and 3.12, as before. Standard library only at runtime;
pytestremains the
single test-time dependency.
8. Tests and gates
Measured on the packaging head with Python 3.10.12: 18 suites, 1,708 assertions, 0 failures.
CI runs the same suites on Python 3.9 and 3.12. The gates that matter for this release:
| suite | assertions | what it holds |
|---|---|---|
tests/test_evidence_integrity.py |
346 | the frozen legacy evidence matrix (docs/V142_COUNTEREXAMPLES.md): quality law, epochs, trust boundary, retry and conflict |
tests/test_candidate_path.py |
107 | the issue #15 matrix, run through the real CLI in a sandbox, plus 13 mutants applied to a throwaway copy of tools/, each required to turn its oracle red |
tests/test_mutation.py |
77 | every repaired invariant has a mutant that must go red |
tests/test_release_shadow_only.py |
10 | the shadow-only boundary: nothing is promoted, persisted or mutated |
tests/test_install_paths.py |
14 | the pinned install routes name this version, and release notes exist for it |
tests/test_oneliner_cleanup.sh (the published OpenClaw one-liner cleans up on every outcome)
passes offline. The public-install and upgrade acceptances need a published tag and run after
it exists.
9. Canonical body identity
CANONICAL_BODY_SHA256 = 7edec9f21e0bd50583e388e0bdc177f92ba8f0db6ce2bb61acd52b39d81e7767
artifact FULL_FILE_SHA256 bytes BODY_SHA256 bytes desc
CLAUDE (canonical) d7c65ee5a4496263 4816 7edec9f21e0bd505 4385 391
CODEX / AGENTSKILLS d7c65ee5a4496263 4816 7edec9f21e0bd505 4385 391
HERMES 9cce7a6c367b8b4a 4472 7edec9f21e0bd505 4385 49
OPENCLAW d7c65ee5a4496263 4816 7edec9f21e0bd505 4385 391
CROSS_HOST_BODY_IDENTITY = PASS
Unchanged from 1.4.1. python3 tools/adapters.py --hashes recomputes it; tests/test_adapters.py
fails if any host's body drifts from the canonical one.
10. Install
| host | install |
|---|---|
| Claude Code | claude plugin marketplace add ipeterpetrus/samewrite && claude plugin install samewrite@samewrite |
| Codex | codex plugin marketplace add ipeterpetrus/samewrite && codex plugin add samewrite@samewrite |
| Hermes Agent | hermes skills install https://raw.githubusercontent.com/ipeterpetrus/samewrite/v1.4.2/adapters/hermes/samewrite/SKILL.md --yes |
| OpenClaw | (d=$(mktemp -d) && trap 'rm -rf "$d"' EXIT && curl -fsSL https://github.com/ipeterpetrus/samewrite/archive/refs/tags/v1.4.2.tar.gz | tar -xz -C "$d" && openclaw skills install "$d"/samewrite-*/skills/samewrite) |
The raw-URL routes are pinned to the release tag, never to main:
python3 tests/test_install_paths.py checks offline that the pins name the version this
release actually is.
Upgrading from 1.4.1
Nothing to migrate. Reinstall by the same route, or bump the plugin through its package
manager. A scheduler that reads the optimizer's --json should accept output_schema_version 2
and expect PARTIAL_EVIDENCE or INSUFFICIENT_DATA where evidence cannot support a promotion.