Skip to content

fix: full-codebase design audit — write-back corruption, secret heuristic, contract repairs - #34

Open
t41372 wants to merge 40 commits into
mainfrom
fix/design-audit
Open

fix: full-codebase design audit — write-back corruption, secret heuristic, contract repairs#34
t41372 wants to merge 40 commits into
mainfrom
fix/design-audit

Conversation

@t41372

@t41372 t41372 commented Jul 26, 2026

Copy link
Copy Markdown
Owner

What this is

A full-codebase design audit: one reviewer read the entire source (~28k lines minus locales/tests) in a single pass, hunting cross-file incoherence — modules that fight each other, code that violates AGENTS.md's own rules, CLI/TUI divergence, UX dead ends — on top of conventional correctness/security/perf review. Findings were verified against source line-by-line before fixing; the review then looped over the fixes themselves until it came back clean (4 rounds total, including a post-#33-merge round).

Fixes, worst first

Round 1 (fix: design-audit round 1) — 12 verified findings:

  • Stored-copy corruption on the default add path (HIGH). The TUI add-review panel wrote parameter blocks back with read_text/write_text — universal-newline fold plus os.linesep re-expansion rewrote every line ending of the just-stored copy (CRLF-ifying it on Windows; first run then fails with $'\r': command not found), while its four sibling write-back sites each hand-rolled the correct bytes discipline. All six sites now share one pair: rewrite.read_for_block_edit / write_block_edit (surrogateescape, LF fold, newline restore, atomic + mode-preserving). The python onboarding lane's non-UTF-8 traceback and --normalize's non-atomic write go away with it.
  • --max-tokens became a permanent password field (HIGH). is_secret_name matched substrings (TOKENMAX_TOKENS), and reader-reflected fields have no override surface — masked, never prefilled, never remembered, forever. Now whole-word matching (snake/kebab/camelCase/sentence-aware).
  • skit remove prompted inside pipes/CI, violating the absolute non-interactive contract its own sibling runner remove implements (and SKILL.md claimed they behaved alike). It now takes --no-input and refuses with exit 2; preset delete (unrecoverable user data, previously deleted with no ask) gets the same confirmation ceremony. SKILL.md and the docs now tell the same story as the code.
  • Cursor-move subprocess freeze. The Library detail pane rebuilt the form plan on every RowHighlighted — a synchronous pwsh spawn (30 s timeout) per cursor move over a PowerShell entry. Plans are now mtime-cached; the add-review panel memoizes its reader probe.
  • One remembered extra-args tail, two expansion regimes. CLI replayed it literally, the TUI token/glob-expanded it — same state, different argv, and each side's comment claimed it matched the other. The tail's provenance is now recorded (extra_args_raw) and every replay follows it in both faces.
  • Ctrl+O meant three things on three sibling screens while README documents one. Choose-variables → Ctrl+L (both screens), Preferences' squatters → Ctrl+G/Ctrl+Y, and AGENTS.md's key grammar now lists Ctrl+O/Ctrl+L plus the unclaimed-chord rule.
  • Six LOWs: README ×3 stop over-claiming uv consent (non-tty downloads without asking, by design); params --manage dead ends now name the --add escape; non-UTF-8 python onboarding no longer tracebacks; ArgSpec moves to the neutral analysis module the four non-python readers were told not to reach around; params --json rows carry an additive binding key so kind no longer puns across sibling commands; preset-delete ordering (unknown-name feedback before the ask).

Round 2 (fix: design-audit round 2) — two regressions the re-review caught in round 1's own fixes:

  • Word-boundary matching dropped the jammed spellings the old rule caught — APIKEY/AUTHTOKEN stopped matching, a C3 regression in the leak direction. Suffix rule + credential-qualifier KEY-compounds restore them (never MONKEY/TURKEY/HOTKEY).
  • The new plan cache keyed only on the script's mtime, blind to meta.toml edits from a concurrent agent. Now keyed on both mtimes.

Tests (test: cover the design-audit fixes) — 2 new files (57 behavior tests: byte-exact CRLF/CR/non-UTF-8 round-trips including the TUI accept path, the 40-case secret matrix, exit-2 refusals, the provenance replay matrix across both faces, cache invalidation, key-remap pilots plus old-chord negatives) + 17 existing files adapted. No pragmas added; four provably-equivalent mutants documented instead of suppressed.

Round 4 (fix: design-audit round 4) — after rebasing onto merged #33: the review read all of #33 with perf-specific lenses (fast/slow path parity, cache freshness proofs, laziness vs contracts, locking) and came back clean at medium+; two lows fixed: extra_args_raw read with is True per the house rule for hand-editable bools, and AGENTS.md records #33's sanctioned read-path registry self-heal next to the contract it bends.

Round 13 (bae1a3af3af36f) — an external line-by-line review of this PR produced 8 findings; 6 were verified real and fixed, 2 were declined with evidence (auto-download under --no-input is the recorded A9 policy this PR did not change — it fixed the pre-existing infinite input() block; the process-global interaction latch is a documented trade-off, kept as a follow-up). A second review pass added 3 more, all fixed:

  • resolve() refuses an ambiguous name. The round-9 sweep answered a name two hand-edited metas carried by returning whichever slug sorted first — executing an entry the user never picked. New AmbiguousNameError (usage exit) lists every claimant; a lone sweep hit is re-verified against its meta like the fast path.
  • State writes join the exit taxonomy. argstate writers (forget included) fail as StateWriteError — an OSError subclass mirroring ConfigWriteError, so the run lane's persistence wrapper needed no change — mapped to the operational exit at the root boundary; the TUI notifies instead of dying (error-and-stay where the work is undone, warn-and-continue where it already happened). store.remove reports state-cleanup failure as honest partial success, and the CLI's three runner-pick memories are best-effort: a read-only state dir warns instead of vetoing the add/run the pick rode on.
  • Secret transitions are fail-closed. All four param-edit lanes purge stale plaintext BEFORE committing the secret flag: no interruption can leave "schema says secret, plaintext still on disk". A prompt's managed list and declared rows commit in ONE meta write.
  • Raw runs honor the C3 scrub. record_run(values=None) now strips the preserved snapshot; the raw lane feeds it the stored schema's own secrecy (grammar-free reads, per the A2 rule), re-read FRESH at stamp time and unioned with the launch-time set — a mid-run flip can only widen the scrub — and an entry removed mid-run gets no posthumous state.
  • The Windows CI blocker was a test fixture (write_text newline translation), fixed as such; byte-preservation in production is untouched.
  • Meta-test infrastructure: four source-reading meta-tests added on this branch (blind-spot ratchet, exit-route whitelist, and friends) read the REAL tree via conftest.real_repo_root — inside mutmut's rewritten mutants/ copy they measured the trampoline machinery and would have killed the nightly's stats phase on merge. Targeted mutation testing over every changed module followed, with each reported survivor re-verified against the live mutant; real gaps got killing tests, and the false-survivor mechanism found on the way is written up in mutmut reports false survivors: the cached test-selection map is partial and never repaired #38.

