Rounds 1–10 record the preceding pass; this pass's additional twenty rounds are numbered 11–30.
- Prefix validation — unsafe empty, relative, control-bearing, and lexical parent-traversing paths are rejected without excluding Unicode or spaces.
- Binary-directory validation — install/uninstall enforce the same rules and reject an explicit empty override.
- DESTDIR confinement — staging concatenation cannot escape through a
lexical
..component. - Explicit root staging —
DESTDIR=/keeps staged cache and diagnostic behavior after the stored prefix is normalized. - Dependency preflight — install, temp, rename, cleanup, and desktop tools are checked before the build or first mutation.
- Atomic binary replacement — a same-directory mode-
0755temporary is renamed over the target without following a destination symlink. - Pre-commit cleanup — EXIT cleanup tracks the sole live temporary and preserves the old executable only until rename commits the binary; later resource failures do not imply rollback. Superseded by round 83's complete staging plan and executable-last publication.
- Atomic desktop replacement — an unpredictable same-directory temporary
removes the predictable
.newstaging race. - Remote-host semantic gate — one application gate combines spoofing and byte-budget checks with shared argv/session/deploy/path validation, and is re-run by the picker, connection launcher, and remote filesystem; app text checks precede any shared diagnostic that could quote a draft value.
- Non-destructive resource bound — incomplete/invalid drafts and entries after the first 128 keep round-tripping for repair; runtime surfaces mark them unavailable and Settings refuses Add at the active limit.
- Install-source preflight — every desktop/metadata/icon source is checked as a readable non-link regular file before build or mutation.
- Non-empty prebuilt contract — zero-byte descriptor input is rejected while the prior executable remains intact.
- Scoped staging ancestry — normalized non-root packaging roots are
checked from
/through every existing component, rejecting disguised symlink roots before install/uninstall while retaining host-prefix compatibility; this is not a concurrent-mutation guarantee. - Atomic metadata/SVG install — explicit mode is applied to sibling temps before atomic rename.
- Atomic raster icons — both PNG resolutions use the same commit boundary.
- Desktop structure validation — exact TryExec and canonical Exec counts, plus rejection of alternate commands, precede publication.
- Unset-PATH resilience — nounset no longer breaks successful install diagnostics.
- Hostile-path regression suite — contract tests exercise empty artifacts, staging ancestor links, and public destination links.
- Index-neutral private errors — internal remote helpers no longer label every invalid reference as profile #1.
- Bounded picker rendering — 256 rows cap UI work while keeping entry 129 visible and all omitted drafts stored.
- Runnable-only keyboard navigation — initial selection and arrows skip invalid and inactive profiles; Enter still revalidates.
- Bounded settings editor — rendering stops at 256 with an explicit retained-off-view count and no vector truncation.
- Active selector boundary — remote file-tree and tab-menu choices stop at the first 128 executable profiles.
- Safe picker metadata — deploy/name text passes bounded inline display before entering iced widgets.
- Actionable save summary — explicit Save distinguishes invalid active drafts from over-limit retained drafts.
- Shared problem accounting — one helper drives those counts and their regression assertions.
- Actual atomic disk round trip — save/reload preserves an invalid draft and entry 129 exactly.
- Neutral fallback regression — empty runtime identity is displayed as “remote host”, never a fabricated index.
- Pre-spawn consumer evidence — remote-fs tests prove unknown, invalid, and high-index profiles fail before process creation, while gate tests prove oversized/RLO deploy drafts are rejected without raw-value echo.
- Documented bounded-draft contract — README records render limits, navigation behavior, save diagnostics, and atomic public assets.
Block Mode convergence continues with rounds 31–33:
- Frost range-safe newest edge — Frost's newer step at the end of a multi-selection contracts to the active newest block before a later step exits selection.
- Explicit selection exit — the eventual key-owned clear produces the same feedback as Ember instead of making the highlight disappear silently.
- Current shared security pin — the exact
jterm_corerevision advances to0f47569, adopting AI origin/credential/no-proxy validation without changing the four-way completed-block or lifecycle-health contracts.
AI chat-library convergence adds rounds 34–46:
- Guarded quit-path write —
persist()ran unconditionally on quit, so a session that never opened the AI panel replaced the saved chat library with the store's empty default;can_persist()now requires that this run actually read the file it is about to replace. - Failed restore blocks writing — an unreadable or undecodable library (truncated, over the read bound, a future sibling's schema version) is left byte-identical for the rest of the run instead of being overwritten by a fresh empty one; the panel stays usable and says why.
- Single-instance write ownership — only the window holding the instance lock republishes the shared file; a second window restores it, uses it, and states in its notice that chats started there are not saved.
- Shared chat state machine — frost's private multi-chat store collapses
to a 35-line shim over
jterm_core::ai::chat_store, the union of the four terminals' drifted copies (1,888 lines, 47 tests). - Explicit busy policy —
BusyChatPolicy::Refuseis pinned at every construction site, because frost's panel has no cancel-then-mutate step; archive/delete on a chat with a request in flight refuse with a stated reason rather than inheriting a silent default. - Compaction before serialisation —
snapshot_for_persistencecompacts live history first, so a grown library can no longer reach a size the shared schema refuses outright and leave nothing saveable at all. - Truncation markers sync back — both compaction passes run on a clone, and what they dropped is carried into the live library so its rows admit it.
- Detaching retry materialisation — retry payloads merge into the throwaway persistence clone, so saving cannot disturb the live composer.
- Pane-scoped suggestion card — the AI command card renders, owns Escape, and inserts only for the pane that asked for it; an off-pane insert is refused instead of clearing and retyping an off-screen prompt.
- Window-wide suggestion generations — the request id is app-level and strictly increasing rather than restarting at 1 per card, so a superseded worker's command can no longer publish onto its successor; exhaustion refuses the request instead of wrapping onto a live id.
- Card teardown follows the pane — closing a pane drops its card, whose
Dropcancels the drafting worker. - Panel keyboard ownership — while the chats panel is open, keys its focused inputs did not capture are swallowed rather than reaching the shell behind it; its own chord and Escape close it.
- Canonical id, chord, and history budgets —
ai_chat:toggle(singular, matchingagent:toggle) onctrl+shift+alt+a, with the palette hint asserted equal to the core's rendering of the default binding; the shared command-history index's command bound is read fromreview_input::MAX_REVIEW_INPUT_BYTESinstead of re-declared, and its cwd bound rises from 4 KiB to the core writer's 16 KiB.
Verification: bash scripts/test-install-paths.sh, Frost config tests, and the
full formatting/check/Clippy/test gates.
Verification for rounds 34–46: cargo fmt --all -- --check, cargo clippy --locked --all-targets --all-features -- -D warnings, and cargo test
(1,017 passing, zero failures). The temporary local [patch] those rounds were
developed under is gone: the gate was rerun with --locked against the
published jterm_core 1a04f1e and jagent f9383ec. The working tree still
carries the uncommitted AI panel itself.
Shared review-first command correction adds rounds 47–58. Rounds 34–46 used this numbered list for the chat-store migration, so it continues here rather than restarting:
- Shared correction engine —
src/command_correction.rscollapses from 1,552 lines to a 424-line shim overjterm_core::command_correction(229 insertions against 1,357 deletions), the union of the four terminals' drifted copies (3,937 lines with tests). The pinned core revision advances tobadcce222fb5471a6afbfc5d5e898e2bc3faf632. Three things stay in frost: the policy, the per-pane request registry, and the iced card. - Consent gate on the correction payload — the failed command, the cwd and
up to 8 KiB of captured output are the largest payload any frost AI surface
sends, and this was the one surface that never consulted
ai_share_command_context. It now reaches the engine asContextSharing, built per request because the value is live config. With the switch off — the default — the AI fallback goes silent and only locally verified corrections (target output, APT index, executable PATH) are offered. - Pipe-to-interpreter refusal — the gate's
syntax_markerssuperset test only asks whether a marker is present, so against an original that already contains a pipe, appending| shadded no new marker and passed. frost had no check at all; the shared rule splits the pipeline quote-aware and compares the set of interpreters its stages run, pinned againstjagent::safety::is_interpreter. - Stated evidence and helper policy —
LocalEvidence::SameNamespace(frost owns its PTYs: no sandbox, no bridge, so this process'sPATHis the namespace the failed command resolved against) andHelperStrategy::FixedCandidates(frost's existing closed candidate list, the copy that got helper trust right, preserved and now stated) are positional arguments with noDefault, following round 38'sBusyChatPolicyprecedent. - Sanitised card strings — the card renders only
display_title,display_badge,display_descriptionandfeedback. The provider'smessageused to be interpolated raw into a label directly above an editable, pre-filled, auto-focused command field, so a reply carrying U+202E could reverse the rendered order of the text beside it. - Destructive-risk label — the
⚠ destructive: {reason}line the Agent approval card already carried, recomputed against the live draft.is_dangerousnever gated whether a candidate is offered, sorm -rf ~/workreached this card in exactly the chromegit statusgot. - 16 KiB classification bound — a failed command line longer than this surface's own declared budget is no longer classified, ranked, probed or prompted about.
- One validated draft behind label and action —
run_allowedandacceptnow answer about the same validated string. They used to disagree, one comparing the raw field text and the other the trimmed one, so a verified proposal differing only by surrounding whitespace was downgraded to "Insert for review". - Bounded inline feedback — one line, 200 characters, sanitised on the way in. This is the card's only remaining channel for text the engine did not author, one line above the command field.
- Absolute-only PATH walk — the candidate-name fallback ignores relative
and empty
PATHentries, so opening a project that sits on a relativePATHelement cannot contribute its filenames as correction candidates. - One trigger decision —
should_start(enabled, CompletionFacts { .. })owns the toggles, the missing exit status, the output sample and the narrow classifier; frost supplies onlyagent_issuedandtrusted_completionas named fields. frost's ownenabledgate is still answered first, before the facts are built, becauseCompletionFactstakes the block output by value. - Documented user-visible cost — README's AI configuration block states
that the correction fallback is now gated on
ai_share_command_context, and a new section lists what stops being offered and what keeps working.
Verification for rounds 47–58: cargo fmt --all -- --check, cargo clippy --locked --all-targets --all-features -- -D warnings, and cargo test --locked --all-targets --all-features --no-fail-fast (1,006 passing, zero failures),
all against the published jterm_core badcce2 with no local [patch].
Fifteen of the twenty-three tests in src/command_correction.rs went with the
engine; the eight that remain are the registry's four, the card's accept-path
wiring, and three new ones pinning frost's evidence/helper policy, the consent
switch in both positions, and the trigger's two frost-supplied refusals. Both
policy tests were mutation-checked here: FixedCandidates → TrustedPathScan
and an inverted consent mapping each turn the suite red. The probe-thread-name
assertion does not — it compares the policy's Debug output against the same
constant it renders, so it proves the constant is plumbed through but cannot
detect a rename; CorrectionPolicy::probe_thread_name has no accessor upstream.
The shared TOML/YAML workflow library adds rounds 59–69. Rounds 47–58 used this numbered list for the command-correction migration, so it continues here rather than restarting:
- Shared workflow subsystem —
src/workflows.rscollapses from 827 lines to 210 (153 insertions against 770 deletions) andsrc/workflow_picker.rsgives up its own fuzzy list and its own value bookkeeping (136/116), overjterm_core::workflows(five files, 3,186 lines, 73 tests). The pinned core revision advances to790d06ab19b9f3dec7c188728fc468f008df5414. Four things stay in frost: the search-path policy, the load order, the iced overlay, and the keyboard routing inmain.rs. - The missing-value guard, reachable at last —
render()refuses a declared argument with no default and no value, and frost implemented and unit-tested that. The form seeded every declared argument with""before the user saw it, so it never fired:kill -9 {pid}with an untouched Pid field renderedkill -9and was typed at the prompt. The contract is now stated once — an empty value is meaningful only if the file says so — andrenderclaims the undefaulted, blank-or-absent arguments into its missing set before building the binding list, so a caller that pre-seeds cannot seed past it. - Unset and supplied kept apart in the type system —
WorkflowArgsStatewrapsArgsForminstead of aVec<String>that cannot represent the difference. Emptying a defaulted field stays a deliberate empty value (deploy --env=stagingstill renders); emptying an undefaulted one is a missing value. Whitespace-only counts as unfilled. - The refusal is visible before it happens — outstanding rows carry
(required)in their label, so Insert'smissing values:error is never the first the user hears of it. - Nested brace pairing — an unterminated
{{no longer claims a later placeholder's}}.awk '{{print $1}' {{log}} | sort -uused to renderawk '{print $1}' access.log | sort -u, a different and executable awk program, while the same leading bytes with nothing after them round-tripped unchanged. - Declared names held to the placeholder spelling — placeholder names are
trimmed, so a padded declared name (
name = "pid ") could load clean, validate clean and match nothing while the user's typed value was dropped on the way to the prompt. Both sides of that lookup are now trimmed, and a padded declared name is rejected at load. - Both halves of a skip log sanitised — frost logged
workflows: skipping {path}: {err}with the path raw and the parser error raw, andtoml::from_strquotes the offending source line back verbatim, so a file whose unterminated string carried an OSC sequence wrote it onto a warn line for whatever tty was tailing the log. Both crossreview_input::safe_inline_display, bounded. - One query boundary for the picker — the three
selected = 0resets and the ad-hoc printable-character filter inmain.rsare replaced byset_query/push_query_text/backspace, so programmatic and accessibility input cross the same one-line andMAX_PICKER_QUERY_BYTESbounds as typing. frost's policy is stated asPickerPolicy::new(15, false)— fuzzy, fifteen results, command template not searchable. - Every divergence-prone choice injected, none with a
Default— the XDG backend (XdgEnvDirshere, glib in anvil/forge), the app identity (SearchPathSpec::for_appderives both thefrost/workflowssegment andFROST_WORKFLOW_DIRfrom one name), the load order (LoadOrder::Precedencehere,ByNamein ember/forge — andLoadOrderhas noDefault, so a silent shim does not compile), and the dev-tree root (env!expands against the compiling crate, so evaluating it in the core would point all four apps at a directory that does not exist while their bundled-library tests kept passing). - An empty search-path entry contributes no tier — a trailing or doubled
:in$FROST_WORKFLOW_DIRis dropped, and a non-absoluteXDG_DATA_DIRSentry no longer contributes a tier resolved against the process CWD. Thedirsbackend never produced a relative user tier here, so this is hardening for frost and a fix for a sibling. - The bundled example the guard would have missed, and the documented
contract —
scripts/workflows/docker-tail-logs.yamldeclareddefault: ""for its requiredcontainerargument, which under the new contract is an explicit empty value: Insert would have produceddocker logs -f --tail 100. The empty default is removed. README's capability bullet, its shortcut table and a new "Workflow 模板" section state the required-argument rule, how to opt a parameter back into being optional, and the two template tightenings.
Verification for rounds 59–69: cargo fmt --all -- --check (no diff),
cargo clippy --locked --all-targets --all-features -- -D warnings (silent),
and cargo test --locked --all-targets --all-features --no-fail-fast (992
passing, zero failures), all against the published jterm_core 790d06a with
no local [patch]. The drop from 1,006 is this migration exactly: workflows had
twenty-three tests and has nine — three in src/workflows.rs pinning the search
path, the precedence load order and the bundled-library contract, six in
src/workflow_picker.rs. The rewritten form test is the load-bearing one: it
previously asserted that deploy api --env= renders fine and called that
emptiness intentional, which was the fossil record of round 60's defect rather
than behaviour to preserve. The install-path and packaging checks were not
rerun; this round touched no script, desktop file or metadata.
Round 70 closes the remaining argument-form affordance gap:
-
Reversible workflow arguments — every parameter row exposes Reset, which calls the shared
ArgsForm::clearcontract: a defaulted argument returns to its declared value, while an undefaulted argument returns to the genuinely-unset state. Editing or resetting also clears stale render feedback, and the form regression proves reset is not equivalent to typing an empty string. -
Current-toolchain clean placement boundary — the completed-graphics filter now states its real predicate directly: with no live lifecycle every placement is stashed, otherwise only rows before the live start are. This removes the Rust 1.96
nonminimal_boolfailure without changing the finished/live graphics contract covered by the clear/undo regression. -
Fail-closed dependency policy — CI now runs a pinned
cargo-denyagainst the committed lockfile. Wildcard requirements, unapproved licenses, unknown registries and unknown git sources fail the build; the two family git dependencies are admitted only at their exact reviewed revisions. Duplicate versions stay visible as warnings while the iced/wgpu graph still requires them. The core git dependency now also states its crate version, so a revision pin no longer counts as a wildcard API requirement. Two unavoidable unmaintained (not vulnerability or unsoundness) notices are exact-ID exceptions; every other advisory remains fail-closed. -
One missing-argument truth — the iced form no longer restates
default.is_none() && value.trim().is_empty(). It snapshots the sharedArgsForm::missing()result once per render and uses those names for every(required)label, so a future renderer-rule change cannot leave the UI claiming the opposite. The existing default/empty/whitespace/reset matrix now tests the delegated result. -
Strict documentation gate — CI now builds all frost documentation with warnings denied, matching forge's release boundary. The local verification list and handoff say the same thing, and the former redundant intra-doc-link exception is retired instead of remaining stale institutional knowledge.
-
Smaller direct dependency contract —
unicode-widthandlruleave frost's manifest because no frost source or feature uses either crate directly. Both remain transitively locked wherejterm_core/wgpu need them, but frost no longer claims their APIs as app-owned build inputs or requires future maintainers to review two misleading direct edges. -
Fixed and exhaustive CI baseline — every validation job now names Ubuntu 24.04 instead of following the moving
ubuntu-latestalias. The test job and documented local command also use--no-fail-fast, so one failing target cannot hide independent failures in the rest of the matrix. -
One fail-closed security entry point — local
--alland CI's--policy,--audit, and--shellmodes now share a single script. It first proves the lockfile is usable, keeps cargo-deny and RustSec on that exact graph, exposes duplicate crates, and discovers every shell script so a new maintenance entry point cannot silently escape Bash and ShellCheck. The unified gate exposed cargo-audit's success exit for warnings and the newly reported unsoundlru 0.16.4below iced/cryoglyph. A project audit policy now denies every new warning outside the two exact unmaintained exceptions, while a Rust-source-identical cryoglyph patch selects fixedlru 0.18.2until upstream publishes that dependency repair. -
Truthful release handoff and discoverable AI entry — the remaining release boundary no longer calls the committed AI panel an uncommitted worktree or reports obsolete core and jagent revisions. It records the sole deliberate crates.io patch and its removal condition, while README's feature list and shortcut table now expose the shipped AI Chats chord and distinguish the review-only command-generation palette action.
-
Installed workflow library — source and prebuilt installs now copy all six accepted workflow fixtures into the selected data tree: the default follows
XDG_DATA_HOME, while an explicit prefix owns itssharetree.--no-desktopkeeps these runtime resources, uninstall removes only the owned examples and leaves a non-empty user directory alone, and the real DESTDIR round trip compares every installed byte and proves install/uninstall symmetry. -
Whole-plan staged uninstall preflight — every binary, current/legacy desktop asset, owned workflow, and cleanup directory is validated before the first removal under a non-root
DESTDIR; per-target checks remain at use time. A regression places the unsafe symlink only under the latersharebranch and proves the earlier safe binary survives unchanged, so an escape refusal can no longer leave a partially uninstalled package tree. -
Truthful and inert configuration handoff — the uninstaller now mirrors the application's
dirs::config_dirrule: only an absoluteXDG_CONFIG_HOMEoverridesHOME/.config. Its final preservation path is shell-quoted, so spaces remain unambiguous and an environment-provided newline or terminal control byte cannot forge another uninstall message. -
Whole-plan staged install destinations — before the executable is replaced, the installer now walks the complete binary, workflow, applications, metainfo, and per-size icon directory chains under a non-root
DESTDIR. A regression puts the symlink only in the final 256px icon branch and proves both the old binary and the outside directory remain byte-for-byte untouched when the entire upgrade is rejected. -
Stage-complete publication — every workflow, transformed desktop entry, metadata file, icon, and executable is copied into a sibling temporary before the first destination rename. Any copy/transform failure cleans the entire queue and leaves the installed generation unchanged; resources are then atomically published one by one and the executable is the final commit marker. The contract explicitly stops short of claiming a cross-filesystem transaction during that short rename phase.
-
Reversible publish phase — before the first rename, every existing destination is snapshotted without following symlinks into a private sibling rollback backup. A failed rename or catchable termination restores every attempted destination in reverse order and removes new destinations that had no predecessor; a deterministic final-binary rename failure proves all pre-existing resources plus the executable regain their prior bytes and modes while an originally absent icon remains absent. If restoration itself fails, the recovery backup is retained and named.
-
Truthful legacy-launcher cleanup — removal of the pre-rename jterm3 desktop entry is explicitly best-effort after the new generation commits. Its staged ancestor is revalidated at the removal point. A deterministic
rmfailure now emits a non-fatal warning, still refreshes caches and prints the success handoff, while the installed binary and new desktop entry remain complete and no transaction artifacts survive. -
Inode-faithful rollback snapshots — a same-directory
ln -Pnow keeps the exact old inode alive through publication, preserving ownership, mode, xattrs, hardlink identity, and dangling symlinks. The final-rename rollback regression checks device/inode/uid/gid for every predecessor, restores a dangling desktop symlink by link value, and checks a user xattr when the host supplies attr tools. Filesystems that reject hardlinks use a documented no-follow copy fallback with deliberately narrower ownership guarantees. -
Copy-safe PATH handoff — the installer emits a direct,
%q-escapedexport PATH=...:"$PATH"line instead of nesting an arbitrary directory in single quotes inside anechocommand. Empty, relative, and trailing-empty PATH cases stay exact; apostrophes and dollars cannot break the suggestion, while newline-bearing prebuilt or shadowing executable paths are reversibly quoted and cannot forge diagnostic lines. -
Side-effect-free special-target preflight — final destinations are checked as a complete set before the first rollback link is created. A late directory, FIFO, or host-creatable Unix socket/device is rejected as neither a regular file nor symlink; an instrumented
lnproves no earlier backup was attempted, the special object is unchanged, FIFO handling never blocks, and every staged temporary is removed. -
Symmetric uninstall target ownership — the complete removal file set is now restricted to absent entries, regular files, and final symlinks before the first
rm. A final symlink is unlinked without touching its referent; late directories, FIFOs, and host-creatable sockets/devices fail closed. Instrumentedrmcoverage proves both special-target and late ancestor rejection leave the earlier executable and the entire staged tree intact. -
Typed non-recursive cleanup — the owned workflow cleanup path is now preflighted as either absent or a real directory before any file removal. Regular files, final symlinks, FIFOs, and host-creatable sockets/devices are rejected without invoking
rm; the earlier binary and replacement object remain unchanged. Valid cleanup still uses only non-recursivermdir, so a non-empty user workflow directory and its custom files are never traversed. -
Transactional uninstall quarantine — after whole-plan preflight, every existing owned file is atomically renamed to a private sibling quarantine. A deterministic third-rename failure and a post-first-rename ancestor swap both restore earlier entries in reverse order with exact inode metadata; a post-rename
TERMalso proves in-flight bookkeeping is reconciled by inode. Only an all-renamed plan commits: later purge/rmdir failures are non-fatal, keep target names absent, retain a named quarantine when needed, and print a copy-safemv -fTrecovery command instead of reporting rollback. -
Inode-owned quarantine lifecycle — mktemp reservations, rollback sources, and purge entries are now checked against their recorded inode at every use point. A later reservation replaced by a symlink aborts staging, restores the earlier binary, and leaves the substitute untouched; a post-rename quarantine substitution likewise invokes no
rm, follows no referent, and is never advertised as the displaced original's recovery copy. Post-action command failures are reconciled by observed inode state. -
Directory-fd-bound uninstall operations — every present target keeps a read-only parent fd, and reservation, quarantine rename, rollback, and purge execute through Linux
/proc/self/fdnames. Deterministic parent swaps from insidemktemp,mv, andrmprove placeholders never escape, interrupted staging restores the exact inode in the bound directory, and purge cannot unlink identically named files behind a replacement symlink. Logical parent identity is still checked before/after each phase; a failed purge names the bound physical directory on both sides of its recovery command; pooled fds close together after the optional post-commit phase. -
Bound post-commit cleanup and cache refresh — workflow-directory cleanup records its target and parent inodes before the commit, while the optional desktop and icon refreshers receive only pre-opened directory-fd paths. Purge-time parent replacement skips cleanup/cache helpers entirely; replacements made inside either cache helper can touch only the displaced bound directory. Helper failures and identity changes remain explicitly non-fatal and never suppress the truthful uninstall success summary.
-
Bound install helpers and capped fd pools — legacy launcher removal, desktop validation, and desktop/icon cache refresh now reuse pre-publish applications/icon directory fds. Parent swaps inside
rmor either cache helper cannot reach a replacement symlink referent; helper failures remain non-fatal and precede the truthful install summary. Install and uninstall pools deduplicate by device/inode under fixed ceilings, close once on every exit path, and preserve per-consumer use-point identity checks. -
Directory-fd-bound install publication — every staged destination now records a pooled read-only parent fd plus exact temporary/original/backup identities. Temporary creation, hardlink/copy snapshots, publish rename, reverse rollback, and successful backup cleanup operate through Linux
/proc/self/fdnames and recheck the logical parent at each use point. Deterministic swaps from insidemktemp,ln,mv, and cleanuprmprove that work remains in the displaced inode: failed publication never removes an identically named replacement target, and successful cleanup leaves no rollback artifact in the old parent. Both directory pools now have a 16-physical-inode ceiling and retain one close-on-exit owner per fd. -
Exact install-artifact transitions — rollback reservations are reused only after their bound name is observably absent, and a hardlink snapshot is accepted only when it has the original target's exact device/inode. Substitutes injected after reservation
rm, backupln, or cleanuprmare retained as unowned names and never retried or removed. Conversely, non-zero wrappers after completed reservation/cleanup unlinks, hardlink creation, or final publish rename reconcile from exact post-action state; deterministic regressions prove the generation commits with no false rollback while every transaction-owned temporary and backup disappears. -
Descriptor-pinned staged and fallback copies — every regular staging temporary stays open while byte copy and mode application address its fd, so cross-device sources cannot trigger destination inode replacement. Regular hardlink fallbacks likewise copy into an opened, exact reservation inode and revalidate source, descriptor, and bound name before ownership is accepted. Non-zero copy helpers reconcile only from complete content/mode plus exact identity; a name swapped after copy has ownership revoked and is retained untouched by cleanup or rollback. Deterministic regressions cover cross-device staging, forced hardlink fallback followed by reverse restore, and both staged-temporary and fallback-reservation ABA replacement.
-
Pinned backup lifetime and post-publish name reconciliation — every regular rollback snapshot now keeps a read-only fd until rollback/cleanup, preventing immediate inode-number reuse from making a replacement name look owned. Publish completion is defined by the staged inode reaching the exact destination and leaving its recorded source name; a different inode inserted at that source is retained as unowned even when
mvreturns non-zero. Backup identity is rechecked at the same boundary and revoked on mismatch. A deterministic two-publish regression replaces both names after the first completed rename, forces the second rename to fail, and proves rollback touches neither substitute while reporting the unrecoverable predecessor. -
Pinned symlink fallback snapshots — a copied symlink backup is accepted only when its link text, uid/gid, and mode match the exact source, then a second same-directory hardlink pins that new inode through publish and rollback. The pin reservation itself stays descriptor-pinned while its placeholder name is removed, closing immediate inode-number reuse. Exact post-state reconciles
cp/lnwrappers that finish then return non-zero; deterministic reservation and post-publish ABA regressions prove cleanup never unlinks substitutes, while failed rollback retains and diagnoses the original symlink inode under its recovery pin. -
Private-anchor symlink cleanup state machine — each symlink snapshot now keeps a third hardlink inside a random
0700, descriptor-bound private directory, plus the exact in-memory link text and uid/gid/mode (including preservation of trailing newlines). Cleanup treats main, pin, and anchor unlinks as independent use points, revalidating the logical parent, private directory, remaining peers, inode, and semantic fingerprint before advancing. Parent/private-dir renames, post-unlink substitutions, public-name hardlink merges, and even a forced three-name deletion with numeric inode reuse all fail closed without touching replacements. Any exact survivor yields one copy-safe%qrecovery command based only on bound physical names; diagnostics never expose link text or its referent. -
Start identity captured at
Cand never minted atD— the OSC 133 field loop now recognisessession_id,seqandstarted_at_ms, gated on theCmark, and stashes them with the execution id as one all-or-nothingStartLifecycle. jsh puts all four onCand none of the last three onD, so a completion that supplied them would be naming a Start generation this terminal never observed;Dmay only be compared against the captured id. The finished record carries that token tomain.rs, where it becomes ajterm_core::parser::CommandMetaand then anExecutionLifecycle— the journal's only durable-output capability, which core's writer re-checks against the authoritative on-disk Start under the journal lock. A boundary-inferred completion and a synthesized agent termination both carryNone: neither corresponds to a journal Finish. -
Every interactive shell has a session identity —
Session::spawn_*passedNonefor--session, so every jsh under frost ran anonymously even thoughpty.rshad implemented the flag for both argv shapes. Without it jsh omitssession_id=fromC, the Start envelope is permanently incomplete,ExecutionLifecycle::from_command_metaalways returnsNone, and the journal submit is a silent no-op — while jsh's ownstartevents record a null session and its per-session snapshot never restores. The id isagent_task_ui::terminal_session_id, the same string the task reducer already binds terminals with, filtered throughis_valid_jsh_session_id; the "is the shell actually jsh?" half of the gate stays inpty.rs, where the configured shell is finally resolved, and an explicit one-shotcommand_argvgets no session at all. -
The journal gate is asked before the copy, not after — the submit loop cloned each finished block's whole captured output and only then let
submitdiscover the journal was disabled, and its comment stated the gate backwards (the journal is on unlessJSH_EXECUTION_JOURNALturns it off).output_capture_enabled()is now checked once, ahead of the loop.CompletionFacts::outputbecame&'a strupstream in the same round, so the correction trigger's per-command copy of up toMAX_CAPTURED_OUTPUT_BYTESis gone as well. -
A finished block names the directory the command RAN in —
D'scwd_urlis jsh's cwd after the command, so every block that changed directory was labelled with its destination:cd /tmpfrom/home/u/projrecorded/tmpas the working directory it ran in, and that value reaches the block menu, the Markdown export and the agent prompt.Dmay now only fill an empty cwd, never replaceC's. ember and forge both already guarded this. -
Untrusted archives are validated, staged privately, then published — a remote directory download extracted the far side's tar straight into the destination's parent with nothing about its members checked, so an archive naming
../../.ssh/authorized_keys, an absolute path, or a second top-level tree beside the requested one wrote wherever it named. Every member is now required to sit under the one expected top-level component (streamed and bounded, never retained), extraction happens in an owner-onlycreate_newdirectory this process owns, the extracted shape is re-checked, and only that single verified tree is published with oneRENAME_NOREPLACEnamespace operation. The staging directory is removed on both paths. -
Notification text is sanitised before it leaves the terminal — OSC 9/777 titles and bodies reached
notify-sendwith control characters and bidi overrides intact, soprintf '\e]777;notify;<RLO>Security Update;<RLO>approve\a'put attacker-reordered text into a desktop toast wearing the desktop's chrome. Offending scalars become U+FFFD rather than disappearing, on the terminal-strict spoofing class, matching core's ownbounded_notification_field; a field that trims to nothing falls back to the application identity. -
One exit, and it is the one that flushes — closing the last session returned the exit task after writing only the session snapshot, so the Agent transcript, the AI-chat store, the jsh execution journal and the shared command-history index were abandoned mid-flight. Whether a session's last commands survived depended on which quit gesture the user used. Both gestures now return through
exit_flushing_durable_state, each flush keeps its own short deadline so a stalled state filesystem cannot turn quitting into a hang, and a structural regression pins the exit task to that one construction site. -
Repeated OSC 133 slots and contradictory exit slots fail closed — repeated keys were last-wins, so PTY output could retract a truncation disclosure with a second
cmd_truncated=0, choose between two contradictory directories, or name an execution twice. ADcarrying two outcome slots committed to the first, renderingD;1;exit=0as a successful block, and a malformedexit=deferred to the next field instead of occupying the slot it claimed. Both now follow core's parser exactly: a repeated slot is ambiguity, and ambiguity is Unknown. -
The OSC 133 alias set, decoding and text rules match core —
command_urlandcmdlinewere unrecognised and a barecommand=was taken without percent-decoding, so identical bytes decoded differently depending on which spelling a shell chose. Metadata was filtered for control characters only and then rendered raw in the block menu and the export, so a cwd or id spelled with a zero-width joiner, a bidi override or an interlinear annotation control drew as one path and named another; ids, commands and cwds now go through the now-publicreview_input::is_terminal_visual_spoofing_character, and cwds additionally throughexecution_journal::is_valid_jsh_cwd. -
The OSC 7 working directory is bounded — the OSC 133
cwdparam was capped while OSC 7 was not, even though OSC 7 is the wider liability: it is retained for the life of the pane, cloned into every command zone with no cwd of its own, written into the session snapshot, and inherited by every split. Both now use the same 4 KiB ceiling, which is also Linux's own pathname limit, and the cap is on the decoded path so percent-encoding buys no extra room. -
OSC 52 writes are gated in both directions — frost was the one app of the four that let PTY output replace the host clipboard with no gate, while already gating reads. A program in the terminal — including one on the far side of an ssh connection — could silently choose what the user's next paste produced, into a shell or a password field.
allow_remote_clipboard_writedefaults off, matching anvil and forge's identically named option and ember'sosc52_clipboard_write, and is independent of the read gate. -
The command-zone rebase is amortised — past the scrollback cap every scrolled line trims a row, and rebasing the retained zones for it was a per-line walk of the whole 256-entry deque on the PTY ingest path. The shift is uniform, so it accumulates and is applied once per ingest batch; every path that can observe a zone's anchors settles it first, so no reader ever sees a stale row. A regression proves the batched result is byte-identical to the per-row one and that one batch rebases once however many rows it trimmed.
-
The keyboard surfaces read the keyboard table — the command palette and the Keyboard Shortcuts panel printed hardcoded default chords, so the two surfaces whose entire job is to teach the keyboard were the two most likely to be lying to a user who had configured one, and still showed a chord for a command they had unbound.
KeyBindings::chords_for/shortcut_labelrender from the live table in core's frozen modifier order (which exposed two palette hints spelledCtrl+Alt+Shift+…), an unbound command shows nothing rather than its former default, and the genuinely non-configurable app chrome keeps its literal hint. -
One ad-hoc block-context adapter, not three — the three surfaces that attach a block to the AI as untrusted evidence each hand-rolled a
BlockContext, with an unknown-status note none of the other three apps used and a second local bounding helper.ad_hoc_block_context— the family-shared adapter carried by anvil, ember and forge, and documented there as frost's ownAskAiadapter — is re-adopted with its six regressions, so the sentinel, the explanatory note, the unavailable-output placeholder and the head/tail bound are the family's in every surface. -
The last-session quit flushes while its child can still answer — unifying the two quit gestures (item 108) reordered the one they did not share: the last session's PTY was signalled before
exit_flushing_durable_stateran, andsave_session_snapshotis the third flush inside it.terminatereturns as soon as SIGHUP/SIGTERM are away and its reaper waits before reaping, so the snapshot read the/proc/<pid>/cwdlink of a zombie. For a shell that reports OSC 7 — jsh does — nothing was lost, but a plain bash session has no other source for its directory, so the last thing frost persisted before quitting wascwd: Noneand the next launch opened somewhere else. The flush now runs first and the signal second. -
The lifecycle-bound journal submit is pinned, not merely present — rounds 102–104 and 112 landed with no test between them and the code: neutering the submit loop, inverting the capture gate, dropping a slot from the
CommandMetathe submit path builds, or restoring the unconditional OSC 52 clipboard write all compiled and left the whole matrix green, which is the "compiles, passes, never journals" shape the round existed to remove. The conversion is now a namedjournal_lifecycle, driven end to end from a real OSC 133 A/B/C/D sequence and from the boundary-inferred completion that must yield nothing; the submit site, its two gates, and the--sessionidentity handed to the one PTY spawn are pinned structurally, in the style ofthe_process_has_exactly_one_exit_and_it_flushes_first, because none of them is observable after the fact.
Agent-TUI fidelity and family parity adds rounds 118–129 (2026-09-19). Real Claude Code, Codex and Kimi Code sessions recorded through a live emulator were replayed into frost and into libvte 0.76; screen text matched libvte everywhere except a resize, and an audit against ember found the rest.
-
Wide over wide keeps its pair — writing a double-width character whose right half lands on another wide lead orphaned that neighbour's continuation, so a later write blanked the new glyph's right half (CJK redraws in Claude Code shifted by one column).
put_charnow splits the pairs around the write, as ember does. -
IRM shifts respect wide pairs — insert mode no longer leaves a lead without its continuation at the cut or the row end.
-
Shrinking a pane drops blank rows first — a height shrink evicted the rows above the cursor into scrollback and jumped the prompt to the top even with blank rows below; blank rows below the cursor now go first.
-
Colour queries report the theme — OSC 10/11/12 and OSC 4 (0–15) answered hard-coded white/black; frost now pushes the active theme into every session, and explicit OSC sets still win.
-
Replies use the query's terminator — BEL-terminated OSC queries get BEL-terminated replies, including OSC 52 and 5522 status replies.
-
DSR 996 and mode 2031 notifications —
CSI ? 996 nreports dark/light, and a theme flip sends the same report to mode 2031 subscribers unprompted. -
DECXCPR keeps its marker —
CSI ? 6 nanswersCSI ? r ; c R. -
XTWINOPS 16t — the cell size in pixels, which Claude Code requests.
-
ANSI-mode DECRQM — IRM (4) and LNM (20) are answered alongside the private-mode table.
-
VT and FF feed lines — they act as LF instead of being dropped.
-
Three-byte escapes are consumed whole —
ESC % G,ESC * B,ESC # 3no longer leak their final byte, including across reads. -
Formatting gate — rustfmt-only fixes to this evening's agent-task code, which left
cargo fmt --checkfailing on master. -
Pending OSC 5522 reads precede EPERM — while an asynchronous host read is still outstanding, later MIME-data batches in the same session are counted as bounded refusals rather than answered with an immediate EPERM, so a permission flip mid-flight cannot be mistaken for the older read's completion. A focused regression drives the outstanding-read path with consent revoked between batches.
-
Process-global OSC clipboard reads — iced's clipboard API is one in-flight read per process, but frost tracked
clipboard_read_in_flightper session, so two panes could each spawniced::clipboard::read()and race the host clipboard. A single owner(session id, PTY fd)now gates every OSC 52 and OSC 5522 host read; other sessions receive the interoperable busy/empty refusal without starting another read, closing a pane releases an outstanding owner, and late async completions clear the slot even when the originating session is already gone. -
OSC 52 GET permission pinned structurally —
allow_clipboard_readdefaults closed, but only runtime config tests saw it. A structural regression now provesiced::clipboard::read()is enqueued only inside the read-permission block and never from the host-read-blocked refusal path, matching the write-permission pin added earlier. -
OSC 5522 read permission pinned structurally — extended MIME reads share the same default-closed permission and process-global host-read owner as OSC 52 GET. A structural regression now proves
service_osc5522_readstays behindallow_clipboard_read, host-read blocking cannot enqueue anothericed::clipboard::read(), and read-disabled paths answer EPERM without touching the clipboard. -
OSC 5522 completion stays behind read permission — async clipboard completions for extended MIME reads now pin
complete_osc5522_readbehind the same default-closed permission, answering EPERM without touching host data when reads are disabled. -
OSC 52 completion read permission pinned structurally — async OSC 52 clipboard completions stay behind the default-closed read gate; a structural regression pins
complete_osc52_readclearing host data when reads are disabled. -
OSC 52 SET write rate limit — once
allow_remote_clipboard_writeis granted, OSC 52 SETs are still capped at two host writes per rolling second so a remote PTY cannot spam-replace the user's paste buffer. A unit regression pins refusal of a third write inside the same window. -
Evolve round-25 path-patch smoke —
cargo test --bin frost -- workflows command_correction(12) against localjterm_coref2d2ce0- jagent
a7474e9afterbetween()76, Find continue bookmark-empty, and STAGE 64 handoff sync. Manifests stay on published pins.
- jagent
-
Evolve round-27 path-patch smoke —
cargo test --bin frost -- workflows command_correction(12) against localjterm_coreafter STAGE 67 classify--peel, daemonize flag-only-a/-v, leftovers6-envdirpin, and anvil/forge STAGE 67 docs (rounds 88 / 122). Round 26 already sat on the handoff tip. Manifests stay on published pins. -
Evolve round-28 path-patch smoke —
cargo test --bin frost -- workflows command_correction(12) against localjterm_coreafter STAGE 68 (gnome-session-inhibit), fail-closed nest/transparency deepen, Find/organism polish, and anvil/forge STAGE 68 docs (rounds 92 / 126). Manifests stay on published pins. -
Evolve round-29 path-patch smoke —
cargo test --bin frost -- workflows command_correction(12) against localjterm_coreee39a12+ jagent543415bafter STAGE 70 (uclampset/gamemoderun), Guard→Celebrate None survey, and anvil/forge sticky/ find polish (rounds 95–97 / 129–130). Manifests stay on published pins. -
Evolve round-30 path-patch smoke —
cargo test --bin frost -- workflows command_correction(12) against localjterm_coreafter gnome-session-inhibit STAGE arity edges, timeout/nice classify/jagent nest, and membership/DISPATCHES/CLASSIFY_FORMS len-70 lockstep. Round 29 already sat on the handoff tip. Manifests stay on published pins. -
Evolve round-31 path-patch smoke —
cargo test --bin frost -- workflows command_correction(12) against localjterm_coreafter wave-26 PATH non-launcher leftover pins, anvil sticky whitespace (100)- forge file-tree permission/missing (134), and STAGE 70 docs already present. Manifests stay on published pins.
-
Evolve round-32 path-patch smoke —
cargo test --bin frost -- workflows command_correction(12) against localjterm_coreHEAD68982a3+ jagent9cd0211(busybox/pipe nest + CLASSIFY/STAGE 70 set-eq; anvil 101–104 / forge 135–138 pins). Manifests stay on published pins. -
Evolve round-33 path-patch smoke —
cargo test --bin frost -- workflows command_correction(12) against localjterm_coreafter busybox applet + pipe-to-bash nest deepenings for uclampset/gamemoderun/ gnome-session-inhibit, CLASSIFY_FORMS set-equality with STAGE 70, and timeout/nice classify peels. Manifests stay on published pins. -
Evolve round-34 path-patch smoke —
cargo test --bin frost -- workflows command_correction(12) against localjterm_coreafter ambient→vigil/celebrate None, PATH wave-27 identity/agent leftovers out of STAGE, and anvil 105–107 / forge 139–141 organism/find/history edges. Manifests stay on published pins. -
Evolve round-35 path-patch smoke —
cargo test --bin frost -- workflows command_correction(12) against localjterm_coreafter PATH wave-28 systemd inspector leftovers out of STAGE, systemd-cat/inhibit busybox + timeout/nice nest deepenings, and prior waves. Manifests stay on published pins. -
Evolve round-36 path-patch smoke —
cargo test --bin frost -- workflows command_correction(12) against localjterm_coreafter ambient→hold/rest None, CelebrateBig fifteen/between()91 UI lockstep, anvil 111–116 / forge 145–150 sticky/find/organism edges, and PATH wave-28. Manifests stay on published pins. -
Evolve round-37 path-patch smoke —
cargo test --bin frost -- workflows command_correction(12) against localjterm_coreafter openvt STAGE 71 + timeout/nice nest, Inspect/Sit→Unknownbetween()93, anvil 117–121 / forge 151–155 sticky/find/organism edges, and prior waves. Manifests stay on published pins. -
Evolve round-38 path-patch smoke —
cargo test --bin frost -- workflows command_correction(12) against localjterm_coreafter STAGE 71 busybox/DISPATCHES set-eq + openvt pipe nests, anvil 122–125 / forge 156–160 sticky/find/cancel/notice + STAGE 71 tip docs, and Inspect/Sit→Unknownbetween()93. Manifests stay on published pins. -
Evolve round-39 path-patch smoke —
cargo test --bin frost -- workflows command_correction(12) against localjterm_coreafter openvt/daemonize busybox applet + pipe-to-bash deepenings, DISPATCHES STAGE 71 set-eq beside CLASSIFY_FORMS, and prior waves. Manifests stay on published pins. -
Evolve round-40 path-patch smoke —
cargo test --bin frost -- workflows command_correction(12) against localjterm_coreafter anvil 129–130 / forge 164–166 sticky punct/thin/hair + find empty-query- notice catch-up beside STAGE 71 / between() 93 / prior waves. Manifests stay on published pins.
-
Evolve round-41 path-patch smoke —
cargo test --bin frost -- workflows command_correction(12) against localjterm_coreafter anvil 131–132 / forge 167–169 sticky quad-space/RLI/FSI + find All empty+stale + notice catch-up beside STAGE 71 / between() 93 / prior waves. Manifests stay on published pins. -
Evolve round-43 path-patch smoke —
cargo test --bin frost -- workflows command_correction(12) against localjterm_coreafter PATH wave-31 ctl/utility leftovers out of STAGE beside CLASSIFY/DISPATCHES lockstep at 71, anvil 133–136 / forge 170–174 Hangul/whitespace/Watch* pins, and prior waves. Manifests stay on published pins. -
Evolve round-42 path-patch smoke —
cargo test --bin frost -- workflows command_correction(12) against localjterm_coreafter anvil 137–140 / forge 175–179 Ogham sticky + All whitespace find + Guard/Celebrate Full-motion + notice catch-up beside STAGE 71 / between() 93 / prior waves (fills gap before round-43). Manifests stay on published pins. -
Evolve round-44 path-patch smoke —
cargo test --bin frost -- workflows command_correction(12) against localjterm_coreafter anvil 148–151 / forge 188–191 Idle/Rest Guard + hold→Watch + Failure→holds Full-motion beside anvil 141–147 / forge 180–187 hold/ambient/Watch/ CrossBlock/Retry/FVS/NBSP pins, STAGE 71 / between() 93, and prior waves. Manifests stay on published pins. -
Evolve round-45 path-patch smoke —
cargo test --bin frost -- workflows command_correction(12) against localjterm_coreafter anvil 152–154 / forge 192–195 bidi sticky/find/notice + ambient disposition completeness beside anvil 148–151 / forge 188–191 Idle/Rest Guard pins, wave-32 PATH leftovers + aa-exec/socket-activate deepen, STAGE 71 / between() 93, and prior waves. Manifests stay on published pins. -
Evolve round-47 path-patch smoke —
cargo test --bin frost -- workflows command_correction(12) against localjterm_coreafter anvil 155–157 / forge 196–199 sticky VS/FVS4 + find marks + GlanceAside ambient beside anvil 152–154 / forge 192–195 bidi/ambient, STAGE 71 / between() 93, and prior waves. Manifests stay on published pins. -
Evolve round-49 path-patch smoke —
cargo test --bin frost -- workflows command_correction(12) against localjterm_coreafter PATH wave-33 device/sysctl leftovers + setsid busybox/timeout/nice deepen beside anvil 158–160 / forge 200–202 CrossBlock cancel + Celebrate survey, STAGE 71 / between() 93, and prior waves. Manifests stay on published pins. -
Evolve round-48 path-patch smoke —
cargo test --bin frost -- workflows command_correction(12) against localjterm_coreafter anvil 161–164 / forge 203–207 sticky FE02/nirugu + Hangul find + Watch* Unknown- near-wrap finished beside anvil 158–160 / forge 200–202, STAGE 71 / between() 93, and prior waves (fills gap before round-49). Manifests stay on published pins.
-
Evolve round-50 path-patch smoke —
cargo test --bin frost -- workflows command_correction(12) against localjterm_coreafter anvil 171–174 / forge 215–219 sticky FE04/syllable + FE03 find + MAX-3 cancel + Celebrate→Unknown beside anvil 165–170 / forge 208–214, STAGE 71 / between() 93, and prior waves. Manifests stay on published pins. -
Evolve round-51 path-patch smoke —
cargo test --bin frost -- workflows command_correction(12) against localjterm_coreafter anvil 175–178 / forge 220–224 sticky FE05/Manchu + FE04 find + MAX-4 cancel + Rest→Unknown beside anvil 171–174 / forge 215–219, STAGE 71 / between() 93, and prior waves. Manifests stay on published pins. -
Evolve round-52 path-patch smoke —
cargo test --bin frost -- workflows command_correction(12) against localjterm_corea4d3b5d+ jagent1c3d308after anvil 179–183 / forge 225–230 sticky FE06/FE05/ MAX-5/Celebrate-Rest/SitNear-Inspect beside prior waves (fills gap wave-34 skipped). STAGE 71 / between() 93 held. Manifests stay on published pins. -
Evolve round-53 path-patch smoke —
cargo test --bin frost -- workflows command_correction(12) against localjterm_coref530cfb+ jagente8272a4after anvil 184–187 / forge 231–235 sticky FE07/birga + FE06 find + MAX-6/GuardRecovery→Unknown beside prior waves. STAGE 71 / between() 93 held. Manifests stay on published pins. -
Evolve round-54 path-patch smoke —
cargo test --bin frost -- workflows command_correction(12) against localjterm_coref530cfb+ jagente8272a4after wave-35 host/hw inventory leftovers + chrt/ionice STAGE deepen beside prior waves. STAGE 71 / between() 93 held. Manifests stay on published pins. -
Evolve round-55 path-patch smoke —
cargo test --bin frost -- workflows command_correction(12) against localjterm_coref530cfb+ jagente8272a4after anvil 188–191 / forge 236–240 sticky 1801 + FE07 find + MAX-7/GuardRecovery UI sync beside prior waves. STAGE 71 / between() 93 held. Manifests stay on published pins. -
Evolve round-56 path-patch smoke —
cargo test --bin frost -- workflows command_correction(12) against localjterm_core5fa0f0b+ jagent2204f36after anvil 192–195 / forge 241–245 sticky 1802 + 1801 find + MAX-8/Unknown→GuardRecovery beside prior waves. STAGE 71 / between() 93 held. Manifests stay on published pins. -
Evolve round-58 path-patch smoke —
cargo test --bin frost -- workflows command_correction(12) against localjterm_core8a8e607+ jagent8825c4fafter anvil 196–199 / forge 246–250 sticky 1803 + 1802 find + MAX-9/Unknown→GuardCautious beside prior waves. STAGE 71 / between() 93 held. Manifests stay on published pins. -
Evolve round-59 path-patch smoke —
cargo test --bin frost -- workflows command_correction(12) against localjterm_corebbb361e+ jagentcbbe241after anvil 200–203 / forge 251–255 sticky 1804 + 1803 find + MAX-10/Unknown→GuardStuck beside prior waves. STAGE 71 / between() 93 held. Manifests stay on published pins. -
Evolve round-60 path-patch smoke —
cargo test --bin frost -- workflows command_correction(12) against localjterm_cored442073+ jagentf85723fafter PATH wave-38 process-table monitor leftovers + softlimit/cgexec STAGE deepen beside sticky 1804/MAX-10 / round-59. STAGE 71 / between() 93 held. Manifests stay on published pins. -
Evolve round-61 path-patch smoke —
cargo test --bin frost -- workflows command_correction(12) against localjterm_corebbb361e+ jagentcbbe241after anvil 200–203 / forge 251–255 sticky 1804 + 1803 find + MAX-10/Unknown→GuardStuck beside round-60 wave-38. STAGE 71 / between() 93 held. Manifests stay on published pins. -
Evolve round-62 path-patch smoke —
cargo test --bin frost -- workflows command_correction(12) against localjterm_core00fde9b+ jagent49655f5after anvil 204–207 / forge 256–260 sticky 1805 + 1804 find + MAX-11/Unknown→GuardFailure beside round-61. STAGE 71 / between() 93 held. Manifests stay on published pins. -
Evolve round-63 path-patch smoke —
cargo test --bin frost -- workflows command_correction(12) against localjterm_corecb3c8e6+ jagentd05c9cfafter anvil 208–211 / forge 261–265 sticky FF1A + 1805 find + MAX-12/Unknown→Idle beside round-62. STAGE 71 / between() 93 held. Manifests stay on published pins. -
Fail-closed named ANSI color resolution —
src/color.rsno longerunwrap()s a partial named-color map after matching Default/Indexed/RGB. Named slots are an exhaustiveColormatch that returnsNonefor the three non-named variants; a miss falls back to the theme default instead of panicking. Bold-brightening, dim attenuation, OSC palette overrides, and the 256-color cube/gray ramps are pinned by unit tests; the palette-less wrappers stay live through those tests rather thanallow(dead_code). -
Bounded find-preview windows —
get_match_contexttreatedcontains("")as a hit on every line and accepted an unbounded context radius, so an empty pattern orusize::MAXneighbors could dump the whole buffer. Empty patterns now return nothing, each window is capped at 8 neighbor lines, and only the first 64 matches are previewed with an explicit omitted count. -
Release-clean debug dump —
debug::enabledis compiled only underdebug_assertions, matchingdebug_log!, so a release build no longer carries a dead OnceLock probe.format_bytesnow names ESC/CR/LF/TAB, hex-escapes the rest, and truncates at 96 bytes with a remainder marker, pinned so a control byte cannot be echoed raw into a debug line. -
Empty regex replace is a no-op — the public search-and-replace entry now refuses an empty pattern before choosing the literal or regex engine, so
replace_allcannot insert the replacement at every empty match the regex crate would otherwise report. -
Out-of-range ANSI theme fallback —
ThemeExt::ansi_coloris pinned to return the terminal foreground for every index outside 0–15, includingusize::MAX, instead of relying on an untestedelsearm. RGBA iced conversion is also pinned so alpha isu8 / 255. -
Sanitized regex compile errors — find and find-replace both quoted the regex crate's error, which echoes the draft pattern. ESC and bidi marks in an invalid pattern now go through
safe_inline_display(160 bytes) before reaching the search bar or the replace-panel status line. -
Complete numeric config clamp matrix — load-time normalization already bounded font/line-spacing/window size, but padding, opacity, scrollback, and scroll speed were untested. NaN/∞ now fall back, overflow clamps to the documented extrema (font 72, padding 20, opacity 1.0, speed 10, scrollback 100–100_000), and the live clamp helpers no longer need a stale
dead_codeallow. -
Fail-closed link columns and a per-row cap — highlight mapping used
line[..byte_offset], which panics on a mid-codepoint or past-the-end offset after trimming a URL or path. Offsets now go throughstr::getand invalid spans are skipped. One row also stops at 64 actionable highlights so a paste of thousands of URLs cannot unbounded-allocate click targets. -
Bounded history-picker query — the Ctrl+Shift+H overlay accepted iced
text_inputand raw key text into an unboundedString, so a paste could grow the fuzzy haystack without the workflow picker's 4 KiB one-line budget.set_query/push_query_text/backspacenow drop controls, truncate on a UTF-8 boundary, and are the only write path from both the widget and the keyboard handler. -
Bounded command-palette query — Ctrl+Shift+P assigned iced
text_inputand raw key text to an unboundedString, so a paste could grow the fuzzy haystack without the shared 4 KiB one-line budget.set_query/push_query_text/backspacenow drop controls, truncate on a UTF-8 boundary, and are the only write path from both the widget and the keyboard handler. -
Bounded tab-switcher query — Ctrl+Shift+L assigned iced
text_inputand raw key text to an unboundedString, so a paste could grow the fuzzy haystack without the shared 4 KiB one-line budget.set_query/push_query_text/backspacenow drop controls, truncate on a UTF-8 boundary, and are the only write path from both the widget and the keyboard handler. -
Bounded terminal-find query — the in-buffer search bar assigned iced
text_inputand raw key text to an unboundedString, so a paste could compile an unbounded regex against scrollback.set_query/push_query_text/backspacenow drop controls, truncate on a UTF-8 boundary at the same 4 KiB budget as block search, and history recall applies the same bound without dropping the navigation index. -
Bounded find-replace fields — Ctrl+Alt+R assigned iced
text_inputinto unbounded find and replace strings, so a paste could compile an unbounded regex or grow a prompt/clipboard payload without the review budget. Find now shares the 4 KiB search-query cap; replace shares the 256 KiB prompt-insert cap; both drop control characters. -
Bounded workflow argument fields — the parameter form assigned iced
text_inputintoArgsFormwithout a byte cap, so a paste could sit in memory until render refused it.set_valuenow drops controls and truncates on a UTF-8 boundary atMAX_WORKFLOW_FIELD_BYTES(4 KiB). -
Bounded Ask-AI request overlay — the iced field rejected an oversized paste wholesale and the raw-key path dropped extra bytes without truncating, so a 4 KiB+1 paste left the previous request intact and a newline could sit in the overlay until submit. Both paths now drop controls and truncate on a UTF-8 boundary at
MAX_SUGGESTION_REQUEST_BYTES. -
Bounded AI suggestion draft — the review card rejected an oversized iced paste wholesale and stored a model reply verbatim, so a 256 KiB+1 edit bounced and a newline in the generated command sat in the card until insert failed. Both the widget and
apply_replynow drop controls and truncate on a UTF-8 boundary atMAX_REVIEW_INPUT_BYTES. -
Bounded Tasks follow-up composer — the iced field rejected an oversized paste wholesale, so a 16 KiB+1 edit left the previous draft intact and ESC/BEL could sit in the composer until send.
set_follow_upnow keeps newlines and tabs, drops other controls, and truncates on a UTF-8 boundary atNATIVE_AGENT_FOLLOW_UP_MAX_BYTES. -
Bounded AI-chats library filter — the panel search kept the first 1,024 Unicode scalars including ESC/newline, so a paste could restyle the filter and disagreed with the family's 4 KiB overlay budget.
set_searchnow drops controls and truncates on a UTF-8 boundary atMAX_PICKER_QUERY_BYTES. -
Bounded AI-chats title draft — the rename iced field stored the raw paste while only the ChatStore title was normalised, so ESC/newline and a 256-byte+1 paste could sit in the widget next to an 80-char/256-byte persisted title.
renamenow drops controls, replaces visual spoofing with U+FFFD, and truncates at the store envelope before the field updates. -
Bounded Agent-panel composer — iced assigned the prompt into
AgentUi.inputunbounded, so a paste could sit past the 16 KiBsubmit_userenvelope and carry ESC into the session.set_inputnow keeps newlines and tabs, drops other controls, and truncates on a UTF-8 boundary atNATIVE_AGENT_FOLLOW_UP_MAX_BYTES. -
Bounded command-correction draft — the iced card assigned the edited command into
CorrectionProposalunbounded, so a paste could sit past the engine's 16 KiB single-line gate and carry ESC until accept refused.set_draftnow drops controls and truncates on a UTF-8 boundary atMAX_CORRECTION_COMMAND_BYTES. -
Bounded tab-rename draft — the iced rename field stored the raw paste while only submit applied a 64-char filter, so ESC/newline and a 256-byte+1 title could sit in the strip editor next to the snapshot envelope.
TabRenameInputandapply_tab_renamenow sharebound_tab_title_draft(drop controls, truncate atMAX_RESTORED_TAB_TITLE_BYTES). -
Bounded sidebar filter — the files-panel filter assigned iced
text_inputintosidebar_filterunbounded, so a paste could restyle the tree match and grow without the family's 4 KiB overlay budget.bound_sidebar_filternow drops controls and truncates on a UTF-8 boundary atMAX_PICKER_QUERY_BYTES. -
Bounded sidebar path bar — the files-panel path editor assigned iced
text_inputunbounded, so ESC/bidi and a 4 KiB+1 paste could sit until submit refused.bound_sidebar_path_inputnow drops controls and directional marks and truncates on a UTF-8 boundary atMAX_NAVIGATION_PATH_BYTES. -
Bounded sidebar create/rename name — the New File / New Folder / Rename dialog assigned iced
text_inputunbounded, so/, ESC, and a 255-byte+1 paste could sit until submit refused.bound_new_namenow drops controls and slashes and truncates on a UTF-8 boundary at 255 bytes. -
Bounded API-key draft — the settings field stored the raw paste while only
write_api_key_filerefused, so ESC/newline and a 16 KiB paste could sit in RAM next to a credential that must be one line and one byte under the file cap.SetAiKeyDraftnow drops controls and truncates on a UTF-8 boundary atMAX_API_KEY_DRAFT_BYTES. -
Bounded AI model settings field — iced assigned the model name into config unbounded, so ESC/bidi and a 256-byte+1 paste sat until
normalized()replaced it with the default.SetAiModelnow drops controls and visual spoofing and truncates atMAX_CONFIG_NAME_BYTES. -
Bounded AI base-URL settings field — iced assigned the endpoint URL unbounded, so ESC/newline and a 4 KiB+1 paste sat until
normalized()replaced it with the default.SetAiBaseUrlnow usesbound_config_textatMAX_CONFIG_VALUE_BYTES. -
Bounded AI temperature draft — the settings field kept the raw paste so a 32-byte+1 edit sat in the widget while only a parsed 0..=2 value reached config.
SetAiTemperaturenow drops controls and visual spoofing and truncates atMAX_AI_TEMPERATURE_DRAFT_BYTES. -
Bounded AI provider settings field — iced assigned the provider name unbounded, so ESC/bidi and a 256-byte+1 paste sat until
normalized()replaced it with the default.SetAiProvidernow usesbound_config_textatMAX_CONFIG_NAME_BYTES. -
Bounded API-key file path field — iced assigned the credential path unbounded, so ESC/newline and a 4 KiB+1 paste sat until
normalized()dropped it.SetAiKeyFilenow usesbound_config_textatMAX_CONFIG_VALUE_BYTES. -
Bounded remote host name field — iced assigned the display name unbounded, so ESC/bidi and a 256-byte+1 paste sat in config until
validate_remote_hostrefused every consumer.RemoteHostNamenow usesbound_config_textatMAX_CONFIG_NAME_BYTES. -
Bounded remote host address field — iced assigned the hostname unbounded, so ESC/newline and a 4 KiB+1 paste sat until
validate_remote_hostrefused.RemoteHostHostnow usesbound_config_textatMAX_CONFIG_VALUE_BYTES. -
Bounded remote host user field — iced assigned the login name unbounded, so ESC/newline and a 4 KiB+1 paste sat until
validate_remote_hostrefused.RemoteHostUsernow usesbound_config_textatMAX_CONFIG_VALUE_BYTES. -
Bounded custom theme name draft — the theme editor assigned iced
text_inputunbounded, so/, ESC, and a 160-byte+1 paste sat until save refused.ThemeEditNamenow drops controls, path separators, and visual spoofing, and truncates at the core filename envelope. -
Bounded theme hex draft — each color slot stored the raw paste, so ESC and a megabyte hex string sat until save called
hex_to_rgb.ThemeEditColornow keeps an optional#plus six hex digits. -
Bounded Agent proposal-edit field — iced
validate_single_lineon every keystroke cleared the buffer on an oversized or control-bearing paste, wiping the reviewed command.bound_agent_edit_commandnow drops controls and visual spoofing and truncates atMAX_AGENT_COMMAND_BYTES. -
Bounded theme picker value —
SetThemeassigned the pick-list string into config unbounded, thenapply_config()ran immediately, so ESC/bidi and a 256-byte+1 name could restyle chrome untilnormalized()reset it. The handler now usesbound_config_textatMAX_CONFIG_NAME_BYTES. -
Bounded font-family picker value —
SetFontFamilyassigned the pick-list string unbounded, thenapply_config()ran immediately. The handler now usesbound_config_textatMAX_CONFIG_NAME_BYTES. -
Bounded remote host deploy field — iced assigned the deploy mode unbounded, so ESC/bidi and a 256-byte+1 paste sat until
validate_remote_hostrefused.RemoteHostDeploynow usesbound_config_textatMAX_CONFIG_NAME_BYTES. -
Bounded toast chrome —
push_toaststored interpolated paths and errors verbatim, so ESC/bidi and a longio::Errorcould restyle the overlay. Every toast now goes throughsafe_inline_displayat 256 bytes. -
Bounded AI-chats composer — iced assigned the draft through
set_active_draft, which truncates at 64 KiB but keeps ESC, so a paste could restyle the composer until send.set_draftnow keeps newlines and tabs, drops other controls, and truncates atMAX_LIVE_MESSAGE_BYTES. -
Bounded sidebar transfer/busy notices — transfer progress interpolated filenames and
DirectoryError::busystored backend text verbatim, so ESC/bidi and a long name could restyle the files panel. Both now go throughsafe_inline_displayat 192 bytes. -
Bounded Agent status line — protocol and IO errors were assigned into
AgentUi.statusverbatim, so ESC/bidi and a long transport error could restyle the panel.set_statusnow usessafe_inline_displayat 256 bytes. -
Bounded AI-chats notice line — restore/provider errors were assigned into the panel notice verbatim, so ESC/bidi and a long decode error could restyle the library chrome.
set_noticenow usessafe_inline_displayat 256 bytes. -
Bounded AI provider chrome label —
display_name()was copied into Agent and AI Chats headers verbatim, so a hostile local-provider name could restyle the panel. Both now usebound_provider_labelat 256 bytes. -
Bounded files-panel notice store — worker errors, transfer ticks, and paste/delete failures were assigned into
sidebar_noticeraw, so ESC/bidi could restyle the Files chrome even when a later busy path was bounded.set_sidebar_noticeand the notice view now runbound_sidebar_notice. -
Bounded AI-chats lifecycle status — store Thinking/Info/Error text was drawn verbatim in the panel status line, so a persisted or transport error with ESC/bidi could restyle the library chrome.
status_linenow runsbound_chat_noticeat 256 bytes. -
Agent edit start truncates instead of bouncing —
AgentEditStartrefused the whole proposal when the command was over budget or contained a control, so Edit never opened. It now usesprepared_agent_edit_commandso an oversized paste still opens a truncated, control-stripped buffer. -
Bounded theme-editor error line — save/validation failures were drawn as danger chrome verbatim, so an IO error with ESC/bidi could restyle the overlay. Store and draw now run
bound_theme_editor_errorat 256 bytes. -
Bounded files-dialog path and error chrome — New File / Rename overlays drew
path.display()and validation errors as iced text, so a hostile filename or quoted problem could restyle the modal. Both now runbound_sidebar_notice. -
Bounded block-search query-error chrome — regex compile failures quote the draft and were drawn as danger chrome verbatim. The matcher now sanitizes
InvalidRegexpayloads; the picker stores and drawsbound_query_errorat 160 bytes. -
Bounded delete-confirm path list — the Files delete modal drew up to five
path.display()values as iced text, so a hostile filename could restyle the confirmation. Those rows now usebound_sidebar_path_label. -
Bounded drop-import target path chrome — hover/import notices and the transfer label interpolated
target_dir.display()raw. Those now usebound_sidebar_path_labelso a hostile drop destination cannot restyle the Files notice. -
Bounded files context-menu path header — the right-click menu drew
path.display()as the header, so a hostile filename could restyle the overlay. Single-item headers now usebound_sidebar_path_label. -
Bounded startup diagnostics overlay — config, session, and keybinding load failures were drawn verbatim, so ESC/bidi in a path or parse reason could restyle the attention banner. Store and draw now run
bound_diagnostic_text(keeps newlines, 1 KiB). -
Bounded drop-plan path problems — import refusals interpolated
path.display()into IO and plan errors, so a hostile dropped name could restyle later Files notices. Those messages now usebound_sidebar_path_label/bound_sidebar_notice. -
Bounded Find-bar error chrome — scrollback search assigned engine errors into the overlay status verbatim.
set_error_messageand the Find bar now runbound_query_errorso a quoted pattern cannot restyle it. -
Bounded workflow-form render feedback —
render()errors were copied into the argument overlay as danger chrome, quoting the template/values.set_feedbacknow usessafe_inline_displayat 256 bytes. -
Bounded AI-suggestion card chrome — insert refusals interpolated the prompt-boundary reason verbatim, and the provider badge copied
display_name()raw. Feedback is now 256-bytesafe_inline_display; the badge usesbound_provider_label. -
Bounded command-correction card chrome — accept refusals and the destructive-risk line interpolated engine/PTY reasons as danger chrome. Store and draw now run
bound_correction_feedbackat 256 bytes. -
Files notices store through one setter — leftover multiline
sidebar_notice = Some((…))assignments still stored raw labels and navigation errors. They now callset_sidebar_notice, including follow destination labels. -
Bounded Agent transcript chrome — user/thought/say/protocol lines, observation samples, streaming preview, and the destructive-risk label were drawn verbatim. They now run
bound_transcript_text(keep newlines, 1 KiB) including streamed proposed commands. -
Bounded Files-tree scan-error rows — root and child Error/RefreshError rows interpolated
DirectoryErroras danger chrome. Those lines now runbound_sidebar_notice. -
Bounded tab labels including close-confirm chrome —
bound_tab_title_draftdropped Cc but not bidi, and close-confirm interpolated process names verbatim. Drafts and session/pane labels now strip bidi like OSC titles. -
Bounded workflow picker name chrome — list names, tags, descriptions, and the argument-form header were drawn from disk files verbatim. Those strings now run
bound_workflow_feedbackat 256 bytes. -
Bounded history-picker cwd chrome — recall rows interpolated the 16 KiB-index cwd as iced text.
display_cwdnow escapes spoofing and truncates to 80 characters. -
Bounded pane-header cwd, git, and process chrome — the split pane header interpolated
cwd_display(),format_strip, and the foreground process verbatim. Those now use the tab-title envelope / toast bound. -
Bounded OSC desktop-notification chrome — OSC 9/777 strings from the PTY were forwarded to
notify-sendverbatim. Title and body now share the toast envelope (controls/spoofing stripped, 256 bytes). -
Bounded status-bar segment chrome — bottom-bar cwd/git/grid labels from
jterm_core::bottom_bar::composewere drawn as iced text verbatim. Each segment now uses the toast envelope. -
Bounded Agent attached-context command chrome — the sidebar card interpolated
last_manual_completed.cmd(up to the history command budget) as iced text. Display now uses the diagnostic envelope. -
Bounded Files-tree entry names — directory listings interpolated remote/local filenames as iced text. Names now share the sidebar envelope (without the empty-notice fallback).
-
Bounded command-block menu preview — the overlay truncated the command to 240 chars but left OSC/bidi and the zone cwd raw. Preview and cwd now reuse
display_command/display_cwd. -
Bounded block-search hit previews — picker rows clipped length but interpolated OSC/bidi from command/output lines. Overlay draw now runs
visible_bounded(match spans stay on the stored original). -
Bounded long-block desktop-notification command — OSC 133 completion toasts forwarded the raw command to
notify-send. The title now usesdisplay_command. -
Bounded workflow-arg form labels — argument names/descriptions from disk workflows were interpolated as iced labels and placeholders. They now share the workflow-feedback envelope.
-
Bounded AI chat turn chrome — library transcript/partial replies were drawn as iced text with stored spoofing/control characters. Display now keeps newlines, replaces spoofing, and stays inside the live-message budget.
-
Bounded Files-panel header leaf — the Files title button used the current directory's raw
file_name(). It now shares the sidebar filename envelope (breadcrumbs were alreadysafe_inline_display). -
Bounded Agent-task provider chrome — task list, action buttons, and toasts interpolated
provider.display_name()verbatim. Those now share the provider-label envelope. -
Bounded shortcut-label chrome — help overlay grouped chords and the palette reverse-lookup join were drawn as iced text. Labels now share a 256-byte
safe_inline_displayenvelope. -
Bounded block-search context and block-menu meta chrome — the secondary hit line and the command-block menu status/cwd row concatenated extras after sanitizing the command. The whole line now runs
display_block_search_text. -
Bounded native-session snapshot chrome — phase Debug, command status, and approval-kind Debug were interpolated as iced text. Those now use the toast / visible_bounded envelopes.
-
Bounded settings theme-picker names — custom theme filenames from disk were listed in the iced pick_list verbatim. Names now run
bound_custom_theme_namebefore display. -
Bounded command-correction card title chrome — title, badge, and description were drawn from the engine as iced text. Those now share the correction-feedback envelope at draw.
-
Ask-AI request strips visual spoofing — overlay typing dropped controls but kept bidi/invisible characters in the NL request (and the review card interpolated
compact_one_lineverbatim). Request ingest now replaces spoofing; the card line uses the toast envelope. -
Ask-AI draft strips visual spoofing — the reviewable command draft dropped controls but kept bidi in model replies and user edits. Draft ingest now replaces spoofing with U+FFFD.
-
Bounded Agent-task git-diff header — the diff card interpolated
requested_base()as iced text. The header now uses the toast envelope (display_requested_base). -
Correction-card draft strips visual spoofing — overlay typing dropped controls but kept bidi in the editable correction command. Draft ingest now replaces spoofing with U+FFFD.
-
Find-bar query strips visual spoofing — the Find overlay dropped controls but kept bidi in the regex/literal query (and history restore). Query ingest now replaces spoofing with U+FFFD.
-
Find & Replace fields strip visual spoofing — search and replace inputs dropped controls but kept bidi. Field ingest now replaces spoofing with U+FFFD.
-
Bounded Find & Replace status chrome — engine errors were stored and drawn as iced text. Status now uses the query-error envelope at ingest and draw.
-
Command-palette query strips visual spoofing — overlay typing dropped controls but kept bidi in the fuzzy query. Query ingest now replaces spoofing with U+FFFD.
-
History-picker query strips visual spoofing — recall overlay typing dropped controls but kept bidi in the filter query. Query ingest now replaces spoofing with U+FFFD.
-
Workflow overlay typing strips visual spoofing — picker queries and argument fields dropped controls but kept bidi. Ingest now replaces spoofing with U+FFFD before the core picker/form.
-
Files-tree filter strips visual spoofing — the inline name filter dropped controls but kept bidi. Filter ingest now replaces spoofing with U+FFFD.
-
AI chat library search strips visual spoofing — the library filter dropped controls but kept bidi. Search ingest now replaces spoofing with U+FFFD.
-
Tab-switcher query strips visual spoofing — the jump-to-tab overlay dropped controls but kept bidi in the filter. Query ingest now replaces spoofing with U+FFFD.
-
API-key draft strips visual spoofing — the settings key field dropped controls but kept bidi. Draft ingest now replaces spoofing with U+FFFD.
-
AI chat composer strips visual spoofing — the live chat draft kept newlines but also kept bidi. Composer ingest now replaces spoofing with U+FFFD.
-
Agent composer strips visual spoofing — the session composer kept newlines but also kept bidi. Composer ingest now replaces spoofing with U+FFFD.
-
Task follow-up composer strips visual spoofing — the Tasks follow-up field kept newlines but also kept bidi. Follow-up ingest now replaces spoofing with U+FFFD.
-
New-file name drafts strip visual spoofing — New File / Rename dialogs dropped slashes and controls but kept bidi. Name ingest now replaces spoofing with U+FFFD.
-
Agent edit-command drafts replace visual spoofing — the proposal editor dropped bidi silently. Edit ingest now replaces spoofing with U+FFFD so the reviewed command stays visible.
-
Settings text fields replace visual spoofing — live config ingest dropped bidi silently. Settings drafts now replace spoofing with U+FFFD.
-
Custom theme names replace visual spoofing — the theme-editor name dropped bidi silently. Name ingest now replaces spoofing with U+FFFD.
-
Files path bar replaces remaining visual spoofing — the path field dropped bidi marks but kept zero-width spoofing. Ingest now replaces all visual spoofing with U+FFFD, and navigation rejects replacement text.
-
OSC window titles replace remaining visual spoofing — PTY titles dropped bidi marks but kept zero-width spoofing. Title ingest now replaces all visual spoofing with U+FFFD.
-
Tab-rename drafts replace remaining visual spoofing — the rename field stripped bidi marks but kept zero-width spoofing. Draft ingest now replaces all visual spoofing with U+FFFD.
-
Restored tab titles replace remaining visual spoofing — snapshot loads stripped bidi marks but kept zero-width spoofing. Restore now replaces all visual spoofing with U+FFFD.
-
Agent git-diff display replaces controls with U+FFFD — review diffs mapped ESC/bidi to ASCII
?, which could hide the neutralization. Diff display now uses U+FFFD and re-truncates after the wider replacement. -
OSC 8 hyperlinks reject visual spoofing — interned link URIs and ids were size-checked but still admitted bidi and zero-width marks. Spoofed OSC 8 fields now close the current link instead of being interned.
-
OSC 7 cwd reports reject visual spoofing — decoded pane cwd rejected only NUL. Newlines, bidi, and zero-width marks now refuse the report and keep the last accepted directory.
-
Foreground process names are bounded for pane chrome —
/proccomm strings reached the pane header raw. Names are now display-bounded and visual spoofing is replaced before the ▶ chip is drawn. -
Remote archive members reject visual spoofing — download containment refused controls but still admitted bidi and zero-width names. Spoofed members now fail the archive instead of extracting.
-
Remote home probe rejects remaining visual spoofing — login-home ingest dropped bidi marks but kept zero-width spoofing. The probe now refuses any visually spoofed absolute path.
-
API key files reject visual spoofing — credential IO dropped controls but still stored bidi and zero-width marks. Read and write now refuse spoofed keys instead of persisting them.
-
Kitty associated-text drops visual spoofing — CSI-u reports stripped control bytes but forwarded bidi/zero-width as associated text. Spoofing is now dropped before those fields are encoded.
-
xterm modifyOtherKeys skips visual spoofing in committed text — the report used the first non-control scalar, including bidi. Spoofing is skipped so the real layout character remains the key codepoint.
-
Files listings skip control and spoofed names — remote/local directory rows kept newline and bidi filenames. Those names are now omitted before they can become clickable tree entries.
-
File-path links refuse visual spoofing before open — a click resolved any existing path, including bidi filenames. Spoofed file links now fail closed instead of launching the opener.
-
File-path detection skips visual spoofing — highlighted paths still became clickable when they contained bidi or zero-width marks. Spoofed paths are no longer detected as file links.
-
Block-search queries strip visual spoofing — the overlay query was length-bounded but kept bidi. Query ingest now drops controls and replaces spoofing with U+FFFD.
-
Kitty graphics errors strip visual spoofing — protocol error replies dropped controls but kept bidi. Spoofing is now replaced with U+FFFD before the APC response is written to the PTY.
-
Restored session cwds reject visual spoofing — snapshot working directories were only checked for size and NUL. Bidi and zero-width marks now discard the cwd before a pane is spawned there.
-
New-file names refuse visual spoofing — dialog ingest replaced bidi with U+FFFD, but create/rename still accepted the neutralized string. Validation now refuses spoofing and replacement characters.
-
Custom theme names refuse U+FFFD — editor ingest replaced bidi with replacement characters, but save still accepted them as filenames. Persist validation now refuses U+FFFD.
-
Restored tab titles refuse spoofing — snapshot titles with controls, bidi marks, or U+FFFD are discarded instead of becoming custom labels. Rename persist uses the same fail-closed check.
-
OSC 9/777 refuse rewritten toasts — PTY-authored desktop notifications whose title or body contained controls or visual spoofing are dropped instead of being handed to notify-send as U+FFFD text.
-
OSC 52 clipboard refuses spoofing — decoded clipboard sets that carry bidi marks, replacement characters, or odd controls are dropped before they reach the host clipboard. Newlines and tabs remain allowed.
-
OSC 5522 mime refuses spoofing — clipboard-read mime types that are empty, overlong, non-ASCII, or visually spoofed are refused with ENOSYS instead of being queued for a host clipboard read.
-
Review text refuses U+FFFD — single-line review validation and prompt payload sanitization refuse replacement characters so a neutralized spoofed draft cannot be approved or inserted into the shell.
-
Find-replace output refuses spoofing — apply refuses a replacement that still contains U+FFFD, odd controls, or visual spoofing so it cannot reach the clipboard or be typed into the prompt.
-
Config persist refuses U+FFFD — normalized settings, history paths, and remote-host validation refuse replacement characters so a neutralized draft cannot become a model name, theme, path, or ssh operand.
-
History cwd refuses U+FFFD — shared-history working directories that contain replacement characters are dropped so a neutralized path cannot sit beside a replayed command.
-
Ask-AI requests refuse spoofing — command-suggestion sessions refuse natural-language requests that still contain U+FFFD, controls, or visual spoofing instead of sending them to the model.
-
Ask-AI drafts refuse spoofing — a generated command that still contains U+FFFD or visual spoofing fails the card instead of entering review, and insert validation refuses replacement characters.
-
Ask-AI cwd/shell refuse spoofing — suggestion sessions refuse spoofed working directories and shell names so they cannot be sent as model prompt context.
-
Ask-AI block context refuses spoofing — selected-block command, output, or cwd that still contains visual spoofing or U+FFFD cannot be attached as model prompt context.
-
Agent block context refuses spoofing — manual completions, Fix/Explain, and Ask-AI-about-block refuse visual spoofing and U+FFFD so they cannot become model evidence.
-
Agent composer refuses spoofing — composer submit and Fix/Explain initial prompts refuse U+FFFD and visual spoofing so a neutralized draft cannot become a model user turn.
-
Correction accept refuses U+FFFD — the command-correction card refuses a draft that still contains replacement characters so a neutralized edit cannot be inserted or run.
-
Correction begin refuses spoofing — a failed command that still contains visual spoofing or U+FFFD never opens a correction card, and worker candidates with the same defect are not presented.
-
Workflow insert refuses spoofing — a rendered workflow command that still contains U+FFFD or visual spoofing is refused instead of being typed at the prompt for review.
-
Block reinput refuses spoofing — reinput of selected block commands refuses the whole batch when any command still contains visual spoofing or U+FFFD, rather than typing it at the prompt.
-
Block recall refuses spoofing — recalling a selected block command refuses visual spoofing and U+FFFD so a neutralized OSC 133 line cannot be typed at the prompt.
-
Block clipboard copy refuses spoofing — copying selected block commands, output, or Markdown refuses visual spoofing and U+FFFD so they cannot reach the host clipboard.
-
Last/single block output copy refuses spoofing — copying last command output, a single block's output, or a whole block refuses visual spoofing and U+FFFD so they cannot reach the host clipboard.
-
Visible selection copy refuses spoofing — copying a visible terminal text selection (including primary on mouse-up) refuses visual spoofing and U+FFFD so they cannot reach the host clipboard.
-
Directory listings refuse U+FFFD names — local and remote directory listings skip names that contain U+FFFD so a neutralized filename cannot appear in the Files tree.
-
Block Markdown copy refuses spoofing — copying a single block as Markdown refuses visual spoofing and U+FFFD, and a selected Markdown copy that fails that check reports spoofing instead of a size error.
-
Files Copy Path refuses spoofing — Files "Copy Path" refuses visual spoofing and U+FFFD so a neutralized listing path cannot reach the host clipboard.
-
AI chat send refuses U+FFFD — sending an AI Chats composer draft that still contains U+FFFD or visual spoofing is refused so a neutralized paste cannot leave the machine.
-
AI chat rename refuses U+FFFD — renaming an AI chat refuses a title that still contains U+FFFD or visual spoofing so a neutralized name is not persisted.
-
Session export refuses spoofing — session Markdown/JSON export refuses command, output, or cwd that still contains visual spoofing or U+FFFD so it cannot be written to disk.
-
OSC 8 hyperlinks refuse U+FFFD — OSC 8 hyperlinks refuse a URI or id that still contains U+FFFD so a neutralized target cannot be interned or clicked.
-
OSC 133 metadata refuses U+FFFD — OSC 133 command, cwd, and correlation metadata refuse U+FFFD so a neutralized shell identity cannot be retained on a block.
-
Task follow-up send refuses U+FFFD — native Tasks follow-up send refuses a composer payload that still contains U+FFFD or visual spoofing so a neutralized paste cannot leave the machine.
-
AI chat shell context skips spoofing — recent shell context shared with AI Chats skips commands that still contain visual spoofing or U+FFFD so they cannot leave the machine.
-
Git diff cwd refuses spoofing — opening a Tasks Git diff refuses a worktree cwd that still contains visual spoofing or U+FFFD so Git is not spawned on a neutralized path.
-
Failed-block Agent eligibility refuses spoofing — creating a Fix/Explain Agent or Tasks job from a failed block refuses a command or cwd that still contains visual spoofing or U+FFFD.
-
API key files refuse U+FFFD — reading or writing an API key file refuses U+FFFD so a neutralized credential cannot be stored or loaded.
-
Block search query refuses spoofing — cross-block search refuses a query that still contains visual spoofing or U+FFFD so a neutralized paste cannot scan command history.
-
Find query refuses spoofing — terminal find refuses a query that still contains visual spoofing or U+FFFD so a neutralized paste cannot scan scrollback.
-
Find & Replace fields refuse spoofing — Find & Replace refuses to run when the find or replace field still contains visual spoofing or U+FFFD so a neutralized paste cannot rewrite a selection.
-
Workflow arguments refuse spoofing — workflow argument fields refuse visual spoofing and U+FFFD so a neutralized paste is not stored or rendered into the prompt.
-
Command palette query refuses spoofing — the command palette returns no matches when the query still contains visual spoofing or U+FFFD so a neutralized paste cannot select an action.
-
History picker query refuses spoofing — the command-history picker returns no matches when the query still contains visual spoofing or U+FFFD so a neutralized paste cannot recall a command.
-
Tab switcher query refuses spoofing — the tab switcher returns no matches when the query still contains visual spoofing or U+FFFD so a neutralized paste cannot jump to a tab.
-
Workflow picker query refuses spoofing — the workflow picker returns no matches and cannot accept a workflow when the query still contains visual spoofing or U+FFFD.
-
AI chats library search refuses spoofing — the AI chats library returns no matches when the search still contains visual spoofing or U+FFFD so a neutralized paste cannot open a saved chat.
-
Files-panel filter refuses spoofing — the files-panel name filter yields no matches when the query still contains visual spoofing or U+FFFD so a neutralized paste cannot reveal a tree path.
-
OSC titles refuse U+FFFD — OSC 0/1/2 window and icon titles refuse a payload that still contains U+FFFD after sanitization so a neutralized title cannot restyle app chrome.
-
Kitty protocol errors refuse U+FFFD — Kitty graphics protocol error replies omit neutralized text so U+FFFD or visual spoofing cannot leave through an APC-G response.
-
Image drops refuse U+FFFD — dropped image paths that still contain U+FFFD are refused so a neutralized filename cannot be typed at the prompt.
-
Correction draft refuses spoofing — the command-correction card does not save a draft that still contains visual spoofing or U+FFFD, so a neutralized paste cannot become the insert/run command.
-
Ask-AI review draft refuses spoofing — the Ask-AI review card does not save a draft that still contains visual spoofing or U+FFFD, so a neutralized paste cannot become the insert command.
-
Agent composer refuses spoofing — the Agent composer does not save a message that still contains visual spoofing or U+FFFD, so a neutralized paste cannot become the next turn.
-
Tasks follow-up refuses spoofing — the Tasks follow-up composer does not save a message that still contains visual spoofing or U+FFFD, so a neutralized paste cannot become the next native turn.
-
AI chats composer refuses spoofing — the AI chats composer does not save a message that still contains visual spoofing or U+FFFD, so a neutralized paste cannot become the next turn.
-
AI chat rename draft refuses spoofing — the AI chats rename editor does not keep a title that still contains visual spoofing or U+FFFD, so a neutralized paste cannot become the saved name.
-
Find & Replace fields refuse spoofing — Find & Replace does not keep a find or replace field that still contains visual spoofing or U+FFFD, so a neutralized paste cannot run.
-
Find query field refuses spoofing — terminal find does not keep a query that still contains visual spoofing or U+FFFD, so a neutralized paste cannot scan scrollback.
-
Find history restore refuses spoofing — terminal find does not restore a history entry that still contains visual spoofing or U+FFFD, so a neutralized saved query cannot scan scrollback.
-
Block search query field refuses spoofing — cross-block search does not keep a query that still contains visual spoofing or U+FFFD, so a neutralized paste cannot scan command history.
-
Command palette query refuses spoofing — the command palette does not keep a query that still contains visual spoofing or U+FFFD, so a neutralized paste cannot select an action.
-
History picker query refuses spoofing — the command-history picker does not keep a query that still contains visual spoofing or U+FFFD, so a neutralized paste cannot recall a command.
-
Tab switcher query refuses spoofing — the tab switcher does not keep a query that still contains visual spoofing or U+FFFD, so a neutralized paste cannot jump to a tab.
-
Workflow picker query refuses spoofing — the workflow picker does not keep a query that still contains visual spoofing or U+FFFD, so a neutralized paste cannot accept a workflow.
-
AI chats library search refuses spoofing — the AI chats library search does not keep a query that still contains visual spoofing or U+FFFD, so a neutralized paste cannot open a saved chat.
-
Files-panel filter refuses spoofing — the files-panel name filter does not keep a query that still contains visual spoofing or U+FFFD, so a neutralized paste cannot reveal a tree path.
-
Ask-AI request overlay refuses spoofing — the Ask-AI overlay does not keep a request that still contains visual spoofing or U+FFFD, so a neutralized paste cannot leave as a suggestion prompt.
-
API key draft refuses spoofing — the settings API-key field does not keep a draft that still contains visual spoofing or U+FFFD, so a neutralized paste cannot be stored as a credential.
-
Custom theme name refuses spoofing — the theme-editor name field does not keep a name that still contains visual spoofing or U+FFFD, so a neutralized paste cannot sit as a filename.
-
Files-panel path bar refuses spoofing — the files-panel path bar does not keep a path that still contains visual spoofing or U+FFFD, so a neutralized paste cannot sit as a navigation target.
-
Remote host address refuses spoofing — the remote-host address field does not keep a hostname that still contains visual spoofing or U+FFFD, so a neutralized paste cannot sit as a connection target.
-
Remote host name refuses spoofing — the remote-host label field does not keep a name that still contains visual spoofing or U+FFFD, so a neutralized paste cannot sit as a saved host identity.
-
Remote host user refuses spoofing — the remote-host user field does not keep a login that still contains visual spoofing or U+FFFD, so a neutralized paste cannot sit as an SSH identity.
-
Remote host deploy mode refuses spoofing — the remote-host deploy field does not keep a mode that still contains visual spoofing or U+FFFD, so a neutralized paste cannot sit as a session-deploy policy.
-
AI provider setting refuses spoofing — the settings AI-provider field does not keep a provider that still contains visual spoofing or U+FFFD, so a neutralized paste cannot sit as a backend name.
-
AI model setting refuses spoofing — the settings AI-model field does not keep a model that still contains visual spoofing or U+FFFD, so a neutralized paste cannot sit as a backend identifier.
-
AI base URL setting refuses spoofing — the settings AI base-URL field does not keep a URL that still contains visual spoofing or U+FFFD, so a neutralized paste cannot sit as an endpoint.
-
API key file path refuses spoofing — the settings API-key file field does not keep a path that still contains visual spoofing or U+FFFD, so a neutralized paste cannot sit as a credential-file location.
-
Theme setting refuses spoofing — the settings theme field does not keep a name that still contains visual spoofing or U+FFFD, so a neutralized paste cannot sit as the active theme identity.
-
Font family setting refuses spoofing — the settings font-family field does not keep a family that still contains visual spoofing or U+FFFD, so a neutralized paste cannot sit as the terminal typeface.
-
AI temperature draft refuses spoofing — the settings temperature field does not keep a draft that still contains visual spoofing or U+FFFD, so a neutralized paste cannot sit as a sampling value.
-
Agent edit command refuses spoofing — the Agent command-edit field does not keep a command that still contains visual spoofing or U+FFFD, so a neutralized paste cannot sit as an approved agent command.
-
Agent edit open refuses spoofing — Edit on an Agent proposal does not open a draft that still contains visual spoofing or U+FFFD, so a neutralized command cannot sit waiting for Approve.
-
Agent edit open names unsafe commands — refusing a spoofed Agent proposal reports an unsafe command instead of calling it empty, so the operator can tell a neutralized paste from a blank one.
-
Foreground process names omit spoofing — a
/proccomm that still contains visual spoofing or U+FFFD is omitted from the pane header, so a neutralized process name cannot sit in chrome. -
Provider labels omit spoofing — a provider display name that still contains visual spoofing or U+FFFD is replaced with a generic label, so a neutralized backend name cannot sit in chrome.
-
Diagnostic overlay omits spoofing — startup and session diagnostics drop visual spoofing and U+FFFD instead of drawing replacement characters, so a neutralized error cannot sit in the overlay.
-
Toast chrome omits spoofing — toasts drop U+FFFD leftover from neutralized spoofing, so a replacement character cannot sit in chrome.
-
Theme-editor errors omit spoofing — theme-editor error chrome drops U+FFFD leftover from neutralized spoofing, so a replacement character cannot sit next to the swatches.
-
Files-panel notices omit spoofing — files-panel status chrome drops U+FFFD leftover from neutralized spoofing, so a replacement character cannot sit next to the tree.
History recall improvements continue with rounds 388–393:
-
Directory and exit-status filters — the persisted-history picker snapshots the invoking pane's cwd and combines exact-directory matching with All/Success/Failed filters, before applying the visible-result cap. Missing or unsafe cwd disables the directory filter. Mouse chips and Ctrl+D / Ctrl+O share the same selection-resetting state transitions.
-
Review-only duplicate folding — Unique / Ctrl+U keeps the newest matching execution per command+cwd after the status filter, so later success cannot hide earlier failure. Folding never mutates the history snapshot or its shared JSONL file; Reset restores the unfiltered query.
-
Cached history matching — immutable record indices are computed on query/filter changes and reused for drawing, arrow navigation and Enter. Rejected or unchanged queries preserve the cache and current selection. Fuzzy ties retain newest-first order, and matches beyond the 15-row cap remain available through a narrower query or filter.
-
Explicit result counts and empty states — the picker shows displayed rows, matching total and recent-record count; an empty filtered result suggests adjusting/resetting filters rather than claiming no history. A scrollable result list leaves filters and keyboard hints accessible.
-
Execution-preserving history snapshot — the picker reads complete raw JSONL records from a 4 MiB tail, up to 2,000 executions, instead of the core reader's command-only deduplication. Incomplete head/tail records and unsafe commands are omitted, older records outside either bound are explicitly marked, and reads reuse the no-follow, nonblocking, owner/single-link descriptor checks of private persistence. Disk-backed regression coverage proves a newer success cannot erase an older failed execution or a command's distinct cwd.
-
Restored strict quality gates — five failures reproduced on the original commit are aligned with the current safe-display, exact-replay and last-good-cwd contracts. Test helpers leave production builds, obsolete hardcoded color defaults are removed, and existing formatting and Clippy findings are corrected. OSC 5522 debug logging now uses the bounded byte preview and records only the payload length.
Verification for rounds 388–393: formatting, warnings-denied Clippy and rustdoc, all 985 tests, and the locked release build pass. An isolated X11 window verifies combined directory/status/unique filtering, mouse Reset, keyboard selection scrolling and command insertion at an OSC 133 prompt; the inserted sentinel command remains unexecuted.