Land it now. Everyone gets it.

Agents made writing code cheap; waiting on review queues and upstreams is now the expensive part. index is one open monorepo where every change lands now and reaches everyone: packages, NixOS modules, VM images, patched toolchains, and the agent infrastructure that ties them together.

33 / 88
  1. 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 — 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:

    ixterm open /tmp/report.html

    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 — 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:

    services.ix-term = {
      enable = true;
      port = 7533;            # loopback by default
      user = "andrewgazelka"; # sessions are this account's login shells
    };

    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.

    • terminal
    • rust
    • svelte
    • interesting
  2. 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.

    privateliveverifiedhumanleakorg-runtotal
    Scale SEAL210222 9/12
    ARC-AGI-2212022 9/12
    LiveBench122012 8/12
    LMArena120211 7/12
    SWE-bench Pro202012 7/12
    METR time horizon102112 7/12
    Epoch FrontierMath102012 6/12
    AA Intelligence Index111012 6/12
    Humanity's Last Exam101002 4/12
    Terminal-Bench 2.1012001 4/12
    Aider polyglot002001 3/12
    SWE-bench Verified002000 2/12
    Hover a cell for the score justification. Column meanings appear on the headers.

    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.

    Fable 5GPT-5.6 SolKimi K3Qwen3.7 MaxQwen3.8 Maxrubric
    AA Intelligence Index v4.1~603p593p57.13p46.03p-6/12
    LMArena text Elo15093p14863p1486-15003p14753p-7/12
    SWE-bench Verified95.0sc89.8v-80.4v-2/12
    SWE-bench Pro80.0sc64.6v-60.6v-7/12
    Terminal-Bench 2.184.3sc88.8v88.3v74.53p-4/12
    Humanity's Last Exam53.33p47.23p43.5v38.13p-4/12
    GPQA Diamond-94.6 / 91.2v93.5v92.4v--
    ARC-AGI-2-92.53p---9/12
    LiveBench80.83p82.43p---8/12
    GDPval-AA v2 Elo17603p1748v16683p12733p-6/12
    BrowseComp-90.4v91.2v---
    AA output speed (tok/s)-53.13p62.03p---
    API price (USD per M, in / out)-5 / 30v3 / 15v2.50 / 7.50v--
    Hover a cell for provenance and harness notes. Marks: v vendor self-reported, 3p third party ran the model, sc local system card.

    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.

    LiveBench 8/12

    Fable 5 80.8
    GPT-5.6 Sol 82.4
    Kimi K3 none
    Qwen3.7 Max none
    Qwen3.8 Max none

    SWE-bench Pro 7/12

    Fable 5 80.0
    GPT-5.6 Sol 64.6
    Kimi K3 none
    Qwen3.7 Max 60.6
    Qwen3.8 Max none

    AA Intelligence Index v4.1 6/12

    Fable 5 ~60
    GPT-5.6 Sol 59
    Kimi K3 57.1
    Qwen3.7 Max 46.0
    Qwen3.8 Max none

    Terminal-Bench 2.1 4/12

    Fable 5 84.3
    GPT-5.6 Sol 88.8
    Kimi K3 88.3
    Qwen3.7 Max 74.5
    Qwen3.8 Max none

    Humanity's Last Exam 4/12

    Fable 5 53.3
    GPT-5.6 Sol 47.2
    Kimi K3 43.5
    Qwen3.7 Max 38.1
    Qwen3.8 Max none

    SWE-bench Verified 2/12

    Fable 5 95.0
    GPT-5.6 Sol 89.8
    Kimi K3 none
    Qwen3.7 Max 80.4
    Qwen3.8 Max none

    GPQA Diamond

    Fable 5 none
    GPT-5.6 Sol 94.6 / 91.2
    Kimi K3 93.5
    Qwen3.7 Max 92.4
    Qwen3.8 Max none

    BrowseComp

    Fable 5 none
    GPT-5.6 Sol 90.4
    Kimi K3 91.2
    Qwen3.7 Max none
    Qwen3.8 Max none
    All axes run 0 to 100. Solid: a third party ran the model. Hatched: self-reported by the vendor or its system card. Empty track: no published number for that model.

    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.

    • interesting
    • ai
    • benchmarks
  3. 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).

    • interesting
    • mcp
    • agents
    • elixir
  4. 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.

    • ci
    • interesting
  5. 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.

    • ci
    • interesting
  6. 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).

    • ci
    • interesting
  7. 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.

    • shell
    • rust
    • agents
    • interesting
  8. 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.

    import { siteUpdates } from '@indexable/site/updates';
    import { buildFeedXml } from '@indexable/site/feed';
    import '@indexable/site/styles/tokens.css';
    • site
    • interesting
  9. 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.

    • interesting
    • nix
    • mcp
    • agents
    • postmortem
  10. 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.

    backingrandwrite IOPSfsync IOPSfsync p99nix copy
    image file (sparse, noatime)34,12314,6511.7 ms131.6 MiB/s
    partition, sync Full12,0481,02416.7 ms116.0 MiB/s
    partition, raw node14,58152432.4 ms70.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).

    • interesting
    • nix
    • darwin
    • vm
    • builder
  11. 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 — the costs persistent high-churn guests pay most.

    nix run .#vmkit -- drive-linux --disk ./linux.raw --disk /dev/disk4s2

    Safety first: a device that hosts mounted filesystems — including through APFS physical-store linkage, where the volumes mount under a synthesized container disk — 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.

    • interesting
    • vm
    • vmkit
    • macos
  12. 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.

    • weave
    • agents
    • performance
    • interesting
  13. 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.

    • fleet
    • ops
    • interesting
  14. 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:

    ix-mcp-strict-typecheck> error: Cannot find implementation or library
    stub for module named "sentence_transformers"  [import-not-found]

    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:

    [mypy-sentence_transformers.*]
    ignore_missing_imports = true

    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:

    from sentence_transformers import SentenceTransformer  # type: ignore[import-not-found]

    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:

    ix-mcp-strict-typecheck> Success: no issues found in 22 source files

    #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.

    • interesting
    • ci
    • nix
    • mcp
    • postmortem
  15. 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).

    • interesting
    • nix
    • ci
    • darwin
    • postmortem
  16. 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.

    • interesting
    • security
    • nix
    • ci
  17. 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.

    • interesting
    • images
    • agents
  18. 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.

    • interesting
    • rust
    • mcp
    • cli
    • integration
  19. 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×–8×, 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.

    • interesting
    • ui
    • dashboard
    • agents
  20. 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.

    nix build .#claude-md --no-link --print-out-paths | xargs cat   # always-on core
    nix build .#skills    --no-link --print-out-paths | xargs ls    # handwritten + generated skills
    nix run  .#agent-context -- --write                             # write the core to disk (gitignored)

    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.

    • interesting
    • nix
    • agents
    • cli
  21. 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.

    • interesting
    • ui
    • css
  22. 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:

    from playwright.async_api import async_playwright
    
    if "browser" not in globals():
        p = await async_playwright().start()
        browser = await p.chromium.launch()
    page = await browser.new_page()

    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.

    • interesting
    • mcp
    • agents
    • python
    • browser
  23. 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:

    from playwright.async_api import async_playwright
    
    p = await async_playwright().start()
    browser = await p.chromium.launch()
    page = await browser.new_page()
    await page.goto("https://example.com")
    print(await page.title())
    await browser.close()

    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.

    • interesting
    • mcp
    • agents
    • python
    • browser
  24. 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:

    my-llm --prompt "tell me a story" | nix run .#elevenlabs-say

    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.

    • interesting
    • python
    • cli
    • audio
  25. 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:

    { "name": "python_session_create", "arguments": { "cwd": "/path/to/project" } }

    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.

    • interesting
    • mcp
    • agents
    • python
  26. 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:

    import asyncio
    from tui import Tui
    
    async def main() -> None:
        async with Tui("python", "-q") as t:
            await t.enter("1 + 2")
            snap = await t.wait_for("3", timeout=2.0)
            print(snap.text)
    
    asyncio.run(main())

    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.

    • interesting
    • python
    • async
    • agents
  27. 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:

    python_exec(source="""
    import httpx
    client = httpx.AsyncClient()
    r = await client.get("https://example.com")
    """)
    python_eval(expression="r.status_code")  # 200, same client, same loop

    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.

    • interesting
    • mcp
    • agents
    • python
    • async
  28. 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:

    cargo clippy --frozen --offline --workspace --all-targets -- 
      -D warnings -D clippy::all -D clippy::pedantic -D clippy::nursery -D clippy::cargo 
      -A clippy::multiple_crate_versions

    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.

    • interesting
    • nix
    • rust
    • builder
  29. 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.

    nix build --dry-run --impure --expr '
    let
      f = builtins.getFlake (toString ./.);
      pkgs = import f.inputs.nixpkgs {
        system = "x86_64-linux";
        config.contentAddressedByDefault = true;
      };
    in
    pkgs.stdenv.mkDerivation {
      name = "ca-stdenv-smoke";
      dontUnpack = true;
      installPhase = "echo ok > $out";
    }
    '

    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.

    • interesting
    • nix
    • cache
  30. 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.

    ix.buildSvelteSite pkgs {
      pname = "site";
      src = siteSrc;
      serve.port = 8080;
      devServer = {
        name = "site-dev";
        checkoutSubdir = "site";
        port = 5173;
      };
    }

    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:

    nix run .#site-dev

    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.

    • interesting
    • nix
    • svelte
    • vite
  31. 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 run Codex; fork PRs wait for a trusted approval, both ending at the same gate.
    jobs:
      ai-review-gate:
        permissions:
          actions: read
          checks: read
          contents: read
          issues: write
          pull-requests: write
        uses: indexable-inc/index/.github/workflows/ai-review-gate.yml@main
        with:
          caller_event_name: ${{ github.event_name }}
          required_check_name: ai review approved
        secrets:
          openai_api_key:   ${{ secrets.OPENAI_API_KEY }}
          repository_token: ${{ secrets.GITHUB_TOKEN }}

    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.

    {
      "findings": [
        {
          "title": "fork PRs can leak OPENAI_API_KEY",
          "priority": 0,
          "code_location": { "relative_file_path": ".github/workflows/foo.yml", "line_range": {"start": 12, "end": 18} },
          "suggested_replacement": "...",
          "confidence_score": 0.92
        }
      ],
      "overall_correctness": "patch is incorrect"
    }

    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.

    • interesting
    • ci
    • agents
    • github
  32. 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.

    One stdio session, one persistent Python interpreter, many tool calls.
    python_session_create  python_session_list  python_session_close
    python_eval            python_exec          python_reset

    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:

    {
      "name": "python_session_create",
      "arguments": {
        "session_id": "build-analysis",
        "cwd": "/Users/me/Projects/indexable-inc/index"
      }
    }

    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:

    python_exec(source="""
    import polars as pl
    
    nix_event_dtype = pl.Struct([
        pl.Field("action", pl.String),
        pl.Field("id", pl.Int64),
        pl.Field("text", pl.String),
        pl.Field("type", pl.Int64),
    ])
    events = (
        pl.read_ndjson(".ix/run/latest/lines.jsonl")
          .filter(pl.col("text").str.starts_with("@nix "))
          .select(
              "ended_elapsed_ns",
              pl.col("text").str.slice(5).str.json_decode(nix_event_dtype).alias("event"),
          )
          .unnest("event")
    )
    starts = events.filter(pl.col('action') == 'start').select('id', 'text', pl.col('ended_elapsed_ns').alias('t0'))
    stops  = events.filter(pl.col('action') == 'stop' ).select('id',         pl.col('ended_elapsed_ns').alias('t1'))
    slow = (starts.join(stops, on='id')
                  .with_columns(((pl.col('t1') - pl.col('t0')) / 1e9).alias('seconds'))
                  .sort('seconds', descending=True))
    """)
    
    python_eval(expression="slow.head(20)")
    python_eval(expression="slow.filter(pl.col('text').str.contains('rust')).head(20)")

    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.

    • interesting
    • mcp
    • agents
    • python
    • polars
  33. 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/.

    One PTY in, one structured artifact set out, ready for polars.
    nix run github:indexable-inc/index#run -- cargo test -p my-crate
    ls .ix/run/latest/
    # events.jsonl  lines.jsonl  live          output.log  replay
    # session.cast  summary.json  timing.log   typescript

    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:

    import polars as pl
    
    df = pl.read_ndjson(".ix/run/latest/lines.jsonl")
    (
        df.sort("delta_since_previous_line_ns", descending=True)
          .select("line_no", "delta_since_previous_line_ns", "text")
          .head(20)
    )

    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:

    @nix {"action":"start","id":12,"text":"querying info about missing paths","type":105,...}
    @nix {"action":"stop","id":12}

    Record that through run, drop the @nix prefix, decode the rest, and join starts to stops to surface the slowest phases:

    import polars as pl
    
    nix_event_dtype = pl.Struct([
        pl.Field("action", pl.String),
        pl.Field("id", pl.Int64),
        pl.Field("text", pl.String),
        pl.Field("type", pl.Int64),
    ])
    events = (
        pl.read_ndjson(".ix/run/latest/lines.jsonl")
          .filter(pl.col("text").str.starts_with("@nix "))
          .select(
              "ended_elapsed_ns",
              pl.col("text").str.slice(5).str.json_decode(nix_event_dtype).alias("event"),
          )
          .unnest("event")
    )
    starts = events.filter(pl.col("action") == "start").select(
        "id", "text", pl.col("ended_elapsed_ns").alias("t0"),
    )
    stops = events.filter(pl.col("action") == "stop").select(
        "id", pl.col("ended_elapsed_ns").alias("t1"),
    )
    (
        starts.join(stops, on="id")
              .with_columns(((pl.col("t1") - pl.col("t0")) / 1e9).alias("seconds"))
              .sort("seconds", descending=True)
              .head(20)
    )

    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.

    • interesting
    • nix
    • cli
    • polars