Round 14 (bf6e1ab) — the third external review pass raised one P1 against round 13's own boundary: the raw lane got re-resolve + secret-set union, but the normal lane every accepted run takes still trusted a launch-time slug and a launch-time secret set across a run that can last hours — and _unique_slug legally reissues a removed entry's slug to a later add, so post-run persistence could land on a stranger (added_at can't tell the owners apart; now_iso drops sub-second precision). Fixed as one universal mechanism, not a second local patch:

  • Entries now have an identity. ScriptMeta.id (uuid4 hex), minted at the store's one meta-write door (_write_meta): adds reach disk stamped, every edit and rename preserves it, a meta from before ids existed parses as the wildcard and heals on its next write (reads stay reads), and a hand-mistyped id is meta corruption like a mistyped runner. Never surfaced in --json or registry rows; an unstamped id is omitted on serialization so legacy metas round-trip byte-identically.
  • One guard, four doors. flows.persistence_target re-resolves the held entry and compares identities — gone, unreadable, or a different incarnation means write NOTHING. Every post-acceptance state write now passes through it: save_after_run (takes the Entry, no longer a slug), save_after_raw_run (refactored onto the shared guard), new save_preset_for (run --save-preset and the form's Ctrl+S — refusal is reported with the entry's name, never silent, because a preset is an explicit request), and new clear_remembered_tail (--forget-args — a vanished entry is vacuously forgotten; writing the clear would resurrect the state file remove() deleted). The strip set everywhere is the union of launch-time secrecy (the plan's, heuristics included) and both stored readings — a race can only widen the scrub.
  • The census. A source test pins every remaining direct argstate writer call site in src/skit with its justification (immediate resolve→write command lanes hold no handle long enough to go stale); a new write path must use a door or amend the census in the open. The one surviving window of the same class — settings screens holding an Entry across user-paced time, writing through the store's re-resolving APIs — is recorded as TUI settings screens hold a stale Entry: saves can land on a reincarnated slug #39 with an expected_id design sketch.

Round 15 (a7ac6ac) — the identity-guard review: both P1s verified real against round 14's own code, and the P2's deferral overruled:

  • Exact-match write authorization replaces the wildcard. The legacy-id wildcard ("" matches anything) let an upgrade user's long run land state on a reincarnated slug — unknown identity may serve reads, but it cannot authorize a write. persistence_target now requires exact id equality, and every lane that holds an Entry across user-paced time claims it at hold-start via store.ensure_identity (CLI run, the TUI's run/rerun/settings lanes and the post-edit reconcile picker): a pre-id meta is stamped before the hold begins, a stamped meta is never rewritten (its mtime keys the plan cache and row stamps), and a read-only library degrades honestly — nothing else can stamp there either, so "" == "" holds exactly where the ids cannot diverge.
  • The doors are transactions now, not checks. persistence_target was check-then-act: verify, release everything, then write. Every flows door now holds the ENTRY LOCK — the same lock every meta mutator and remove() (through its state forget) already hold — across verify + strip-set read + all writes, so a remove, a reincarnation, or a secret flip can no longer slip between the identity check and the last write. The two secret-transition commits became locked store transactions with the C3 scrub inside: write_parameters (declared lane) and the new write_source_params (spec lane), collapsing all four CLI/TUI transition lanes onto them — purge-then-commit is enforced by construction, not call-site ordering. Lock-held probes pin the contract for every door, and the lock file's path is pinned as the cross-version contract it is. Declined as designed: merging the door's three per-slug state writes into one RMW — under the entry lock there is no interleave window left, each write is individually atomic, and every crash intermediate is C3-consistent; a merged mega-writer would couple argstate's per-writer API to the door's shape for no observable-state gain.
  • TUI settings screens hold a stale Entry: saves can land on a reincarnated slug #39 fixed, not deferred. Store mutators take expected_id, checked under the lock (StaleEntryError, with the reopen remedy); the settings screen authorizes every axis of its save against the identity it was OPENED on — pinned by a per-axis stale matrix (rename, template, interpolate, declared rows, source block, deps, npm clear, needs, launch policy, runner pin) — its preset cleanup goes through the guarded flows.delete_presets_for door, and its write pass gained the one catch the old bare calls never had (a mid-save refusal keeps the screen instead of crashing the app). The census tightened: tui_settings no longer touches argstate at all.

Round 16 (eb351c3) — the doors-outside-the-doors review: round 15 built the locked, identity-authorized transactions and then mis-filed several CLI writers as "immediate" that genuinely wait on a human or derive their write from an earlier read. All four findings accepted (one sub-point declined with a proof):

  • preset save/preset delete go through the doors. preset save waited on an interactive intake and then wrote argstate bare — inside a secret transition's own window, it could re-persist plaintext the scrub had just removed, un-serialized with the very transaction round 15 built. It now claims identity at hold-start (after its static refusals) and saves through flows.save_preset_for — entry lock, exact-id re-proof, launch∪current strip — refusing an entry that vanished or a slug reissued mid-intake (exit 127). preset delete claims before its confirmation ask and deletes through flows.delete_presets_for: an answer given about the old entry can never delete the new owner's same-named preset. The argstate-writer census now allows NO direct writer outside flows' doors and store's two locked scrubs.
  • Every params/deps edit lane authorizes its write. The rule the review stated is now the code's rule: a write that depends on an earlier read carries the identity claimed before that read — "usually fast" is not an authorization. The params edit lanes (spec, declared, launch policy, runner pin, interpolate) and both deps axes claim via ensure_identity (the read views stay pure reads — no claim, no stamp) and pass expected_id to every store call; a reissued slug gets an honest stale refusal (exit 125), never the dead handle's analysis. --normalize — the worst case, a semantic rewrite computed from a pre-consent read — moved into store.rewrite_source: read, transform, and byte-disciplined write all under the entry lock with the identity check first, re-derived from the fresh text.
  • run stamps nothing on a refused invocation. The hold-start handshake moved below every static refusal, so run x --raw --set a=1 (exit 2) on a legacy entry no longer heals its meta — "a refused invocation leaves no fingerprints" now includes the id stamp.
  • "" is an expectation, not an off-switch. The id or None idiom silently disabled the guard for exactly the handle that most needed it; expected_id now passes verbatim, and an unstamped handle meeting a stamped entry refuses (the asymmetry proves the disk changed owners). ensure_identity re-reads after a failed stamp, so a half-landed id (meta written, registry row failed) can never strand a handle behind its own entry. Declined with a proof: skipping post-run persistence outright for unstamped handles — a reincarnated slug always meets the guard stamped (_add_entry cannot write a meta without an id), so "" == "" proves no reincarnation happened and the write is sound; refusing it would only strip read-only-library users (writable state dir, frozen data dir — a legitimate deployment) of run history for no safety gain.

Round 17 (eec0533) — the claim-by-address review: all four findings verified real and closed:

  • Compare-and-claim replaces claim-by-address. Round 16's ensure_identity(slug) re-resolved an address and adopted whoever owned it — a remove + same-name re-add between a lane's resolve and its claim was silently blessed, with every later guard protecting the stranger. store.claim_identity(entry) verifies UNDER THE ENTRY LOCK that the disk still holds the entry the caller resolved (exact id; unstamped accepts only unstamped), stamps a pre-id meta there, and refuses a changed owner. Every lane passes its held Entry; TUI ghost rows stop their lane and refresh the list with the reason. The races are tested REAL — reincarnations injected between resolve and claim, the claim itself unpatched.
  • skit remove is identity-authorized on both faces. store.remove takes expected_id, checked under the lock: the deletion is authorized against the entry the confirmation ask NAMED, so a slug reissued while the user answered refuses (with the stale message) instead of deleting the new owner's registry row, stored copy, meta and state.
  • Editor sessions are staged. The longest user-paced hold skit has no longer touches the stored path: copy-mode edits work on a unique draft in skit's drafts dir and land through store.commit_copy_edit's identity-checked, mode-preserving transaction. A stale landing keeps the draft and names it in the refusal ("Your edit was kept at: …"); an invalid prompt edit now never reaches the stored copy at all — strictly better than the old in-place refusal. Reference mode stays direct: it edits the user's own file, which no reincarnation moves.
  • The "" == "" proof is withdrawn. The reviewer's cross-version counterexample stands: an OLDER skit's adds write no id, so a symmetric blank can be a reincarnation this version never saw. Unknown identity persists nothing — post-run persistence requires a STAMPED exact match; a library this process cannot stamp (not provably unwritable by others) runs fine and simply keeps no state.

Gates

ruff format --check · ruff check · ty check (strictest) · pytest --cov5983 passed, 31 skipped, 100.00 % coverage · i18n gate 100 % both locales (the stale-refusal msgid reworded face-neutral in round 16; 13 new msgids translated across the audit + review rounds; pybabel fuzzy mismatches corrected) · targeted mutmut on the changed modules with every reported survivor re-verified against the live mutant: the round-14 doors ran 191/191 killed; round 15 re-ran the full identity/door/settings surface with a FRESH stats map (the #38 false-survivor trap) — every mutant on changed lines killed, three documented selector equivalents pragma'd, and the only remaining targeted survivors are pre-existing mutants on untouched validation-pass lines (the #38-recorded baseline, out of this PR's scope). Rounds 16–17's targeted sets ran 1000+ mutants each across the reworked lanes — every mutant on changed lines killed (round 17's three reported survivors were proven killed against the live mutants, the #38 stale-selection artifact; one true equivalence was refactored away rather than pragma'd). Full remote matrix green through b6d73db, Windows included (round 17 needed one test-side portability follow-up: platform-neutral mode/newline assertions).

Deliberately deferred (on the books)

Reader-lane secret override: exact-word false positives (sort_key, foreign_key) on reader-reflected forms still have no off switch; the audit ruled the fix shippable without it, but the [[parameters]]-rider override should follow as its own design round — it also doubles as the recovery path for any future heuristic miss.

Summary by CodeRabbit

  • New Features
    • Added --no-input support to removal commands, with safe refusal when confirmation is required.
    • Improved replay of remembered trailing arguments, preserving quoting and expansion behavior.
    • Added parameter binding details to JSON output.
    • Updated TUI shortcuts for variable selection, agent management, and skill installation.
  • Bug Fixes
    • Preserved original bytes, line endings, and file permissions during script edits.
    • Improved secret-name detection to reduce false matches.
  • Documentation
    • Clarified non-interactive installation, deletion, command behavior, and argument replay.
    • Refreshed localized interface translations.

t41372 added 4 commits July 26, 2026 04:35
… contract repairs

Findings from a full-codebase design audit, worst first:

- The TUI add-review panel wrote parameter blocks back with read_text/write_text,
  silently rewriting every line ending of the just-stored copy (CRLF-ifying it on
  Windows) while its four sibling write-back sites each hand-rolled the correct
  bytes discipline. All six sites now share one pair: rewrite.read_for_block_edit /
  write_block_edit (surrogateescape, LF fold, newline restore, atomic + keep-mode).
  The python onboarding lane's strict-decode traceback on non-UTF-8 input and the
  --normalize lane's non-atomic write go away with it.
- is_secret_name matched substrings, so --max-tokens (TOKEN) became a permanent
  password field on reader-reflected forms with no override anywhere. Whole-word
  matching (snake/kebab/camel/sentence-aware) fixes the false positives at the
  source for every lane.
- skit remove prompted inside pipes/CI, violating the non-interactive contract its
  own sibling runner remove implements; it now takes --no-input and refuses with
  exit 2, and preset delete (unrecoverable user data, previously deleted with no
  ask) gets the same confirmation ceremony. SKILL.md and the docs now tell the
  same story.
- The remembered extra-args tail replayed under two different expansion regimes
  (CLI literal, TUI token/glob-expanded). The tail's provenance is now recorded
  (argstate extra_args_raw) and every replay follows it in both faces.
- The Library detail pane rebuilt the form plan on every cursor move — a
  synchronous pwsh spawn per RowHighlighted for PowerShell entries; plans are now
  mtime-cached (the old drift cache generalized), and the add-review panel
  memoizes its reader-modeled probe.
- Ctrl+O meant three things on three sibling screens while README documents one:
  Choose variables moves to Ctrl+L (both screens), Preferences' squatters move to
  Ctrl+G/Ctrl+Y, and AGENTS.md's key grammar now lists Ctrl+O and Ctrl+L.
- READMEs stop claiming uv is always consent-gated (non-tty downloads without
  asking, by design); params --manage dead-ends now name the --add escape;
  params --json rows carry an additive "binding" key so "kind" no longer puns
  across sibling commands; ArgSpec moves to the neutral analysis module the four
  non-python readers were told not to reach around.

Tests/coverage follow in the next commit.
…cache

Two regressions the round-2 review caught in round 1's own fixes:

- Word-boundary secret matching dropped the jammed spellings the old substring
  rule caught — APIKEY/apikey/AUTHTOKEN stopped matching, a C3 regression in the
  dangerous direction (an unmarked literal is published into current_defaults,
  --json output, and plaintext state). Words ending in the long secret words
  (TOKEN/SECRET/PASSWORD/PASSWD) now match, and KEY-compounds match behind a
  credential-qualifier prefix list (APIKEY, SSHKEY — never MONKEY/TURKEY/HOTKEY).
- The new display-plan cache keyed only on the script's mtime, but a plan is a
  function of meta.toml too (declared [[parameters]] rows, a prompt's managed
  list / interpolate switch): an agent running skit params --add beside an open
  TUI left the detail pane stale forever. The cache now keys on both mtimes.
Repairs the suite for the two source commits and adds the behavior coverage that
keeps each verified bug dead.

Contract repairs — the API changes the fixes made:

- flows.save_after_run's now-required extra_raw keyword threaded through its seven
  test call sites, each carrying the value its scenario really simulates (a TUI-form
  save is True, a CLI tail False).
- tui.PendingRun's new positional extra_raw field at its five construction sites.
- MenuApp._drift_cache -> _plan_cache: the seeded/inspected sentinels are now
  ((script mtime, meta.toml mtime), FormPlan) and still prove what they meant to —
  the cache hit, and the pop on edit / settings close.
- test_store_mut's atomic-write spy repointed at rewrite.atomic_write_bytes_keep_mode,
  the seam _sync_python_block reaches through write_block_edit now; the two
  cli_design_cov write-back spies likewise, which also lets them pin the landing as
  atomic (neither Path.write_bytes nor write_text may touch a stored copy).
- Ctrl+O -> Ctrl+L (prompt review, Script settings) and Ctrl+O/Ctrl+K -> Ctrl+G/Ctrl+Y
  (Preferences) in every pilot test that pressed them.
- remove / preset delete need -y or a real terminal now, so their existing tests say so.

New coverage (tests/test_design_audit_fixes.py, tests/test_design_audit_tui.py):

- rewrite.read_for_block_edit / write_block_edit: CRLF, lone-CR and LF copies each
  round-trip with only the block changed and every other byte identical; non-UTF-8
  bytes survive via surrogateescape; the executable bit survives the atomic write;
  cli._onboard_python degrades on a non-UTF-8 python file instead of raising.
- The round-1 HIGH pinned at the surface it shipped from: an AddReviewScreen accept
  on a CRLF shell script leaves the stored copy CRLF and byte-exact outside the block.
- is_secret_name's matrix in both directions, including one jammed spelling for every
  suffix and every KEY-prefix the rule recognizes (--max-tokens stays public).
- The non-interactive contract for remove and preset delete (worded exit-2 refusal
  naming --yes, nothing removed), their -y and confirm paths, the unknown-preset error
  landing before any ask, and the same error when a preset vanishes mid-flight.
- Extra-args provenance end to end: the argstate marker is written, cleared, and
  defaulted for legacy docs; save_after_run threads it; the CLI expands a replayed
  raw tail and replays an unmarked one literally; a fresh `-- args` clears the marker;
  --forget-args clears both; the TUI's `r` follows the record while the form's own
  tail saves marked; and the marker rides PendingRun into the deferred exit-mode save.
- The display-plan cache: one build per (script mtime, meta.toml mtime), a rebuild
  when either moves, no caching at all when the script can't be stat'ed, _has_drift
  served off the same entry and short-circuiting before any build, and both pop sites.
- AddReviewScreen._reader_modeled probes once per text and recomputes after an edit
  (a single-option getopts pins "modeled" at one field, not more than one).
- Positive pilot tests for Ctrl+L / Ctrl+G / Ctrl+Y with a negative for each vacated
  chord, and Ctrl+O still restoring a run-form default.
- params --manage on an exe names the --add door it does have — and the two degraded-
  spec refusals now assert they do NOT carry that hint; params --json rows carry
  "binding" beside the frozen "kind" without dropping a key an existing consumer reads.
The round-4 review of merged PR #33 came back clean at medium-and-above and
verified all four interaction seams with the audit branch. Two low notes fixed:

- argstate reads the extra_args_raw marker with `is True` instead of bool(),
  matching the house rule for hand-editable bools (models.interpolate,
  config.enabled): a hand-edited scalar must degrade to the safe literal-replay
  default, never coerce truthy toward re-expansion. Pinned by a test.
- AGENTS.md records #33's one sanctioned bend of the read-command contract (a
  listing may self-heal skit's own registry index) next to the rules it bends,
  the way the A5 exception is recorded.
@coderabbitai

coderabbitai Bot commented Jul 26, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR centralizes ArgSpec, preserves bytes during script block edits, tracks extra-argument provenance, strengthens non-interactive deletion behavior, updates TUI caches and keyboard chords, improves secret detection, expands tests, and synchronizes documentation and localization catalogs.

Changes

Core behavior

Layer / File(s) Summary
Argument provenance and analysis contracts
src/skit/analysis.py, src/skit/argstate.py, src/skit/flows.py, src/skit/cli.py, src/skit/tui.py, src/skit/langs/*
Adds shared ArgSpec ownership and persists extra_args_raw so CLI and TUI replay can distinguish raw tails from shell-processed arguments.
Byte-preserving block edits
src/skit/rewrite.py, src/skit/cli.py, src/skit/store.py, src/skit/tui_add.py, src/skit/tui_settings.py, tests/test_design_audit_fixes.py, tests/test_design_audit_tui.py
Routes block edits through newline-aware, surrogateescape-preserving atomic writes that retain file modes and original bytes.
CLI safety and data contracts
src/skit/cli.py, src/skit/params.py, docs/content/docs/cli.mdx, skills/skit/SKILL.md, tests/test_design_audit_fixes.py
Adds non-interactive refusal handling for destructive commands, pre-confirmation unknown-preset checks, additive JSON binding output, whole-word secret detection, and clearer parameter-management guidance.
TUI cache and keyboard behavior
src/skit/tui.py, src/skit/tui_add.py, src/skit/tui_prefs.py, src/skit/tui_settings.py, tests/test_design_audit_tui.py, tests/test_prompt_tui.py
Replaces drift caching with mtime-keyed plan caching, memoizes reader probing, and assigns Ctrl+L, Ctrl+G, and Ctrl+Y to their updated actions.
Documentation, tests, and catalogs
AGENTS.md, README*, docs/content/docs/*, tests/*, src/skit/locales/*
Documents keyboard, bootstrap, replay, and confirmation contracts while updating regression coverage and gettext catalogs.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant CLI_or_TUI
  participant flows
  participant argstate
  participant Launcher
  User->>CLI_or_TUI: run with remembered or new extra arguments
  CLI_or_TUI->>argstate: load extra_args_raw
  CLI_or_TUI->>flows: assemble with expand_extra
  flows->>Launcher: launch entry with resolved arguments
  CLI_or_TUI->>flows: save_after_run with extra_raw
  flows->>argstate: persist extra_args and provenance
Loading

Possibly related PRs

  • t41372/skit#26: Also modifies argstate.py persistence and related write-back paths.

Poem

I’m a rabbit with bytes in my burrow,
Keeping each newline neat, never blurry.
Chords hop to their proper new keys,
Raw tails replay with the greatest of ease.
Safe prompts refuse when consent isn’t shown—
And catalogs bloom in languages known.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 56.15% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the audit-driven fixes for write-back corruption, secret detection, and contract repairs.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codspeed-hq

codspeed-hq Bot commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 18 untouched benchmarks
⏩ 2 skipped benchmarks1


Comparing fix/design-audit (b6d73db) with main (2a35f19)2

Open in CodSpeed

Footnotes

  1. 2 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

  2. No successful run was found on main (206f9ef) during the generation of this report, so 2a35f19 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/skit/cli.py`:
- Around line 4526-4536: The error message currently concatenates two
independently translated strings, making the combined sentence difficult to
localize. Update the message construction in the managed-parameters failure path
around entry.meta.name and entry_spec.params_io to use a single
gettext-translated msgid containing the optional --add hint, while preserving
both existing message fragments and their conditional behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: bada59f9-e67c-4fe4-9afe-20f21954b12e

📥 Commits

Reviewing files that changed from the base of the PR and between 2a35f19 and 9ed2152.

📒 Files selected for processing (49)
  • AGENTS.md
  • README.md
  • README.zh-CN.md
  • README.zh-TW.md
  • docs/content/docs/cli.mdx
  • docs/content/docs/parameters.mdx
  • skills/skit/SKILL.md
  • src/skit/analysis.py
  • src/skit/argstate.py
  • src/skit/cli.py
  • src/skit/flows.py
  • src/skit/langs/base.py
  • src/skit/langs/fish/cli_reader.py
  • src/skit/langs/javascript/cli_reader.py
  • src/skit/langs/powershell/cli_reader.py
  • src/skit/langs/python/argspec.py
  • src/skit/langs/shell/cli_reader.py
  • src/skit/locales/skit.pot
  • src/skit/locales/zh_CN/LC_MESSAGES/skit.mo
  • src/skit/locales/zh_CN/LC_MESSAGES/skit.po
  • src/skit/locales/zh_TW/LC_MESSAGES/skit.mo
  • src/skit/locales/zh_TW/LC_MESSAGES/skit.po
  • src/skit/params.py
  • src/skit/rewrite.py
  • src/skit/skills/skit/SKILL.md
  • src/skit/store.py
  • src/skit/tui.py
  • src/skit/tui_add.py
  • src/skit/tui_prefs.py
  • src/skit/tui_settings.py
  • tests/test_argstate_mut.py
  • tests/test_cli.py
  • tests/test_cli_design_cov.py
  • tests/test_cli_gaps_cov.py
  • tests/test_cli_mut_part03.py
  • tests/test_default_semantics_review_fixes.py
  • tests/test_design_audit_fixes.py
  • tests/test_design_audit_tui.py
  • tests/test_flows.py
  • tests/test_prompt_tui.py
  • tests/test_source_default_semantics.py
  • tests/test_store_mut.py
  • tests/test_tui_edit.py
  • tests/test_tui_mut.py
  • tests/test_tui_mut_part01.py
  • tests/test_tui_mut_part05.py
  • tests/test_tui_mut_part09.py
  • tests/test_tui_prefs_agents_cov.py
  • tests/test_tui_prefs_mut.py

Comment thread src/skit/cli.py Outdated
@codecov

codecov Bot commented Jul 26, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

t41372 added 22 commits July 26, 2026 06:16
…tself

The workflow review (max effort) confirmed 15 findings against PR #34's own
fixes; the seven correctness ones, worst first:

- is_secret_name regressed plurals (API_KEYS, SECRETS, GITHUB_TOKENS) and
  acronym+lowercase jams (APIkey, SSHkey) to non-secret — false negatives that
  publish live literals into --json and state files. The heuristic is now
  segment-based (no camel regex to mangle nonstandard casing; one trailing S
  folds away) with an explicit TOKEN count-context rule: count qualifiers
  (max_tokens, token_limit, nTokens) suppress, credential qualifiers
  (github_tokens, session_token) mask, bare plural 'tokens' reads as a count.
- The launch menu was a third face of the provenance rule: a CLI-captured
  literal tail prefilled into the form and submitted untouched was re-expanded
  and its stored marker flipped to raw for every later replay. An untouched
  tail now keeps its recorded provenance; only text the user actually edited
  is re-captured as form text.
- Replaying a marker-less tail that carries token/glob syntax now says so on
  stderr (legacy pre-provenance state replays literally by design — silently
  was the bug), and the docs explain the one-time re-capture.
- _cached_plan re-resolves the entry from the store on a cache miss: the
  Library's in-memory row can predate the very meta.toml change that
  invalidated the key, and building from it pinned a stale plan under the
  fresh key past every reload. The key also tightens to (mtime_ns, size) per
  file, narrowing the coarse-mtime blind spot to same-tick same-size writes.
- The add panel's reader-modeled memo keys on the reader's runtime fingerprint
  (new CliReader field, wired for PowerShell) so installing pwsh mid-session
  is seen instead of serving the tool-less verdict forever.
- Preferences action docstrings caught up with the chord move (Ctrl+G/Ctrl+Y).

Cleanups: the strict block-edit read lanes (--normalize, deps sync) now go
through read_for_block_edit(errors="strict") — the fold/detect discipline
lives in exactly one place; the destructive-trio confirmation guard is one
helper (_require_yes); the --add hint is one msgid instead of two sentences
spliced with a hard-coded ASCII space; _SECRET_WORDS/_SECRET_SUFFIXES no
longer shadow each other; analysis.py's header lists its third resident
(ArgSpec). i18n at 100% for the new msgids.

Tests follow in the next commit.
Repairs the suite for 67d7ee1 and adds the behavior coverage that keeps each of
its verified bugs dead.

is_secret_name (segment rule):
- The matrix grows both directions the round repaired — plurals (API_KEYS,
  SECRETS, GITHUB_TOKENS, DB_PASSWORDS), acronym+lowercase jams (APIkey, SSHkey,
  GPGkey, AWSkey), and the qualified plurals that stay credentials
  (github_tokens, "access tokens", session_token).
- Three cases are RE-RULED with the reason in place: photokens now reads secret
  (anything ending in TOKEN without a count qualifier errs toward masking), while
  MAX_TOKEN / token_limit / token_count now read public (the count context
  suppresses the singular too).
- One case per member of _SECRET_SUFFIXES, _KEY_PREFIXES and _COUNT_WORDS, each
  with a completeness assertion, so no member can be dropped, renamed or added
  without a red test; plus the fused qualifier (maxTokens, nTokens), the plural
  qualifier (token_limits) and the bare-plural count rule.

Launch-menu provenance (tui._submitted, the rule's third face):
- A prefilled literal tail submitted untouched is delivered verbatim, assembles
  with expand_extra=False, and does NOT flip its stored marker; a prefilled raw
  tail submitted untouched still expands; an edited tail is re-captured as form
  text (marked raw) — the documented one-time repair for a legacy tail. The new
  `expansions` fixture records the expand_extra decision itself.

CLI as-is note:
- A marker-less replay carrying {, *, ? or [ prints the note on stderr (one case
  per character) and still delivers the tail literally; a plain-word tail and a
  raw-marked tail both stay quiet.

Plan cache:
- The key's shape moves to conftest.plan_cache_key ((mtime_ns, size) per file)
  and the three tests that spelled it out inline now share it.
- New: a meta.toml change made through the store API is reflected in the
  rebuilt plan even though the Library's row is stale (asserted on plan fields),
  and an entry that stops resolving mid-render serves the snapshot and caches
  nothing.

Reader memo fingerprint:
- A tool-gated reader (shaped like PowerShell's) re-probes when its fingerprint
  appears and vanishes, memoizes while it holds, and the purely-static python/js
  readers keep memoizing on text alone; powershell.runtime_fingerprint tracks
  _find_powershell's answer, and the registry wiring is asserted on the builder
  itself (capabilities are built once per process and cached).

read_for_block_edit(errors=):
- errors="strict" raises on the bytes the default carries, with the same fold +
  detect either way; the deps sync now also pins a CRLF copy round-tripping
  through one block, and the two lanes' docstrings name the shared read.

Review findings in the suite itself:
- The two write-back spy tests share a copy_io_spy fixture (their assertions are
  unchanged); _without_block and the block constants move to conftest and are
  imported by both design-audit files.

Mutation: killed the round's new survivors in _powershell_caps and the
_edit_params refusal msgid, plus the flip note's wording and its >0 threshold.
…fixes left seams

The re-review confirmed 15 findings, most of them narrow gaps at the edges of
round 5's own fixes. This round replaces the point patches with structure:

- is_secret_name matches TWO word sources per segment — the jammed segment
  (APIkey) AND its camelCase sub-words (awsSecretKey → AWS/SECRET/KEY). Rounds
  2 and 5 each kept only one source and regressed the other's cases. Count
  suppression is scoped by shape: names suppress on a count word anywhere
  (maxOutputTokens), sentence prompts only on 'count word, then token word' —
  'Enter your API token (max 64 chars):' is a credential ask and stays masked.
- Freshness has ONE owner: MenuApp._fresh() re-resolves the record for the
  detail render AND both launch paths, so the pane and the run it advertises
  describe the same generation (the pane was made fresher than the launch it
  fronted; description/deps also no longer mix generations with the plan).
  _cached_plan no longer resolves internally — it caches whatever generation
  its caller renders, validates the key with a second stat so a racing write
  can never pin stale content under a fresh key, caches snapshot fallbacks
  (a corrupt meta stopped causing a subprocess per cursor move), and keys on
  the reader-tool fingerprint so installing pwsh mid-session reaches the pane.
- Extra-tail provenance is judged by the FORM, not by diffing values against
  re-read state: RunFormScreen tracks a real dirt bit on the extra row (armed
  after mount, set by Input.Changed) against its own compose-time snapshot,
  and returns the verdict in FormResult. A concurrent CLI write no longer
  fakes an edit; a cleared-and-retyped identical tail now honestly counts as
  typing into the launch menu. One state read per interaction (was three).
- The as-is note prints on BOTH faces (r-rerun and the exit-after-run path,
  same msgid as the CLI), its predicate lives in flows.tail_looks_expandable
  and covers leading ~ (tokens.expand expands it), and the docs state honestly
  that deliberately-quoted CLI tails get the note too.
- The --add hint shell-quotes the entry name it tells the user to paste.

Tests follow in the next commit.
…oof, platform quoting

Six confirmed findings from the third re-review, the two severe ones both
credential-leak direction:

- is_secret_name judges per SEGMENT now (_judge_segment): a segment's own count
  words (fused nTokens, camel maxOutputTokens) veto its token hit, but ONLY a
  segment that is itself count-shaped is count context for its neighbors —
  camel fragments never leak out, so N8N's stray digit-boundary N no longer
  vetoes the TOKEN next door (digits also stay inside segments; _forms strips
  them per word so api_key2 still finds its KEY, and a pure number counts as a
  count: '60 tokens'). The sentence rule flips from suppress-if-ANY-mention-
  count-preceded to secret-if-ANY-mention-is-NOT — 'Paste your GitHub token
  (rate limit 60 tokens/min):' names a rate AND asks for a credential, and the
  credential wins.
- _cached_plan's caching now needs BOTH freshness proofs: the second stat
  (write after first stat) AND a meta re-read equality check (_meta_unchanged)
  for the other half of the window — a meta write between the caller's resolve
  and the first stat pinned a plan built from the old meta under the new key,
  which stat equality alone cannot see.
- The --add hint quotes with argv_text.join (shlex on POSIX, list2cmdline on
  Windows) — shlex.quote's single quotes are word-splitting noise to cmd.exe,
  on the platform the product explicitly targets.
- flows.tail_looks_expandable delegates token grammar to tokens.has_tokens
  (THE authority on what expand() changes) — the hand-rolled brace check
  missed }} halving and over-fired on bare { — and the as-is note msgid has
  ONE home, flows.as_is_note(), called by both faces.

Tests follow in the next commit.
…ds in front

The round-7 digit handling made any numeric segment count context for the whole
name, so GITHUB_TOKEN_2 / API_TOKEN_1 / slack-token-3 stopped being masked —
digit-INDEXED credentials are common and this was the publishing direction (the
test-repair pass caught it and stopped rather than pin the leak).

A count WORD (max, limit, n) still qualifies a name from anywhere; a bare NUMBER
now qualifies only the segment that FOLLOWS it: '60 tokens' and 2_tokens are
counts by construction, GITHUB_TOKEN_2 is a second GitHub token. In the name
branch, numbers suppress only when EVERY token mention is immediately preceded
by count context; the sentence branch was already positional and gains the
numeric predecessor ('rate limit 60 tokens/min' still suppresses that mention
while the credential ask beside it still wins).

Two documented cases re-ruled with the refactor, both deliberate: a bare-plural
'tokens' knob now reads as a count in ANY casing (toKens/TokenS — no compound
can hide inside six letters, unlike APIkey), and the synthetic jam
EnterYourAPIToken(max64chars) flips to masked (safe direction; every real count
spelling — max_64_tokens, max64Tokens, gpt4_max_tokens — still reads count).
Repairs the suite for 01fb12a + 2d2fc77 and adds the behavior coverage that keeps
each of their verified bugs dead.

is_secret_name (per-segment verdicts):
- The matrix grows the round's own families: the digit-boundary credentials that
  round 7 repaired (N8N_TOKEN, n8nToken, gpt4_token, api_key2, base64Key), the
  indexed credentials 7b repaired (GITHUB_TOKEN_2, API_TOKEN_1, slack-token-3),
  the sentence that names a rate AND asks for a credential (the GitHub
  rate-limit ask), and the counts that must stay public (max_2_tokens, 2_tokens,
  "60 tokens", MAX2TOKENS, max64Tokens).
- Two documented cases are RE-RULED with the reason in place: the bare-plural
  knob now reads as a count in ANY casing (toKens/TokenS — six letters cannot
  hide a compound the way APIkey does), and the synthetic jam
  EnterYourAPIToken(max64chars) flips to masked (the safe direction; every real
  count spelling still reads as a count).
- New structural tests for the new pieces: _forms' plural + digit folds, the
  single-qualifier slice in _token_form, a segment's four verdict bits, the
  jam-only county that stops N8N's camel fragment from vetoing the TOKEN next
  door, the forward-only numeric context, the name branch's ALL-mentions
  quantifier, and the sentence rule's exists-unsuppressed shape with a killing
  case for each of its three pieces.

Display-plan cache:
- The other half of the cache window: a snapshot older than the meta on disk is
  built from, and NOT cached under, the current key — the next render with the
  fresh record sees the declared parameter. Plus _meta_unchanged at its three
  answers (meta half, dir half, unresolvable-is-unchanged), which keeps round
  6's corrupt-meta single-build guarantee pinned beside it.

The as-is note and its predicate:
- flows.as_is_note() is the one home: tui._as_is_note is gone (asserted), and
  both faces are pinned against the exact sentence, not a prefix.
- The J matrix follows tail_looks_expandable's delegation to tokens.has_tokens —
  `}}` and `{{` now note, a bare `{x}` does not (it never expands) — with an
  oracle test that asks the real expander whether each piece would change, and a
  `}}` case on the CLI replay and the exit-after-run face.

--add hint quoting:
- The hint is asserted through argv_text.join, and a Windows-convention run
  (monkeypatched platform) pins the double-quoted spelling cmd.exe can paste;
  argv_text's own suite gains the join half of its platform pair.

Mutation: killed the round's new survivors, including the camel split's
separator (it cuts on the space its own sub inserts) and the two-framework flip
note, whose ", " separator no single-framework test could reach.
…ak slug

Nine distinct defects from the fourth re-review (the round-8 workflow's first
run died to a session limit and reported a hollow CLEAN — rerun properly, it
found five more leak-direction holes in the round-7 secrecy rework):

- is_secret_name v6: THREE word sources per segment (jam + camelCase sub-words
  + digit-split parts) feed secret/token MATCHING — base64key/sha256key/s3key
  mask again, N8NToken/N8NTOKEN mask via the un-shattered jam. COUNT CONTEXT
  got the opposite treatment, because every leak so far was a shard posing as
  a qualifier: county is the unstripped jam only (N26 no longer strips to a
  count N), the camel regex drops its digit→Upper boundary (no more stray N
  fragments), internal count words veto only in NAME shape (a prompt spelling
  perUserToken is still an ask for that value), and a bare NUMBER counts only
  a PLURAL mention right after it — '2 tokens' is a count, STEP_2_TOKEN and
  'Enter step 2 token:' are indexed credentials and stay masked.
- Paste-able command hints interpolate the SLUG, not the display name, at all
  seven sites (the --add hint, drift resync ×2, placeholder drift, flood cap,
  runner pin, normalize): no quoting convention survives every shell (shlex's
  single quotes are noise to cmd.exe, list2cmdline leaves & | ^ bare), but a
  slug's charset needs none anywhere and resolve() accepts it everywhere.
  drift_lines gains a target param so prose keeps the display name.
- tail_looks_expandable's glob half now reads _GLOB_CHARS, the same authority
  assemble's glob pass consults; _meta_unchanged delegates its degrade policy
  to _fresh — one owner for 'what counts as the current generation'.

Tests follow in the next commit.
The tests for 84f90a9, plus the one source line mutation testing proved was
unpinnable as written.

- is_secret_name v6, at each seam the round-8 rework introduced: the six-bit
  segment verdict (token_plural and internal_count split apart from county,
  which is what let a prompt spelling perUserToken stop reading as a count);
  county judged on the unstripped jam (N26 no longer strips to a count N);
  the camel regex's missing digit boundary (N8NToken); _digit_split as a
  MATCHING-only third source (base64key/sha256key/s3key/md5key/x509key/gpt4key);
  a bare number counting a plural and indexing a singular (2_tokens vs
  STEP_2_TOKEN); and the NAME/SENTENCE asymmetry of the internal veto.
- Every paste-able hint, one test per site, all registered under a name that
  needs quoting in every shell ("a & b"): the --add dead end, drift_lines'
  new target param (and its name fallback), the launch-menu drift banner, the
  prompt-body drift line, the flood cap, the no-runner refusal, the normalize
  hint, and the mid-run resync line. One of them lifts the printed command out
  of the output, resolves it, and runs it — a hint is only worth printing if it
  works.
- tail_looks_expandable's glob half parametrized over _GLOB_CHARS itself and
  cross-checked against glob_feedback, so the predicate and the pass that does
  the globbing cannot drift apart silently.
- _meta_unchanged proved to DELEGATE (patch _fresh, the answer follows; the
  store is not consulted behind its back) rather than merely to agree.

One source change, and it is a spelling: internal_count's shard guard read
`len(v) >= 2`, whose distinguishing input does not exist — N is the only
single-letter count word and there is no two-letter one, so `>= 2`, `> 2` and
`>= 3` are the same function and four mutants survived as equivalents. The rule
it is proxying is "a one-letter remnant of stripping never counts", so it now
says `!= 1`: same behaviour on the whole cumulative matrix, and both mutation
directions are killable (maxsTokens and tokenN8 are the names that separate
them).

Gate: 5624 passed, 100.00% coverage, ruff/ty/i18n clean. Targeted mutmut on the
round-8 surface (params.py whole module, analysis.drift_lines,
flows.tail_looks_expandable/_placeholder_body_plan, tui._meta_unchanged): zero
survivors.
…that vanished

Six defects from the ninth pass, all in territory the last four rounds
under-read (store/doctor/argstate, not params.py secrecy). Every one verified
against an isolated library before it was touched.

- A lost or corrupt registry.toml emptied the whole library in silence, and
  `skit doctor` — the command whose job is checking the library is intact —
  printed `✓ 0 entries registered` and exited 0 while both entries sat untouched
  on disk. `doctor --rebuild` recovers them instantly and was named nowhere.
  _fs_truth ALREADY cross-checks the index against disk so a lost registry can't
  let `add` overwrite a stored script; store.unindexed_slugs is the read side of
  that same check, and it now feeds healthcheck.collect — so doctor, the TUI
  Health screen, `doctor --json` (additive `unindexed`), and BOTH blank-library
  surfaces report it. Neither face asserts "no entries yet" without asking disk
  first; `list --json` keeps stdout to one array and carries the line on stderr.
  A promise the code makes to itself is not a promise to the user.

- `skit params X --secret NAME` on a template placeholder with no declared row
  was skipped with a warning, a green "Updated" line and exit 0 — and the value
  it was meant to protect then landed in the state file in plaintext (C3). The
  heuristic is RIGHT to miss `cookie`; the explicit override exists for exactly
  that case, and it was dropped by the codebase whose contract is refuse, never
  drop. `--add` already knew how to materialize the row (and reaches the
  plaintext scrub), so the rule now lives in edit_declared where both doors
  share one constructor: a placeholder the entry asks for IS an editable
  parameter. Fixing only --secret would have left the other seven flags silent.

- store.resolve trusted the index row's `name` with no freshness check while
  _summary_from_row deliberately verifies the same stamp — so a hand-edited meta
  name made `skit list` show an entry `skit run`/`show` called not-found, until
  some unrelated listing happened to heal the index. list_summaries' own
  docstring gives the reason ("the CLI would list entries the TUI, doctor and
  `run` all refuse"); `run` reaches the store through resolve, the one door that
  never checked. A NAME match is now verified against the meta resolve already
  reads a line later, and only the MISS path pays for the sweep.

- record_run(values=None) replaced the whole last_run table, so `skit run --raw`
  deleted the value snapshot its own call site promises to preserve — leaving
  exactly the shape `preset save --from-last` calls legacy state, which then
  refused with "no remembered values yet — run it once first" about an entry
  whose values were in the same file and which had just run twice. It now
  follows the convention save_last states one screen up.

- doctor prints the config and state roots two docs pages have always claimed it
  prints and nothing in skit exposed at all (`config_dir`/`state_dir` in --json,
  the same three lines on the Health screen): "what do I back up?" had no answer
  from the tool that exists to answer it.

- LangSpec.takes_argv is gone. No code read it, while three comments and
  docs/design/prompt.md credited it with the rule placeholder_params enforces —
  a trait no code consults is a story, not a contract. The design docs carry a
  dated correction rather than a quiet rewrite.

SKILL.md (+ packaged copy) teaches agents the `unindexed` key and the three
roots; cli.mdx/environment.mdx document them; troubleshooting.mdx gains the
recovery recipe. i18n back to 100% (zh_TW/zh_CN), docs site builds with the
link checker green.

Gate: 5662 passed, 100.00% coverage, ruff/ty clean.
…key that was never bound

Eight defects from the tenth pass. One of them was mine, shipped in round 9.

- `--no-input` was a cli.py LOCAL. It threaded through cli's own gates and stopped
  there, so the one interactive gate below it — uvman's uv-download consent —
  re-derived interactivity from sys.stdin.isatty(), an oracle the flag cannot
  reach, and `skit run x --no-input` on a machine without uv printed a question
  and blocked on input() forever. That is exactly what the bundled Agent Skill
  promises an agent cannot happen. Threading a quiet= keyword down the call chain
  would have fixed today's one gate and left the next one to repeat it, so the
  verdict now lives in `interaction`: set once at the front door, readable at any
  depth, with no parameter to forget. Under a refusal the gate takes the path a
  pipe already takes (A9), because --no-input is an assertion of exactly that.

- The panic pane told a user whose index had just vanished to "open Health (h)".
  There is no h binding — it is D — and round 9 wrote that line, on the one screen
  where the reader has a single instruction and no patience. The glyph now has ONE
  spelling (tui.HEALTH_KEY) behind the binding, the chip, the help overlay and the
  line, and the line is a CHIP rather than prose: correct by construction, and
  clickable, which prose naming a key never was.

- …and the status line was the THIRD blank-library surface. #detail is display:none
  at -h-short/-h-tiny while the status line is documented as the channel that
  survives every tier, so on a small terminal it was the only blank-state copy on
  screen — still asserting a first run over an intact library. One question
  (_lost_index_count), asked by all three.

- Entry settings forked on `kind == "prompt"` where AGENTS.md says to key off
  placeholder_params — a level worse than the `family` spelling the rule forbids.
  A command entry opened a section headed "Parameters (the run form's fields)"
  showing none of them: to type {width} you had to retype its name from memory,
  with no list in front of you.

- One store.NotFoundError had two exit codes: 127 from `run`, 1 from nine other
  commands, against a docs table publishing 127 CLI-wide and a SKILL.md telling
  agents to trust exit codes over output text. 1 is inside the band reserved for
  the launched script, so the two cases were indistinguishable by any means the
  agent was allowed to use. One helper now answers for all ten.

- --forget-args cleared the remembered tail ABOVE four gates that can still refuse,
  so `run x --forget-args --set typo=1` destroyed the tail and then exited 2 — with
  the invariant written in the comment beside it. Deferred into _on_accepted (the
  home --save-preset already used), and the reuse is suppressed too: "forget it"
  that replays the tail one last time and writes it straight back forgets nothing.

- The mirror radios rendered on/off/custom untranslated in the one section whose
  audience is Chinese-speaking users — and the i18n gate reported "every scanned UI
  sink routes through gettext", because the literals sat one hop away in a module
  constant and again behind a loop variable. The labels are now a vocabulary of
  their own (the stored token stays English, as the CLI requires), and the gate
  resolves both hops and stops exempting lowercase identifiers inside LABEL sinks,
  where no CSS class or slug can appear. Verified by reverting the fix: the gate
  fails. Its remaining limit (a loop name reused by several rows names one of them)
  is documented rather than claimed away.

- Unticking a preset destroyed unrecoverable user data with no ask — the fifth
  destructive door and the only one without one, on the surface where an untick
  plus an unrelated Ctrl+S is easiest to trip.

Round 9's mutation survivors are closed with it: resolve's index-row fast path is
now pinned by a test that proves the sweep is NOT taken on a hit (an optimization
no behaviour can observe is otherwise unkillable), and both corrupt-meta refusals
are pinned to name what the user typed. store.unindexed_slugs keeps one known
survivor — "META.TOML" is equivalent on case-insensitive macOS; Linux CI kills it.

Gate: 5701 passed, 100.00% coverage, ruff/ty/i18n clean, docs site + link checker
green.
…d that killed the app

Six defects from the eleventh pass. Two of them re-opened work from the two
rounds before it, which is where this loop keeps earning its keep.

- `skit edit` was the ONE editor door with no interactivity gate — and the door
  the bundled Agent Skill teaches. Two of the four lanes refused on their own
  ("an editor session IS interaction"), two did not. In a pipe it spawned $EDITOR
  against a stdin nobody was typing into: `vi` hung forever, `cat` dumped the
  file into the caller's stdout, and skit then printed "Saved" about an edit that
  could not have happened. This is round 10's defect one layer up, so it gets
  round 10's answer: the gate lives in editor.open_in_editor, where all four
  lanes pass, and reads `interaction` rather than a local isatty pair.

- Ctrl+L on Entry settings read #st-interpolate — composed only for prompts —
  BEFORE its pure-Python guard, so on every python/shell/js/exe/command entry it
  raised NoMatches out of the action handler and took the whole workbench down,
  losing every unsaved edit on the screen. Ctrl+L is the terminal's universal
  clear-screen reflex; it gets pressed by people who meant nothing by it. Now one
  predicate (_can_choose_candidates) drives the chip AND check_action, so the
  chord is disabled where it cannot work instead of merely inert — the rule the
  Ctrl+R chip beside it already stated in prose — and both prompt-only actions
  stay total for callers that reach them directly.

- The preset-delete confirmation round 10 added was a BOOLEAN, set on confirm and
  never cleared. Confirm one deletion, abort the save on an unrelated validation
  error, untick a second preset, save again: the latch was still standing and the
  second preset was deleted with no question ever naming it. It now tracks the
  NAMES already agreed to, which is correct under retick/untick churn where a
  reset-on-abort boolean still is not.

- Two interactivity oracles disagreed about the same terminal in both directions,
  and the module introduced last round to end exactly this created the second one
  by not being adopted where the prompts live. `skit run x > out`: cli declined to
  prompt while uvman blocked on one. `skit run x 2> log`: cli opened a form while
  uvman silently downloaded and executed a network binary with no consent at all.
  interaction.allowed() now takes the stream it is answering about (stdout for
  cli.py's Prompt.ask, stderr for uvman's consent) and cli._is_interactive
  delegates to it — so a refusal can never apply to half of skit.

- `skit remove` repeated "your original file will not be deleted" for a copy-mode
  entry whose original the user had already deleted — trusting that very promise.
  The TUI modal already withheld the line. launcher.original_survives is now the
  one predicate behind both faces, and the third case says what is actually true:
  skit holds the only copy.

- Two honesty fixes: the --plain form now names the spelling it cannot offer for a
  cleared delivers-empty field (`--set NAME=`; no '-' sentinel, because on a
  free-text or path field '-' is very often a real value), and a kind whose
  language parser failed to import (the A2 degradation) says so instead of telling
  a shell user that "programs have no managed parameters".

Also: interaction.reset()'s docstring named a caller that does not exist. It is a
test seam and now says so — the same story-not-contract line LangSpec.takes_argv
was deleted for two rounds ago.

Round 10's mutation sweep came back clean apart from one equivalent mutant in
reset() (`= None` and `= False` are both falsy at the single read), documented in
place rather than worked around.

Gate: 5718 passed, 100.00% coverage, ruff/ty/i18n clean, docs site + link checker
green.
Seven defects from the twelfth pass. This round's fixes were PLANNED and the plan
REVIEWED before any code was written — the review changed four of the seven, and
one of its catches would have been a fresh instance of the exact defect class this
loop keeps producing (adding a check_action to a screen whose Enter is not a
binding: a branch that cannot fire).

- `skit params` absorbed every invalid edit. A rejected `--type` printed a stderr
  line, wrote everything else, exited 0, and then reported the state it had NOT
  written through `--json` — both of the channels SKILL.md tells agents to trust
  said success. This is round 9's `--secret` finding at the level round 9 stated
  it: that round fixed the one path where the drop leaked a credential and left
  eight sharing the shape. `params` now refuses ATOMICALLY (exit 2, nothing
  written), the answer `--set`/`--dep`/`--python`/`--preset`/`config` already give
  and the validate-then-write rule the TUI's own save has always applied.

  Three things had to move before that was even expressible. `not-declared` and
  `not-managed` each covered an idempotent op AND a refused one under one string
  (`--rm GHOST` vs `--type GHOST=int`), so they are split at emission and render
  the same sentence. `_apply_env_sources` was the third warning producer and the
  only one returning finished prose, so nothing could classify it — which left
  `--env-source` warning-and-continuing on the analyzer lane while the same flag
  refused on an exe entry. And the prompt managed-list write sat ABOVE the
  decision point, so a refused invocation would unmanage a name and only then
  refuse: compute → decide → write, now.

- Round 11 unified the removal PREDICATE and left the ANSWER forked: "skit holds
  the only copy" existed on the CLI, which makes you type a name, and not in the
  Library, where Delete acts on whatever row the cursor is on. launcher.
  removal_stake returns the VERDICT and each face writes its own whole sentences
  (composing `question + " " + note` would have broken the one-msgid rule cli.py
  states 4000 lines up).

- `skit edit` was outside the CLI contract: exit 1 for not-found where the other
  ten entry-name commands exit 127, and no `--no-input`, so under a pty with
  nobody typing round 11's editor gate was a no-op and it hung. It never raises
  NotFoundError — it offers to create instead — which is why round 10's sweep
  could not see it. Now: 127 for a missing name or a gone target, 2 for a kind
  with no source and for the interactivity refusal, which moved to the front door
  (down in editor.py it shares an exception class with "could not launch", so the
  two could only ever get one code) and names the file to edit directly.

- …and that gate made `_reconcile_prompt_after_edit`'s non-interactive branch
  unreachable — it had been so since round 11, covered only by tests that patched
  interactivity AFTER the gate had passed. Deleted: coverage cannot tell an
  unreachable branch from a covered one.

- The TUI collapsed three edit refusals into one sentence that denied the source
  existed and misclassified the kind — a reference-mode Python entry whose file
  had moved was told "programs and command templates run as-is". One shared plan
  (launcher.plan_edit) with a reason id per case, and the Library's `e` chip is
  now conditional on the same predicate, because a dead chip is what sent the user
  there.

- Stored enum tokens shipped raw English inside translated output: `(copy 模式)`,
  `工作目錄:store`, and `(Python · copy)` translating the kind but not the mode
  inside one parenthesis. No static gate can catch it — the literal is in the
  user's meta.toml, not the source. kindnames now owns value labels for all three
  axes; the stored token stays English, and a user-typed absolute workdir passes
  through unrelabelled.

- Declining a destructive confirm died as click's untranslated red `Aborted.` at
  exit 1 — the correct, deliberate answer reading as an error, in the band the
  docs reserve for the launched script. Now a translated line at 130, like the
  add lanes, and it catches Ctrl+D too (click raises Abort on EOF regardless of
  abort=True, so handling only the typed "n" would have been half a fix).

- Round 11's chip↔check_action rule was applied to one action on one screen. The
  Health screen's FIRST chip was dead whenever the library was healthy; resync's
  condition existed in two spellings; and the add review had two different
  predicates for one rule, so Ctrl+L opened a picker no chip advertised.

Deferred, recorded rather than rationalized: kept drafts are listable, resumable
and deletable only from the TUI. That is a principle-4 gap ("every TUI capability
is also a CLI command"), not a non-defect.

Gate: 5753 passed, 100.00% coverage, ruff/ty/i18n clean, docs site + link checker
green.
t41372 and others added 8 commits August 3, 2026 16:13
Path.write_text without newline="" lets Windows translate the body to CRLF,
and plan.text — byte-preserving by contract — then faithfully reports bytes the
test did not mean to write. The fixture now writes the exact bytes it asserts;
the production byte-preservation contract is untouched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mn62wufznYk79v5gezAqxa
The miss-path sweep (round 9) served hand-edited metas by returning the FIRST
summary carrying the requested name — so two metas hand-edited to one name made
'skit run deploy' execute whichever slug sorted lowest. The pre-sweep code
refused that state; the sweep now does too, with the remedy in the message:
AmbiguousNameError (a StoreUsageError, exit 2) listing every claimant slug.
A lone sweep hit is also re-verified against the meta it names, the same
freshness proof the registry fast path already pays.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mn62wufznYk79v5gezAqxa
argstate writers let raw OSError escape: a read-only state dir turned
'skit preset delete' into a traceback with Click's generic exit 1, and took the
whole TUI down from any screen that touched state. Every writer now re-raises
as StateWriteError — an OSError subclass mirroring config.ConfigWriteError, so
flows.post_run_persistence_error keeps degrading run-lane failures unchanged —
the root boundary maps it to the operational exit alongside ConfigWriteError,
and the TUI notifies: an error that keeps the screen where the work is undone,
a warning that continues where the work already happened (a saved runner pick
must not veto an accepted run or strand a completed add).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mn62wufznYk79v5gezAqxa
The declared lane committed write_prompt_managed and write_parameters as two
independent metadata transactions (two locks, two atomic replacements), so a
failure between them left the managed list new and the rows old — a form asking
for a field whose type, default and secrecy hadn't landed. write_parameters now
takes the managed list along (None = untouched, [] = cleared, prompt-only rule
preserved) and commits both halves in one locked _write_meta_and_row; the CLI's
declared lane and the settings screen's save both ride it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mn62wufznYk79v5gezAqxa
Both param-edit lanes committed the secret schema first and scrubbed the old
plaintext second, so a failure between the two writes left the one state the
transition exists to forbid: schema says secret, plaintext still in the state
file. Purge now runs first in all four sites (CLI spec + declared lanes,
settings screen twins): every interruption lands on public+value,
public+no-value or secret+no-value, and a failed purge aborts the save with
the schema still public — typed, notified, retryable.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mn62wufznYk79v5gezAqxa
record_run's values=None branch re-persisted the preserved last_run snapshot
verbatim, ignoring secret_names outright — the one write entry point that broke
argstate's 'every write strips' contract line — and the raw lane passed nothing
anyway, so a parameter flipped secret out of band (a $EDITOR block edit, a
purge that died mid-transition) kept its old plaintext through every later
'run --raw'. The preserved snapshot now takes the same strip as new values, and
the raw lane feeds it flows.stored_secret_names(entry): the stored schema's own
secrecy, read grammar-free (declared rows + comment-block flags, no analyzer —
the A2 rule keeps launch paths stdlib-only), applied via save_after_raw_run,
the purge+stamp twin of save_after_run that still rewrites no form memory.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mn62wufznYk79v5gezAqxa
Four meta-tests added on this branch (the blind-spot ratchet, the exit-route
whitelist, the no-input-verdict-first walk, the editor gate-placement check)
read skit's source via module __file__ or a test-relative path. Under mutmut's
own baseline the suite runs inside mutants/, where every undecorated function
is a trampoline rewrite — so the ratchet tripped on mutmut's machinery and the
whitelist on x_-variants of _fail, killing the nightly mutation run's stats
phase before a single mutant ran. All four now resolve through
conftest.real_repo_root (round 10's strip-the-mutants-prefix idiom, shared):
the real tree is the subject, in either context.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mn62wufznYk79v5gezAqxa
Targeted mutmut over the changed modules (argstate, store.resolve,
store.write_parameters, flows' raw-lane helpers), with every reported survivor
re-verified by running the relevant suites against the live mutant. Real gaps
got killing tests: the ambiguous-name refusal and the non-prompt managed guard
pin their whole sentence; stored_secret_names pins source-union and non-UTF-8
tolerance; record_run's own strip is proven with the purge removed; purge's
removed-set accumulates across surfaces. Equivalent mutants got the recorded
treatments: the read_text kwargs take cli.py:502's pragma rule, and the
guarded last_run access drops its dead .get default for a subscript.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mn62wufznYk79v5gezAqxa
Three paths the round-13 fixes left open, from the second external review:

- argstate.forget joins the typed-writer contract it was left out of: same
  values lock as every mutator (an unlocked unlink raced a concurrent RMW),
  same StateWriteError. store.remove answers its failure the way the rmtree
  branch always has — entry removed, honest partial-success message naming the
  surviving file — instead of a raw traceback out of a decorated command body.

- Remembering a runner pick is incidental prefill state, and the CLI now holds
  the same line the TUI already does: all three save_last_runner sites go
  through one best-effort helper that warns the pick wasn't remembered and
  keeps going — a read-only state dir no longer vetoes the add or run the pick
  rides on with an operational exit.

- save_after_raw_run reads the meta FRESH at stamp time and strips with the
  union of launch-time and stamp-time secret sets: a parameter flipped secret
  during a long raw run is scrubbed, one flipped public cannot be talked out
  of the scrub by the race, and an entry removed mid-run gets no posthumous
  state file.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mn62wufznYk79v5gezAqxa
t41372 and others added 5 commits August 4, 2026 14:00
The normal run lane trusted a launch-time slug and secret set across a
run that can last hours: a param flipped to secret mid-run persisted in
plaintext, and a slug freed by remove() and reissued to a later add
received the dead run's state (round 13 closed exactly this for --raw
only). One universal mechanism instead of a second local patch:

- ScriptMeta.id — per-add identity (uuid4 hex), minted at the store's
  one meta-write door; edits preserve it, legacy metas heal on their
  next write, reads stay reads, a mistyped id is meta corruption.
- flows.persistence_target — re-resolve + identity compare; every
  post-acceptance state write passes through it: save_after_run (takes
  the Entry now), save_after_raw_run, new save_preset_for
  (run --save-preset + the form's Ctrl+S, refusal reported, never
  silent), new clear_remembered_tail (--forget-args, vacuous when the
  entry is gone). Strip set = launch ∪ both stored readings.
- A census test pins every remaining direct argstate writer call site;
  the surviving same-class window (settings screens) is #39.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mn62wufznYk79v5gezAqxa
…try lock

The identity-guard review's two P1s, plus #39 closed instead of deferred:

- The legacy-id wildcard authorized writes it could not verify: an
  unstamped handle matched any stamped entry, so an upgrade user's long
  run could still land state on a reincarnated slug. persistence_target
  now requires EXACT id equality — unknown identity may serve reads,
  never authorize a write — and every lane that holds an Entry across
  user-paced time claims it at hold-start (store.ensure_identity): CLI
  run, the TUI run/rerun/settings lanes and the reconcile picker. A
  pre-id meta is stamped before the hold begins; a stamped meta is never
  rewritten; a read-only library degrades honestly ("" == "" holds
  exactly where nothing can diverge).
- persistence_target was check-then-act. Every flows door now holds the
  ENTRY LOCK — the lock every meta mutator and remove() already hold —
  across verify + strip-set read + writes, and the secret-transition
  commits became locked store transactions with the C3 scrub inside
  (write_parameters, new write_source_params), collapsing all four
  CLI/TUI transition lanes onto them. Declined as designed: merging the
  door's three per-slug writes into one RMW — under the lock there is no
  interleave window, each write is atomic, every crash intermediate is
  C3-consistent.
- #39: store mutators take expected_id (StaleEntryError on mismatch,
  checked under the lock); the settings screen authorizes every axis of
  its save against the identity it was OPENED on, its preset cleanup
  goes through the guarded flows.delete_presets_for door, and a mid-save
  refusal keeps the screen instead of crashing the app.

Mutation-verified: every mutant on changed lines killed (door lock
probes, the per-axis stale matrix, exact refusal copy, the lock-path
contract); three documented selector equivalents pragma'd; the only
remaining targeted survivors are pre-existing mutants on untouched
validation-pass lines — the #38-recorded baseline.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mn62wufznYk79v5gezAqxa
The doors-outside-the-doors review: round 15 built the locked,
identity-authorized transactions, then mis-filed several CLI writers as
"immediate" that genuinely wait on a human or derive their write from an
earlier read. All four findings closed:

- preset save waited on an interactive intake and then wrote argstate
  bare — inside a secret transition's own window it could re-persist
  plaintext the scrub had just removed. It now claims identity at
  hold-start and commits through flows.save_preset_for (entry lock,
  exact-id re-proof, launch-union-current strip); preset delete claims
  before its confirmation ask and deletes through
  flows.delete_presets_for. The census now allows NO direct argstate
  writer outside flows' doors and store's two locked scrubs.
- Every params/deps edit lane claims identity before the read its write
  depends on and passes expected_id to every store call — "usually
  fast" is not an authorization. --normalize moved into the new
  store.rewrite_source: read, transform and byte-disciplined write all
  under the entry lock with the identity check first, re-derived from
  the fresh text.
- run claims identity only after its static refusals: a refused
  invocation leaves no fingerprints, the id stamp included.
- expected_id="" is a real expectation, not an off-switch: the
  `id or None` idiom is gone, an unstamped handle refuses a stamped
  stranger, and ensure_identity re-reads after a failed stamp so a
  half-landed id can never strand a handle behind its own entry.
  Declined with a proof: skipping "" == "" persistence — a reincarnated
  slug always meets the guard stamped (_add_entry cannot write a meta
  without an id), so the symmetric case proves no reincarnation
  happened.

The stale-refusal copy is face-neutral now ("changed while this edit
was underway"), and the preset commit logic lives in undecorated
helpers — mutation-visible, and the blindspot ratchet stays under
budget. Targeted mutation runs: every mutant on changed lines killed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mn62wufznYk79v5gezAqxa
The claim-by-address review: round 16's claim re-resolved a slug and so
adopted whoever owned it, blessing the very reincarnation the guards
exist to refuse — and the two longest, most destructive user-paced
lanes (remove's confirmation ask, the external editor session) had no
identity model at all. All four findings closed:

- store.claim_identity(entry) replaces ensure_identity(slug):
  compare-and-claim verifies UNDER THE ENTRY LOCK that the disk still
  holds the entry the caller resolved (exact id; an unstamped handle
  accepts only an unstamped disk), stamps a pre-id meta there, and
  refuses a changed owner (StaleEntryError). Every lane passes its
  held Entry; the TUI's ghost rows stop their lane and refresh the
  list with the reason. The races are tested REAL: reincarnations
  injected between a lane's resolve and its claim, the claim unpatched.
- store.remove takes expected_id, checked under the lock: both faces
  authorize the deletion against the entry the confirmation ask NAMED —
  a slug reissued while the user answered refuses instead of deleting
  the new owner's registry row, copy, meta and state.
- Copy-mode editor sessions are STAGED: the editor works on a unique
  draft in skit's drafts dir (never the skit- prefix, never the stored
  path), and the save lands through store.commit_copy_edit's
  identity-checked, mode-preserving transaction. A stale landing keeps
  the draft and names it in the refusal; an invalid prompt edit now
  never touches the stored copy at all (strictly better than the old
  in-place refusal). Reference mode stays direct - it edits the user's
  own file, which no reincarnation moves.
- The "" == "" safety proof is withdrawn: an OLDER skit's adds write no
  id, so a symmetric blank can be a reincarnation this version never
  saw (the reviewer's cross-version counterexample). Unknown identity
  persists nothing, and the `id or None` hole this claim closed stays
  closed: persistence requires a STAMPED exact match.

Mutation-verified: all in-scope mutants killed (three reported
survivors proven killed by test_prompt_utf8 against the live mutants —
the #38 stale-selection artifact); the action_remove sentinel
equivalence was refactored away rather than pragma'd.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mn62wufznYk79v5gezAqxa
Windows folds permission bits and write_text writes CRLF: the round-17
mode assertions now capture what the platform actually applied and
assert THAT survives the commit (test_atomic's idiom), and the
prompt-edit refusal tests compare the stored copy against its own
pristine bytes instead of an LF literal. Production behavior untouched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mn62wufznYk79v5gezAqxa
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant