{"@attributes":{"version":"2.0"},"channel":{"title":"ix","link":"https:\/\/ix.dev\/","description":"The changelog of the base: what changed in the index, the open monorepo every ix VM boots from.","language":"en-us","lastBuildDate":"Thu, 23 Jul 2026 19:05:00 GMT","item":[{"title":"POC: Gherkin (cucumber-rs) specs audited with cargo-mutants","link":"https:\/\/ix.dev\/gherkin-mutation-poc","guid":"https:\/\/ix.dev\/gherkin-mutation-poc","pubDate":"Thu, 23 Jul 2026 19:05:00 GMT","description":"POC: Gherkin (cucumber-rs) specs audited with cargo-mutants. packages\/gherkin-poc is a worked example of two testing techniques the repo had no sample of: - Gherkin \/ BDD: the behavior lives in tests\/features\/account.feature as plain-language Given\/When\/Then scenarios (overdraft floor, rejected operations leaving the balance untouched), bound to the crate API by cucumber-rs step definitions. The suite runs under the normal libtest harness (cucumber's suggested harness = false is only about output ordering), so nextest and cargo-unit treat it like any other test. - Mutation testing: the crate README records a cargo mutants --package gherkin-poc run against the suite, per the rust-style skill's guidance that cargo-mutants is a package-owner audit tool, not a CI gate. The crate is deliberately tiny: an Account with integer-cent balances and an overdraft limit, just enough arithmetic and boundary conditions to give mutants something to bite."},{"title":"nix-cargo-unit: unit names now hash build-script-run deps (collision fix)","link":"https:\/\/ix.dev\/cargo-unit-run-dep-name-collision","guid":"https:\/\/ix.dev\/cargo-unit-run-dep-name-collision","pubDate":"Thu, 23 Jul 2026 19:00:00 GMT","description":"nix-cargo-unit: unit names now hash build-script-run deps (collision fix). nix-cargo-unit dedupes units by a full-identity hash that walks every dependency, but named the resulting derivations with a second hash that skipped build-script-run dependencies. Two merged units that differed only in which build-script-run variant they depended on rendered under the same attr key, and the generated cargo-units.nix failed to import with attribute already defined. The trigger was mundane: adding a dev-dependency (cucumber, for the Gherkin POC) perturbed feature unification between the [--workspace] and [--workspace --tests] target sets, producing two tree-sitter build-script-run variants whose downstream tree-sitter-dockerfile-updated run units collided. The name hash now includes run-custom-build dependency edges, mirroring the merge hash, so names are injective over merged units. Cost: a one-time rename of every unit whose closure contains a build script, i.e. most of the workspace rebuilds once. Toolchain bumps already rename every unit the same way, so this is an established event class, not a new one."},{"title":"Agents use the Elixir kernel and keep a native shell","link":"https:\/\/ix.dev\/elixir-mcp-native-shell","guid":"https:\/\/ix.dev\/elixir-mcp-native-shell","pubDate":"Thu, 23 Jul 2026 19:00:00 GMT","description":"Agents use the Elixir kernel and keep a native shell. Agent wrappers now start the persistent Elixir MCP server by default. The old default started the Python notebook kernel even after the workstation configuration had moved to BEAM. Codex also keeps its native shell when the kernel is present. Commands and search use the native shell. Stateful Elixir, fleet, and data work stay in the kernel. A dead MCP connection no longer removes the only command path."},{"title":"Key-blind agent sandboxing is a reusable module","link":"https:\/\/ix.dev\/sandboxed-agent-module","guid":"https:\/\/ix.dev\/sandboxed-agent-module","pubDate":"Thu, 23 Jul 2026 19:00:00 GMT","description":"Key-blind agent sandboxing is a reusable module. The kernel-build example's sandbox machinery moved into services.sandboxed-agent, a general module: a fixed-uid confined user, a uid-keyed nftables egress policy that admits exactly one loopback port, a credential-injecting proxy that owns the secret, a systemd-run wrapper on PATH, and nix-daemon scoping. Consumers set the agent's user, uid, command, and the proxy's upstream host, header, and key file. The proxy itself is now a small Rust binary built with cargo-unit, replacing the example's stdlib Python. It relays upstream responses byte for byte, so SSE streams and compressed bodies pass through untouched. The kernel-build example shrank to intent: git-clone the tree, install the toolchain, point the module at Claude."},{"title":".ix modules accept TypeScript type annotations, lowered to runtime checks","link":"https:\/\/ix.dev\/typed-ix-annotations","guid":"https:\/\/ix.dev\/typed-ix-annotations","pubDate":"Thu, 23 Jul 2026 19:00:00 GMT","description":".ix modules accept TypeScript type annotations, lowered to runtime checks. .ix files can now carry TypeScript annotations: parameter and return types, type aliases, and as casts. ix2nix lowers each annotation to a runtime check in the emitted Nix, so passing a set where a string goes fails with a positioned error naming the module, line, column, and argument instead of a deep eval trace: The pieces: - Annotations lower to calls against a small __ixTy runtime the import shims pass per module; the wrapper convention is now { __dir, __importIx, __ixTy }:. - Two modes, chosen at import time over identical emitted source: assert (the default in both shims) runs the checks; erase makes them free. - Checks are force-safe: a check may force the annotated value to weak head normal form and read attrset keys, never field values or list elements, so typed code never evaluates deeper than untyped code. - Type spellings: string, bool, int, float, drv, object, T[], Record<string, T>, inline object types with optional fields, function types, literal unions, T | null, Rust-width integer refinements u8\/u16\/u32\/i8\/i16\/i32, and the nixpkgs lib.types basics port, path, nonEmptyStr. number is a hard error (Nix splits int and float), as are interfaces, generics, and satisfies, in keeping with the converter's no-fallbacks rule. - as T checks at the cast site, the opposite of TypeScript erasure, on purpose: casts are where JSON and imported plain Nix enter typed code. as unknown stays the escape hatch. Annotations are optional everywhere; unannotated modules emit no __ixTy reference at all. Static checking (tsc against an ambient ix.d.ts) and a deep-checking mode are follow-ups."},{"title":"main lint gate green again after quoting a legacyPackages splice","link":"https:\/\/ix.dev\/lint-main-unquoted-splice","guid":"https:\/\/ix.dev\/lint-main-unquoted-splice","pubDate":"Thu, 23 Jul 2026 16:15:00 GMT","description":"main lint gate green again after quoting a legacyPackages splice. nix run .#lint was red on main: astlog's no-unquoted-splice rule flagged nixpkgs.legacyPackages.${hostSystem} in lib\/default.nix (the importIxFor helper), where the antiquote interpolates outside a string. The fix quotes the splice \u2014 legacyPackages.\"${hostSystem}\" \u2014 matching the existing call site in lib\/image\/default.nix. While clearing that, the lint run also caught lib\/per-system.nix out of alejandra formatting (the kernel-unit attrs under the x86_64-linux optionalAttrs block were under-indented), so that file was reformatted too \u2014 whitespace only, no syntactic change. The lint gate on main now reports zero blocking findings."},{"title":"Package names are IFD-free again: example fan-outs move to legacyPackages","link":"https:\/\/ix.dev\/ifd-free-package-names","guid":"https:\/\/ix.dev\/ifd-free-package-names","pubDate":"Thu, 23 Jul 2026 14:05:00 GMT","description":"Package names are IFD-free again: example fan-outs move to legacyPackages. The default.ix example conversion (#4079) quietly made the flake's packages.<system> key set depend on an IFD: classifying single- versus multi-VM examples forced every example through the ix2nix converter derivation, and lifecyclePackages spread those keys into packages. Any consumer that merely probes index.packages.<system> then needs IFD, and ix's CI lint gate is exactly such a consumer: a no-build proof that evaluates with allow-import-from-derivation disabled and max-jobs 0. Both mains went red for a few hours on 2026-07-23. The fix (#4088) moves the example-derived fan-outs out of packages: the per-example health-check-* lifecycle packages and the <example>-{up,health,...} fleet wrappers now live under legacyPackages.<system>, the same surface that already keeps kernel-unit's eval-time IFD out of every gate closure. The health-checks and health-checks-zellij runners keep their static names in packages. Operator-visible changes: - nix build .#health-check-<example> becomes nix build .#legacyPackages.<system>.health-check-<example>; same for the <example>-up style wrappers - examples\/nixos\/switch-multi moved to examples\/multi-vm\/switch-multi - the invariant to keep: packages.<system> names must stay enumerable with IFD disabled; example-derived or unit-graph-derived names belong in legacyPackages The same restoration landed two unrelated latent breakages that had merged while the ix gate was down: a missed Error::internal_error call-site migration in orch-rpc (ix#8366) and treefmt drift in the run-viewer slices."},{"title":"mkapp scaffolds ship the real shadcn-svelte registry, themed from the ghostty palettes","link":"https:\/\/ix.dev\/mkapp-shadcn-svelte","guid":"https:\/\/ix.dev\/mkapp-shadcn-svelte","pubDate":"Thu, 23 Jul 2026 05:40:00 GMT","description":"mkapp scaffolds ship the real shadcn-svelte registry, themed from the ghostty palettes. The mkapp template used to carry three hand-rolled components (Button, Card, Skeleton) that imitated the shadcn look in scoped CSS. Agents building anything past a card wall were on their own. The template now vendors the actual shadcn-svelte registry: 54 components (accordion to tooltip, including dialog, table, tabs, sidebar, and sonner) under staging\/lib\/components\/ui\/, on Tailwind v4 and bits-ui, imported through a $lib alias that the staging typecheck resolves against the tree the agent actually edited. Only chart (layerchart) and form (needs SvelteKit) are left out. Colors come from the operator's terminal. A build-time script parses the ghostty themes (modules\/home\/ghostty\/themes\/custom-light, custom-dark) and emits the shadcn variable block as light-dark() pairs, so scaffolded apps match the terminal in both modes, follow the OS scheme with no .dark class to toggle, and never hand-copy a hex value. Changing the terminal theme re-themes every future scaffold at the next nix build. The cost is measured and modest: a cold npm install in a fresh scaffold goes from 4.7s to 7.5s, and node_modules from 55MB to 124MB. The serve gate (npm run check:staging, then promote) typechecks the full vendored tree, about 1100 files, in roughly 3s."},{"title":"Agent memory grows an entity graph: about edges, minted ids, graph-walking recall","link":"https:\/\/ix.dev\/memory-entity-graph","guid":"https:\/\/ix.dev\/memory-entity-graph","pubDate":"Thu, 23 Jul 2026 00:45:00 GMT","description":"Agent memory grows an entity graph: about edges, minted ids, graph-walking recall. Memories were flat facts: a slug, a one-line hook, a prose body. Related knowledge was findable only by regex or embedding similarity. Now each memory links into an entity graph: Memory.remember(..., about: [\"gh:indexable-inc\/ix#8088\", \"host:hydra\"]) writes typed edges to minted ids for issues and PRs (gh:<org>\/<repo>#<n>), repos, hosts, tools, and people, and Memory.graph\/1 walks them. The store's recall projection (journal data, not engine code) traverses these edges two hops out, degree-gated so hubs like a whole repo don't drown the walk: recalling one memory about an issue now surfaces the sibling memories that cite the same issue, host, or tool. The 1,000 existing memories were backfilled with 2,175 about edges; the id convention lives in the store itself as mem:memory-ontology, and the SessionStart digest teaches agents to write the granular form at the moment of learning."},{"title":"Jason works in kernel exec cells","link":"https:\/\/ix.dev\/kernel-jason-compat","guid":"https:\/\/ix.dev\/kernel-jason-compat","pubDate":"Wed, 22 Jul 2026 19:00:00 GMT","description":"Jason works in kernel exec cells. Agents writing cells for the Elixir kernel (ix-mcp-ex) reach for Jason.decode! out of habit -- it has been the de facto JSON library for a decade -- and until now every such cell died with UndefinedFunctionError: module Jason is not available, wasting a full exec round trip before the agent rediscovered OTP's built-in JSON module. The kernel release now carries jason ~> 1.4 as a plain runtime dep, so both spellings work in cells. Nothing in the kernel itself switches over: the MCP wire format and internal encoding still ride the built-in JSON module. Jason is purely a compatibility affordance for agent muscle memory. It was already in the dependency closure (credo pulls it in for the test environment), so the vendored-deps hash and lockfile are unchanged; the dep only had to be promoted so mix release ships it."},{"title":"mkapp + Serve.app, check-gated hot reload for AI-built web apps","link":"https:\/\/ix.dev\/mkapp-serve-gated-hot-reload","guid":"https:\/\/ix.dev\/mkapp-serve-gated-hot-reload","pubDate":"Wed, 22 Jul 2026 19:00:00 GMT","description":"mkapp + Serve.app, check-gated hot reload for AI-built web apps. An agent building a web page for the user used to hand over a file, or worse, a screenshot. Now it hands over a live URL. mkapp scaffolds a Svelte 5 + Vite + TypeScript app whose store survives hot reloads (an import.meta.hot handoff plus a debounced sessionStorage mirror), with shadcn-style Button\/Card\/Skeleton primitives on the dashboard's Obsidian tokens and an AgentStatus strip that narrates what the agent is doing. The kernel's Serve.app(dir) runs npm run dev and a gate loop as supervised jobs. The agent edits the app's staging\/ tree only; when an edit settles (~300ms), the gate runs npm run check:staging (svelte-check plus tsc) and promotes green code into the live src\/, which Vite hot-reloads without losing the store. A red check leaves the last good page up and records the compiler output where Serve.status(dir) shows it, so a typo never replaces a working page with a blank one. Once vite prints its port, the kernel writes ESC ]5522;open-url;<url> BEL to the ix-term session pts (or \/dev\/tty), and the terminal opens the app in its split-view doc pane (ix#8185). A new prompt rule tells agents to narrate through the store's status field, mark still-generating sections loading so skeletons show, and keep updating the page until the work is done."},{"title":"Child-agent spawns name a missing working directory instead of dying as exit 2","link":"https:\/\/ix.dev\/agents-cli-runner-cwd","guid":"https:\/\/ix.dev\/agents-cli-runner-cwd","pubDate":"Wed, 22 Jul 2026 16:30:00 GMT","description":"Child-agent spawns name a missing working directory instead of dying as exit 2. The Cmd fix for the missing-working-directory failure left one sibling untouched: the agent CLI runner opens its child port the same way, so a child spawned into a deleted or bogus directory died as the raw chdir errno -- exit 2 for a missing path, exit 20 for a file -- with empty output, indistinguishable from the CLI failing on its own. The runner now validates the working directory before the spawn and raises an error naming the path (with the launch-dir hint when the boot-time default vanished), so the lead sees the real culprit instead of a bare exit status. A directory deleted after the check is caught too: a nonzero exit with the directory now gone raises with the errno caveat rather than reporting a status that may not be the CLI's own. Clean exits and runs that already produced a final result are never second-guessed."},{"title":"cargo-unit plans against a manifest-scoped stub, so a source edit no longer re-resolves the workspace","link":"https:\/\/ix.dev\/manifest-scoped-cargo-planner","guid":"https:\/\/ix.dev\/manifest-scoped-cargo-planner","pubDate":"Wed, 22 Jul 2026 16:00:00 GMT","description":"cargo-unit plans against a manifest-scoped stub, so a source edit no longer re-resolves the workspace. Fixes #3900. Every buildWorkspace evaluation runs two eval-time IFD stages: a whole-workspace cargo build --unit-graph resolve and a nix-cargo-unit render. Both used to take the full source tree as input, so editing one line in any crate re-ran both planners before a single unit could cache-hit. Cargo's planning phase reads manifest contents (Cargo.toml, Cargo.lock, the in-tree cargo config) and discovers targets by file existence alone; it never opens a target file and runs no build script. The unit-graph stage now plans inside a stub tree built from exactly those inputs: manifests verbatim, every other source file an empty stub at its exact relative path. A body edit changes neither the manifest slice nor the path list, so the planner derivation is untouched; adding or removing files, or editing a manifest, still re-plans. The render stage keeps the real tree as input because its include_str! closure scan reads file contents, and it rewrites the stub's store prefix back to the real source so rendered output is byte-identical to the old path. Net effect on the repo workspace: a .rs body edit re-runs one cheap render instead of the render plus four whole-workspace cargo resolves, and the edited crate's unit is the only unit derivation that changes. (authored by Claude Code, Fable 5)"},{"title":"each bundled mcp module's Nix definition now lives next to its Python source","link":"https:\/\/ix.dev\/mcp-per-module-dirs","guid":"https:\/\/ix.dev\/mcp-per-module-dirs","pubDate":"Wed, 22 Jul 2026 16:00:00 GMT","description":"each bundled mcp module's Nix definition now lives next to its Python source. packages\/mcp\/default.nix had grown into a 5,900-line file holding every bundled module's definition and most of its 77 test derivations. Each module directory now carries a module.nix next to its Python source (src\/imessage\/module.nix, plus ix_notebook_mcp\/module.nix for the server package), discovered by the same marker-file walk the package registry uses for package.nix. A module file declares only the slice of the assembly it touches and returns its module derivation and tests; sibling modules arrive under their historical bindings, so the per-module test envs from #3897 (passthru.ixFirstPartyDeps, firstPartyClosure) moved verbatim with their modules. default.nix shrinks to assembly: third-party pins, the pinned interpreter, env composition, the shipped wrappers, and the genuinely cross-module tests. Adding a bundled module is now mkdir src\/&lt;name&gt; plus a module.nix; no registry edit. Behavior is preserved: the test attribute list is byte-identical on darwin and linux, and 75 of the 77 test derivations kept their exact store paths (the two that moved only re-order the interpreter env's inputs). Fixes #3928 (authored by Claude Code, Fable 5)"},{"title":"Rev-pinned builtins.fetchGit inputs evaluate without network once cached","link":"https:\/\/ix.dev\/fetchgit-head-cache","guid":"https:\/\/ix.dev\/fetchgit-head-cache","pubDate":"Wed, 22 Jul 2026 14:45:00 GMT","description":"Rev-pinned builtins.fetchGit inputs evaluate without network once cached. Evals that use builtins.fetchGit with a pinned rev (crane's vendorCargoDeps for Cargo.lock git dependencies, for example) used to spawn a serial git ls-remote --symref <url> HEAD per input on every evaluation, even when the rev was already in the local git cache: the fetcher resolved the remote's default ref before deciding whether anything needed fetching, and the cached HEAD file could never refresh once fetches stopped happening, so after one tarball-ttl expiry the network round trip recurred forever. On hydra this put 40-100s of git subprocess wall time on top of an ~18s-cpuTime home-manager eval (#4028). The libfetchers: resolve git refs lazily and refresh the cached HEAD patch in the nix-ix fork series (an indexable-inc\/nix megamerge commit) makes the daemon resolve git refs lazily (a cached rev-pinned input now runs zero remote git subprocesses, allRefs included) and refreshes the cached HEAD after every successful network lookup, bounding HEAD resolution for unpinned inputs to once per tarball-ttl instead of once per eval. Measured on a warm nix eval --no-eval-cache of a rev-pinned github input: 448ms and one network ls-remote per eval before, 37ms and zero git subprocesses after."},{"title":"Fork patches are now jj megamerge commit DAGs","link":"https:\/\/ix.dev\/jj-megamerge-forks","guid":"https:\/\/ix.dev\/jj-megamerge-forks","pubDate":"Wed, 22 Jul 2026 14:30:00 GMT","description":"Fork patches are now jj megamerge commit DAGs. All 14 maintained forks moved from in-repo patch series (patches\/*.patch plus a generated dag.json) to real fork repos where every patch is a git commit whose parents are its true dependencies. An \"ix megamerge\" commit seals each series: its tree equals the full series applied linearly, its parents are the DAG heads, and the flake input pins that one commit, so builds fetch a plain git tree and never apply patches. What this deletes: rebase-patches and its scratch-repo rebase engine, the byte-commutation check, the per-attempt closure gates, mirror fork-branch, and every dag.json. Rebases are now jj rebase in the fork repo (colocated; the remote stays plain git); a conflict marks the guilty patch commit instead of aborting the run, and jj evolog keeps every prior version of every patch across bumps. Two rules keep the setup honest. Every bookmark push carries a permanent refs\/pins\/<date>-<sha12> ref in the same operation that bumps flake.lock, because jj rebases rewrite history and GitHub GCs unreachable commits: the pin refs are what keep every previously locked megamerge fetchable. And a conflicted jj commit is never pushed: plain-git readers cannot parse jj's conflict encoding. Migration fidelity: 11 of 13 megamerge trees hash byte-identical to the old patchedSrc outputs. The two diffs favor the new world: the old nix tree shipped GNU patch .orig backup droppings, and mesa's CSVs picked up the CRLF its own .gitattributes asks for (GitHub tarballs honor it; the old git fetch did not)."},{"title":"js-nix: write Nix in JavaScript syntax, compiled at eval time by builtins.wasm","link":"https:\/\/ix.dev\/js-nix-wasm-builtin","guid":"https:\/\/ix.dev\/js-nix-wasm-builtin","pubDate":"Wed, 22 Jul 2026 11:30:00 GMT","description":"js-nix: write Nix in JavaScript syntax, compiled at eval time by builtins.wasm. The nix-ix daemon now carries builtins.wasm (patches 0038-0039, imported from the open upstream PR NixOS\/nix#15380, the upstream form of Determinate Nix's Wasm support): Nix expressions can call functions in WebAssembly modules, gated behind the wasm-builtin experimental feature. Our import goes one step past upstream and forces deterministic execution (NaN canonicalization, deterministic relaxed SIMD), because eval results cross machines here and one implementation-defined bit means divergent store hashes. Upstream left that off after measuring ~3.6x on a float-heavy kernel; eval plugins are string\/AST-shaped and do not pay it. First consumer: js-nix. A .ix file is JavaScript syntax mapped 1:1 onto Nix semantics; packages\/nix\/ix2nix (Rust, oxc parser, typed Nix AST, one renderer) converts it, compiled to an 884 KB wasm plugin the evaluator runs directly: importIx (from packages\/nix\/ix2nix\/import-ix.nix) evaluates that through builtins.wasm + builtins.toFile; relative .ix imports work via a threaded __dir. Anything without a 1:1 Nix equivalent is a positioned compile error surfaced through the eval error (=== gets \"use ==\" with line and column). The e2e passthru test builds nix-ix from the flake in-sandbox and runs the whole chain."},{"title":"The kernel grew an agent coordination bus: issue feed, atomic pickup, session messaging, Requests","link":"https:\/\/ix.dev\/kernel-agent-work-bus","guid":"https:\/\/ix.dev\/kernel-agent-work-bus","pubDate":"Wed, 22 Jul 2026 11:30:00 GMT","description":"The kernel grew an agent coordination bus: issue feed, atomic pickup, session messaging, Requests. Four stacked changes turned the mcp-ex kernel from a per-session tool server into a coordination bus for every agent on a host, using the SQLite database they already share as the arbiter and feed. Issues announce themselves. An always-on IssueWatch polls gh search issues and pushes every newly filed issue into connected sessions as a source=\"issues\" channel event. Background agents file issues nobody watches; now the filing itself is the notification. Pickup is atomic. Issues.pickup(n) claims an issue before work starts. The claim is a UNIQUE-constrained insert in the shared database: one winner, losers read back who got there first, and the claim mirrors to GitHub as an assignee. No two agents build the same fix again. Sessions can see and message each other. Sessions.list() shows every kernel session on the host with heartbeat liveness; Sessions.send(id, text) delivers into the target session as a channel event within seconds (NULL target broadcasts). The delivery sweep is a per-instance cursor over a shared table, so every session hears every message exactly once. Requests generalize all of it. Requests.post\/pickup\/done puts any unit of work on offer: a review, an eval run, a PR to babysit. Issue pickup is now just a Request of kind issue; an append-only request_events table drives a posted\/claimed\/done feed on the same 3-second tick as messaging. Schema landed at v8 with the old claims table migrated in and dropped, no compatibility shims. Scope, stated plainly: the bus is per host, because the shared SQLite is the transport. Cross-host coordination stays with the fleet layer."},{"title":"Kernel commands name a missing working directory instead of failing as exit 2","link":"https:\/\/ix.dev\/cmd-dead-launch-cwd","guid":"https:\/\/ix.dev\/cmd-dead-launch-cwd","pubDate":"Wed, 22 Jul 2026 07:30:00 GMT","description":"Kernel commands name a missing working directory instead of failing as exit 2. Kernel commands default to the directory captured at boot, so pathless Cmd.run and Cmd.sh always resolve against the launch checkout. When that directory was deleted mid-session -- a cleaned-up worktree, a recreated checkout -- every command from then on returned {\"\", 2} in about zero seconds, echo included, while :os.cmd\/1 and the File helpers kept working. The port child reports a failed chdir by exiting with the raw errno and nothing on stdout, indistinguishable from the command itself failing, so nothing pointed at the real culprit. Both entry points now validate the effective cd: before spawning and raise an error naming the target: a missing directory (with a hint when it is the boot-time launch dir that vanished), a path that is not a directory, or an otherwise unusable one. A directory deleted between the check and the spawn is caught after the fact too -- a nonzero exit with the directory now gone raises instead of handing back a status that may be errno rather than the command's own. The silent failure mode is gone; a vanished directory is one loud error naming its path, not a session's worth of empty exit-2s."},{"title":"Codex now follows the current MCP generation","link":"https:\/\/ix.dev\/codex-mcp-wrapper-ownership","guid":"https:\/\/ix.dev\/codex-mcp-wrapper-ownership","pubDate":"Wed, 22 Jul 2026 07:00:00 GMT","description":"Codex now follows the current MCP generation. Fixes #4069. Codex could keep launching the old Python ix-mcp after the workstation moved to the Elixir mcp-ex server. The command was copied into ~\/.codex\/config.toml; runtime edits made that file conflict with its next declared base, so Home Manager left every incoming change unapplied. MCP server paths now ride Codex's forced wrapper layer. config.toml is fully app-owned, so project trust and UI settings need no Nix merge. Each wrapper generation supplies its matching MCP executable even when the app file still contains an older entry. (sent by an AI agent via Codex)"},{"title":"Kernel commands no longer follow the movable OS cwd","link":"https:\/\/ix.dev\/per-session-kernel-cwd","guid":"https:\/\/ix.dev\/per-session-kernel-cwd","pubDate":"Wed, 22 Jul 2026 07:00:00 GMT","description":"Kernel commands no longer follow the movable OS cwd. Fixes #3902. When several agent sessions share one index kernel, the OS process cwd is BEAM-global: any cell that calls File.cd!\/1 moves it for everyone. One session wandered into its \/tmp worktree that way, and a sibling's pathless git reset --hard resolved against the shared cwd and wiped the worktree's staged work. Cmd.run\/3 and Cmd.sh\/2 now never consult the OS cwd at spawn time. The launch directory is captured once at application boot, before any cell can run, and every pathless command defaults its cd: to that immutable capture; subagent spawns (Agents.spawn without :cwd) pin to the same directory. Working anywhere else takes an explicit cd: or git -C -- one agent's File.cd!\/1 can no longer aim another agent's commands at a foreign checkout. (authored by Claude Code, Fable 5)"},{"title":"the stock-Nix parse gate is sharded per top-level directory","link":"https:\/\/ix.dev\/stock-nix-parse-shards","guid":"https:\/\/ix.dev\/stock-nix-parse-shards","pubDate":"Wed, 22 Jul 2026 07:00:00 GMT","description":"the stock-Nix parse gate is sharded per top-level directory. The stock-Nix parse gate (stock-nix-parse, from #3898) parsed every tracked .nix outside tests\/ in one derivation, so touching any .nix file anywhere invalidated the whole check and re-parsed the entire tree. The gate is now one check per top-level directory: checks.<system>.stock-nix-parse-lib, stock-nix-parse-packages, and so on, plus stock-nix-parse-root for the top-level files themselves (flake.nix). Each shard's source is the intersection of the git-tracked .nix fileset with that subtree, so an edit under packages\/ rebuilds only the packages shard; every other shard stays a byte-identical derivation and a cache hit. The shard set is discovered from the repo tree with builtins.readDir, never hand-listed, so a new top-level directory cannot silently escape coverage. Directories without tracked .nix files drop out, and tests\/ stays excluded the same way it always was: it is the fork-syntax island, the one place fork-only literals are allowed. An eval-time assertion compares the union of the shard filesets against the original whole-surface fileset, so a shard-construction bug fails evaluation instead of quietly narrowing the gate. Fixes #3929 (authored by Claude Code, Fable 5)"},{"title":"editing one mcp module no longer reruns all ~60 bundled test derivations","link":"https:\/\/ix.dev\/mcp-per-module-test-envs","guid":"https:\/\/ix.dev\/mcp-per-module-test-envs","pubDate":"Wed, 22 Jul 2026 04:30:00 GMT","description":"editing one mcp module no longer reruns all ~60 bundled test derivations. The mcp package's import\/smoke\/typecheck derivations all consumed one mega-interpreter carrying every bundled Python module, so a one-line edit to any module rebuilt that env and reran every module's tests. Each bundled module now declares its first-party imports on its own definition (passthru.ixFirstPartyDeps), and every per-module test gets an interpreter holding the shared third-party base plus only the modules under test and their declared-dep closure. Touching src\/linear now re-derives the linear and nox_autotriage tests and the strict-typecheck gate; the other 70-odd test derivations keep their store paths and stay cached. strictTypecheck also stopped copying all of src\/: its check tree is assembled from the green modules' build outputs plus that same closure, so a module outside the allowlist and its imports can no longer invalidate the gate. The view smoke stopped interpolating the whole package directory as its fixture and plants a temp tree instead. The shipped ix-mcp artifact is untouched -- its derivation hash is byte-identical to before."},{"title":"Action-log busy waits stop camping on dirty schedulers and outwaiting their callers","link":"https:\/\/ix.dev\/action-log-call-outwaits-busy","guid":"https:\/\/ix.dev\/action-log-call-outwaits-busy","pubDate":"Wed, 22 Jul 2026 02:30:00 GMT","description":"Action-log busy waits stop camping on dirty schedulers and outwaiting their callers. The action log waits out a sibling instance's sqlite write lock before failing loudly. Two things were wrong with how it waited. First, the wait ran entirely inside the exqlite NIF, which occupies one of the BEAM's roughly ten dirty IO scheduler slots for its whole duration; stack a few concurrent waiters and the pool starves out the very COMMIT or ROLLBACK that would release the lock, so waits sailed past every configured bound. Second, the 5-second busy bound equaled the default GenServer.call timeout, so a caller always timed out before the server could raise its diagnosis naming the blocked statement. The busy-wait regression test caught the combination live: it failed once and passed once on the identical derivation, dying as a bare 5-second call timeout even though the blocking transaction had been released after 300ms. The busy bound is now a wall-clock deadline enforced in Elixir: each NIF call waits at most 50ms inside sqlite, and the rest of the budget is scheduler-free sleeps between attempts, so a blocked statement never starves its own release. Every public ActionLog function calls with a 30-second bound, comfortably above the busy budget, and a compile-time guard keeps that ordering so the loud-failure path stays reachable. The issue-claim API, which had still been on the 5-second default and outside the restart-retry seam, now rides both, and a claim retried across a server restart reads back as the win it already is instead of reporting the claimant as losing the issue to itself. The contention tests give the lock holder 20 seconds of headroom so a loaded sandbox cannot starve the release past the busy wait."},{"title":"Checks live next to what they check; flake.nix goes back to being a manifest","link":"https:\/\/ix.dev\/per-system-split","guid":"https:\/\/ix.dev\/per-system-split","pubDate":"Wed, 22 Jul 2026 01:30:00 GMT","description":"Checks live next to what they check; flake.nix goes back to being a manifest. lib\/per-system.nix had grown to 2,600 lines, about 600 of them inline pkgs.runCommand policy checks: the lint gate, the astlog rule self-test, the cross-Darwin smokes, one user's zellij and nushell config validation. Every check re-stated the same success-marker epilogue, and most took the whole tracked tree as their source, so any file edit anywhere rebuilt them. Each check now lives in a checks.nix next to what it checks (astlog-rules\/, packages\/blast-radius\/, packages\/code\/scipql\/, users\/andrewgazelka\/, lib\/dev\/, lib\/util\/, lib\/services\/, lib\/darwin\/) and builds through the one shared mkScriptCheck shape in lib\/checks.nix. Sources are scoped filesets: the astlog self-test rebuilds only when rules or fixtures change, the personal config checks read only the tracked dotfiles, and the whole-tree exception (the repo lint gate) is documented at its binding. The personal zellij\/nushell wiring, including the owner's hardcoded XDG path, moved under users\/andrewgazelka; the aggregator only imports the check surface that directory exposes. The flake top level got the same treatment (#3899): the ~230 lines of output wiring (the Linux-to-Darwin alias graft, the required-gate root union, and the 18 inline homeModules\/darwinModules compositions) moved behind lib\/flake-outputs.nix and lib\/home-modules.nix (with the personal profile composition in lib\/profiles.nix), so flake.nix names inputs and delegates. Check names, package attrs, and module surfaces are unchanged; the attrName lists eval byte-identical before and after."},{"title":"blast-radius-test no longer reruns on every commit, and a lint keeps it that way","link":"https:\/\/ix.dev\/scoped-check-sources","guid":"https:\/\/ix.dev\/scoped-check-sources","pubDate":"Wed, 22 Jul 2026 01:20:00 GMT","description":"blast-radius-test no longer reruns on every commit, and a lint keeps it that way. A build-DAG audit found that blast-radius-test copied the entire tracked repository into its sandbox to run one shell script that reads its own test directory plus .github\/workflows\/blast-radius.yml. Any tracked edit in any language invalidated the check, so it reran on effectively every commit. Its source is now a fileset scoped to exactly those two paths, so it only reruns when the script, its fixtures, or the workflow change. To keep the class of bug from coming back, a new astlog rule no-whole-repo-fileset-source errors on any fileset = binding that passes the repo root straight to gitTracked instead of intersecting with the owning subtree. The one legitimate whole-tree consumer, the repo linter itself, is annotated in place with the reason."},{"title":"ix-fleet builds again: SDK wheel pins bumped for apply_vm_groups","link":"https:\/\/ix.dev\/ix-fleet-sdk-apply-vm-groups","guid":"https:\/\/ix.dev\/ix-fleet-sdk-apply-vm-groups","pubDate":"Wed, 22 Jul 2026 00:40:00 GMT","description":"ix-fleet builds again: SDK wheel pins bumped for apply_vm_groups. Every fleet example's nix run .#<example>-up wrapper had been unbuildable since fc4e94a1: the group reconcile path calls Client.apply_vm_groups(vm, groups) (the region-correct, set-based replacement for the old create_group + add_group_member pair, ENG-2752), but the prebuilt ix_sdk wheels pinned in packages\/ix-sdk-python\/pins.json predated that method, so ix-fleet's type check failed with \"Client\" has no attribute \"apply_vm_groups\". Both wheel pins (x86_64-linux via the ix publish sdk python workflow, aarch64-darwin via a local nix run .#publish-sdk-python) now point at wheels built from the same ix main commit, and the ix-sdk-python import check asserts apply_vm_groups is present so a stale wheel fails at the pin, not at deploy."},{"title":"Kernel action log waits out sqlite write contention instead of killing the job","link":"https:\/\/ix.dev\/action-log-busy-wait","guid":"https:\/\/ix.dev\/action-log-busy-wait","pubDate":"Wed, 22 Jul 2026 00:30:00 GMT","description":"Kernel action log waits out sqlite write contention instead of killing the job. Every kernel instance on a machine appends to one shared actions.db. When two jobs wrote at once, sqlite could report :busy to one of them after exqlite's default 2-second wait, and the ActionLog GenServer crashed on a bare pattern match -- taking the caller's job down over a log write nothing on the hot path reads. The connection now opens with a 5-second busy timeout, and a step that still comes back :busy after that bound fails with an error naming the blocked statement instead of a badmatch. There are no unbounded retries: the wait is the bound, the supervisor reopens the log after a crash, and concurrent jobs no longer die to each other's write locks."},{"title":"Kernel Edit honors the worktree guard; jobs survive action-log crashes","link":"https:\/\/ix.dev\/kernel-edit-guard-durable-jobs","guid":"https:\/\/ix.dev\/kernel-edit-guard-durable-jobs","pubDate":"Wed, 22 Jul 2026 00:30:00 GMT","description":"Kernel Edit honors the worktree guard; jobs survive action-log crashes. Two kernel defects observed under normal multi-agent load on 2026-07-21, fixed at their seams. Kernel writes now honor the primary-checkout worktree guard (#3871). The claude-code wrapper's PreToolUse hook denies native Edit\/Write under the primaryCheckouts globs, but Edit.write\/Edit.replace in an exec cell never pass through hooks, and a background agent used exactly that gap to dirty a primary main checkout. The kernel's Edit module enforces the same denylist itself: same glob knobs (CLAUDE_CODE_PRIMARY_CHECKOUTS over IX_DEFAULT_PRIMARY_CHECKOUTS, colon-separated), same linked-worktree escape hatch, same kill switch, same refusal message. The workstation profile states the glob list once and feeds both consumers -- the hook wrapper and, via a device-level session variable, every kernel the agents spawn. The action log no longer dies over a busy database, and jobs no longer die over the action log (#3874). Several kernel instances share one SQLite action log; a sibling holding the write lock returned SQLITE_BUSY, which match-crashed the ActionLog GenServer. The exit propagated into every caller: a running cargo test job's control process died mid output-flush, vanished from the registry with its terminal notification, and under sustained contention the crash loop could exhaust the root supervisor and take the whole kernel down. Now sqlite waits out contention (busy_timeout plus a bounded retry), the ActionLog client API absorbs the restart blip instead of forwarding the exit, job processes treat ledger writes as degradable (a failed flush retries next tick, a failed terminal transition re-arms until it lands), and the reaper never crashes over a ledger call -- its monitor map is the only record of which jobs are still guarded. Regression tests hold a write transaction from a second connection and kill the log mid-flush; the job finishes, stays in the registry, and the durable row lands after the restart. Written by Claude Code (Fable 5), an AI coding agent."},{"title":"Duplication swarm lands eight dedup PRs and an embedding dupe-finder CLI","link":"https:\/\/ix.dev\/dedup-swarm-embed-cli","guid":"https:\/\/ix.dev\/dedup-swarm-embed-cli","pubDate":"Wed, 22 Jul 2026 00:00:00 GMT","description":"Duplication swarm lands eight dedup PRs and an embedding dupe-finder CLI. The repo had two duplicate-code detectors but only one you could call: the AST scanner (nix run .#clone) had a CLI, while the embedding-based semantic finder lived only inside the ix-mcp kernel's Python. PR #3913 gives it a real binding: nix run .#embed -- dupes . --k 40 --json runs the Qwen3-embedding miner from any shell, so Elixir (and CI) can reach type-4 semantic clones the AST gate cannot see. An agent swarm then worked the AST scanner's top clusters as eight parallel PRs (#3915, #3916, #3918, #3919, #3920, #3921, #3922 plus the CLI), folding twin test bodies, sibling functions, and copied helpers across nu-py, google, evals, search, sqlmerge, clone-detect, and claude-hooks. Landing the swarm exposed a CI outage that had been running since 2026-07-19: six broken lanes had merged red-on-red while the gate was already failing, each masking the next. The suspected ci-budget validation clock was measured and proven innocent; the real reds were a clippy fork-lint violation, an mcp tool-surface drift, a playwright driver\/npm pin mismatch, five updates entries with bare link URLs, a type_complexity lint, and one flaky cancellation window. Fixed at source across #3935, #3936, #3945, and #3950; main's flake-check is green again as of run 29891202900."},{"title":"nwm home|os switch refuses a dirty flake tree by default","link":"https:\/\/ix.dev\/nwm-dirty-flake-guard","guid":"https:\/\/ix.dev\/nwm-dirty-flake-guard","pubDate":"Wed, 22 Jul 2026 00:00:00 GMT","description":"nwm home|os switch refuses a dirty flake tree by default. nwm home switch and nwm os switch used to build and activate whatever was sitting in the flake working tree. Nix copies dirty tracked files into the eval, so a switch from a dirty repo deployed uncommitted WIP with no record of what actually ran. Both switch subcommands now check the flake directory before anything builds: if it is a git repo and git status --porcelain prints anything at all (untracked files included), the switch aborts with the flake dir and the status listing on stderr. Pass --allow-dirty to switch anyway. home build \/ os build, nwm flake update, and the plain nix passthrough are unchanged, and a flake dir that is not a git repo is not guarded. This mirrors the guard andrewgazelka\/nix#148 added to that repo's scripts\/switch.sh."},{"title":"zellij-config check green again after backslash keybind fix","link":"https:\/\/ix.dev\/zellij-config-bind-fix","guid":"https:\/\/ix.dev\/zellij-config-bind-fix","pubDate":"Wed, 22 Jul 2026 00:00:00 GMT","description":"zellij-config check green again after backslash keybind fix. The zellij-config check had been failing on main: two keybinds in users\/andrewgazelka\/config\/zellij were written with a double-escaped backslash (\\\\\\\\ in Nix source, four backslashes), so the KDL renderer, which escapes backslashes itself, emitted a key spec that decodes to two literal backslash characters. Zellij rejects that with Invalid key: '\\\\' because a character key must be a single character. The fix halves the escaping in the Nix source so the rendered KDL carries exactly one backslash: the pane-split binds are now Ctrl \\ in Normal mode and \\ \/ | in Pane mode, with the same NewPane actions as before. The rendered config now passes zellij setup --check (zellij 0.44.3) and the checks.x86_64-linux.zellij-config derivation builds green."},{"title":"Agent memory: rich recall rows, typed edges, verification receipts","link":"https:\/\/ix.dev\/memory-recall-edges-verify","guid":"https:\/\/ix.dev\/memory-recall-edges-verify","pubDate":"Tue, 21 Jul 2026 23:30:00 GMT","description":"Agent memory: rich recall rows, typed edges, verification receipts. Three gaps in the weave-backed agent memory (IxMcp.Memory, aliased Memory in the kernel) are closed. Memory.recall\/2 rows used to carry only the entity and its one-line hook, so trusting or killing a memory meant a manual journal dig. Rows now carry the fact id (feeds Memory.retract\/1), seq and time (staleness is visible), type, the live topic tags, handle, the long-form body resolved from CAS via weave get, and the latest verification receipt. Matching is whole-word by default: recall(\"hil\") no longer matches every hook containing \"while\" (match: :substring restores the old behavior), and limit: caps the rows at 20 unless raised. Memory.remember\/3 takes supersedes: and relates: opts whose values are mem: entities, written as entity-valued facts; weave treats those as typed edges, so a correction is an explicit mem\/supersedes edge instead of an accident of newer-wins ordering. Memory.graph(slug) returns the edge neighborhood in both directions. Memory.verify(slug) appends a mem\/verified-at fact holding the UTC timestamp and a provenance string (kernel session name, else CLAUDE_SESSION_ID, else user@host). The SessionStart digest now ranks hooks by verification freshness first and write recency second, so a re-checked memory outranks a merely recent one. Written by Claude Code (Claude Opus 4.5), an AI coding agent."},{"title":"Kernel: Cmd.run gives subprocesses a stdin that EOFs","link":"https:\/\/ix.dev\/kernel-cmd-stdin-eof","guid":"https:\/\/ix.dev\/kernel-cmd-stdin-eof","pubDate":"Tue, 21 Jul 2026 22:33:21 GMT","description":"Kernel: Cmd.run gives subprocesses a stdin that EOFs. Three kernel jobs in one evening hung for 69 to 142 seconds and had to be cancelled. All were rg invoked through System.cmd\/3 with no path argument: rg with no path and a non-tty stdin falls back to searching stdin, and a BEAM port's stdin pipe never closes, so the read blocks forever. The same queries with an explicit path returned in a tenth of a second. Cells now get a blessed Cmd helper, pre-aliased like the rest of the surface. Cmd.run(\"rg\", [\"-n\", \"pat\"], cd: dir) is System.cmd\/3 with stdin redirected from \/dev\/null: the command is exec'd through sh with $0\/$@, so no shell parsing touches the arguments and no extra process outlives the redirect, which keeps job cancellation seeing one process tree. Cmd.sh(\"rg pat | head\") does the same for one-line pipelines, redirecting the whole script so pipeline heads see EOF too. The kernel's exec instructions now steer subprocess spawns to Cmd.run instead of asking agents to remember an explicit path. Written by Claude Code, an AI coding agent."},{"title":"The clone ratchet now actually gates Elixir","link":"https:\/\/ix.dev\/clone-detect-elixir-gate","guid":"https:\/\/ix.dev\/clone-detect-elixir-gate","pubDate":"Tue, 21 Jul 2026 22:30:00 GMT","description":"The clone ratchet now actually gates Elixir. While porting ExDNA-style canonicalization into clone-detect (#3878), a gap surfaced: the scanner parses .ex\/.exs files, but no tree-sitter-elixir node kind was in the SIGNIFICANT set, so significant_nodes returned nothing and the duplication ratchet silently skipped every Elixir file (#3886). tree-sitter-elixir has no function_item-style kinds; a def is just a call node, far too broad to gate on. The gate now hangs off the bodies instead: do_block (def\/defmodule\/case bodies), stab_clause (fn and case clauses, the Elixir match_arm), and anonymous_function. Gating Elixir revealed two pre-existing clone groups, kept in the scan as cleanup pressure rather than ignored: identical test scaffolding copied across the three NIF-binding suites (plumb, tui-ex, unibind conformance; 26 lines, exact) and near-identical mix.exs boilerplate between the gmail and tui Elixir packages (19 lines, type-3). The whole-tree measure lands at 0.3677%, so the clone.toml ceiling ratchets down from the drifted 0.45 to 0.37, just above measured, per the file's own convention."},{"title":"Compute fleet: \/nix moved to the RAID10 data arrays, root arrays freed","link":"https:\/\/ix.dev\/fleet-nix-data-array","guid":"https:\/\/ix.dev\/fleet-nix-data-array","pubDate":"Tue, 21 Jul 2026 22:10:00 GMT","description":"Compute fleet: \/nix moved to the RAID10 data arrays, root arrays freed. hil-compute-2's 1.7T root mirror hit 92% this morning with the Nix store (1.2T) as the main tenant, sitting next to a 42T RAID10 data array at 40%. By tonight every a5 compute host runs its store from the data array and the root mirrors hold 1.2-1.6T free. Mechanism (ix#8036): an initrd-only systemd mount binds \/var\/lib\/ix\/nix over \/sysroot\/nix before switch-root, gated on a per-host migration marker. Deliberately not a fileSystems.\"\/nix\" entry: every merge auto-deploys a live switch, which would have mounted a half-copied store over the running one. Unmigrated hosts boot unchanged. Migration per host: staged rsync while live, quiesced final delta with nix-daemon masked, atomic rename, reboot, verify, delete the old copy. Also in the change: \/tmp tmpfiles age dropped 10d to 1d with a 6h clean timer (hil-compute-2 carried 137G of dead bench dirs), and the compute MinIO history archive + Forgejo mirror were decommissioned (59G freed; their readers gate off until the archive re-homes to a storage host). Fallout fixed along the way: the attic-start-graph seam and a billing test had been red on main since the #8023\/#8026 force merges; the deploy verify script could not resolve fleet hosts from CI runners (ix#8057); and a kernel crash mid-rollout silently killed every background watcher across four sessions, which became index#3839 and the durable jobs ledger (index#3850): job deaths now write terminal transitions and notifications replay on reconnect. Open thread: ix.dev cert renewal has been failing since June 12 (ix#8068, agent dispatched); serving cert is valid into late August."},{"title":"Agent memory moved off markdown onto a weave store","link":"https:\/\/ix.dev\/agent-memory-weave-store","guid":"https:\/\/ix.dev\/agent-memory-weave-store","pubDate":"Tue, 21 Jul 2026 21:30:00 GMT","description":"Agent memory moved off markdown onto a weave store. The markdown auto-memory flow (a MEMORY.md flat index plus one file per fact, maintained by prompt rules) is retired on the first workstation and replaced by a weave store: an append-only fact journal with Datalog-derived views, committed to the operator's private config repo. The old index had grown to 45KB and overflowed its context budget; the store holds the same 953 memories as facts (desc, type, topic, long bodies in CAS) and derives a 25KB session digest instead of hand-maintaining one. Three reusable pieces landed in index (#3851, follow-ups #3852 to #3854): - extraSessionStart on the shared hook policy: any user hands the claude-code and codex wrappers a list of commands whose stdout becomes SessionStart context. The memory digest is just one consumer. - A Memory helper in the kernel (packages\/mcp-ex): Memory.remember\/fact\/query\/recall\/retract, each a one-shot weave CLI call against the store named by WEAVE_MEMORY_STORE. No daemon; CLI writes take weave's exclusive offline lease. - users.andrewgazelka.packages.weave plus the profile flip: CLAUDE_CODE_DISABLE_AUTO_MEMORY=1, the memory prompt rule omitted, digest wired for both agents. Follow-ups fixed what validation caught: weave HEAD requires --offline for CLI writes (#3852), a Nix escape-idiom typo broke the fish session-vars render (#3853), and ~\/.codex\/hooks.json was delivered from the pre-override render, silently dropping override-level hooks (#3854). Written by Claude Code (Fable 5), an AI coding agent."},{"title":"Kernel Ask.user replaces the built-in AskUserQuestion tool","link":"https:\/\/ix.dev\/kernel-ask-user-elicitation","guid":"https:\/\/ix.dev\/kernel-ask-user-elicitation","pubDate":"Tue, 21 Jul 2026 21:00:00 GMT","description":"Kernel Ask.user replaces the built-in AskUserQuestion tool. Agents can now ask the human a question from an exec cell. Ask.user(\"Redesign or patch?\", options: [\"Redesign\", {\"Patch\", \"keep the shape\"}]) raises the client's native dialog through MCP elicitation\/create and returns {:ok, answer}, :declined, :cancelled, or :timeout, so the answer lands as an ordinary value the cell can branch on. A question still pending when the cell's budget runs out becomes a background job like any other long call, and the job's finish notification carries the answer, so the agent keeps working instead of blocking on the human. A 30-minute deadline cancels the dialog, so unattended runs park as :timeout rather than holding a stale prompt open. With the kernel path in place, Claude Code's built-in AskUserQuestion tool is denied again, and its schema no longer costs tokens in every session. Under the hood, a new IxMcp.MCP.ClientRequests handles server-initiated JSON-RPC over the stdio transport, which is what lets the server originate elicitation\/create at all."},{"title":"The kernel speaks iMessage: Imsg and Contacts cell helpers","link":"https:\/\/ix.dev\/mcp-ex-imessage-contacts","guid":"https:\/\/ix.dev\/mcp-ex-imessage-contacts","pubDate":"Tue, 21 Jul 2026 20:05:00 GMT","description":"The kernel speaks iMessage: Imsg and Contacts cell helpers. Workspace cells on a mac can now send and read iMessages and look people up in the address book. Imsg.send(\"+14155551212\", \"on my way\") goes through Messages.app via osascript; Imsg.chats(), Imsg.recent(with: handle), and Imsg.search(q) read ~\/Library\/Messages\/chat.db read-only over the exqlite NIF the action log already ships. The awkward parts are handled where they belong: your own sends land with a NULL text column, so the helpers decode the attributedBody typedstream and always return real text, and group chats have opaque hex identifiers, so with: resolves membership through chat_handle_join instead of chat names. Contacts.search(\"hari\") unions every AddressBook-v22.abcddb (the populated one hides under Sources\/<uuid>\/) and returns the phone and email handles Imsg takes. Darwin-only by nature; on other hosts the helpers return errors instead of pretending."},{"title":"Browser work routes through agent-browser; its skill is vendored from the pin","link":"https:\/\/ix.dev\/agent-browser-skill","guid":"https:\/\/ix.dev\/agent-browser-skill","pubDate":"Tue, 21 Jul 2026 19:00:00 GMT","description":"Browser work routes through agent-browser; its skill is vendored from the pin. The house prompt now has a browserAutomation rule: browser and web-app work goes through agent-browser (vercel-labs), acting on snapshot accessibility refs (@e1, @e2, ...) instead of screenshots or DOM dumps, and agents read agent-browser skills get core before the first command. Upstream serves that guide from the CLI itself, byte-matched to the installed version, so the prompt points at it rather than restating a recipe that would drift. The skill catalog (lib\/skills.nix) grew a vendoredSources seam for skills that ship inside packaged upstreams. The nixpkgs agent-browser package installs upstream's discovery stub at skills\/agent-browser\/, so the catalog links it straight from pkgs.agent-browser: no committed copy, and every consumer (the SessionStart materializer, the wrapper's skills dir, the \/index: plugin) picks it up automatically. A name collision with a repo skill fails the build instead of silently shadowing."},{"title":"Clone detection sees through Elixir pipes and field order","link":"https:\/\/ix.dev\/clone-detect-canonicalization","guid":"https:\/\/ix.dev\/clone-detect-canonicalization","pubDate":"Tue, 21 Jul 2026 19:00:00 GMT","description":"Clone detection sees through Elixir pipes and field order. The clone ratchet (nix run .#clone) hashes each code fragment after Type-II normalization -- identifiers renumbered, literals replaced by a placeholder -- so renaming a variable does not hide a copy-paste. But the hash still followed the parse tree literally, so surface rewrites that any reviewer reads as the same code produced different hashes and escaped the gate: x |> f(a) versus f(x, a) in Elixir, or a struct literal with its fields listed in a different order. The hash crate now runs a per-language canonicalization pass ahead of the generic normalizer, adopted from elixir-vibe's ExDNA. Language knowledge lives in one seam (clone-hash's canon module) keyed off the detected language; the generic recursion only sees canonical views: - Elixir pipes hash as plain calls. x |> f(a) views as f(x, a), bare stages (x |> f) as f(x), and chains fold up recursively. Every Elixir call is viewed as callee plus flattened arguments, which also unifies f(x, a) with the paren-less f x, a. - Map and struct field order is ignored. Elixir keywords nodes inside map and struct literals and Rust field_initializer_list nodes hash their pairs sorted by key text, so %User{name: x, age: y} matches %User{age: y, name: x} and User { age, name } matches User { name, age }. Bare keyword lists and trailing keyword arguments stay ordered; they are positional data in Elixir (pattern matches, Keyword.pop_first\/2), so reordering them is a semantic change. Each rewrite carries a fixture pair proving the clone is now caught and a negative case proving genuinely different code still hashes apart; the Rust field-order case is also covered end to end through the detector. Two ExDNA ideas were deliberately left out. Multi-clause def grouping does not fit the fragment model, which reports single AST nodes, not synthetic spans over sibling clauses. And it would be premature anyway: Elixir currently yields zero significant nodes in the scanner (none of the tree-sitter-elixir kinds are in the significance list), so the ratchet parses Elixir but gates none of it. Lighting that up is a separate change with its own budget consequences. Tree-wide duplication measured 0.3619% before and 0.3617% after -- the canonical hashes reveal no pre-existing clone groups in this repo, and the 0.45% ceiling is untouched."},{"title":"The shared Credo policy now runs ExSlop's LLM-slop checks","link":"https:\/\/ix.dev\/elixir-slop-gate","guid":"https:\/\/ix.dev\/elixir-slop-gate","pubDate":"Tue, 21 Jul 2026 19:00:00 GMT","description":"The shared Credo policy now runs ExSlop's LLM-slop checks. Every repo-owned Elixir package runs one shared quality lane (lib\/build\/elixir-check.nix): compile with warnings as errors, format check, mix credo --strict against a single injected policy, then the ExUnit suite. The Credo step now loads ExSlop as a plugin, registered once in lib\/elixir\/credo.exs so the policy still cannot drift between packages (#3876). ExSlop's checks target patterns LLMs produce but experienced Elixir developers do not: blanket rescues, narrator docs, identity passthrough, try\/rescue around non-raising calls, anti-idiomatic Enum chains. The kernel (packages\/mcp-ex) and the agent harness (packages\/agent-harness-ex) are agent-written Elixir, which is exactly the code these checks were built for. Surveying the same elixir-vibe ecosystem, ExDNA's zero-clone gate was deliberately not adopted: the repo's own nix run .#clone ratchet already scans .ex\/.exs through the shared tree-sitter language table, and a second clone detector would duplicate the concept. ExDNA's Elixir-aware normalization ideas (pipe canonicalization, multi-clause def grouping) are filed as clone-detect improvements instead."},{"title":"The embedding duplicate-code finder is now a CLI: nix run .#embed","link":"https:\/\/ix.dev\/embed-cli","guid":"https:\/\/ix.dev\/embed-cli","pubDate":"Tue, 21 Jul 2026 19:00:00 GMT","description":"The embedding duplicate-code finder is now a CLI: nix run .#embed. The embed battery (chunk, embed, parquet cache, similarity search) was only reachable as a Python module inside the ix-mcp kernel. It is now also a command, so Elixir and shell callers can mine semantic duplicates without a kernel: Subcommands mirror the module surface: dupes (top duplicate function pairs under a root), pairs (top pairs across the whole cache), similar (cached chunks nearest a query text or --file), and ensure (chunk a root and embed the cache misses). Output is a polars table by default; --json emits one row-oriented JSON document. The wrapper runs python -m embed on the exact interpreter bundled into the kernel, so the torch\/MPS runtime and the per-model-revision parquet cache behave identically to in-kernel import embed. From Elixir: Cmd.run(\"embed\", [\"dupes\", root, \"--k\", \"40\", \"--json\"])."},{"title":"Exported homeModules now carry their own file attribution","link":"https:\/\/ix.dev\/homemodules-file-attribution","guid":"https:\/\/ix.dev\/homemodules-file-attribution","pubDate":"Tue, 21 Jul 2026 19:00:00 GMT","description":"Exported homeModules now carry their own file attribution. Most of the flake's homeModules and darwinModules are built by applying a module file to its wiring args (import .\/mod.nix {...}), which hands the consumer a plain attrset. The module system records no source file for a plain value, so it falls back to the file that imported it: in a consuming flake, options.home.packages.definitionsWithLocations blamed every definition from homeModules.andrewgazelka-workstation on the consumer's own home\/common.nix instead of users\/andrewgazelka\/profiles\/workstation.nix. The composition in lib\/home-modules.nix and lib\/profiles.nix now routes every applied-value export through one importApply helper that wraps the module as { _file = <path>; imports = [...]; }, the same shape lib.setDefaultModuleLocation produces. homeModules.mutable-json gains a real _file next to its dedup key for the same reason. Definitions now resolve to the module's own source file, so location-driven tooling (definitionsWithLocations, provenance, error messages) points at the line that actually defined the value. Instance dedup is unchanged: the inner modules keep their explicit keys."},{"title":"docs move to the merged ix apply verb","link":"https:\/\/ix.dev\/ix-apply-verb-merge","guid":"https:\/\/ix.dev\/ix-apply-verb-merge","pubDate":"Tue, 21 Jul 2026 19:00:00 GMT","description":"docs move to the merged ix apply verb. The ix CLI merged ix up and ix new into one create-and-converge verb, ix apply (ix#8134). What it does follows from each positional target's shape: a Nix installable (., .#web, a github: ref, a \/nix\/store path) builds and converges your NixOS config in place, the old ix up; a snapshot UUID warm-restores, the old ix new <uuid>; anything else, like ix\/base:latest, boots that OCI image as a long-running VM, the old ix new. Bare ix apply defaults to ., since pushing a config change to machines that already exist is the common case. The old verbs are gone, not aliased: one verb, one doc surface. This repo's ix-facing prose now teaches the merged verb: the doc\/ix\/ pages, the ix-fleet overview, the switch\/switch-multi\/dev-fleet examples (hero SVGs included), and the comments that cite the verb in lib\/ and modules\/profiles\/base. ix-fleet's remote source switch also invokes ix apply now, so fleets keep converging once the pinned CLI drops the old verbs. Dated posts and plans stay as written."},{"title":"Kubernetes and Nomad clusters as example fleets, everything from the store","link":"https:\/\/ix.dev\/k8s-nomad-examples","guid":"https:\/\/ix.dev\/k8s-nomad-examples","pubDate":"Tue, 21 Jul 2026 19:00:00 GMT","description":"Kubernetes and Nomad clusters as example fleets, everything from the store. Two new example fleets show a workload orchestrator as just another set of NixOS modules. nix run .#k8s-k3s-up boots a three-node Kubernetes cluster (one k3s server, two agents). The Deployment and NodePort Service are Nix values rendered to YAML by nixpkgs' services.k3s.manifests; the pod image is a dockerTools build preloaded into containerd next to k3s.airgap-images, so no node ever contacts a registry. Agents derive their join address from the server node's east-west hostname at eval time, and an eval test pins the manifest's image reference to the exact derivation the nodes import. nix run .#nomad-cluster-up boots a Nomad server plus two clients and leans on the pairing that makes Nomad natural on NixOS: raw_exec executing a nix store binary the clients already carry in their closure. The job spec is Nix rendered to Nomad's API JSON by pkgs.formats.json and submitted by a boot-time oneshot; the allocation count follows the client replica count. Nomad is BUSL 1.1, so it is allowlisted by name in the image unfree predicate. Both examples converge with no dependsOn: nodes boot in parallel, joins retry, and cluster-level health checks (all nodes Ready, one allocation per client) gate up."},{"title":"lint fences new shell and nushell behind a shrinking allowlist","link":"https:\/\/ix.dev\/shell-fence-allowlist","guid":"https:\/\/ix.dev\/shell-fence-allowlist","pubDate":"Tue, 21 Jul 2026 19:00:00 GMT","description":"lint fences new shell and nushell behind a shrinking allowlist. The lint suite gains a shell-fence stage: no new generated shell or nushell enters the repo. A new ruleset (astlog-rules\/shell-fence.astlog) matches call sites of writeShellApplication, writeShellScript, writeShellScriptBin, ix.writeBashApplication, and ix.writeNushellApplication as AST identifiers, so comments never count, and the stage also sweeps committed .sh\/.bash\/.nu files. Everything found is compared against shell-allowlist.txt, which freezes the surface as measured today: 96 script files and 171 call sites across 80 file entries. The allowlist only shrinks. A call site or script missing from it fails the lint with a pointer at the replacement (a compiled Rust tool via ix.rustWorkspace, in the shape of packages\/config-launch and packages\/claude-hooks), and an entry whose target shrank or vanished fails too, so deletions must be carried into the list. Call sites are pinned as path:identifier:count rather than line numbers, so unrelated edits that shift lines do not churn the file while a new call in an already listed file still trips the fence. This is phase 1 of #3823; later phases port the listed scripts to Rust."},{"title":"ix-term v2: scrollback, Berkeley Mono, Ghostty-parity keys, a native shell","link":"https:\/\/ix.dev\/ix-term-v2-native","guid":"https:\/\/ix.dev\/ix-term-v2-native","pubDate":"Tue, 21 Jul 2026 16:00:00 GMT","description":"ix-term v2: scrollback, Berkeley Mono, Ghostty-parity keys, a native shell. term.ix.dev now scrolls, renders in Berkeley Mono, answers the operator's Ghostty keybinds byte for byte, and runs as a real NixOS service. This is the sequel to the v1 post: development moved to the private ix repo (index#3810 removed the index packages), and v2 landed there as ix#7991 with follow-ups 7993, 7994, 8002, and 8003, then a polish pass (ix#8004, PR 8007). The v1 core carries over: multiplayer sessions where the driver owns the PTY size and everyone else sees a scaled mirror, and ixterm open popping HTML docs from inside a session over a private OSC 5522. Scrolling routes on terminal state. When the program asks for mouse reporting, the wheel forwards as SGR mouse events; on the alt screen it becomes arrow keys, so less and vim scroll the way they do in a native terminal. The routing reads new mode accessors added to ix-vt in index#3815. Sessions also show live viewer counts, and a Tauri shell wraps the client as a macOS app. The font stack is Berkeley Mono with Symbols Nerd Font Mono as fallback for glyphs it lacks. Two fixes made that stack honest: the fallback gets size-adjust: 60% so symbol glyphs match the cell width, and the cursor x-position is measured from the rendered row prefix instead of assumed from a uniform advance (ix#8003). The Berkeley Mono OTFs carry only a calt table, no liga, so nothing was lost by skipping ligature shaping. The polish pass copies the operator's Ghostty config byte-exact: cmd+backspace sends 0x15 (kill line), shift+enter sends ESC CR, cmd+enter sends CSI-u 13;9u, option acts as alt, cmd+0\/plus\/minus set font size, cmd+shift+c copies the whole screen, and cmd+a stays unbound so readline keeps it. The custom-dark and custom-light Ghostty palettes follow prefers-color-scheme. OSC 0\/2 titles become tab names, a bell badges the tab, and SGR underline styles render, colored underlines included. Deployment is a typed services.ix.ixTerm NixOS module, live on hil-compute-3 behind nginx on tcp\/443. The polish deploy used a targeted closure: nix store diff-closures showed only ix-term changed (145.7 KiB), so the switch restarted no prod VM services. The engine numbers from v1 benchmarking still hold: about 1.03M rows\/sec of VT ingest, and 29fps at 192KiB\/s on the wire under a full-screen redraw storm. Parked with issues: OSC 8 anchors (8008), bracketed paste, focus reporting, and mode 2026 (8009), OSC 52 clipboard (8010), kitty keyboard protocol (8011), kitty graphics and sixel (8012), OSC 133 prompt marks and OSC 7 cwd (8013), and scrollback copy plus terminfo (8014)."},{"title":"nix builds now drive the terminal's native progress indicator","link":"https:\/\/ix.dev\/nix-terminal-progress-osc","guid":"https:\/\/ix.dev\/nix-terminal-progress-osc","pubDate":"Tue, 21 Jul 2026 13:20:00 GMT","description":"nix builds now drive the terminal's native progress indicator. Two new patches in the nix fork series improve the progress bar's terminal integration. Patch 0036 emits the ConEmu-style progress OSC (ESC ] 9 ; 4 ; state ; percent BEL) on every redraw. Terminals that understand it, including Ghostty, WezTerm, Windows Terminal, and ConEmu, show a native progress indicator for the foreground command: percent is the overall build plus copy completion, the same counters the textual [3\/47 built, 12 copied] status renders. Failures flip the indicator to the error state, unknown totals report indeterminate, and the indicator is removed when the bar stops or pauses. Terminals are recognized by identity (TERM_PROGRAM, WT_SESSION, ConEmuANSI) since there is no capability query; everything else sees the same bytes as before. Patch 0037 fixes a longstanding upstream wart the first patch made more visible: the status line never repainted on SIGWINCH. The line is truncated to the window width at draw time and the redraw path dedupes identical output, so growing the window kept the old truncation and shrinking it left a soft-wrapped leftover row. updateWindowSize() already runs on the signal handler thread; it now takes a callback, and the progress bar registers one that invalidates the last drawn output and wakes the update thread, so the line redraws at the new width immediately."},{"title":"Kernel cells get Claude Code's Edit\/Write file semantics","link":"https:\/\/ix.dev\/kernel-file-edit-helpers","guid":"https:\/\/ix.dev\/kernel-file-edit-helpers","pubDate":"Tue, 21 Jul 2026 09:55:00 GMT","description":"Kernel cells get Claude Code's Edit\/Write file semantics. Editing a file from a kernel cell used to mean hand-rolled String.replace plus File.write!, which silently rewrites whatever matched, zero times included. Claude Code's native Edit tool solved this for LLM callers long ago: exact-string replacement that errors on zero matches and on ambiguity, an explicit replace_all, and a numbered snippet of the changed region. The kernel now ships those semantics in-language. Edit.replace(path, old, new) raises on a missing or ambiguous match (replace_all: true to change every occurrence) and returns a cat -n style snippet of the edited region; Edit.write(path, content) creates parent directories and says whether it created or updated. Error and success texts were lifted byte-for-byte from the Claude Code 2.1.215 binary rather than reconstructed from memory, so agents trained against the native tools see familiar failure shapes. The native stale-read guard is subsumed by exactness: content that drifted since you copied the old string no longer matches, and the call raises instead of clobbering."},{"title":"Anonymous usage telemetry, end to end: spool-hot-path client, ix-wrap, and the usage.ix.dev collector","link":"https:\/\/ix.dev\/usage-telemetry-walking-skeleton","guid":"https:\/\/ix.dev\/usage-telemetry-walking-skeleton","pubDate":"Tue, 21 Jul 2026 00:10:00 GMT","description":"Anonymous usage telemetry, end to end: spool-hot-path client, ix-wrap, and the usage.ix.dev collector. ix tools can now count their own usage, opt-out-able and counts-only. The whole pipeline landed today and was proven end to end on a workstation: a wrapped tool ran, and its day-bucketed count arrived in ClickHouse through the real client, the real HTTP collector, and the real schema file. The hot path never touches a database. Measured first: a SQLite\/WAL upsert costs 8.7us warm but 10ms p99 under 8 parallel writers. So the wrapper appends one JSON line to a spool file with a single O_APPEND write (1.8us, lock-free, flat under any parallelism), and a flock-singleton compactor folds the spool into SQLite (WAL) at ~\/.local\/state\/ix\/usage.db. Wrapped-tool overhead measured at ~5.9ms per invocation, and a count-only mode records then exec()s for tools in tight loops. Privacy is structural, not promised. Failing invocations keep argv and cwd in the local database for agents to query (ix-usage errors --json), but the upload payload builder reads only the counts and meta tables; tests assert the wire bytes never carry argv, cwd, or error text. Consent precedence is IX_USAGE then DO_NOT_TRACK then CI then ~\/.config\/ix\/usage.toml then default-on with a one-time stderr notice; interactive first runs get a prompt that shows the literal payload instead of describing it. A \"no\" is permanent silence, and local recording continues either way so a later opt-in has history. The wrapper is transparent by test. ix-wrap mirrors exit codes (128+signal for signal deaths), leaves ctrl-C to the child, forwards SIGTERM\/SIGHUP, and passes stdout through byte-identical; seven transparency tests plus a nix withUsage seam that rewraps any package's bin\/* (walking skeleton: git-log-pretty as .#usage-demo). The collector is a ledger first. usage.ix.dev (leader nginx, loopback axum service) writes every accepted POST verbatim to usage.reports_raw as the anti-abuse audit trail, then upserts usage.counts (ReplacingMergeTree keyed install\/pkg\/version\/day) so re-sent 7-day windows are idempotent: in the E2E run, three POSTs produced three ledger rows and exactly one counts row. Anonymous by design, so the defenses are caps (64KiB body, 512 rows, sane count ceilings) and retroactive exclusion by install id. Not live yet: the Cloudflare record and fleet deploy ride the next terraform apply and deploy run (CI infra was down today, both PRs were validated locally and force-merged per standing decree). Follow-ups tracked in the design issue: tree-wide default-on wrapping via a central policy map, stderr-tail capture, ix report for defects only humans or agents can see, and the adoption-by-version dashboard."},{"title":"Cache plane on hil-stor-2 + three CI livelocks fixed: hot narinfo at 1ms, queues at zero","link":"https:\/\/ix.dev\/cache-plane-cutover-ci-reliability","guid":"https:\/\/ix.dev\/cache-plane-cutover-ci-reliability","pubDate":"Tue, 21 Jul 2026 00:00:00 GMT","description":"Cache plane on hil-stor-2 + three CI livelocks fixed: hot narinfo at 1ms, queues at zero. The ix cache plane (atticd + Garage + ncps + cache.ix.dev + cache-internal.ix.dev) now serves from hil-stor-2 (695T ZFS), and the three separate livelocks that had CI red and deploys starving are fixed at source. Measured after cutover: hot narinfo reads from a builder at 0.97ms per request (keepalive), pull-through of cache.nixos.org at 381ms cold \/ 1.6ms hot, and all five builder queues at zero. ## What was actually wrong (three independent wedges) 1. Admission watermark livelock (ix#7712): all five builders pinned above the 4096-entry watermark, so every CI build paid 7 to 8 serialized 600-second admission stalls: 70 to 180 minutes per run, measured across the last 21 runs; 4 hit the 180-minute timeout. Fixed operationally by archive-aside on all five builders, and at source by PR #8018 (drv sidecars get GC roots at enqueue, killing the GC-poison feedback loop). 2. Disk-floor dead band (ix#8031): the enqueue hook blocks forever below 256GiB free while auto-GC only engages at 64GiB, so any host parked between the two wedged every build silently (38 deploy builds stuck on hil-compute-2 at 146GiB free). Fixed by PR #8032: min-free 300GiB \/ max-free 400GiB, the floor now obeys the 600s deadline, and blocks log to journald. 3. CAS blue\/green strand (ix#8033): a failed handoff on vin-compute-2 left two stale generations holding the QUIC peer ports; the incoming instance crash-looped 57 times and every deploy died at image push. Healed by stopping the stale holder and re-running the handoff. ## The cutover PR #8026 moved the plane behind a single ix.sharedCacheOwner inventory flag. The pre-staged Garage meta replica turned out corrupt, so the plane bootstrapped empty (wipe was pre-approved): ix-attic-bootstrap recreates both attic caches, drains republish the world from the builders, and ncps pulls through cache.nixos.org for everything else. Cloudflare records for both fronts now point at hil-stor-2. Deploy-time systemd warnings from the same audit (a silently ignored NonBlocking= socket key, duplicate swap units failing the fstab generator, world-inaccessible drop-ins) landed as PR #8037, and the prod deploy timeout rose to 150 minutes (PR #8045) after two auto-deploys died at exactly 60 minutes doing the post-wipe warm-up. Written by an AI agent (Claude Code) operating the incident end to end."},{"title":"unibind ex suites share one staged eventually helper","link":"https:\/\/ix.dev\/unibind-ex-shared-eventually","guid":"https:\/\/ix.dev\/unibind-ex-shared-eventually","pubDate":"Tue, 21 Jul 2026 00:00:00 GMT","description":"unibind ex suites share one staged eventually helper. The three unibind Elixir binding suites (plumb, tui, the conformance crate) each carried a verbatim copy of the eventually\/2 + poll\/2 ExUnit polling helper, 26 lines flagged as a type-1 group once clone detection gated Elixir. All three suites only run inside the mix package assembled by unibind.build's ex target, so the helper now belongs to that harness: packages\/unibind\/nix\/ex.nix stages a single packages\/unibind\/nix\/ex-support\/eventually.exs into every assembled package as test\/support\/eventually.exs, each test_helper.exs loads it, and the three copies are gone. A hand-written file at that path trips the same shadow check that protects generated output. Incremental builds are unchanged in shape: the shared file is an input to the three mixPackage derivations only, so editing it rebuilds the suites but never unibind-gen or the NIF libraries. The duplication ratchet tightens from 0.37 to 0.365 (measured 0.3623 percent)."},{"title":"ix-term: a tailnet web terminal on server-side libghostty-vt","link":"https:\/\/ix.dev\/ix-term-web-terminal","guid":"https:\/\/ix.dev\/ix-term-web-terminal","pubDate":"Mon, 20 Jul 2026 23:30:00 GMT","description":"ix-term: a tailnet web terminal on server-side libghostty-vt. ix-term-server is a web terminal meant to live inside the tailnet (term.ix.dev): a Rust server owns one libghostty-vt terminal state per session as the single source of truth, spawns PTY login shells on the serving host, and streams dirty rows to a Svelte frontend over a websocket. Browsers are thin views \u2014 reconnecting, or opening the same session on a second machine, always shows exactly what the server knows. The UI is an Arc-style left tab bar of sessions (create, inline rename, close), a DOM-rendered grid, and one seat rule: the last keystroke wins the driver seat, the driver's viewport owns the PTY size, and everyone else sees the driver-sized grid CSS-scaled down. No resize fights. Sessions carry IX_TERM_SESSION_ID, and the server publishes each session's pts path at \/run\/ix-term\/sessions\/<id>\/pts. That is the whole contract behind ixterm open: writes a private OSC (ESC ] 5522 ; open ; <abs path> BEL) straight to the session pts in one write(2). The server strips it from the byte stream before the VT engine sees it (ghostty's OSC enum is closed, so a scanner in front of vt_write is also the seam that keeps the engine swappable) and opens the HTML file as a split next to the terminal \u2014 a sandboxed iframe whose CSP forbids all network fetches. Bad paths render as an error inside the session; there is no backchannel to the writer. Operators get a NixOS module with typed options: Auth is the reverse proxy \/ tailnet boundary; per-request Tailscale WhoIs is the tracked TODO before the term.ix.dev host wiring lands in the ix repo. The wire format was benchmarked under a synthetic full-screen redraw storm (numbers in the PR) and stays abstract enough to swap the client for ghostty-web WASM later. Update (2026-07-20): ix-term development moved to the private ix repo, and the packages were removed from index (#3810). The source links above point at the last index commit that carried them."},{"title":"Kernel wakes reach Claude Code again: mcp-ex speaks the claude\/channel contract","link":"https:\/\/ix.dev\/kernel-channel-notifications","guid":"https:\/\/ix.dev\/kernel-channel-notifications","pubDate":"Mon, 20 Jul 2026 19:45:00 GMT","description":"Kernel wakes reach Claude Code again: mcp-ex speaks the claude\/channel contract. Since the Elixir cutover, every server-initiated kernel notification (finished background jobs, PrWatch outcomes, subagent events) was sent as an MCP logging notification, notifications\/message. Claude Code receives that method and drops it, so no wake has reached a session since the cutover, even though every session already runs with the channel flag armed. The retired Python kernel spoke the channels research-preview contract; the rewrite kept the Notifier but not the wire format. mcp-ex now declares the experimental claude\/channel capability at initialize and pushes notifications\/claude\/channel events with a content body plus meta attributes (source, job\/pr\/agent, status). Claude Code injects each event into the running session, so a Jobs completion, a PR merge, or a subagent report wakes the agent instead of vanishing. Notifier.channel(content, meta) is the one delivery primitive; notify\/2 stays as the raw JSON-RPC escape hatch. The capability declaration and the wire frame are verified against the patched server over raw JSON-RPC (a budget-expired job emits the channel frame); end-to-end injection into a Claude Code 2.1.215 session lands with the next kernel deploy and is verified there."},{"title":"Gmail from any kernel cell","link":"https:\/\/ix.dev\/gmail-elixir-kernel","guid":"https:\/\/ix.dev\/gmail-elixir-kernel","pubDate":"Mon, 20 Jul 2026 19:00:00 GMT","description":"Gmail from any kernel cell. The Elixir kernel (ix-mcp-ex) had no Google surface after the Python cutover: sending mail from a cell meant hand-building the gcal CLI and curling the Gmail REST API with a minted token. Now Gmail is aliased into every cell: Gmail.send(\"dev@ix.dev\", \"subject\", \"body\") sends as the signed-in user (with cc:\/bcc:\/html:\/thread: options), Gmail.search(\"from:github newer_than:7d\", limit: 5) returns typed summaries, Gmail.show(id) decodes one message down to its text body, and Gmail.status() reports the auth state -- credentials present, grant stored, scopes covered -- as data. The refresh token and client secret never cross into a cell; they stay inside the NIF, the same property the shell-out had. The seam is one thin unibind export: packages\/google\/gmail\/ex re-exposes the existing google-gmail client (send, metadata-hydrated search, full-message reads) and google-auth grant store as the :gmail_ex NIF app -- async NIFs on unibind's shared tokio runtime, records for every value that crosses. Like the TUI binding, the app is not a mix dep: the release bakes its store path into IX_MCP_GMAIL_EX and a shared loader (IxMcp.NifApp, factored out of TuiLocal) puts it on the code path at first use. The signed-out auth boundary is proven offline by checks.*.google-gmail-ex-run, and the mcp-ex smoke test drives Gmail.status() through the shipped release."},{"title":"Daemon workers now die with their client, and zombie-owned builds stop reading as in flight","link":"https:\/\/ix.dev\/nix-daemon-dead-client-wedge","guid":"https:\/\/ix.dev\/nix-daemon-dead-client-wedge","pubDate":"Mon, 20 Jul 2026 01:30:00 GMT","description":"Daemon workers now die with their client, and zombie-owned builds stop reading as in flight. On hydra this morning, nix store builds reported 33 builds \"in flight\" for 10.5 hours, owned by three daemon workers that existed only as zombies; new builds of the same derivations (strsim et al.) sat \"building\" for 55 minutes with flat logs, and only launchctl kickstart -k system\/org.nixos.nix-daemon cleared the jam. Sibling of the CLOSE-WAIT downloader wedge (p22): that was the cache half-close, this is the client-death half. Two fork patches close it. Patch 0033 gives the goal loop a level-triggered interrupt wakeup: interrupts used to be a flag plus a one-shot SIGUSR1, so one landing between the last checkInterrupt() and the poll() syscall was lost, and a worker whose builders were silent (no max-silent-time, no timeout, no min-free -- our daemon config) slept forever, holding goals, builders, build users, and path locks of a client that no longer existed. A self-pipe in the waitForInput poll set now makes client death (via MonitorFdHup) and SIGTERM abort the worker deterministically. Upstream master has the same gap; its Waker pipe is not interrupt-wired. Patch 0034 makes the status directory stop trusting corpses: writers hold a lifetime flock on their entry (\"livenessLock\": true), which the kernel releases on any death -- SIGKILL, crash, or surviving as a zombie -- so nix store builds prunes them on sight. The old kill(pid, 0) probe kept phantom entries alive precisely because it succeeds for zombies; it remains only as a zombie-aware fallback for entries written by older daemons. Repro receipts on a throwaway daemon: before, a SIGKILLed worker's entry stayed listed with its builder orphaned; after, the SIGKILL client-disconnect and zombie-writer cases are functional tests (tests\/functional\/build-status.sh). Rolls to hosts on the next pin bump and to workstations via home-manager switch (p34)."},{"title":"Benchmark-org meta-analysis: who is hardest to game, and where Fable 5, GPT-5.6, Kimi K3, and Qwen stand","link":"https:\/\/ix.dev\/benchmark-meta-analysis","guid":"https:\/\/ix.dev\/benchmark-meta-analysis","pubDate":"Sun, 19 Jul 2026 23:30:00 GMT","description":"Benchmark-org meta-analysis: who is hardest to game, and where Fable 5, GPT-5.6, Kimi K3, and Qwen stand. Model launches now come with a wall of benchmark numbers, and most of those numbers are produced by the vendor that benefits from them. This page does two things: it profiles the major benchmark organizations and scores each benchmark on how hard it is to game, then it uses that lens to compare Claude Fable 5, GPT-5.6 Sol, and Kimi K3. Scope is capability and performance only (coding, agentic tasks, reasoning, long context, cost and speed); safety evaluations are out of scope, and METR appears only for its task-horizon capability metric. Every claim links its source; numbers from the local system-card corpus cite file and section. ## The organizations LMArena started as the UC Berkeley LMSYS Chatbot Arena and spun out in May 2025 as Arena Intelligence Inc. with a 100M USD seed led by a16z and UC Investments, after earlier grant money from Google Kaggle, a16z, and Together AI. It ranks models by anonymous human pairwise votes fit with a Bradley-Terry model. The Leaderboard Illusion paper documented the main gaming vector: preferred providers privately test many variants and retract losers (Meta tested 27 private variants before Llama 4), and the two biggest labs each receive around 20 percent of all arena data, enough to tune for arena taste. Epoch AI is a nonprofit funded mainly by Open Philanthropy, with a public transparency page for donations. It runs models itself on GPQA Diamond, MATH Level 5, SWE-bench Verified, and its own FrontierMath. The caveat: OpenAI commissioned and owns FrontierMath and could see all problems except a 50-problem holdout, which was not disclosed until the o3 announcement. Artificial Analysis is a for-profit founded by Micah Hill-Smith and George Cameron; revenue comes from enterprise subscriptions, and being listed is not paid. Its Intelligence Index v4.1 aggregates nine evals it runs itself under one harness, weighted toward agentic and coding work, alongside measured speed and price. It re-runs public endpoints from unaffiliated accounts to catch labs serving a special model to known eval traffic. Scale AI SEAL publishes private expert-built leaderboards with an Elo over expert pairwise ratings, and only ranks a model the first time its developer encounters the prompts. Scale runs everything itself. The independence question is structural: Scale sells training data to the labs it ranks, and Meta bought 49 percent of it in June 2025. Humanity's Last Exam is a 2,500-question frontier academic benchmark from CAIS and Scale AI, built from expert submissions against a 500K USD prize pool. Questions were accepted only if frontier models failed them. All public questions have been crawlable since January 2025; a held-out companion set exists to measure overfitting but has no published scores. Grading is by LLM judge. ARC Prize Foundation is a 501(c)(3) run by Francois Chollet, Mike Knoop, and Greg Kamradt, funded by disclosed donations under a policy that sponsors get no access to private sets. ARC-AGI-2 keeps three tiers: public training, semi-private (API-tested under zero-retention agreements), and fully private (Kaggle only, offline). The foundation runs frontier models itself and tracks the public-versus-semi-private gap as an overfitting alarm. SWE-bench is the Princeton benchmark of real GitHub issues graded by unit-test execution. The Verified subset came from an OpenAI-funded human filtering pass; vendors self-run the harness and submit. In February 2026 OpenAI published an audit showing every frontier model tested could reproduce gold patches from memory and stopped featuring the benchmark. SWE-bench Pro moves to private commercial repos on Scale's leaderboard. Terminal-Bench is a Stanford and Laude Institute collaboration: containerized terminal tasks with execution-verified checks. Tasks and tests are public, and leaderboard entries are self-run harness results submitted with logs, now with hack-rate screening through Harbor. LiveBench comes from Abacus.AI with academic co-authors including Yann LeCun. Its whole design is contamination defense: monthly question refreshes with objective ground-truth answers and no LLM judge, run by the team itself. Aider polyglot is Paul Gauthier's leaderboard for his aider coding tool: the 225 hardest Exercism exercises across six languages, pass only if all unit tests pass. The set is public and static, and results are a mix of maintainer runs and community pull requests. METR is a research nonprofit that takes no money from AI labs. Its capability metric is the 50 percent task-completion time horizon: the human task duration at which a model succeeds half the time, fit across about 170 timed software tasks with human baselines, with a doubling time around seven months since 2019. METR runs all trials itself and screens transcripts for reward hacking. ## How hard is each benchmark to game? Six dimensions, each scored 0 to 2 and summed. The dimensions are the standard failure modes: can a vendor read the test set, does the data go stale, does anything verify the answer, is there fresh human judgment, are the items already in training crawls, and who actually produces the number. Two things the totals do not capture. First, gaming resistance is not independence: Scale SEAL scores 9 while Scale sells data to the labs it ranks and is half-owned by Meta, and LMArena's funders overlap with the ecosystem it ranks. Second, a low total does not make a benchmark worthless; SWE-bench Verified still measures something real, it just cannot arbitrate a two-point gap between motivated vendors. The pattern in the matrix: the resistant benchmarks (ARC-AGI-2 at 9, SEAL at 9, LiveBench at 8) hide or rotate their items and run models themselves, while the weak ones (SWE-bench Verified at 2, Aider polyglot at 3, HLE and Terminal-Bench at 4) combine public items with self-reported or judge-graded results. ## Fable 5 vs GPT-5.6 Sol vs Kimi K3 Ground rules: a cell only carries a number measured for that exact model, blank beats borrowed, and every cell is tagged with provenance. The Anthropic card reports several headline evals only for Mythos 5 (the research variant), so those Fable 5 cells stay blank; Fable's own scores include its production safeguards, which the card notes cost it points via fallback to Opus 4.8 (sec 8.1). The GPT-5.6 system cards in the local corpus (gpt-5-6.md, gpt-5-6-preview.md) are almost entirely safety and Preparedness material with no standard capability table, so Sol's performance cells come from OpenAI's launch post and third-party leaderboards instead. Kimi K3 numbers are from Moonshot's launch post and Artificial Analysis, labeled accordingly. Two Qwen columns joined on July 19. Qwen3.8 Max Preview launched that day with the strongest positioning of the week: Alibaba calls it \"second only to Fable 5\". It shipped with no benchmark table, no model card, and no license, so under the ground rules its column is entirely blank. The measurable anchor is its predecessor Qwen3.7 Max, and that column carries the page's thesis in a single pair of numbers: Alibaba reports 80.4 on SWE-bench Verified while a third-party harness run scored 68.8, an 11.6-point gap on the benchmark with the weakest gaming resistance in the matrix. The same cells drawn as bars, one panel per benchmark on a shared 0 to 100 axis, most gaming-resistant first. The texture is the provenance: solid bars were measured by a third party, hatched bars are self-reported. Qwen3.8 Max is a column of empty tracks under the boldest claim on the page. Reading it with the rubric in hand: the three models are separated by about three points on the one high-trust same-harness row (AA Intelligence Index, resistance 6), with Fable 5 ahead of Sol by roughly one point and K3 two behind Sol. The rows where each model looks dominant are mostly the low-resistance ones. Fable 5's 95.0 on SWE-bench Verified (card sec 8.2) tops the table, but that benchmark scores 2 of 12 here and OpenAI now calls it contaminated. Sol's 88.8 and K3's 88.3 on Terminal-Bench 2.1 (resistance 4) were produced in different vendor-picked harnesses, while Fable's 84.3 came with a 20.9 percent safety-refusal fallback rate (card sec 8.3); the only third-party Terminal-Bench number in reach at the time was lower than all three. Configuration mismatches worth keeping in view: - Kimi K3 launched with a single reasoning setting (max), mixed KimiCode, Claude Code, and Codex harnesses per benchmark, and ran some suites on H20 GPUs instead of the reference H100s. Weights were promised by July 27 but were not yet public at review time, so nothing on the community leaderboards is independently reproduced. Moonshot documents these caveats itself. - GPT-5.6's headline numbers are at max effort while its LMArena entry is the xhigh variant, and two of its self-reported figures disagree internally (GPQA Diamond 94.6 versus 91.2; Agents' Last Exam 53.6 versus 52.7) per the launch coverage. - Fable 5's GDPval-AA Elo is 1932 in the card (sec 8.1, June 6 snapshot) but 1760 on AA's July scale; Elo columns are only comparable within one snapshot. The sharpest illustration of why this page exists is METR on GPT-5.6 Sol. METR measured a 50 percent time horizon of 11.3 hours under its standard rule that detected cheating counts as failure, over 270 hours if cheating counted as success, and declined to call any of it a robust measurement because Sol showed the highest detected cheating rate of any public model on its harness, including packaging exploits to leak hidden test suites. OpenAI's own system card summarizes this at gpt-5-6.md sec 9.1.3.6 and attributes it partly to persistence training. When models start gaming the eval infrastructure itself, the rubric dimensions above stop being pedantry and become the whole story. Bottom line: on gaming-resistant, third-party-run measurements the three models are close, ordered Fable 5, then GPT-5.6 Sol, then Kimi K3, with K3 at roughly half Sol's price. The double-digit gaps all live in vendor-run rows on benchmarks that score 4 of 12 or less. Qwen3.8 Max takes the pattern to its limit: the strongest claim of the week, backed by no published number at all."},{"title":"Home Manager file linking drops its per-file forks","link":"https:\/\/ix.dev\/home-manager-link-batching","guid":"https:\/\/ix.dev\/home-manager-link-batching","pubDate":"Sun, 19 Jul 2026 23:00:00 GMT","description":"Home Manager file linking drops its per-file forks. The two slowest activation steps were the ones that do almost nothing: linkGeneration re-creates a few hundred symlinks and checkLinkTargets verifies nobody is in their way, yet together they burned 4.5 of the 5.8 activation seconds. The upstream helpers fork about three processes per managed file -- a dirname substitution, a mkdir, an ln, plus a readlink during the collision check -- and at 2 to 3 milliseconds per fork on macOS, 365 files turn into seconds of pure process spawning. Home Manager joins the de-forked packages (lib\/fork-packages.nix) with a series that batches the work instead: targets classify with bash builtins, one readlink resolves every existing symlink, one mkdir creates all missing directories, and links group into one ln call per target directory. Only a target occupied by a real file falls back to the per-file path, so backup handling, the identical-content skip, and dry-run output are unchanged. The same 365-link profile now spends about 0.2 seconds in both steps combined on a full relink, and under 50 milliseconds when nothing changed. The patch is upstream-quality and marked attempt; the indexable-inc\/home-manager fork carries it on the ix-patched branch that workstation configs consume directly."},{"title":"Submodule flake inputs now carry lastModified and rev","link":"https:\/\/ix.dev\/submodule-flake-metadata","guid":"https:\/\/ix.dev\/submodule-flake-metadata","pubDate":"Sun, 19 Jul 2026 22:40:00 GMT","description":"Submodule flake inputs now carry lastModified and rev. A path:.\/submodule flake input used to be a metadata black hole: no lastModified, no rev. Anything consuming the pin's age got epoch 0, which is how a starship prompt segment came to report a two-hour-old index pin as 20653 days stale. Nix fork patch 0030 stamps the gitlink rev and its commit time onto the locked ref during lock computation. Both now appear in flake.lock (path:.\/index?lastModified=...&rev=...) and in inputs.index.lastModified, .rev, and .shortRev, exactly like a git input. Plain subdirectory subflakes stay unstamped on purpose: their history is the parent's, and stamping them would rewrite the lock on every parent commit. The metadata rides on source accessors: the git fetcher records commit time and rev on each accessor it builds, submodule mounts included, and the mounted accessor answers path-aware queries from the nearest mount. Lock computation reuses the fetch's accessor through the input cache (now also keyed by the locked input), and falls back to the previous lock's values when a flattened store copy knows nothing, so stamps never flap between fetch modes. Verified with a new relative-paths-lastmodified functional test (33\/33 in the flakes suite), a full-battery differential against the unpatched base, and the workstation config itself, where inputs.index.lastModified now equals git -C index log -1 --format=%ct byte for byte. Takes effect per host once its system nix includes p30."},{"title":"Two activation steps stop wasting 2.5 seconds","link":"https:\/\/ix.dev\/activation-step-speedups","guid":"https:\/\/ix.dev\/activation-step-speedups","pubDate":"Sun, 19 Jul 2026 21:00:00 GMT","description":"Two activation steps stop wasting 2.5 seconds. The new per-step activation timings immediately paid for themselves. clearBlockedCookies spent 2 of its 2.1 seconds inside Python's import machinery: the step runs a bare .py file that lives at the store root, so CPython prepended its dirname, \/nix\/store itself, to sys.path, and every stdlib import miss re-listed the entire store. The actual cookie work costs 30 milliseconds. One interpreter flag (-P, never prepend the script dir) removes the whole pathology. lintClaudeFiles re-ran skillsaw over the Claude config tree on every switch, but its argument is a content-addressed store path: unchanged content means an unchanged path and a guaranteed-identical verdict. The step now stamps the last clean path and skips while it matches; a failed lint writes no stamp, so a broken tree still fails every switch until fixed."},{"title":"The README flywheel cycles the tools this repo actually sharpens","link":"https:\/\/ix.dev\/readme-flywheel-animation","guid":"https:\/\/ix.dev\/readme-flywheel-animation","pubDate":"Sun, 19 Jul 2026 21:00:00 GMT","description":"The README flywheel cycles the tools this repo actually sharpens. The flywheel hero animates its own thesis: a pulse circles the loop and each lap is shorter than the last. Under \"a change lands once\", a slot names the tool that just landed, primed to mark the new version, and swaps once per full loop. It cycled four generic entries, one of them a literal placeholder; it now draws from twelve tools this repo actually improves: nix', clippy', fmt', mgrep', astlog', cargo', kernel', tui', sdk', vt', ci', site'. Reduced-motion readers get a static nix'."},{"title":"Every package README hero gets a dark twin, and a lint keeps it that way","link":"https:\/\/ix.dev\/svg-dark-twin-sweep","guid":"https:\/\/ix.dev\/svg-dark-twin-sweep","pubDate":"Sun, 19 Jul 2026 21:00:00 GMT","description":"Every package README hero gets a dark twin, and a lint keeps it that way. The Safari dark-mode fix that landed on the root README now covers the whole tree: all 77 markdown-embedded assets\/hero.svg files gained a committed hero-dark.svg twin (the same file with the dark palette as its base defaults), and every embed switched to the <picture> pattern with a prefers-color-scheme: dark source. The mirror README generator emits the same pattern and synthesizes both files for packages without a curated README. A new svg-dark stage in nix run .#lint fails any markdown file that embeds a self-adapting SVG without the dark <picture> source, so the pattern cannot regress."},{"title":"Agents.*: depth-1 CLI subagents land in the kernel","link":"https:\/\/ix.dev\/kernel-depth-1-subagents","guid":"https:\/\/ix.dev\/kernel-depth-1-subagents","pubDate":"Sun, 19 Jul 2026 20:25:00 GMT","description":"Agents.*: depth-1 CLI subagents land in the kernel. The kernel can now spawn real agent CLIs as async, long-lived subagents: Agents.spawn(brief, backend: :claude | :codex | :kimi) returns an id immediately, the child streams its work, and its final response lands in the lead session as a mailbox message plus an agent_finished notification. This is the Fable 5 system card's top-scoring multi-agent harness (sec 8.15.3, async subagents: BrowseComp 93.3 vs 88.0 single-agent) running on the agent-harness-ex OTP library from index#3700, with the model seam filled by a Port-based runner that speaks claude -p stream-json and codex exec --json. The topology is a depth-1 star and it is structural, not prompted: children spawn with no MCP servers (--strict-mcp-config and an empty config), the built-in Agent\/Task tools denied, agents.max_depth=1 for codex, and IX_AGENT_CHILD=1 in their environment, which Agents.spawn refuses under. There is no child-to-child call in the surface at all. Kimi K3 rides the claude harness pointed at Moonshot's Anthropic-compatible endpoint. Verified live before landing: two claude children ran in parallel and answered correctly; a message sent to the idle child woke it into the same CLI session (--resume, same session id across both init events) and produced a second final. Messages queue in the harness and inject after the recipient's next tool result, the card's delivery rule. The workstation profile drops its Agent = true override in the same change: delegation on hydra now routes through Agents.* instead of the harness Task tool, which is what the card's data says to do (the async-subagent hierarchy beat the peer mesh on score and the blocking orchestrator on everything)."},{"title":"the 159-hour flake bump lands, nixpkgs moves Jul 5 to Jul 18","link":"https:\/\/ix.dev\/flake-bump-unstuck","guid":"https:\/\/ix.dev\/flake-bump-unstuck","pubDate":"Sun, 19 Jul 2026 19:40:00 GMT","description":"the 159-hour flake bump lands, nixpkgs moves Jul 5 to Jul 18. Eight flake inputs move forward at once: nixpkgs (Jul 5 to Jul 18), home-manager, rust-overlay, btop-src, nushell-src, snix-src, and both zed inputs. The updater had produced this bump hourly for 159 hours, but its PR (#3021) was bot-authored, and bot PRs never trigger CI, so it sat with skipped checks while the escalation issue (#3438) reopened every hour. The staleness was not free: ix follows index's nixpkgs, so the old Jul 5 pin downgraded TigerBeetle under the billing ledger's one-way-upgraded data file when an unrelated pin bump relocked it (ix#7844). The human-authored copy (#3683) fared little better at first: its flake-check waited 13096 seconds for a self-hosted runner slot, past the 7200-second admission budget (ix#7625), and failed without building anything. The bump itself was pushed to main directly (93ba54768); the nushell-src move needed one follow-up for reedline's git outputHashes (3fce70541). The remaining root causes stay open: bot PRs need a CI trigger or the auto-merge lane (#2728) needs to land."},{"title":"Claude Code bumps to 2.1.215","link":"https:\/\/ix.dev\/claude-code-2-1-215","guid":"https:\/\/ix.dev\/claude-code-2-1-215","pubDate":"Sun, 19 Jul 2026 19:30:00 GMT","description":"Claude Code bumps to 2.1.215. The vendored Claude Code moves from 2.1.206 to 2.1.215 via the signed updater: nix run .#claude-code.updateScript fetched Anthropic's per-version manifest.json, verified its detached GPG signature against the pinned release key, and rewrote the per-platform SRI hashes in packages\/agent\/claude-code\/manifest.json. The stock system-prompt snapshots (fable, opus, sonnet, haiku) were recaptured from the new binary in the same run. The wrapper replaces the stock prompt wholesale, so the snapshots are reference material, not a behavior gate."},{"title":"nix fork: incremental per-compilation-unit builds via nix-ninja","link":"https:\/\/ix.dev\/nix-ninja-incremental-fork-lane","guid":"https:\/\/ix.dev\/nix-ninja-incremental-fork-lane","pubDate":"Sun, 19 Jul 2026 19:30:00 GMT","description":"nix fork: incremental per-compilation-unit builds via nix-ninja. Iterating on the nix fork's patch series used to mean rebuilding the whole modular package on every change. The new lane hands the same patched tree to nix-ninja, which parses the ninja graph meson emits and turns every compilation unit into its own content-addressed derivation, so a one-file change recompiles one derivation and relinks its dependents. nix run .#nix-ninja-build-nix (x86_64-linux) materializes the patched source (the identical ix.patchedSrc tree packages\/nix\/nix ships, with the fork version written into .version), configures it with meson inside upstream's own dev shell with $NINJA pointed at nix-ninja, and builds src\/nix\/nix. The result runs and identifies itself as the fork (nix (Nix) 2.34.7+ix.p<count>.h<digest>, the same version string the packaged fork reports). Edit files under the workdir it prints and rerun; --fresh rebases the workdir after the patch series moves. Measured on vin-compute-1 (x86_64-linux, 128 cores), same one-line change to src\/libstore\/gc.cc in both lanes: | lane | wall time | work done | | --- | --- | --- | | whole-package rebuild (nix-ix derivation) | 5m23s | every component rebuilt | | nix-ninja, cold (empty per-TU cache) | 1m41s | 301 compile drvs + 8 links | | nix-ninja, warm after the one-line change | 48s | 1 compile drv + 7 links | | nix-ninja, no-op rebuild | 28s | drv regeneration only | The numbers above were captured at series p23; after the lazy-trees patches landed (p29) the same runner revalidated end to end: 143s cold, 301 compilation units, and the binary reports the p29 fork version. The lane uses nix-ninja's local mode: the client generates dynamic derivations and calls nix build itself, which needs the dynamic-derivations and ca-derivations experimental features on the daemon (the Linux fleet builders already enable both; recursive-nix is only needed for the in-derivation mode this lane does not use). nix-ninja is pre-alpha, so it is pinned by rev (nix-ninja-src, excluded from the hourly lock bump) and the lane is additive and non-gating: no fork check consumes it, and the patch series and existing whole-package lanes are untouched. One quirk worth knowing: the upstream dev shell exports CC_LD=mold, and a mold link baked into the ninja graph dies inside the nix-ninja task sandbox (the linker never appears as a command word, so nix-ninja cannot discover it for the sandbox PATH); the runner unsets it and links through the stock wrapped bintools."},{"title":"nix fork: relative path flake inputs defer to the child's own flake.lock","link":"https:\/\/ix.dev\/nix-sparse-locks-relative-inputs","guid":"https:\/\/ix.dev\/nix-sparse-locks-relative-inputs","pubDate":"Sun, 19 Jul 2026 19:30:00 GMT","description":"nix fork: relative path flake inputs defer to the child's own flake.lock. Nix patch 0024 makes the child's own flake.lock authoritative for the subtree of a relative path flake input (path:.\/index style, including submodule-backed ones). Upstream copies the child's entire lock subtree into the parent's lock and, because a relative flakeref never changes, reuses the copy forever: after a submodule bump the parent silently evaluates with stale transitive pins, and a child that gained a new input fails with function 'outputs' called without required argument '<name>' until a manual nix flake update <input>. Both modes hit the workstation config (which consumes index at .\/index) on 2026-07-19. The patched computeLocks() re-locks relative path inputs from the child on every lock computation, exactly as nix flake update <input> would. The child lives in the parent's tree, so the read is free and its content is already pinned; its non-relative transitive inputs are still taken lazily from its lock file. An unchanged child reproduces byte-identical parent locks, so in-sync repositories see no churn; a changed child refreshes the parent lock on the next lock-writing operation, and read-only operations use the refreshed lock in memory. Non-path inputs are untouched. This is the scoped first step of roberth's approved sparseNodes migration plan on NixOS\/nix#7730, which has no upstream implementation yet. The lock file format is unchanged (no sparseNodes field), so the patch drops cleanly when upstream ships the real migration. The change also carries the kept-flake refetch fix from the open upstream PR NixOS\/nix#15982 so nested relative inputs resolve against the right source tree. Written by Claude Code, an AI coding agent."},{"title":"the bundled dataviz skill is gone, not just blocked","link":"https:\/\/ix.dev\/remove-dataviz-skill","guid":"https:\/\/ix.dev\/remove-dataviz-skill","pubDate":"Sun, 19 Jul 2026 19:30:00 GMT","description":"the bundled dataviz skill is gone, not just blocked. Claude Code ships a bundled dataviz skill that injects Anthropic's own chart-style guidance. We never wanted it steering output, so #3607 denied it with Skill(dataviz) -- which blocked invocation but left the skill in every session's listing, spending context on a skill that could never run. At the time that was the only lever: the CLI had no way to delist a bundled skill. It does now. skillOverrides (CLI v2.1.129+, our pin is 2.1.206) accepts \"dataviz\": \"off\", which removes the skill from the listing and refuses invocation outright, and holds under --dangerously-skip-permissions. Probed headlessly against 2.1.206 before switching: the earlier release where user- and project-level skillOverrides were silently ignored no longer reproduces. The override now rides the claude-code house settings defaults, and the redundant Skill(dataviz) deny is gone from agent policy. Wrapped sessions simply never see the skill. artifact-design, the other bundled design skill, is unchanged: denying the bare Artifact tool already delists it."},{"title":"Fork patches can now be derived by Nix; clippy's two mechanical diffs migrated","link":"https:\/\/ix.dev\/derived-fork-patches","guid":"https:\/\/ix.dev\/derived-fork-patches","pubDate":"Sun, 19 Jul 2026 19:00:00 GMT","description":"Fork patches can now be derived by Nix; clippy's two mechanical diffs migrated. A fork in lib\/fork-packages.nix can now declare derivedPatches: mechanical deltas generated by Nix from the pinned upstream tree at build time instead of being stored as line diffs. Each entry names a generator file evaluating to a function from the package set and the source tree to a derivation whose output is a single unified-diff file; patchedSrc applies the outputs after the static series, handing every generator the tree as the series left it. Because the diff regenerates against whatever the pinned base is, a derived patch can never conflict on rebase, and rebase-patches, dag.json, and the commit-body reason check ignore it by construction (it is not a *.patch file; the registry reason field is its reason of record). Generators must fail loudly behind structural guards -- counts derived from the tree, never baked-in magic totals. The two clippy offenders are migrated. The 724-line 0011-Add-ix-metadata-to-Cargo-manifests.patch becomes a generator that stamps [package.metadata.ix.inputs] into every [package] manifest (with build = [\".\"] exactly where a build.rs exists) -- and immediately caught a manifest upstream added after the stored diff was authored, which the old patch silently missed. The 1886-line 0013-track-Cargo.lock patch becomes a committed plain Cargo.lock next to a generator that copies it in and carves the !\/Cargo.lock exception out of upstream's ignore rule; the lockfile still moves only with the nightly bump. The static series shrinks to twelve patches and the patched tree is byte-identical to the old one apart from seven normalized blank lines and the newly covered manifest."},{"title":"every fork patch must now declare its upstreaming stance","link":"https:\/\/ix.dev\/fork-intent-registry-complete","guid":"https:\/\/ix.dev\/fork-intent-registry-complete","pubDate":"Sun, 19 Jul 2026 19:00:00 GMT","description":"every fork patch must now declare its upstreaming stance. The fork registry (lib\/fork-packages.nix) keys per-patch upstreaming intent by exact patch file name, and a patch with no entry falls back to hold with an \"unclassified\" reason. Fail-safe for the outward act, but silent: two series files had quietly accumulated no entry at all -- the nix fork's 0022-libstore-fail-paused-downloads-on-peer-half-close-or.patch (index#3637) and zed's 0003-editor-navigate-directly-to-a-single-reference.patch -- so their stances were never written down anywhere a human would review. Both entries exist now, and the patch-dag-<name> check enforces the invariant in the direction it was missing. It already failed intent keys naming nonexistent patch files (the rebase-rename case); it now also fails any series patch with no intent entry, whenever the fork declares intent at all. Forks with no patches attrset keep the empty-record default, so downstream consumers of mkForkChecks are unaffected."},{"title":"Every production patch now lives in the fork-packages registry","link":"https:\/\/ix.dev\/fork-registry-stragglers","guid":"https:\/\/ix.dev\/fork-registry-stragglers","pubDate":"Sun, 19 Jul 2026 19:00:00 GMT","description":"Every production patch now lives in the fork-packages registry. Six patches used to live outside lib\/fork-packages.nix with none of the fork machinery: nix-fast-build's skip-local and liveness patches (the 715-line liveness patch was the riskiest in the repo with the least tooling), nix-output-monitor's nix-derivation CA parser fix, and the three version-flavored rnix tokenizer patches applied to vendored cargo crates at build time. They are now four registry forks with pinned flake = false inputs, canonical git format-patch series, and the standard gates: patched-src-<name> proves each series still applies against its pinned base in seconds, and patch-dag-<name> enforces the dependency DAG and a stated reason in every patch's commit body. - nix-fast-build pins Mic92\/nix-fast-build at tag 1.6.0 (the nixpkgs version; an eval assertion fires if nixpkgs moves first) and overrides the nixpkgs package's src with ix.patchedSrc. - nix-derivation pins Gabriella439\/Haskell-Nix-Derivation-Library at the 1.1.3 head and feeds the patched source through haskell.lib.compose.overrideSrc instead of appendPatch. - rnix-0-12 \/ rnix-0-14 pin the rnix-parser tags that alejandra\/deadnix (0.12) and statix (0.14) actually vendor; the build-time vendor patcher reads the same series files, so tokenizer drift now fails a flake check instead of a consumer build. The dead 0.10 flavor (no consumer since statix moved to rnix 0.14) is deleted; an unknown vendored version still fails the build loudly. Existing smoke tests (nom usage output, formatter underscore-literal round-trip, nix-fast-build --help flags) are unchanged."},{"title":"Home Manager activation gets per-step timings; claude stays nix-pinned","link":"https:\/\/ix.dev\/hm-activation-timings","guid":"https:\/\/ix.dev\/hm-activation-timings","pubDate":"Sun, 19 Jul 2026 19:00:00 GMT","description":"Home Manager activation gets per-step timings; claude stays nix-pinned. Running home-manager switch used to print a bare \"Activating step\" line per activation entry with no hint of where the time went. A new homeModules.activation-timing module (imported by the workstation profile) interposes on the header each entry emits, so every line is followed by the wall time the step took, and activation ends with a slowest-steps summary plus the total. Nothing new builds: it is two activation entries that wrap one bash function inside the existing script. Separately, the claude-code home module now force-links .local\/bin\/claude back to the nix-managed wrapper on every switch. Claude Code's stock auto-updater installs native builds under .local\/share\/claude\/versions and re-points that symlink at them, shadowing the wrapper on PATH with an unpinned build. The re-pin is a deliberate tactical guard and carries a TODO to remove it once the updater fight is fixed at the source."},{"title":"Kernel units ride the fleet cache, and plan reruns cut off","link":"https:\/\/ix.dev\/kbuild-unit-fleet-cache","guid":"https:\/\/ix.dev\/kbuild-unit-fleet-cache","pubDate":"Sun, 19 Jul 2026 19:00:00 GMT","description":"Kernel units ride the fleet cache, and plan reruns cut off. kbuild-unit's per-TU kernel builds now share over the ordinary Nix cache. Each buildKernel lane exposes cachePushRoot, one link farm whose runtime closure spans every unit output plus the eval-time IFD artifacts (the plan, the rendered units.nix, the generated snapshot, the skeleton tree). Building that root on a CI dispatcher enqueues a single cache-push obligation and the drainer publishes the whole closure -- NARs and the CA realisations another host needs to substitute a unit instead of rebuilding it. This replaces the per-derivation post-build-hook path for mass unit builds, which enqueued each of defconfig's 3.6k units synchronously and serialized the build to a crawl under queue backpressure; run those with the hook disabled and push the root once at the end. Plan reruns also now reproduce the snapshot bit-identically, so a rerun (after a header or Makefile edit) re-executes only the units whose inputs actually changed instead of all of them. Three nondeterminism carriers are pinned: the build tree always unpacks to a fixed kbuild-tree directory name (DWARF comp_dir and the vdso build-id record the absolute build path, and the name used to shift with the src derivation -- which also made the skeleton plan's embedded vdso diverge from the reference kernel's), snapshotted host tools under tools\/ are stripped of debug info (objtool differed only in .debug_line_str across identical-source plan builds), and the link-env dump is emitted by a dedicated bash helper with sorted names and printf %q quoting instead of whichever shell CONFIG_SHELL resolves to. Finally, unit dep lists shrank to what a replay actually reads: only thin-archive consumers carry the archive's member expansion, everything else stops at direct deps. A one-module body edit now rebuilds that module's TU, its .ko link, and the modpost pass -- the other modules no longer re-link at all."},{"title":"mcp-ex instructions now steer agents to the fleet for the work it wins","link":"https:\/\/ix.dev\/mcp-ex-fleet-guidance","guid":"https:\/\/ix.dev\/mcp-ex-fleet-guidance","pubDate":"Sun, 19 Jul 2026 19:00:00 GMT","description":"mcp-ex instructions now steer agents to the fleet for the work it wins. IxMcp.Fleet reaches beamd on the fleet hosts (700+ idle server cores, root, OTP 28) from the laptop that runs the MCP server, but nothing in the server's instructions mentioned it, so agents hand-rolled ssh for work that should fan out. The exec tool description, the server instructions, and the in-cell Api.help(IxMcp.Fleet) guide now name Fleet and, in two honest sentences each, say when to prefer it and when not to. Prefer Fleet for CPU-heavy or parallelizable pure-Elixir compute (Fleet.multicall\/3, Fleet.exec_least_loaded\/2), linux-only behavior checks, and work wanting many cores or hosts. Keep work local when it touches the workstation's files, repos, or bindings, is a small low-latency eval, is stateful (remote bindings do not persist -- code ships as source strings), or is darwin-specific. Fleet.nodes() == [] means no fleet is configured and the helpers return {:error, :no_nodes}; the remote env is minimal."},{"title":"nix fork: lazy trees vendored as an off-by-default lazy-trees setting","link":"https:\/\/ix.dev\/nix-fork-lazy-trees-setting","guid":"https:\/\/ix.dev\/nix-fork-lazy-trees-setting","pubDate":"Sun, 19 Jul 2026 19:00:00 GMT","description":"nix fork: lazy trees vendored as an off-by-default lazy-trees setting. The fork's nix now carries lazy trees: with lazy-trees = true, flake inputs are no longer copied into \/nix\/store at evaluation time. mountInput() computes each input's store path and NAR hash with a dry run, mounts the input's source tree at that path inside the evaluator, and only materializes the store object when something forces it (instantiating a derivation that references the path, builtins.storePath, IFD, nix eval output that leaks the path). For flake-heavy work the win is skipping the repeated copy of large source trees; Determinate reported 3x wall \/ 20x disk on such workloads for their variant. What got vendored is the variant upstream actually merged to master (NixOS\/nix#15711 plus its two post-merge fixes, backported as patches 0025-0028), not the better-known sources named in the plan. Both were examined: - edolstra's lazy-trees-v2 (NixOS\/nix#13225) mounts inputs at random virtual store paths and devirtualizes on demand. roberth blocked it: \"Using randomness or even fingerprints for placeholders makes the language non-deterministic and impure.\" Path equality and ordering must remain observably identical to eager evaluation; his preferred alternative, rope-structured strings with lazily computed fixed-length substrings (lazy hashing), has not been built by anyone. The PR was closed on 2026-07-16 pointing at #15711 as the mergeable subset, \"modulo the StorePath::random() bits\". - Determinate's fork shipped lazy trees to their users via staged rollouts since Determinate Nix 3.8.0, and was the preferred source going in. But their current tree (v2.35.1) no longer contains the random-virtual-path implementation at all; they ride the same upstream mechanism the fork now backports. #15711 describes itself as \"a slightly less lazy (but also more deterministic) approach than determinate nix has taken\": it still hashes every input once, so the mounted path is the real content-addressed store path and paths, hashes, and lock files are byte-identical to eager mode by construction. The known wrinkle survives determinism: a derivation built from a store-path string whose context was discarded (builtins.toString plus unsafeDiscardStringContext tricks) was always unsound, but lazy mounting changes when it surfaces, since the path may not exist on disk until something forces the copy. #13225 handled this with a devirtualize-without-reference context type plus a new warning; #15711 forces the copy at every escape point instead and adds a builtins.getFlake compatibility lookup for discarded-context store paths. Because of that history the setting defaults to off (patch 0029, our only divergence from upstream master, which enables the behavior unconditionally). Off means off: with the setting unset, mountInput() copies eagerly exactly like the 2.34.7 base, and the drvPath of index's own attrs is byte-identical to the previous fork build. Turning the fleet on is a separate decision that needs its own drv-hash and eval-result equivalence sweeps."},{"title":"The Plans map now pans and zooms like a real map","link":"https:\/\/ix.dev\/plans-map-pan-zoom","guid":"https:\/\/ix.dev\/plans-map-pan-zoom","pubDate":"Sun, 19 Jul 2026 19:00:00 GMT","description":"The Plans map now pans and zooms like a real map. The scatter on the Plans page used to be a static picture. It is now a direct-manipulation map. Drag to pan and the point under the cursor stays under the cursor, one to one, with no easing while you drag. Pinch on a trackpad or touchscreen, or hold Control and scroll, to zoom about the cursor; a plain two-finger scroll pans, the way a map does. Double-click zooms in on the spot you clicked. Zoom is semantic: the marks, their labels and the grid keep a constant size on screen while only their positions spread apart, so a cluster of plans that scored the same becomes legible instead of a single overlapping blob. The view is clamped so a corner of the plot is always on screen, and a Reset control appears once you have moved it. An Expand button grows the map to fill the window; Escape brings it back. Clicking a dot still opens its Plan: a press that does not travel past a few pixels is a click, anything further is a pan, so the two never fight."},{"title":"House prompt: subagent tools are never a superset of yours","link":"https:\/\/ix.dev\/prompt-subagent-tool-subset","guid":"https:\/\/ix.dev\/prompt-subagent-tool-subset","pubDate":"Sun, 19 Jul 2026 19:00:00 GMT","description":"House prompt: subagent tools are never a superset of yours. The house prompt gains a subagentToolSubset rule: treat a subagent's toolset as never a superset of your own. If you cannot run shell commands, assume your children cannot either; when you lack the tools for a task, fail fast and name the missing tool instead of delegating. The incident behind it: a parent agent without exec tools delegated to a subagent hoping the child had them. The child inherited the same limits, reasoned the same way, and delegated again. The relay chain burned roughly 30k tokens of pure coordination and produced no work. Nothing tells an agent what tools its children will get, so the only safe default is the subset assumption. It is not formally guaranteed (some agent types do attach extra tools), but assuming it breaks the recursion."},{"title":"README hero SVGs get committed dark twins so Safari readers can see them","link":"https:\/\/ix.dev\/readme-hero-safari-dark","guid":"https:\/\/ix.dev\/readme-hero-safari-dark","pubDate":"Sun, 19 Jul 2026 19:00:00 GMT","description":"README hero SVGs get committed dark twins so Safari readers can see them. Safari never evaluates prefers-color-scheme inside an SVG loaded through an <img> tag (WebKit bug 199134, fixed only in Safari Technology Preview 234), so every README hero rendered its light palette on GitHub's dark theme: near-black text on a near-black page. Each hero under .github\/readme\/ now has a committed -dark.svg twin with the dark palette as its defaults, and the README embeds both through a <picture> element whose page-level media query works in every engine. The readme-globe generator emits both files, and the creating-a-readme skill now documents the pattern instead of forbidding it."},{"title":"The Elixir kernel can drive local TUIs","link":"https:\/\/ix.dev\/tui-local-elixir-kernel","guid":"https:\/\/ix.dev\/tui-local-elixir-kernel","pubDate":"Sun, 19 Jul 2026 19:00:00 GMT","description":"The Elixir kernel can drive local TUIs. The Elixir MCP kernel (ix-mcp-ex) lost the Python kernel's local TUI driver in the cutover: Tui.act\/2 only reaches federated resources on other hosts, so a cell could not spawn vim, less, or a REPL on its own machine. Now it can. TuiLocal is aliased into every cell: TuiLocal.spawn(\"vim\", []) puts a real PTY child under a VT100 emulator and returns a terminal id; send\/2 types literal text (arrows stay DECCKM-aware in the driver), send_key\/2 takes named keys (enter, esc, f1..f12, ctrl+c chords), snapshot\/1 reads the rendered screen as plain text, and wait_for\/3, alive?\/1, exit_code\/1, kill\/1, close\/1 cover the poll-and-teardown loop. The plumbing is the second production consumer of unibind's Elixir backend: packages\/tui\/ex exports the Rust tui PTY manager as the :tui_ex NIF app (id-keyed free functions on dirty IO schedulers, since PTY I\/O legitimately blocks). The app is not a mix dep of the kernel -- the release bakes its store path into IX_MCP_TUI_EX (the IX_MCP_GH pattern) and IxMcp.TuiLocal loads it onto the code path on first use, so the kernel and the binding ship independently. A conformance-style ExUnit suite (checks.*.tui-ex-run) drives real children -- cat, stty, an interactive read loop -- through the generated bindings, and the mcp-ex smoke test now proves the embedded-mode runtime load inside the shipped release itself."},{"title":"visual-explainers skill: interactive, explanation-first visualization","link":"https:\/\/ix.dev\/visual-explainers-skill","guid":"https:\/\/ix.dev\/visual-explainers-skill","pubDate":"Sun, 19 Jul 2026 19:00:00 GMT","description":"visual-explainers skill: interactive, explanation-first visualization. Agents now carry a house skill for building visualizations. The Claude-bundled dataviz skill was invocation-blocked in #3607 because it injected Anthropic's own chart styling; visual-explainers fills the gap with a different philosophy. The visualization is the explanation: the reader learns by doing, every moving pixel is either caused by the reader or encodes causality in the system being explained, and decorative motion gets deleted. The skill also fixes the artifact shape: overview first with details on demand (click a stage to reveal its explanation in place), a counterexample when it deepens understanding, one accent color on a dark-friendly page, real payloads instead of lorem ipsum, and a single self-contained HTML file in vanilla JS that opens from file:\/\/ with the network off. The name is not dataviz because Skill(dataviz) stays hard-denied in agent policy, and a scoped deny blocks invocation by name no matter which skill carries it."},{"title":"Kernel edit loops stop re-paying the whole tree","link":"https:\/\/ix.dev\/kbuild-unit-edit-constant","guid":"https:\/\/ix.dev\/kbuild-unit-edit-constant","pubDate":"Sun, 19 Jul 2026 16:00:00 GMT","description":"Kernel edit loops stop re-paying the whole tree. A one-line kernel body edit rebuilds a handful of derivations in seconds, but the measured defconfig loop was 1m51s: the rest was a flat per-edit tax -- whole-tree re-skeletonization, whole-tree source re-ingestion, and flake eval -- paid regardless of what changed. Three changes flatten it. Skeletonization is now sharded. Instead of one derivation that re-reduces all ~80k files after any edit, each top-level source directory (drivers\/ one level deeper, since it alone is most of the tree) reduces in its own content-addressed derivation fed by a builtins.path slice of just that directory, and a compose step reassembles the tree byte-identically -- same store path as the old whole-tree reduction, so the plan contract does not move. A body edit re-reduces exactly the subtree it touched; every other shard, the composed skeleton, and the plan behind it resolve to already-realised outputs. The shards ride cachePushRoot (as skeleton-shards), so fleet hosts substitute them instead of re-reducing. Everything downstream now keys on those slices: the per-file source farm and the link unit's directory scopes resolve srctree paths through them, so Nix's source-ingestion memoization holds across evals for whatever an edit did not touch. buildKernel grew two entry points to feed it: srcTree (a pre-unpacked tree -- derivation output, or a plain path read in place under --impure), and srcSlices -- a driver pre-ingests each directory once (nix store add) and re-adds only the directory an edit touched, so the driver-side cost scales with the edit (also --impure: pure eval refuses store paths it was not handed context for; outputs stay fully content-addressed either way). What remains of the loop constant is Nix itself re-ingesting eval-time sources once per evaluation; measured numbers live on the issue. .git is excluded from automatic slicing so a checkout slices identically to the tarball it came from. Lane attributes were untangled at the same time: plan-side attrs (plan, skeletonTree, srcTree) no longer force the 3.6k-derivation units import just to be looked at."},{"title":"nix fmt drops Alejandra's promo output","link":"https:\/\/ix.dev\/nix-fmt-quiet","guid":"https:\/\/ix.dev\/nix-fmt-quiet","pubDate":"Sun, 19 Jul 2026 16:00:00 GMT","description":"nix fmt drops Alejandra's promo output. Running nix fmt used to end with Alejandra's marketing: a \"Congratulations! Your code complies with the Alejandra style.\" line and a rotating \"Special thanks to <sponsor> for being a sponsor of Alejandra\" plug on every invocation. That noise is gone. The formatter flake output now wraps pkgs.alejandra with a single -q (--quiet) flag, which Alejandra treats as \"hide informational messages\". Real output survives. A -q flag hides only the chatter, so parse and formatting errors still print and still exit non-zero, and files are still rewritten in place. A second -q would also swallow errors, so the wrapper stops at one. The lint-fix nix lane already ran alejandra --quiet; this brings the interactive entrypoint in line with it. The wrapper is a makeWrapper shim over the cached Alejandra store path, so nothing extra builds."},{"title":"github: flake inputs can fetch submodules","link":"https:\/\/ix.dev\/nix-github-submodule-inputs","guid":"https:\/\/ix.dev\/nix-github-submodule-inputs","pubDate":"Sun, 19 Jul 2026 16:00:00 GMT","description":"github: flake inputs can fetch submodules. The fleet's patched nix now honors submodules (and lfs) on github:, gitlab: and sourcehut: flake inputs. Forge tarballs are git archive output and never contain submodule content, so upstream nix either rejects the attribute outright (NixOS\/nix#13571) or silently produces a tree with empty submodule directories (NixOS\/nix#14982). Patch 0023 constructs such an input as the equivalent git+https input instead, with the same revision, the mapping the in-tree clone() path already used. The redirect happens at input construction, so the lock file records a plain git node that stock nix consumes without complaint; this also covers flakes that set inputs.self.submodules = true, which previously broke every github: consumer of that flake. Plain forge inputs keep tarball semantics and byte-identical hashes. Two caveats ride along, both scoped to inputs that request submodules or LFS: git checkouts do not apply export-ignore\/export-subst attributes the way archives do, and private repositories authenticate through git's credential machinery rather than the forge access-tokens setting."},{"title":"commits stop running the whole lint suite locally","link":"https:\/\/ix.dev\/drop-precommit-lint","guid":"https:\/\/ix.dev\/drop-precommit-lint","pubDate":"Sun, 19 Jul 2026 15:20:00 GMT","description":"commits stop running the whole lint suite locally. .githooks\/pre-commit was exec nix run .#lint: every commit re-ran the entire lint suite on the committer's machine, minutes per commit on a laptop, serially, before CI ran the identical suite again on fleet hardware. Tonight it added multi-minute stalls to a rebase whose lint CI was going to re-run anyway. The hook is gone; lint runs once, in CI, where it gates merges with the same strictness on much faster machines. nix run .#lint remains available for anyone who wants the check before pushing. The commit-msg hook (every commit references a site page) stays: it is instant and is policy, not duplicated validation."},{"title":"the repo drops direnv and the last git hook","link":"https:\/\/ix.dev\/drop-direnv-and-hooks","guid":"https:\/\/ix.dev\/drop-direnv-and-hooks","pubDate":"Sun, 19 Jul 2026 13:20:00 GMT","description":"the repo drops direnv and the last git hook. .envrc existed to export core.hooksPath so git ran the tracked .githooks\/commit-msg hook, and to load the flake devShell. The devShell load re-evaluated the flake on every cold start (minutes after a lock bump), and the sole remaining hook checked one commit-message convention. That is a lot of machinery for one regex. Both are gone: no .envrc, no .githooks\/, nothing to direnv allow. Cloning the repo is the whole setup. The site-page convention stays as written policy in the workflow skill; commits should still reference the page that explains them, it is just no longer enforced by a hook."},{"title":"one frozen pin, five root causes: a morning of CI archaeology","link":"https:\/\/ix.dev\/pin-freeze-postmortem","guid":"https:\/\/ix.dev\/pin-freeze-postmortem","pubDate":"Sun, 19 Jul 2026 13:15:00 GMT","description":"one frozen pin, five root causes: a morning of CI archaeology. The fleet pin (cache-ready) stopped moving at 00:43 and stayed frozen all morning. Pulling that one thread surfaced five independent root causes, each masking the next. Two green PRs made a red main. A plan landed as number 0031 at 00:15; two hours later another PR renumbered a second plan onto 0031. Required checks are non-strict, so nothing ever built the union, and the site build (which enforces uniqueness) went red for 8.5 hours. The publish-hole gate correctly froze the pin over the failed root. Fixed by renumbering (#3668, #3675); the class is #3669, and main now validates every commit to completion in both repos (#3670, ix#7846) instead of cancelling its own runs on each merge. The mac lane starved the linux lane. The 2h hosted-darwin cache lane shared one concurrency group with everything: 24 consecutive cache-push runs cancelled in one morning, pin cycle once per 2h. The darwin jobs now live in their own workflow (#3672) pending deletion once nom cross-compiles. A pin bump downgraded a database. ix's nixpkgs follows index's, and index's nixpkgs was 14 days stale, so an unrelated index-pin bump moved ix's nixpkgs backwards 9 days, shipping TigerBeetle 0.17.8 under a data file already hot-upgraded to 0.17.9. TigerBeetle upgrades are one-way: 1446 crash-loops, billing dispatch down, VM heartbeats unpersisted, golden snapshot capture dead, deploys red (ix#7844). The ledger is now an explicit per-platform pin (ix#7845), verified healthy on the host within 20 minutes of the fix deploy. Why was nixpkgs stale? The hourly updater had produced the correct bump for 159 hours; its rolling PR is bot-authored, bot PRs never trigger CI, and the auto-merge lane never landed. A human re-authoring the identical bump got CI instantly (#3683). The closed-loop design is #3692. Why did the unblocked lane still die? A wedge reaper added two days earlier killed any CI job under 5 CPU-seconds per 15 minutes. A cache-push realise phase is pure network at 3.8, indistinguishable from the reaper's own measured wedge floor of 2.3: five healthy jobs reaped in one morning, three in a row on the pin's lane (ix#7851). The wedge class it hunted was fixed at the source by the p22 nix fork, fleet-deployed the same morning, so the reaper is retired by default (ix#7852). It got one last kill in when a deploy silently re-armed its timer; the deploy-proof bridge is a runtime mask, a lesson now in the incident notes. The pattern across all five: each guard was correct in isolation, and every failure hid inside a layer that reported success. The fixes that matter are the ones that make red visible at the moment it happens: per-commit verdicts on main, one workflow per concurrency domain, explicit pins for one-way state, and CI that runs on the robot's PRs too."},{"title":"Cache watchdog covers both publish lanes, with lane-correct wedge ceilings","link":"https:\/\/ix.dev\/watchdog-split-coverage","guid":"https:\/\/ix.dev\/watchdog-split-coverage","pubDate":"Sun, 19 Jul 2026 13:11:00 GMT","description":"Cache watchdog covers both publish lanes, with lane-correct wedge ceilings. A wedged linux cache push is now flagged after 190 minutes instead of 520, and the darwin publish lane is watched at all. cache-push-watchdog.yml still modeled the world before the darwin split (#3672): its zombie ceiling was the sum of job timeouts that no longer share a workflow, so linux zombie detection ran ~3.4x slower than the lane's real budget, and the darwin lane had no watchdog whatsoever -- no zombie detection, no stall detection, and no drift signal for cache-ready-darwin, a pin ref that has never been created and so could drift unboundedly without an alert. The watchdog's one cron now sweeps a two-row matrix of (workflow, pin ref, ceiling): cache-push.yml \/ cache-ready at 190 min (push 150 + advance-cache-ready 10, plus 30 slack) and cache-push-darwin.yml \/ cache-ready-darwin at 420 min (darwin-build 330 + darwin-push 60, plus 30 slack) -- the same sum-of-serial-timeouts-plus-slack construction the old 520 used, applied per lane. The three signals and the tracking-issue mechanics are unchanged, evaluated per lane: each lane files its own tracking issue naming the workflow that wedged (linux keeps the pre-split thread), and the missing cache-ready-darwin ref reports \"does not exist yet; nothing to compare\" until the first darwin publish creates it. When #3587 deletes the darwin lane, its matrix row goes with it."},{"title":"Every frontier system card, committed as markdown with a one-command regen","link":"https:\/\/ix.dev\/system-cards-corpus","guid":"https:\/\/ix.dev\/system-cards-corpus","pubDate":"Sun, 19 Jul 2026 12:43:18 GMT","description":"Every frontier system card, committed as markdown with a one-command regen. packages\/system-cards commits the frontier-model system cards as searchable markdown: all 17 Anthropic cards listed at anthropic.com\/system-cards, Claude 2 (July 2023) through Claude Sonnet 5 and the Fable 5 \/ Mythos 5 card (June 2026), plus OpenAI's GPT-5.6 preview and final cards. 19 documents, 4.2 MiB of markdown, each with provenance frontmatter carrying the source URL, its pinned SRI hash, and the publication date. The pipeline is pure: catalog.json pins every PDF by hash, a derivation fetches them and converts with pymupdf4llm, and nix run .#system-cards copies the build product over cards\/. The committed markdown is byte-for-byte the derivation output, so drift is a diff. Adding a card is one catalog entry plus a regen. docling was the intended converter (better table structure), but nixpkgs marks docling-parse broken on every platform, in our pin and on current unstable, from a nixpkgs-side nlohmann_json patch. The swap is tracked as issue 3690."},{"title":"visual explainers render human time in the reader's timezone","link":"https:\/\/ix.dev\/visual-explainers-human-time","guid":"https:\/\/ix.dev\/visual-explainers-human-time","pubDate":"Sun, 19 Jul 2026 12:20:00 GMT","description":"visual explainers render human time in the reader's timezone. The visual-explainers skill now has a Time section: pages keep machine ISO-8601 instants in the data layer and render only human forms (\"38m elapsed\", \"eta 5:10-5:25 pm\"), formatted for the reader's local timezone via Intl.DateTimeFormat rather than hardcoded into copy. A live CI status page shipped today with raw 11:45Z timestamps throughout its prose; the reader (in Los Angeles) had to subtract seven hours per glance and asked for the page to be redone in local time. Checking the local timezone first, and letting the page do the formatting, removes that whole round trip."},{"title":"House prompt gains a calibrated-wording rule: no absolutes without a measurement","link":"https:\/\/ix.dev\/prompt-calibrated-claims","guid":"https:\/\/ix.dev\/prompt-calibrated-claims","pubDate":"Sun, 19 Jul 2026 12:18:05 GMT","description":"House prompt gains a calibrated-wording rule: no absolutes without a measurement. Agent reports drifted into absolutes: \"works\", \"never happens\", \"impossible\". The house prompt now carries a calibratedClaims rule that words each claim at the strength of its evidence: \"passed 40 of 41\" over \"works\", \"did not reproduce in 20 runs\" over \"impossible\", and an absolute only as a measurement report. Where the measurement is missing, the rule prescribes the estimative ladder (unlikely, roughly even, likely, almost certainly, plus cannot-rule-out for severe tails) with the check's coverage named, and a regression disclosed next to the win it accompanies. The register is borrowed from the Claude Fable 5 system card, which the faithfulReporting rule already cites: findings there read \"extremely difficult (though not impossible)\" and \"a much less clear judgement than for previous models\", with safety regressions listed beside headline results. The same practice traces back to the intelligence-community estimative lexicon (likely, cannot rule out, low-moderate-high confidence). Rendered prompts pick the rule up on the next wrapper build; omitRules = [ \"calibratedClaims\" ] drops it."},{"title":"plan and update id collisions now fail the merge gate","link":"https:\/\/ix.dev\/site-id-lint","guid":"https:\/\/ix.dev\/site-id-lint","pubDate":"Sun, 19 Jul 2026 11:45:00 GMT","description":"plan and update id collisions now fail the merge gate. The lint suite now validates plan, update, and story identity directly: within each of packages\/site\/src\/lib\/{plans,updates,stories}, a four-digit filename prefix may be claimed only once, frontmatter id must equal the filename stem, and a frontmatter number must agree with the prefix. Check runs the lint on every PR and every main commit, so a collision now fails the merge gate in minutes instead of after it lands. These invariants used to live only inside the SvelteKit site build, which the Check gate never runs, so this class of breakage was invisible until the Pages deploy failed. Two individually-green PRs merged into a duplicate plan number 0031 and main's site build stayed red for 8.5 hours (#3669); the renumber that fixed the collision (#3668) itself left the old number: '0031' frontmatter behind, which the new stage caught on its first local run, and this change repairs."},{"title":"every main commit now gets a completed Check verdict","link":"https:\/\/ix.dev\/check-main-per-commit","guid":"https:\/\/ix.dev\/check-main-per-commit","pubDate":"Sun, 19 Jul 2026 11:35:00 GMT","description":"every main commit now gets a completed Check verdict. Main's Check runs used to share one concurrency slot: each push cancelled the previous commit's run, so during a merge storm no main commit finished validation (every run from 10:56Z to 11:08Z on 2026-07-19 was cancelled). Required checks are non-strict, so two individually-green PRs can rebase into a red union that no PR run ever saw; today that combination let a duplicate plan number sit on main for 8.5 hours, surfacing as a frozen cache-ready pin instead of a named commit. Check now groups main runs per-commit (github.sha) and never cancels them, while PR refs keep the old supersede-and-cancel behavior. A red union now shows up within one Check duration, attributed to the exact merge that caused it."},{"title":"Cache pin advances every merge: the darwin lane moves to its own workflow","link":"https:\/\/ix.dev\/cache-push-darwin-split","guid":"https:\/\/ix.dev\/cache-push-darwin-split","pubDate":"Sun, 19 Jul 2026 11:30:00 GMT","description":"Cache pin advances every merge: the darwin lane moves to its own workflow. The fleet's cache-ready pin now advances minutes after every merge instead of once per ~2 hours: the native darwin cache lane (darwin-build and darwin-push) moved out of cache-push.yml into its own cache-push-darwin.yml with its own concurrency group. GitHub keeps one pending run per concurrency group, so each 2h+ hosted-mac run held the shared cache-push group while every merge behind it was superseded-cancelled: 24 consecutive cancelled runs between 08:51Z and 11:08Z on 2026-07-19, while the linux publish the pin actually waits on needs ~30 minutes worst case. The cache-ready-darwin gate changed shape with the move. The darwin workflow has no linux push job whose failed-roots output it can read, so the linux half of the publish-hole gate now reads the cache-ready ref itself: the pin only advances to a commit that cache-ready already contains, because the linux lane fast-forwards that ref exclusively over hole-free runs. Linux lag logs a notice and exits clean; the next darwin run catches up. All of this is interim plumbing: once the last darwin roots are linux-cross-built (#3586), the whole darwin workflow is dead code (#3587)."},{"title":"claude-code-rainbow can no longer self-install over the wrapped claude","link":"https:\/\/ix.dev\/claude-rainbow-no-self-install","guid":"https:\/\/ix.dev\/claude-rainbow-no-self-install","pubDate":"Sun, 19 Jul 2026 10:30:00 GMT","description":"claude-code-rainbow can no longer self-install over the wrapped claude. The rainbow POC shipped the raw byte-patched binary at bin\/claude, without the DISABLE_UPDATES=1 guard the stock claude-code launch spec sets. Running it directly let Claude Code's self-installer go to work: with installMethod: native recorded in ~\/.claude.json, it downloaded a stock native build into ~\/.local\/share\/claude\/versions and wrote a ~\/.local\/bin\/claude launcher. On a machine whose PATH puts ~\/.local\/bin before ~\/.nix-profile\/bin, that launcher shadows the wrapped claude, so every new session silently starts the stock binary: no house MCP config (the ix-mcp-ex kernel), no system prompt, no injected flags. The package now installs the patched binary under libexec behind a makeBinaryWrapper shim that pins DISABLE_UPDATES=1, matching the stock package's guard, so running the rainbow build can never mutate the machine's claude resolution. The change also clears the lint debt in this package family that made any fresh-worktree commit fail: the byte-swap mappings are typed Nix rules passed to the patcher via passAsFile instead of committed JSON, the manifest import goes through ix.paths, and the unused lambda args in claude-code-debug and claude-code-rainbow-live are gone."},{"title":"The README opens with a spinning ASCII globe, generated by a Nix pipeline","link":"https:\/\/ix.dev\/readme-ascii-globe","guid":"https:\/\/ix.dev\/readme-ascii-globe","pubDate":"Sun, 19 Jul 2026 08:30:00 GMT","description":"The README opens with a spinning ASCII globe, generated by a Nix pipeline. The README hero is now a spinning globe (.github\/readme\/globe.svg): the whole world rendered as braille text, land in the foreground color, ocean muted, adapting to light and dark mode like every other README graphic. The flywheel diagram moves back to lead the Why section. The globe is not hand-drawn. packages\/readme-globe is a Nix derivation that fetches a hash-pinned NASA Blue Marble equirectangular texture from Wikimedia Commons, thresholds it into a land mask, orthographically projects 96 rotation frames onto a braille dot grid (2x4 subdots per cell, so an 80x40 grid renders 160x160 effective dots) where Lambert shading survives as ordered-dither dot density, and emits one animated SVG whose frames cycle with CSS steps(). Regenerate with nix build .#readme-globe and copy the output over the committed file. Why SVG instead of a GIF or webp: text stays crisp at any scale, prefers-color-scheme works (raster formats pick one background), reduced-motion users get a static frame, and 96 frames of braille compress to a fraction of any raster animation."},{"title":"stock Nix now gets an install message instead of a parse error, and the bootstrap seam is enforced","link":"https:\/\/ix.dev\/evaluator-gate-stock-parse","guid":"https:\/\/ix.dev\/evaluator-gate-stock-parse","pubDate":"Sun, 19 Jul 2026 08:05:06 GMT","description":"stock Nix now gets an install message instead of a parse error, and the bootstrap seam is enforced. The repo pins its own Nix evaluator: the fork source lives in-tree (lib\/fork-packages.nix, built as nix-ix) and carries language patches such as underscore digit separators (25_576). tests\/default.nix already uses that syntax, so anyone evaluating a tests-derived check with stock upstream Nix got a raw syntax error, unexpected invalid token with no hint about the fix. Two additions make the boundary explicit: - ix.evaluatorGate.require wraps the import of each fork-syntax island (today: tests\/). Syntax errors happen at parse time, before any version check can run, so the gate sits at the import seam and stays frozen-syntax itself, the same trick as nixpkgs' lib\/minver.nix. Stock Nix now gets an actionable error ending in the one command that fixes it: nix profile install github:indexable-inc\/index#nix-ix. - checks.<system>.stock-nix-parse runs the nixpkgs-pinned stock nix-instantiate --parse over every tracked .nix outside tests\/. External flakes evaluate index with their own evaluator, and the nix-ix bootstrap only works while its import closure parses on stock Nix; CI runs the fork, so nothing else would notice a stray fork-only literal on the shared surface. The check turns \"stock Nix can always build the fork\" from luck into law. The gate detects the fork through the +ix. suffix in builtins.nixVersion rather than a fork-added builtin: a new builtin would reject every currently deployed evaluator until a nix upgrade reaches the fleet."},{"title":"nom on Macs: cross-compiled from Linux by a patched cross-GHC","link":"https:\/\/ix.dev\/nom-cross-ghc","guid":"https:\/\/ix.dev\/nom-cross-ghc","pubDate":"Sun, 19 Jul 2026 06:30:00 GMT","description":"nom on Macs: cross-compiled from Linux by a patched cross-GHC. packages.aarch64-darwin.nix-output-monitor is now a Linux-cross-built artifact: a Mac substitutes a prebuilt Mach-O arm64 nom from the cache instead of building GHC and the 62-package Haskell closure natively. The verdict in #3584 (\"Haskell cross to darwin is infeasible\") only condemned nixpkgs' pkgsCross road, which dies bootstrapping the Apple SDK; building GHC itself as a cross compiler the way upstream's wasm and mingw lanes do -- .\/configure --target=aarch64-apple-darwin + hadrian, with the RFC 0009 clang\/ld64.lld\/SDK toolchain as target tools -- works. It took three small GHC patches (two host-vs-target CPP confusions in the aarch64 NCG and x18 register reservation, one Cabal osx spelling case in GHC_CONVERT_OS) and a bundled-libffi bump to 3.5.2 for a Mach-O CFI atom rule. Template Haskell splice execution is impossible in this lane (no darwin iserv on Linux); an audit showed nom's closure runs zero splices, so nothing had to be faked. A cross-darwin-nom-smoke gate asserts the output stays Mach-O arm64 with the by-name alias symlinks intact. Written by an AI agent via Claude Code (Claude Fable 5)."},{"title":"writing-style skill learns the AI tells","link":"https:\/\/ix.dev\/writing-style-ai-tells","guid":"https:\/\/ix.dev\/writing-style-ai-tells","pubDate":"Sun, 19 Jul 2026 06:00:00 GMT","description":"writing-style skill learns the AI tells. The agent's writing-style skill now carries a catalog of the patterns readers use to spot AI prose, with an instruction to never produce them: the rule of three, \"not X, but Y\" parallelisms, em dashes, the delve\/robust\/seamless lexicon, \"serves as\" where the sentence means \"is\", synonym cycling, significance inflation (\"plays a vital role\"), hedging filler (\"it's worth noting\"), assistant chirp, bolded-phrase bullets, and closing summaries. The catalog is drawn from Wikipedia's Signs of AI writing field guide and the vocabulary studies it cites (Juzek and Ward on RLHF word overuse, Kobak on academic abstracts). The skill also ships a paste-ready prompt block for generating or reviewing prose, so the same bans apply when the agent writes for other contexts. The README intro was rewritten under the new rules and repositioned: it now leads with what index is (one shared software universe, nixpkgs-style) and its relation to ix.dev (ix is the runtime, index is the world it runs), after a reader mistook the old intro for a per-team agent product."},{"title":"Patched git: linked worktrees stop re-cloning submodules","link":"https:\/\/ix.dev\/git-worktree-submodule-alternates","guid":"https:\/\/ix.dev\/git-worktree-submodule-alternates","pubDate":"Sun, 19 Jul 2026 05:30:00 GMT","description":"Patched git: linked worktrees stop re-cloning submodules. git joins the de-forked packages (lib\/fork-packages.nix): upstream v2.54.0 plus one patch that fixes a twelve-year-old gap in git's worktree support. Stock git resolves submodule gitdirs to a per-worktree path, so the first git submodule update --init in every new worktree re-clones each submodule from its remote even though the main checkout already holds a complete object store under .git\/modules\/. Upstream called the reuse \"logical\" in the 2014 commit that shipped the split (df56607dff2) and never implemented it. The patch teaches prepare_possible_alternates() to add $GIT_COMMON_DIR\/modules\/<name>\/objects as a clone reference with --reference-if-able semantics whenever the resolved gitdir is per-worktree. Immutable objects are shared through alternates; per-checkout state (HEAD, index) stays private. Measured on a 5 MB submodule: a new worktree's submodule init drops from 5.0 MB of duplicated store plus a full network refetch to 112 KB and no fetch, and it scales linearly with submodule size and count. With the worktree-per-task agent workflow this removes a full submodule re-download from every task start in superprojects that use submodules. The base is pinned by rev to nixpkgs' git version tag (autoUpdate = false) because the package overlays the nixpkgs recipe onto the patched tree; an eval-time assert fails loudly when nixpkgs' git moves so the pin gets advanced deliberately. Upstream policy is recorded as mailing-list-only (prsWelcome = false): git\/git on GitHub is a read-only mirror, so upstreaming this series is a manual git send-email submission, held for now."},{"title":"plumb: a shell where every run is a value","link":"https:\/\/ix.dev\/plumb-shell","guid":"https:\/\/ix.dev\/plumb-shell","pubDate":"Sun, 19 Jul 2026 02:45:00 GMT","description":"plumb: a shell where every run is a value. packages\/plumb is a bash-subset shell built library-first for LLM agents. Run cargo clippy 2>&1 | tail -n 5 and the run comes back as a value: per-stage argv, status, timing, and bounded captures of every stream, including what each pipe stage fed the next. Every stream stays addressable afterwards (${o[1][0]} is everything tail consumed, negative indexes count from the latest), so a later command can reuse any earlier stream without re-running it. Three crates: plumb-syntax (spanned parser; unsupported bash is a loud error, never a misparse), plumb-core (cloneable Shell over shared state, tee'd capture engine with backpressure, strict semantics: pipefail, unset-var error, failglob, no word splitting), and the plumb CLI (reedline REPL with per-stage summaries and :runs \/ :json \/ :out inspection, -c, script and stdin modes, --json). A public read-only mirror is generated at indexable-inc\/plumb. The Elixir binding (plumb-ex, via unibind) exposes Plumb.Shell.eval\/2 returning the report as JSON, with error variants as atoms and detached runs pollable by id, so the workstation kernel can drive shells in-process. Two boundary gotchas surfaced and are now recorded: the BEAM ignores SIGCHLD (auto-reaping NIF-spawned children into ECHILD; plumb-ex restores the default disposition once), and cargo check on any unibind ex crate false-fails on a duplicate nif_init."},{"title":"The site renders on a TUI character grid and ships as @indexable\/site","link":"https:\/\/ix.dev\/site-tui-grid-library","guid":"https:\/\/ix.dev\/site-tui-grid-library","pubDate":"Sat, 18 Jul 2026 21:35:00 GMT","description":"The site renders on a TUI character grid and ships as @indexable\/site. This page now renders on a strict TUI character grid, matching the ix.dev design language: one 14px monospace cell with an exact 21px line height, every vertical distance a whole multiple of that cell and every horizontal distance in ch units. There is exactly one font size; hierarchy comes from weight, UPPERCASE, letter-spacing, and inverse-video blocks. Panels draw as box frames with their titles inline in the top rule, chips read as [ label ], and the plan score meters render as block-character gauges. The filter bar on this page is now a Todoist-style search box. Words that match a known tag, or a prefix of one (rus for rust), highlight inline as subtle pills and AND together as tag filters; everything else full-text searches titles and bodies, never an error. A box-drawn autocomplete under the caret lists matching tags with entry counts (arrows to move, Tab or Enter to accept, Esc to close), and &, |, ! survive as power syntax. A pixel QA pass tightened both surfaces: the autocomplete wears the same crisp 1px frame as the focused search panel, and plan flow diagrams no longer clip their last column or push the page off the 21px grid. Wide DAGs now break out of the column, centered, and their scroll pane hides scrollbar chrome so its height stays a whole number of cells in every browser. The content also ships as a raw-source Svelte library, @indexable\/site, built by nix build .#site-lib. It exports the updates, plans, stories, and philosophy collections with their loaders, the page-level components, the RSS feed builder (site URL as a parameter), and styles\/tokens.css as the single source of design tokens. Each themeable token is one light-dark() declaration under color-scheme: light dark, so a color is defined once rather than in a :root block shadowed by a @media (prefers-color-scheme: dark) override. ix.dev composes its site from this package, so the same entries and components serve both deployments; this site's routes are thin wrappers over the same sources."},{"title":"Every commit now references the site page that explains it","link":"https:\/\/ix.dev\/commit-site-page-links","guid":"https:\/\/ix.dev\/commit-site-page-links","pubDate":"Sat, 18 Jul 2026 19:00:00 GMT","description":"Every commit now references the site page that explains it. Commit subjects alone rarely explain a change, and long commit bodies are hard to find again. Commits in this repo now point at the site instead: every commit message must include a repo path to a page under packages\/site\/src\/lib\/updates\/, plans\/, or stories\/, and the long-form description lives on that page. A new .githooks\/commit-msg hook enforces this through the same core.hooksPath activation as the existing pre-commit lint hook. The referenced page must exist in the commit's staged tree, so adding the page in the same commit satisfies the check. Merge, fixup, squash, and revert commits are exempt because they carry their own context. For now the reference is a plain repo path, not an https link, and there is no dedicated commits section on the site; a commit either adds an update entry like this one or points at the existing page it extends."},{"title":"README leads with the thesis: writing code got cheap, landing it is the bottleneck","link":"https:\/\/ix.dev\/readme-thesis","guid":"https:\/\/ix.dev\/readme-thesis","pubDate":"Sat, 18 Jul 2026 19:00:00 GMT","description":"README leads with the thesis: writing code got cheap, landing it is the bottleneck. The README now opens with why the repo exists instead of what it contains. Agents made writing code cheap; shipping it is still expensive (review queues, maintainer bus factors, projects that refuse AI-written patches), so the bottleneck moved from writing a change to landing it. index is the bet on the other path: every change lands here now, in one graph everyone consumes the same way, and upstream can take it someday. A new animated hero (.github\/readme\/unblocked.svg) draws the two paths in the house terminal style: the upstream lane queues blocks in front of a review gate, the index lane streams them through main and fans them out to every consumer. All wires are axis-aligned so the diagram reads as a text grid, matching the other README graphics."},{"title":"The prompt rule that survived its own opt-out: omitRules shadowed by an explicit package","link":"https:\/\/ix.dev\/prompt-omit-shadowing-and-kernel-hardening","guid":"https:\/\/ix.dev\/prompt-omit-shadowing-and-kernel-hardening","pubDate":"Sat, 18 Jul 2026 04:49:59 GMT","description":"The prompt rule that survived its own opt-out: omitRules shadowed by an explicit package. A workstation Claude session was still carrying the force-merge prohibition its owner had opted out of. The opt-out was configured correctly: programs.claude-code.systemPrompt.omitRules listed the rule. But the module folded omitRules only into its derived default package, via cfg.basePackage.override, so an explicit package = silently discarded the whole omission list. Permissions said one thing; the rendered system prompt said another (#3537). #3545 fixes it by threading omitRules into the override arguments, and adds a loud eval assertion so the shadowing can never be silent again: it checks options.<ns>.package.highestPrio against (lib.mkDefault null).priority and fails evaluation when omissions are configured on an explicitly set package the module cannot rewrite. The general lesson, that an option folded only into a derived default dies quietly the moment someone sets the option it derives, is now a shared prompt rule, moduleOptionShadowing (#3552). The same debugging session surfaced two kernel bugs in the Elixir MCP server. Binary bytes in a cell's output killed the MCP connection outright and orphaned the job record (#3538); #3542 sanitizes output to valid UTF-8 at the boundary, makes the transport reply-always so a cell can no longer die without answering, and monitors job history so finished jobs always land a record. And a schema-drifted actions.db crashed the whole server at startup (#3539); #3543 adds PRAGMA user_version migrations and routes crash dumps into the state dir instead of the current working directory. #3550 rounds it out: after a checks-only watcher idled for 35 minutes on a PR that had already merged, the kernel instructions now bake in the two-signal pattern, PrWatch for merge state plus gh pr checks --watch --fail-fast for CI, so neither signal alone can strand a watch. Deployed and verified on the workstation: the rendered system prompt file now drops both stale rules and carries the new one. Written by Claude Code, an AI coding agent."},{"title":"A raw NVMe partition loses to a plain APFS image file for the builder VM","link":"https:\/\/ix.dev\/vm-disk-apfs-bypass","guid":"https:\/\/ix.dev\/vm-disk-apfs-bypass","pubDate":"Sat, 18 Jul 2026 04:23:36 GMT","description":"A raw NVMe partition loses to a plain APFS image file for the builder VM. The aarch64 NixOS builder VM on hydra (a Mac Studio) does all of its disk I\/O through an image file sitting on APFS, so every guest write crosses vfkit, the host page cache, and a copy-on-write filesystem. Virtualization.framework can instead attach a raw block device via VZDiskBlockDeviceStorageDeviceAttachment (vfkit type=dev), skipping APFS entirely. The question: does a dedicated raw internal-NVMe partition beat the image file for nix-builder workloads? The A\/B ran on hydra's builder VM under vfkit 0.6.3, a GPT\/repart appliance image with an xfs root. A 256 GiB partition was carved out of the live internal NVMe with diskutil apfs resizeContainer and the image was dd'd onto \/dev\/rdisk0s5 at 5.9 GB\/s. In-guest benchmarks: fio 4k randwrite QD8 O_DIRECT, plain and with fsync=1, plus a cold nix copy of the 3.3 GiB ghc closure. | backing | randwrite IOPS | fsync IOPS | fsync p99 | nix copy | | --------------------------- | -------------- | ---------- | --------- | ---------- | | image file (sparse, noatime) | 34,123 | 14,651 | 1.7 ms | 131.6 MiB\/s | | partition, sync Full | 12,048 | 1,024 | 16.7 ms | 116.0 MiB\/s | | partition, raw node | 14,581 | 524 | 32.4 ms | 70.8 MiB\/s | File backing wins every axis on this stack. The builder stays file-backed, and the partition machinery landed dormant behind a UUID-pinned spec knob (nix#143). The mechanism is the synchronization mode, not APFS. VZ block-device attachments offer only synchronizationMode Full or None: Full turns every guest write barrier into F_FULLFSYNC on the physical disk, 13 to 28x slower on registration-shaped fsync paths, while file-backed attachments get the host page cache plus plain-fsync semantics. Next to that, APFS overhead is noise. Two side findings along the way: vfkit rejects macOS raw \/dev\/rdiskN character nodes for type=dev (an unaligned header read plus an S_IFBLK stat check; a 2-line patch boots fine off rdisk), and VZ propagates the internal SSD's 4Kn sector geometry into the guest, so images need sectorSize 4096. Prior art is thin: VZDiskBlockDeviceStorageDeviceAttachment shipped in macOS 14, lima tracks the same idea in lima-vm\/lima#4866, and vfkit exposes it as type=dev. The open cell in the matrix is partition plus sync None, which the vmkit work in index#3554 makes testable. Written by an AI agent via Claude Code (Claude Opus 4.5)."},{"title":"vmkit attaches raw host block devices as VM disks, bypassing APFS","link":"https:\/\/ix.dev\/vmkit-raw-block-devices","guid":"https:\/\/ix.dev\/vmkit-raw-block-devices","pubDate":"Sat, 18 Jul 2026 02:10:00 GMT","description":"vmkit attaches raw host block devices as VM disks, bypassing APFS. vmkit's Virtualization.framework guests can now use a real disk instead of an image file: any --disk on boot-linux-gui \/ drive-linux (and a macOS bundle's disk.img symlinked at a device node) may name \/dev\/diskN[sM] or \/dev\/rdiskN[sM], attached via VZDiskBlockDeviceStorageDeviceAttachment (macOS 14+). Guest I\/O goes straight to the device, skipping APFS sparse-file fragmentation, COW metadata, and host-side double caching \u2014 the costs persistent high-churn guests pay most. Safety first: a device that hosts mounted filesystems \u2014 including through APFS physical-store linkage, where the volumes mount under a synthesized container disk \u2014 is refused unless --force, and the guest-flush mode is an explicit --disk-sync full|none (default full). A root-owned \/dev node is opened by a sudo'd re-exec of vmkit that hands the fd back over a private unix socket, so the VM never runs as root and no sudoers entry is ever installed (the pitfall that got lima's equivalent feature reverted). The libkrun backend (boot-linux) rejects device paths loudly; it only takes image files."},{"title":"Weave agent memory at scale: all 23 plan tracks executed in one day","link":"https:\/\/ix.dev\/weave-agent-memory-executed","guid":"https:\/\/ix.dev\/weave-agent-memory-executed","pubDate":"Sat, 18 Jul 2026 00:11:27 GMT","description":"Weave agent memory at scale: all 23 plan tracks executed in one day. weave#281 laid out the agent memory plan in docs\/weave-agent-memory.html: 23 sub-issues (#284 to #306) spanning storage, query, claims, recall, client surface, and evals. All 23 landed on 2026-07-17, executed by a fleet of parallel AI agents working the tracks concurrently. The measured baseline that motivated the work (linked above) is stark. Cold start scaled roughly x4.4 per x2 facts (40 s at 125k facts, 150 s at 250k, 693 s at 500k, 2484 s at 1m), watch deltas tracked cold start almost 1:1 because every tick re-derived state from the journal, resident memory ran about 470x the journal bytes (18.6 GB RSS over a 39.8 MB journal at 500k facts), and a 1e9 fact ingest was OOM-killed near 4e8 facts with the journal fully resident. The 10m and 100m rungs could not be measured at all under a 2 hour budget. What landed, by track: - Storage and query: EAV\/AEV\/AVE covering indexes on fjall (weave#315), a tiered T0\/T1 query planner with demand transformation (weave#321), incremental view maintenance for watches (weave#329), as-of checkpoints (weave#289), and journal compaction with cold-segment archival (weave#312). - Claims and provenance: claim and observation schema facts plus the epistemic prelude rules (weave#313), provenance envelope stamping at the write path (weave#322), a basis DAG with derived staleness (weave#320), and trust-as-lens example RulePacks (weave#324). - Recall: an embedding sidecar ANN index (weave#314), ANN entry points feeding Datalog graph expansion (weave#323), and auto-surfaced hints of roughly 40 tokens on tool results (weave#331). - Client surface: thin Rust, Python, and TypeScript clients (weave#317), a headless lean server build (weave#310), and weave.claim and weave.recall client verbs (weave#319). - Evals and gates: a nix-built eval sandbox (weave#328), memory-use eval suites on local models (weave#334), and enforced scale perf gates in CI (weave#332). Written by Claude Code, an AI coding agent."},{"title":"Fleet SSH sessions are now reaped at logout","link":"https:\/\/ix.dev\/fleet-logind-reap-on-logout","guid":"https:\/\/ix.dev\/fleet-logind-reap-on-logout","pubDate":"Fri, 17 Jul 2026 15:51:34 GMT","description":"Fleet SSH sessions are now reaped at logout. Three orphaned root du diagnostic probes ground hil-stor-2 at roughly 58% full IO pressure (PSI) for more than 30 hours. Killing them dropped the pressure to 5% within seconds (ix#7615). The probes had been launched over SSH long before, and nothing ever cleaned them up. The root cause is a pair of defaults. sshd does not reap a command's children after the connection drops, and logind's default policy leaves session processes running forever; root is even exempt from KillUserProcesses out of the box. Any process forked from an SSH session could outlive it indefinitely, invisibly. ix#7616 changes that on every fleet host: KillUserProcesses=true with KillExcludeUsers= cleared, so everything still inside a session scope dies at logout. This is a behavior change for everyone. nohup, tmux, and ssh host \"cmd &\" no longer survive disconnect. Intentional survivors must escape the session scope explicitly: systemd-run --unit=<name> --property=RuntimeMaxSec=<s> as root, or loginctl enable-linger plus systemd-run --user for regular users. The same PR removed weave and weave-pack from prod hosts; they are not stable enough yet, and the modules stay in the tree for a later re-enable."},{"title":"The green CI that wasn't: an embed typecheck hidden behind a truncated log","link":"https:\/\/ix.dev\/mcp-embed-typecheck-hunt","guid":"https:\/\/ix.dev\/mcp-embed-typecheck-hunt","pubDate":"Thu, 16 Jul 2026 08:51:49 GMT","description":"The green CI that wasn't: an embed typecheck hidden behind a truncated log. Bundling in-process code embeddings into the MCP server (torch\/MPS, #3417) should have been a clean package addition. Instead PR #3427 sat red on the required flake-check gate for two sessions, behind three separate red herrings. The durable lesson is not about embeddings: it is that a \"mystery\" CI failure is usually a real error you have not read yet. ## Symptom flake-check failed on #3427 with no obvious cause. The GitHub Actions step log ended mid-stream in a nix-fast-build progress dump, with no error line at the tail. Three plausible stories competed for the blame. ## The three red herrings - Billing lock. During a GitHub org billing-lock window the required gate had genuinely returned false-green: jobs failed in 2 to 3 seconds with empty steps. Real, but already over by the time #3427 was failing in hundreds of seconds with real work in the log. - Validation clock. The embed deps balloon the base-image OCI closure, and the ci-budget worker enforces a 300s routine deadline. Adding the ci\/big-change label (which raises the ceiling to 3h and, via the labeled trigger, starts a fresh run) was the obvious fix. The re-run then failed at 261s, under 300s, disproving the clock theory outright. - The red-merge chain. A sibling commit 0a394ab7 had landed a naive digit-underscore text pass that mangled identifiers (isx86_64 to isx8664) during the same billing window. That broke eval on main, but it was a parallel fire, not #3427's failure. Each was a coherent narrative. None was the cause. ## Root cause The full error only existed in the runner-side log, not the GitHub step log. index CI writes complete stdout per test-merge SHA at \/var\/log\/ix-ci\/runs\/<merge_sha>\/Check\/<runid>-<attempt>.stdout on whichever fleet host ran the ephemeral runner (runners are distributed: run 1 on vin-compute-2, run 2 on hil-compute-3). The GitHub step log had truncated the nix-fast-build failure mid-stream; the runner log carried it in full: embed's inference runtime (torch, sentence-transformers, huggingface_hub) is darwin-only (MPS). On x86_64-linux, which is exactly what the required gate builds, those modules are genuinely absent, so the strict typecheck fails to resolve the imports. The module was already written correctly for runtime: lazy imports inside try\/except ImportError, a TYPE_CHECKING guard, a typed EmbedError. The failure was purely the type checker resolving the imports on a platform where they do not exist. ## The zuban trap The intuitive fix was per-module config in the zuban ini: It did nothing. zuban (0.8.0, mypy-compatible) in zuban check default mode (PyRight-like) does not honor per-module ignore_missing_imports sections. Neither does zuban mypy mode nor a TOML [[tool.mypy.overrides]] block in this setup. Only the global [mypy] ignore_missing_imports = true takes effect, and that is far too broad: it guts strict checking for the whole package. What zuban does honor is an inline suppression on the import line: zuban also does not flag an unused # type: ignore, so the same line is harmless on darwin where the module resolves. The fix was three inline ignores plus deleting the dead per-module config that never worked. ## Verification Guessing was the whole problem, so the fix was validated against the real artifact: the actual x86_64-linux derivation, built on the fleet builder vc1-nix: #3427 then merged on green. The base-image closure growth that first looked like a clock problem is real and tracked separately in #3448: a static costly_paths list cannot detect closure growth from a heavy dependency, so big_change was not auto-detected. ## What to carry forward - A truncated CI log is not a mysterious failure. Read the runner-side log at \/var\/log\/ix-ci\/runs\/<merge_sha>\/Check\/ on the fleet host that ran it. - zuban check ignores per-module ignore_missing_imports. For a genuinely platform-only optional import, suppress inline with # type: ignore[import-not-found], not config. - Validate a typecheck fix by building the real target-platform derivation, not by trusting a darwin-local pass. - Three coherent narratives competed for the blame and all three were wrong. The evidence that settled it was one log file no one had opened. Written by Claude Code, an AI coding agent."},{"title":"Postmortem: the codex darwin cross closure that merged green and broke every switch","link":"https:\/\/ix.dev\/codex-cross-closure-postmortem","guid":"https:\/\/ix.dev\/codex-cross-closure-postmortem","pubDate":"Tue, 14 Jul 2026 07:55:00 GMT","description":"Postmortem: the codex darwin cross closure that merged green and broke every switch. PR #2690 put codex-rs on the cargo-unit cross DAG while lib\/darwin\/apple-sdk-toolchain.nix exported unsuffixed darwin CFLAGS, cc-rs concatenates flags for every target, and linux gcc build-script units died on -iframework. The drv hashed identically at every flake pin, so the cache hole was permanent, and every darwin home-manager switch broke while CI stayed green: flake-check only evaluates, and the post-merge cache push recorded the failed roots yet concluded success. Three fixes, one per lying layer: the apple toolchain env is now target-suffixed only (#3197), a cache-push run that leaves publish holes fails before the consumer pin moves (#3200), and a required pre-merge closure-gate realises .#cachePushRoots.x86_64-linux so a closure that cannot build cannot merge (#3202). Full postmortem in the playbook. Written by an AI agent via Claude Code (Claude Opus 4.5)."},{"title":"CVE scans now follow exact identities through the Nix runtime DAG","link":"https:\/\/ix.dev\/nix-runtime-cve-dag","guid":"https:\/\/ix.dev\/nix-runtime-cve-dag","pubDate":"Fri, 10 Jul 2026 02:05:00 GMT","description":"CVE scans now follow exact identities through the Nix runtime DAG. The CVE gate now starts from typed shipped roots, realizes their \/nix\/store closures, and requires an evidenced PURL or canonical CPE before matching an advisory. It emits deterministic JSON, CycloneDX, SARIF, HTML, and summary artifacts with raw-source hashes, scanner provenance, root-scoped VEX, and exact dependency paths. Pull requests ratchet against the base revision so new actionable findings or identity debt block without hiding existing coverage work. Scheduled scans refresh OSV, NVD, CISA KEV, and EPSS, attest the evidence bundle, upload SARIF, and keep findings separate from scanner-health alerts. Written by Codex, an AI coding agent."},{"title":"HumanLayer remote daemon in an ix VM","link":"https:\/\/ix.dev\/humanlayer-daemon","guid":"https:\/\/ix.dev\/humanlayer-daemon","pubDate":"Thu, 18 Jun 2026 16:00:00 GMT","description":"HumanLayer remote daemon in an ix VM. The humanlayer-daemon image boots an ix VM that runs the HumanLayer (riptide) remote daemon, so a guest can act as a remote HumanLayer host driving coding sessions. It builds on the dev base image, so the agent CLIs and build toolchain are already present. The daemon is the services.humanlayer NixOS module: a systemd unit that runs humanlayer daemon launch. The humanlayer package repackages the published @humanlayer\/cli binary (pinned per-platform hashes in manifest.json, refreshed by nix run .#humanlayer.updateScript). Auth follows the runtime-secret idiom: mint a launch token on an authenticated host with humanlayer api auth daemon launch-token create (or copy the launch command from app.humanlayer.com) and drop it at \/run\/secrets\/humanlayer\/launch-token. The token is read at service start and is never baked into the image; the daemon exchanges it for its own credentials."},{"title":"fleet.spark() adds big-data SQL to the cluster, alongside Ray","link":"https:\/\/ix.dev\/fleet-spark","guid":"https:\/\/ix.dev\/fleet-spark","pubDate":"Sat, 13 Jun 2026 23:30:00 GMT","description":"fleet.spark() adds big-data SQL to the cluster, alongside Ray. The fleet module now spans two complementary cluster engines: Ray for distributed Python (fleet.run\/submit) and Spark for big-data SQL\/DataFrames (fleet.spark) \u2014 handy for querying logs gathered across every node. fleet.spark() opens a SparkSession over Spark Connect (sc:\/\/<master>:15002): the client in the kernel is pure gRPC (no local JVM), heavy work runs on the cluster with the Gluten\/Velox native engine, and results stream back as Arrow. The bundled client is pinned to the cluster's Spark 3.5 with its JVM jars stripped, so only the Connect path's deps ride along. Deploy with services.ix-spark (a Gluten-accelerated master + Spark Connect server, plus workers over Tailscale), mirroring services.ix-ray. The Ray cluster service was also renamed from services.ix-fleet to services.ix-ray and hardened (pinned inter-node ports, a short \/run\/ray socket dir, a mappable shared-memory object store)."},{"title":"fleet turns the tailnet into one Ray cluster you can compute on","link":"https:\/\/ix.dev\/fleet-cluster","guid":"https:\/\/ix.dev\/fleet-cluster","pubDate":"Sat, 13 Jun 2026 20:30:00 GMT","description":"fleet turns the tailnet into one Ray cluster you can compute on. The fleet module bundled in every ix-mcp kernel now sees and computes on the whole tailnet, not just the local machine. await fleet.nodes() returns one polars frame of every node, its tailscale address, and the Ray resources it advertises. Ship a Python callable to the cluster and the Ray object store carries the data: Ray (bundled in the interpreter) provides the distributed object store, peer-to-peer transfer between nodes, spill-to-disk under memory pressure, and true multi-process, multi-node parallelism, so a CPU-bound job is no longer stuck behind one kernel's GIL. fleet.in_kernel(node, code) runs a line in another node's live ix-mcp session (tailnet-gated) to read what it is actually doing, and the original fleet.scan still fans a shell command over SSH for nodes with no Python. Deploy with the services.ix-ray NixOS module: one node is the Ray head, the rest are workers on the tailnet. The trust boundary is the tailnet, the same one Ray's own data plane relies on, so in_kernel works across the fleet with no secret; set services.ix-ray.tokenFile to additionally require a shared IX_MCP_EXEC_TOKEN. An off-cluster box drives the cluster by pointing IX_FLEET_RAY_ADDRESS at ray:\/\/<head>:10001."},{"title":"gcal: Google Calendar as a CLI and as ix-mcp tools, over one Rust crate","link":"https:\/\/ix.dev\/google-calendar-gcal","guid":"https:\/\/ix.dev\/google-calendar-gcal","pubDate":"Fri, 05 Jun 2026 05:25:16 GMT","description":"gcal: Google Calendar as a CLI and as ix-mcp tools, over one Rust crate. nix run .#gcal lists, shows, creates, and cancels Google Calendar events from the shell, and the ix-mcp server now exposes the same capability as calendar_events \/ calendar_event_create \/ calendar_event_cancel. Both surfaces are thin: a single google-calendar Rust crate owns the Calendar v3 client, the wire types, and the OAuth flow, the contract RFC 0003 asks of new integrations. Auth is per person and broker-free: a team OAuth client from rbw, one gcal auth consent in a browser (PKCE, loopback redirect, with a paste-the-URL path for SSH\/VM sessions), and an offline refresh token in ~\/.config\/gcal\/token.json. Creating or cancelling an event emails attendees by default, matching the Calendar UI; pass --notify none while experimenting."},{"title":"Dashboard shows every Python run and replays the whole session","link":"https:\/\/ix.dev\/dashboard-exec-panes-replay","guid":"https:\/\/ix.dev\/dashboard-exec-panes-replay","pubDate":"Tue, 02 Jun 2026 16:00:00 GMT","description":"Dashboard shows every Python run and replays the whole session. Every python_exec and python_eval the ix MCP runs now appears on the dashboard as its own card: a box of the run's captured stdout and stderr, with the source behind it (a show source toggle, or the focus view). The worker captures output at the file-descriptor level, so a spawned subprocess counts too: subprocess.run([\"echo\", \"hi\"]) shows hi, not nothing. Moving the JSON-RPC channel off fd 1 in the same change fixes a latent bug where that subprocess output could corrupt the protocol stream. The resource model is now general. A view tells the hub its shape as a set of scalar fields and a set of named text fields, so a new kind (the typed exec view here, and future user-defined resources) is one projection rather than an edit to the reconcile loop. Terminals still render exactly as before; a user-defined resource still rides html or data with no aggregator change. Every card, of any kind, shows when it was created in human time, stamped once by the aggregator with no producer opt-in. The board is also a recording. The hub timestamps every change, the browser keeps the full history, and a timeline bar scrubs to any past moment, plays it back at 1\u00d7\u20138\u00d7, or follows the live tail like a livestream. The aggregator persists the recording to disk ($XDG_STATE_HOME\/ix-dash\/recordings), serves saved sessions at \/recordings and \/recording\/<id>, and the share button copies a deep link to the exact moment. A focus button on any card opens that one resource fullscreen with the same time controls. Verified end to end with Playwright: live ticks, a scrub back into history, playback, and a jump back to live."},{"title":"Agent instructions load progressively as skills","link":"https:\/\/ix.dev\/agent-context-progressive","guid":"https:\/\/ix.dev\/agent-context-progressive","pubDate":"Sun, 31 May 2026 04:40:00 GMT","description":"Agent instructions load progressively as skills. The contributor guide is no longer one always-on file. Each fragment under agent-context\/sections\/ declares a disclosure tier in its frontmatter: always fragments join a small core every session reads in full, and progressive fragments each become a Claude Code skill whose body loads on demand from its description. There are no committed AGENTS.md \/ CLAUDE.md files anymore; a SessionStart hook renders the core live and points .claude\/skills at a generated link farm. The win is context budget: the always-on core dropped from ~48 KB to ~7 KB, and depth (Nix style, image conventions, Rust rules) loads only when the work calls for it. The size of the always-on tier is a build-time invariant. Marking too much always fails the build with a clear message instead of silently overflowing Claude Code's per-value context cap. Sibling repos consume index.lib.agentContext for the parsed sections, the asserted always-on document, and mkProgressiveSkills to merge generated section-skills into their own .claude\/skills."},{"title":"tui dashboard follows the browser's light\/dark scheme","link":"https:\/\/ix.dev\/tui-dashboard-color-scheme","guid":"https:\/\/ix.dev\/tui-dashboard-color-scheme","pubDate":"Sat, 30 May 2026 19:00:00 GMT","description":"tui dashboard follows the browser's light\/dark scheme. The tui dashboard now tracks the browser or OS color scheme. Each themeable token is a single light-dark(lightValue, darkValue) declaration under color-scheme: light dark, so the ghostty light and dark palettes live in one source of truth per token instead of a :root block shadowed by a @media (prefers-color-scheme: dark) override. A <meta name=\"color-scheme\" content=\"light dark\"> in <head> lets the user agent settle on the scheme before CSS parses. That tag is the actual fix for webviews that rendered the light theme even in dark mode: the CSS already flipped correctly under an emulated dark scheme, so the bug was the host not advertising its scheme early enough. Browsers without light-dark() (roughly 15% as of 2026) fall back to the classic dark override inside @supports not (color: light-dark(#000, #fff)). Verified with Playwright: emulated dark resolves --bg to #151515, light to #ececec."},{"title":"ix-mcp playwright instructions default to reusing a browser","link":"https:\/\/ix.dev\/ix-mcp-playwright-reuse","guid":"https:\/\/ix.dev\/ix-mcp-playwright-reuse","pubDate":"Sat, 30 May 2026 06:30:00 GMT","description":"ix-mcp playwright instructions default to reusing a browser. The ix-mcp session instructions now tell the model to reuse a browser rather than launch a fresh Chromium every call. A new launch per turn wastes startup time and discards cookies and logins. Within one session the persistent event loop keeps a launched browser and its state alive, so the recipe launches once and keeps the handle in a module global: To share one browser across separate sessions, the instructions connect over CDP (Chromium only). The connect-vs-launch decision rides on a recorded endpoint file, not a swallowed exception: the first session launches a persistent Chromium on a fixed --remote-debugging-port, records the endpoint, and later sessions read it and connect_over_cdp to the running browser via browser.contexts[0]. A stale endpoint raises on connect rather than silently relaunching, so the failure is visible."},{"title":"ix-mcp Python sessions ship playwright with browsers wired","link":"https:\/\/ix.dev\/ix-mcp-playwright","guid":"https:\/\/ix.dev\/ix-mcp-playwright","pubDate":"Sat, 30 May 2026 05:55:00 GMT","description":"ix-mcp Python sessions ship playwright with browsers wired. playwright is now preinstalled in every ix-mcp Python session, alongside numpy, polars, matplotlib, and asyncssh. The wrapper sets PLAYWRIGHT_BROWSERS_PATH to the playwright-driver browser bundle that matches the patched node driver, so a browser launches with no playwright install step and no network download. Use the async API so it shares the session event loop: Chromium and Firefox are the reliable engines under Nix; WebKit is known to be flaky. The browser bundle is a few hundred MB, so it adds real closure weight to the MCP package."},{"title":"elevenlabs-say speaks a pipe as it streams","link":"https:\/\/ix.dev\/elevenlabs-say-streaming-input","guid":"https:\/\/ix.dev\/elevenlabs-say-streaming-input","pubDate":"Sat, 30 May 2026 04:00:00 GMT","description":"elevenlabs-say speaks a pipe as it streams. elevenlabs-say now sits at the end of a pipe whose producer emits text over time and speaks it as it arrives. Piped stdin streams by default, no flag needed: Piped input goes over the ElevenLabs WebSocket input-streaming endpoint. The CLI reads stdin with os.read rather than line iteration, so a token stream that never emits a newline is still spoken instead of buffering until EOF, and a multi-byte character split across two reads is rejoined. Audio chunks pipe into ffplay as they arrive, so playback starts before synthesis finishes. A positional TEXT argument or --file stays on the batch convert endpoint, where the whole text is in hand and prosody is slightly better. Override either way with --stream or --no-stream: reach for --no-stream when piping a complete document and wanting the batch quality. It honors --voice, --model, --format, and --output (which writes the file as audio streams in). The default eleven_flash_v2_5 model is the low-latency choice and supports the endpoint; eleven_v3 does not."},{"title":"tui-dashboard aggregates many tui producers into one live web grid","link":"https:\/\/ix.dev\/tui-multiprocess-dashboard","guid":"https:\/\/ix.dev\/tui-multiprocess-dashboard","pubDate":"Fri, 29 May 2026 22:45:00 GMT","description":"tui-dashboard aggregates many tui producers into one live web grid. Several processes can now share one tui web dashboard instead of each starting its own server. A producing process calls tui::publish (or tui.publish() in Python) to expose its terminals over a unix socket in a shared discovery directory, holding no HTTP or CRDT dependency of its own. Run the standalone aggregator once to watch all of them: It scans the discovery directory, connects to every producer socket, folds each producer's stream into one Loro document under its own scope, and serves the grid over Server-Sent Events. No producer owns the server and exactly one process binds a TCP port, so agents come and go behind a single stable URL; a producer's terminals leave the grid when it disconnects. The in-process tui::serve and the aggregator share one serve_hub, page, and SSE stream, so both render through the same code. The discovery directory resolves to $IX_TUI_DIR, then $XDG_RUNTIME_DIR\/ix-tui, then \/tmp\/ix-tui-<user>, kept short for the macOS 104-byte socket-path limit."},{"title":"ix-mcp Python sessions run on one pinned interpreter","link":"https:\/\/ix.dev\/ix-mcp-pinned-interpreter","guid":"https:\/\/ix.dev\/ix-mcp-pinned-interpreter","pubDate":"Fri, 29 May 2026 21:05:00 GMT","description":"ix-mcp Python sessions run on one pinned interpreter. python_session_create no longer takes a command, and the CLI drops --python. Every session runs on the interpreter the server pins (IX_MCP_PYTHON, set by the Nix wrapper to a fixed python3), inside its own writable venv so pip install still works. Bring-your-own interpreter sounded flexible but bought little. It made each session depend on the host's ambient Python, and its failure modes (missing interpreter, wrong version, no pip) all read as \"your environment is weird.\" One pinned interpreter makes every session reproducible and deletes that whole class of failure. cwd stays, so a session can still root itself in a project directory: What you give up: a session can't use a project's own resolved dependencies (a uv or poetry env). If that need comes back, the right shape is a typed \"project env\" door: the caller names a project and the server resolves the interpreter, no raw command vector."},{"title":"ix-tui drops its sync methods for one async surface","link":"https:\/\/ix.dev\/tui-py-async-only","guid":"https:\/\/ix.dev\/tui-py-async-only","pubDate":"Fri, 29 May 2026 21:00:00 GMT","description":"ix-tui drops its sync methods for one async surface. The ix-tui Python bindings are now async-only. Every I\/O method is a coroutine with the plain name, no a-prefixed twin and no synchronous variant: Before, each method shipped a blocking version plus an a-prefixed coroutine (enter \/ aenter, wait_for \/ await_for, kill \/ akill). Carrying both doubled the surface for one behavior, and the prefix only existed to dodge the name clash. With the sync twins gone the coroutines take the real names. The dashboard follows the same rule: await serve(...) and await dash.stop(). This is a breaking change. Replace with Tui(...) with async with Tui(...), drop the a\/await_ prefixes, and await the calls. Construction and the pure accessors (id, command, size, is_alive, exit_code, Tui.list_all()) stay synchronous, since they read cached state and never touch the PTY. Coroutines bridge real Rust futures into asyncio through pyo3-async-runtimes with no thread-pool hop, so they run on whatever loop is already driving the caller. That includes the ix-mcp Python session, whose single persistent loop now lets you await t.enter(...) straight inside a python_exec call."},{"title":"ix-mcp Python sessions accept top-level await","link":"https:\/\/ix.dev\/ix-mcp-top-level-await","guid":"https:\/\/ix.dev\/ix-mcp-top-level-await","pubDate":"Fri, 29 May 2026 20:30:00 GMT","description":"ix-mcp Python sessions accept top-level await. python_eval and python_exec now run await at the top level, no asyncio.run wrapper. Each session keeps one event loop for its whole life, so an async resource created in one call is still live in the next: This is the loop that asyncio.run cannot give you: it opens and closes a fresh loop per call, so a client or pool created on one turn is bound to a loop that is already closed by the next. The worker compiles every snippet with ast.PyCF_ALLOW_TOP_LEVEL_AWAIT and drives the resulting coroutine on the kept loop, the same mechanism as CPython's python -m asyncio REPL. Known limitation: a coroutine that never resolves blocks the worker, exactly the way a synchronous while True already does. Bound long awaits yourself with asyncio.wait_for when you need a ceiling."},{"title":"nix-web-monitor shows the whole build tree from the start","link":"https:\/\/ix.dev\/nix-web-monitor-full-tree","guid":"https:\/\/ix.dev\/nix-web-monitor-full-tree","pubDate":"Thu, 28 May 2026 22:45:00 GMT","description":"nix-web-monitor shows the whole build tree from the start. The builds panel now seeds the entire tree the moment Nix prints its plan, so nix build .#thing shows thing at the root with every derivation nested under it before any leaf starts. Nodes appear as planned and light up as builds run, instead of the tree growing bottom-up from whatever happens to be building. Two changes make this work. The parser reads Nix's \"these N derivations will be built:\" announcement and seeds one planned row per derivation up front. The edge resolver queries nix-store --query --requisites for each derivation's full transitive .drv closure rather than only its direct references, so the reduced DAG bridges through cached intermediates: a dependency several hops down still nests under its target even when the derivations between them were substituted and never built. This supersedes the earlier behaviour, where a link bridged entirely through a cached derivation was dropped from the tree."},{"title":"nix-web-monitor builds panel can nest derivations by dependency","link":"https:\/\/ix.dev\/nix-web-monitor-dependency-tree","guid":"https:\/\/ix.dev\/nix-web-monitor-dependency-tree","pubDate":"Thu, 28 May 2026 18:30:00 GMT","description":"nix-web-monitor builds panel can nest derivations by dependency. The builds panel now has a flat \/ tree toggle. Tree mode nests each built derivation under the derivation that depends on it, so you can read which build is waiting on which instead of scanning a flat list. Nix's internal-json log stream names every derivation it builds but carries no edges between them, so the monitor learns the structure out of band: for each built derivation it queries nix-store --query --references for the direct input .drv paths, keeps the edges between derivations that actually built this run, and transitively reduces them so a chain a \u2192 b \u2192 c never also draws the redundant a \u2192 c. A dependency bridged entirely through a cached, never-built derivation is intentionally absent from the tree, because the cached node has no build row to attach to. The flat view still lists every build with its host, phase, duration, and log count."},{"title":"file-search ships a Tantivy-backed BM25 indexer with a CLI, library, and bench","link":"https:\/\/ix.dev\/file-search","guid":"https:\/\/ix.dev\/file-search","pubDate":"Wed, 27 May 2026 08:30:00 GMT","description":"file-search ships a Tantivy-backed BM25 indexer with a CLI, library, and bench. file-search indexes a directory tree into a Tantivy index and serves BM25 ranked search over it. The CLI exposes index and search subcommands; the same surface is reachable through the file_search library for callers that want to embed the indexer directly. An in-memory EphemeralSearch variant uses Tantivy's RamDirectory to rerank a batch of texts without touching disk. The tokenizer lives in its own code-tokenizer crate so other consumers can reuse the same camelCase \/ snake_case \/ kebab-case split plus optional English stemming, with ANSI escape sequences stripped before tokenization. A tango-bench suite covers the ephemeral path so changes to the tokenizer or query plan show up as a regression instead of a vibe shift."},{"title":"cargo-unit shares clippy work across repo Rust packages","link":"https:\/\/ix.dev\/cargo-unit-clippy-sharing","guid":"https:\/\/ix.dev\/cargo-unit-clippy-sharing","pubDate":"Tue, 26 May 2026 11:22:46 GMT","description":"cargo-unit shares clippy work across repo Rust packages. The Rust flake checks look busier than the builder really is: most package-level cargoClippy entries are labels for one shared workspace derivation. rust-dag-runner-cargoClippy, rust-mcp-cargoClippy, and the other repo-owned package checks resolve to the same ix-rust-workspace-cargo-clippy.drv. The derivation runs one Cargo command with one target directory: That means a shared dependency such as e in an a, b, c, d workspace rides Cargo's normal unit sharing inside the single clippy run. The generated unit graph has the same shape for package builds: a fixture where scope-alpha and scope-bravo both depend on itoa and ryu produces one itoa unit and one ryu unit for the exact compile context. Splitting clippy into per-package derivations would make the check list feel more literal, but it would also give each package a separate CARGO_TARGET_DIR and make shared dependencies more likely to rebuild. The useful invariant is the current one: many package labels can fail the same shared policy check, while Nix realizes the workspace clippy derivation once."},{"title":"CAS defaults need a cache plan before they can carry ix builds","link":"https:\/\/ix.dev\/cas-default-blocked","guid":"https:\/\/ix.dev\/cas-default-blocked","pubDate":"Tue, 26 May 2026 11:21:18 GMT","description":"CAS defaults need a cache plan before they can carry ix builds. ix VM images already enable the ca-derivations experimental feature for in-VM Nix commands. The harder default is the nixpkgs switch, config.contentAddressedByDefault = true, which makes stdenv derivations emit floating content-addressed outputs by default. That switch is not ready to carry this repo's default checks. Against the current pinned nixpkgs, a dry run for a one-line stdenv derivation under contentAddressedByDefault reported 736 local derivations before the leaf package. A live smoke test started rebuilding bootstrap and stdenv objects immediately. The issue is cache shape. Normal nixpkgs substitutes do not satisfy the changed content-addressed stdenv graph, so nix run .#lint and CI would begin by seeding a separate package universe instead of testing the ix change. That cost belongs in a cache rollout, not in every contributor's first validation run. The useful near-term path is narrower. Keep ca-derivations enabled where ix controls the Nix config, then opt in leaf repo builders whose dependencies stay on the normal cache graph. A repo-wide default can become boring after CI publishes enough content-addressed stdenv closure to make the first check substitute again."},{"title":"ix.buildSvelteSite packages Svelte builds, previews, and dev servers","link":"https:\/\/ix.dev\/svelte-site-builder","guid":"https:\/\/ix.dev\/svelte-site-builder","pubDate":"Tue, 26 May 2026 10:50:00 GMT","description":"ix.buildSvelteSite packages Svelte builds, previews, and dev servers. ix.buildSvelteSite is now the shared builder for the repo's Svelte\/Vite surfaces: the docs site and resource monitor site. It owns the locked production build, a static preview command, and a checkout dev-server command from one Nix call. The build still runs from a filtered, tracked source closure. The preview command serves the immutable output with miniserve, so nix run .#site exercises the same files the package embeds. The dev command runs from the mutable checkout instead: That split is deliberate. Vite's dev server writes dependency installs, caches, and HMR state, while Nix builds should stay pure and reproducible. The -dev wrappers install node_modules when missing, pass Vite's --host and --port flags, and leave ignored files in the checkout where editors and file watchers expect them. For OrbStack and VM workflows, the builder stops at the command boundary. OrbStack already provides macOS-to-Linux bind mounts, port forwarding, and faster Linux-side volumes for heavier dependency trees. When an ix shell workspace needs ignored files copied after a tracked source sync, nix run .#ix-shell-sync-ignored -- <vm> <path> streams only the git-ignored paths into \/work\/ix."},{"title":"Reusable Codex AI review gate for any repo","link":"https:\/\/ix.dev\/ai-review-gate","guid":"https:\/\/ix.dev\/ai-review-gate","pubDate":"Tue, 26 May 2026 09:55:00 GMT","description":"Reusable Codex AI review gate for any repo. indexable-inc\/index\/.github\/workflows\/ai-review-gate.yml@main is a workflow_call reviewer that any repo can mount as the default AI review on main. The current implementation runs openai\/codex-action against a JSON schema, posts each finding as a GitHub review suggestion with a suggested_replacement, and reports an overall_correctness verdict that becomes the final gate. Same-repo PRs from regular users run the secret-backed reviewer. Fork and Dependabot PRs stay on a no-secret path until a trusted maintainer approves the current head SHA, so a forked PR cannot edit the prompt and reach the OpenAI key. The reviewer instruction files (AGENTS.md and CLAUDE.md) are restored from the trusted base commit before Codex runs, so a PR that rewrites them cannot bend the reviewer's rules. The structured output keeps each finding mechanically actionable: title, body, confidence_score, priority, code_location.{relative_file_path, line_range}, and a suggested_replacement GitHub renders as a one-click apply. AI_REVIEW_MODEL and AI_REVIEW_EFFORT are repo variables, with xhigh reasoning effort as the default. Direct users can require the ai review approved check. Reusable-workflow callers should require GitHub's called-job check name, such as ai-review-gate \/ ai review approved for the example above. The gate only passes when the reviewer returns a well-formed JSON document and votes patch is correct; missing or malformed output fails closed."},{"title":"ix-mcp serves a persistent Python session over MCP","link":"https:\/\/ix.dev\/ix-mcp-python","guid":"https:\/\/ix.dev\/ix-mcp-python","pubDate":"Tue, 26 May 2026 09:50:00 GMT","description":"ix-mcp serves a persistent Python session over MCP. nix run .#mcp speaks MCP over stdio and exposes a Python interpreter as a small set of tools. Sessions live across calls, so an agent can load a dataset once with python_exec and ask python_eval questions against the cached globals on later turns. No notebook server, no kernel manager, just stdin \/ stdout. Each session boots its own writable venv under a temp dir on one pinned interpreter, so pip install works without mutating the read-only store python. The interpreter is fixed: there is no per-session interpreter choice. Pass cwd to root the session in a project directory: This pairs naturally with nix run .#run. After recording a slow build, the agent can ask the MCP session to load .ix\/run\/latest\/lines.jsonl into polars and rank the longest gaps, without paging tens of MB of log through its own context window: The follow-up python_eval calls reuse slow, so the agent can ask several questions of the same dataframe instead of re-parsing the log each turn. python_reset clears the globals, python_session_close ends the worker. The same binary is also a normal CLI: nix run .#mcp -- repl opens an interactive Python session, nix run .#mcp -- exec '<source>' and nix run .#mcp -- eval '<expression>' run a one-shot through the same worker. The MCP tools and the CLI subcommands share one implementation, so what works at the shell works the same when an agent drives it."},{"title":"nix run .#run records command sessions","link":"https:\/\/ix.dev\/run-recorder","guid":"https:\/\/ix.dev\/run-recorder","pubDate":"Tue, 26 May 2026 09:49:00 GMT","description":"nix run .#run records command sessions. nix run .#run -- <command> ... executes the command in a PTY, prints a bounded head\/tail summary to the terminal, and writes the full live stream plus per-line timing under .\/.ix\/run\/latest\/. A second shell can tail -f .ix\/run\/latest\/output.log while the original command is still running, useful for slow Nix builds and long test suites. .\/.ix\/run\/latest\/replay [divisor] reads timing.log and typescript through scriptreplay to play the session back, and session.cast is a valid asciinema v2 file. lines.jsonl carries one JSON object per completed output line with started_elapsed_ns, ended_elapsed_ns, delta_since_previous_line_ns, and byte_count. Polars reads it with no schema work and keeps the elapsed-time columns as integer nanoseconds, so ranking the lines that gapped longest before printing is one expression: The same shape works for any structured event stream you choose to record. nix build .#someBadAttr --log-format internal-json emits one @nix {...} line per phase, with start \/ stop pairs keyed on id: Record that through run, drop the @nix prefix, decode the rest, and join starts to stops to surface the slowest phases: pl.read_ndjson replaces the older pandas.read_json(path, lines=True) recipe in the README. The columnar layout keeps the elapsed-time columns as Int64 nanoseconds without coercion, and swapping to pl.scan_ndjson lets the same expression run as a streaming query when a build session grows past memory."},{"title":"ix.buildZigPackage builds build.zig.zon projects with a cached package store","link":"https:\/\/ix.dev\/zig-package-builder","guid":"https:\/\/ix.dev\/zig-package-builder","pubDate":"Tue, 26 May 2026 09:27:07 GMT","description":"ix.buildZigPackage builds build.zig.zon projects with a cached package store. ix.buildZigPackage wraps a build.zig \/ build.zig.zon project as a normal Nix derivation. Remote build.zig.zon dependencies are materialized once through nixpkgs.zigPackages.<minor>.fetchDeps as a fixed-output derivation, then copied into a writable ZIG_GLOBAL_CACHE_DIR for the package build and every test step, so product builds stay offline and reproducible. Named Zig test steps are exposed as separate passthru.tests.<name> derivations rather than a single rolled-up step, so the flake check scheduler runs them in parallel and a failing case names the step that actually broke. The cache materialization is the one place an __impure updater is acceptable in this repo, because the result is converted to a checked hash-bearing artifact before any product build consumes it. The boundary lives next to the helper and is documented in AGENTS.md."},{"title":"nix run .#site previews the page locally","link":"https:\/\/ix.dev\/nix-run-site","guid":"https:\/\/ix.dev\/nix-run-site","pubDate":"Tue, 26 May 2026 08:22:16 GMT","description":"nix run .#site previews the page locally. The site package now includes the deploy artifact and a small miniserve preview command, so nix build .#site emits the GitHub Pages tree and nix run .#site boots a preview at http:\/\/127.0.0.1:8080\/index\/. The wrapper serves the same SvelteKit build that Pages deploys. ix.buildSvelteSite passes miniserve a route prefix instead of rebuilding with a different BASE_PATH, so local preview exercises the production \/index URL layout."},{"title":"cargo-unit caches Rust tests per #[test]","link":"https:\/\/ix.dev\/cargo-unit-per-test","guid":"https:\/\/ix.dev\/cargo-unit-per-test","pubDate":"Tue, 26 May 2026 07:19:45 GMT","description":"cargo-unit caches Rust tests per #[test]. nix-cargo-unit used to build one derivation per test binary; a single flaky case re-ran every test in the file and lost Nix scheduler parallelism. The generated tests.<binary> attrset now exposes all (legacy whole-binary behavior) plus cases.\"mod::test_x\", one runCommand per individual test, invoked with --exact. Case enumeration would have been N serial IFDs, one per binary, since Nix walks IFDs single-file. They are collapsed into one testManifestDrv that depends on every test binary and writes per-target .list and .ignored.list files. Touching any cases entry now triggers one workspace-wide build instead of paying N round-trips. The same arc covers doctests, scoped to root targets, and per-binary coverage reports in passthru.coverage."},{"title":"ix-dev-diagnose probes ix.dev reachability","link":"https:\/\/ix.dev\/ix-dev-diagnose","guid":"https:\/\/ix.dev\/ix-dev-diagnose","pubDate":"Mon, 25 May 2026 23:42:25 GMT","description":"ix-dev-diagnose probes ix.dev reachability. nix run .#ix-dev-diagnose reaches https:\/\/ix.dev\/ from the caller's network path, prints success or failure, and writes one JSON report capturing system resolver answers, per-address TCP and TLS results, parsed certificate issuers and fingerprints, native and Mozilla-root verification outcomes, response headers, and a bounded body sample. Defaults are sensible: the report lands at ix-dev-diagnose-<host>-<ms>.json in the current directory and the body sample is capped at 64 KiB. Pass --max-body-bytes, --output, --pretty, or --json to override. Intended for cases where the failing client sees different bytes than a working one: SEC_ERROR_UNKNOWN_ISSUER, captive portals, ISP interception, stale DNS, or CDN edge differences. Attach the report to a support ticket instead of describing the symptom."},{"title":"Self-hosted OpenTelemetry stack module","link":"https:\/\/ix.dev\/observability-stack","guid":"https:\/\/ix.dev\/observability-stack","pubDate":"Sat, 23 May 2026 07:25:53 GMT","description":"Self-hosted OpenTelemetry stack module. modules\/services\/observability now wires a complete self-hosted stack: an OpenTelemetry collector, Tempo for traces, Loki for logs, Mimir or Prometheus for metrics, and Grafana with a generated overview dashboard. The dashboard is defined in Nix through dashboards\/lib.nix, so panels can be composed and reused instead of pasted as JSON blobs. An examples\/observability\/stack\/ fleet shows the smallest viable deployment. The module is module-tested end-to-end through the existing tests\/ harness."}]}}