diff --git a/AGENTS.md b/AGENTS.md index b389fbcd..ab72565b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -53,7 +53,7 @@ client's wiring — project rules go in the former. | the cross-repo workflow itself (`bench`, this README) | here | | **the night shift** — the merge lease, tier 1, the runner that drives it | [hausfold/factory](https://github.com/hausfold/factory); its README is the manual, and haus puts it on `PATH` (`haus.ai.enable`) with its one skill, `/factory`. **In `FAMILY`, unlike trill and snug**, and for the one reason those two don't need: it HOLDS a lock (`factory → snug`) as well as being pinned by one (`haus → factory`), and a pin no verb moves only moves by hand. So `bench ship` walks, bumps and pushes factory like any other family repo — `FAMILY` lists it before `haus` so its own snug bump lands before haus's factory pin is read. Nothing about the shift lives here: the operator half is hausfold.co's `docs/haus/night-shift`, the seams `./haus`'s `docs/night-shift-internals.md`, the policy `factory config print` alone. A live lease (`factory lease status`) is the standing go-ahead for **tier-1** merges as `factory tier` decides them; the rest waits at "PR open" | | **how one of our tools puts a line on screen** | the tool's OWN repo, through **trill**, Apple's banner as fallback — never a bare `osascript -e 'display notification …'`: haus's `haus-notify` (`modules/core/haus-notify.sh`), pounce's per-command `notify()`, `bench`'s own, `scruff hook notify`. Every caller passes its own `--source`, the string `~/.config/trill/rules.json` matches on; no `haus.*` option gates it | -| **a write-up that turned out to be wrong** — a stale claim, a check that passes while what it protects rots | [`docs/drift.md`](docs/drift.md): thirty-three numbered shapes and what catches each. **Row numbering is frozen** — cite by number. Append a shape, or park it under *Seen once, not yet a row* | +| **a write-up that turned out to be wrong** — a stale claim, a check that passes while what it protects rots | [`docs/drift.md`](docs/drift.md): thirty-four numbered shapes and what catches each. **Row numbering is frozen** — cite by number. Append a shape, or park it under *Seen once, not yet a row* | | **how one of our CLIs looks on screen** — a colour, a glyph, a column, a spinner | snug's own `README.md` and `AGENTS.md` are the standard; this repo's `docs/design.md` is the brand's, not the CLI's. Roles resolve against **nebelung**, never a hand-picked 256-colour index, and columns are budgeted with `ui_col` + `ui_trow` + `ui_table_data`, never `%-44s`. In scope: `bench`; haus's `haus.sh`, `haus-show.sh`, `focus`, `github-signal`, `haus-secret`, `awake` (prose only — `status --raw` skips the painter), `statusline.sh`, `image-preview.sh`, `lane-open.sh`; scruff; factory (`share/ui.sh` off `FACTORY_UI_SH`, plain text without it). Named exceptions: `haus-show`'s `field`, `haus set`'s picker padding (counted by haus's `test/phase-painter.bats`), the statusline's row tint. **Installers are exempt from the runtime, not the palette** — `bootstrap.sh` and `haus-activate.sh` inline snug's numbers, `haus/test/installer-palette.bats` diffs them back, and **nothing is inlined without a drift test**. Out by settled decision: maintenance and probe scripts (haus's `script/build-golden-vm.sh`, trill's `scripts/dev-install.sh`, this repo's `script/issue-labels.sh` and `script/probes/*.sh`, plus `script/shoot-hero.sh` — which is read at a real terminal, and is out for a different reason: it runs mid-shoot from whatever checkout is to hand, including a lane worktree where the sibling `snug` that `ui_load` reads is not there at all); trill's CLI, pounce's commands and haus's ten one-file Swift helpers; anything whose stdout is another program's input (`awake --raw`, `agent-state`, `scruff-cache`, `hausrect`, `barvitals`, `hausocr`, `hausax`, `haustabs`, `agent-desktop-guard`, `haus-vm-shot`, `haus-fix`/`haus-fix-github`); rows not drawn on a terminal at all — haus's `find.sh` pads for `fzf`, which owns the window and cuts its own, and a bar plugin's `printf` is read by sketchybar; and anything with no terminal at either end (`floatpin`, `floatring`, `barpop`, `haus-notify`, `trill.sh`, `lidawake`, `haus-github-receiver`, `statusline-refresh`, `portless`, `haus-nix-gc`, `portless-lane`). **Do not collapse those into one "nobody reads these" rule** — the installers and the maintenance scripts ARE read by people at real terminals, and their exemptions rest on when they run and who reads them. **This row owns that scope** — re-open an exemption here, never by quietly converting a file | | **how the brand looks off the terminal** — a logo, a lockup, a hue, an OG card | [`docs/design.md`](docs/design.md), the brand's visual system and binding on every repo: two registers (the house is grey and borrows, a product owns one hue), two surfaces (an artifact is Space Grotesk and dark, or latte where it is light; a page is the Mac's own faces in both themes). A desktop never has a mark. Values resolve against **nebelung**; hausfold.co's implementation is its own `AGENTS.md`. A mark's source of record is its SVG in git, indexed in [`assets/README.md`](assets/README.md), the public media kit hausfold.co's `/brand` 301s onto; `test/design-palette.bats` holds the seam. Which commit a mark change lands in, and the order a product mark's two PRs take, are in the doc's own lede | | **how an agent learns to drive one of our tools** — the `ai/SKILL.md` (and sibling `ai//SKILL.md`) an end user's agent loads, the ` skill` verb, `--json`/exit codes | the tool's OWN repo, to [`docs/agent-surface.md`](docs/agent-surface.md). A `SKILL.md` is for an agent *using* the tool with no checkout, `AGENTS.md` for one working *on* it. Which skills a machine gets is `./haus`'s `haus.ai.skill` | diff --git a/docs/ci.md b/docs/ci.md index caec92e4..1e5a69c2 100644 --- a/docs/ci.md +++ b/docs/ci.md @@ -13,7 +13,7 @@ by not paying twice for the same one. | repo | workflow | where it runs | what it can only prove there | | --- | --- | --- | --- | -| `haus` | `check` | ubuntu ×7 | evaluating a whole darwin system, plus thirty-two platform-independent flake checks. Seven jobs, both halves grouped by what a step needs: the nix half is three (`nix eval` with the two bash suites that read what it builds, `nix flake check` alone, `haus add` alone), the shell half four (the lint, the suites that read snug's painter, the agent surface, every other room) | +| `haus` | `check` | ubuntu ×7 | evaluating a whole darwin system, and every flake check but one — raising a darwin machine needs no Mac, only building one does, so the single check that reads a BUILT `activate` script is the whole of what `nix flake check` on a Mac still has that CI does not. That is the family's one gate whose pole is evaluation rather than build or compile: 186s of `nix flake check`'s 193s, which no store cache reaches. Seven jobs, both halves grouped by what a step needs: the nix half is three (`nix eval` with the two bash suites that read what it builds, `nix flake check` alone, `haus add` alone), the shell half four (the lint, the suites that read snug's painter, the agent surface, every other room) | | `pounce` | `build` | macOS ×2, ubuntu ×2 | the app is built by `xcrun swiftc` through Nix, so it wants a real Mac; the skill guards and the command lint do not | | `perch` | `build` | macOS ×3, ubuntu | Xcode test + analyze on one Mac, Release plus the arm64 slice guard on another — cut at the `-derivedDataPath` the steps already divided on — and the iOS companion on a third; the skill guards are the one job off the Mac | | `trill` | `build` | macOS ×2 | Xcode test + analyze on one Mac, Release plus the no-instrumentation guard on another — cut at the `-derivedDataPath` the steps already divided on, the same boundary as perch's | @@ -458,6 +458,15 @@ the warm ones, so any re-measure starts by reading the "Restore the Nix store" step. 30s-plus means warm; under a second means the key missed and everything below it paid full price. +⚠️ **Those figures no longer describe `nix flake check`.** Every check haus declares but one runs on the Linux runner now, and what the +darwin ones brought is EVALUATION — dozens of nix-darwin systems raised in a +single evaluator. Measured on run 35591587897: **193s, of which 186s is eval +and 6s builds eighty derivations**, behind a 22s restore. A store entry holds +outputs and has never held an evaluation, so the restore still answers for the +6s and none of the 186s. 1s warm belonged to a step whose work a hit could +hand back whole; this one's cannot be. The 55/6 pair on `nix eval` is +untouched — that job did not change. + ⚠️ A cold figure also belongs to the JOB it was measured in. `nix flake check` reads 37s cold behind a `nix eval` that has already paid for nixpkgs, and 57s as the first thing in a job of its own — the same step, and the split between @@ -471,8 +480,10 @@ It is on those two repos because that is where the trade lands: palette under it changes every PR. A hit costs nothing; a miss no longer costs a rustc closure either, because that job reads catppuccin's own cachix — *Reading someone else's public cache*, below. -- `haus` evaluates nixpkgs and then *builds* thirty-two check derivations, and - most PRs touch none of their inputs. +- `haus` evaluates nixpkgs and then *builds* every check derivation in the + flake, and most PRs touch none of their inputs. ⚠️ That is the half the + restore can answer for. The other half of `nix flake check` is evaluation — + see the warning above — and it is paid in full on a hit. **The trade has to be re-tested after the cache is in, and it can go negative.** The test above is usually run once, to decide whether to add a @@ -501,9 +512,14 @@ comes straight back. Only the second is an argument for changing anything. ⚠️ And before narrowing a store that *is* paying, re-check rule 5: the answer on haus's remaining two was to leave them alone, because past the reset that -half drops under the same repo's shell jobs and every second still on the table -belongs to a job the change would not touch. Shave the pole, then re-measure -which job that is — the answer moves. +half dropped under the same repo's shell jobs and every second still on the +table belonged to a job the change would not touch. Shave the pole, then +re-measure which job that is — the answer moves. ⚠️ **It has moved.** +`eval` was that half's pole at 71s; `checks` is now, at 221s against +`eval`'s 62s, because every darwin check evaluates there. Both the paragraph +above and the `71s alone` figure two above it describe the shape before that, +and the store question they settle is unaffected — a restore still cannot +hold an eval either way. It is **not** on the repos that build their own Swift on a Mac. There the expensive derivation is the one whose source just changed, so a restore buys @@ -563,8 +579,9 @@ evaluating nixpkgs, **2.3-3.5s** substituting the 69 paths it needs (216 MiB from `cache.nixos.org` at 60-95 MiB/s), and 12-15s building two derivations — ~6s of `go build`, ~7s of `go test`. Both take the source as an input, so the source change invalidates both on every run: snug's build row is worth nothing -to a restore, and that is the whole difference from haus, whose thirty-two -checks mostly survive a PR untouched. What is left is the substitute row plus +to a restore, and that is the whole difference from haus, whose check +derivations mostly survive a PR untouched. What is left is the substitute row +plus the 47 MiB nixpkgs `-source` the eval pulls — **262 MiB of download, 3-4s of a 41s job**, and of a 34s one now that the installer in front of it is gone. That source arrives in under a second at the rate the substitute row runs at, so the diff --git a/docs/drift.md b/docs/drift.md index 78d9888c..9b180fff 100644 --- a/docs/drift.md +++ b/docs/drift.md @@ -44,6 +44,7 @@ or removed. Count the rows, never increment a number written in prose. | 31 | a reader that addresses an entry in a GENERATED index by NAME, where the generator only hands that name to whoever asked first — right for as long as one holder wants the entry, then silently answering about somebody else's | resolving through the index's own mapping rather than its keys, and mutation-testing by adding a SECOND holder. The tell: the thing being read is regenerated by a tool whose naming rule nobody wrote down, and the change that breaks the reader is in a file the reader never mentions — nothing in the subject's own repo moved | | 32 | a CHECK declared on the wrong side of a PLATFORM guard — the code is right, the census names it, and no runner ever selects it, so the suite goes green having never evaluated the thing | enumerating what the runner actually EVALUATES and diffing that against the source, never reading the declaration. The tell is a check whose own comment names the platform it wants while its declaration sits inside an attrset keyed for another. It survives every review aimed at the check's LOGIC, because the logic is sound; and it survives the census, because a list of names is written from the same declarations | | 33 | a CHECK whose CENSUS is a HAND-MAINTAINED list — every subject it names is evaluated, so the logic is sound and the coverage is whatever somebody last typed. A subject added at the source does not skip and does not redden; it is outside the suite, and green comes back at full strength over a smaller population. Row 32 inverted: there a name no runner selected, here a runner that takes every name and names that are short | a SECOND check walking the other way — enumerate the population where it LIVES, never the list against itself. The tell is coverage that cannot move: adding the subject a person would really add leaves the run byte-identical. Two walk-backs that only look like the fix — one deriving its population FROM the list cannot see a missing WHOLE holder, one needing checkouts CI lacks is a standing skip. Twice in `PRODUCT_MARKS` (perch's iOS icon, then the three light tiles) a hand closed the gap inside the hour; that nothing else would have is the argument | +| 34 | a PLATFORM guard whose stated reason names a WEAKER capability than the one it actually requires — the block reads "it evaluates a real system" and gates on having a BUILDER for that platform, and on the author's own machine those are one thing. Every sentence beside it is true; the conjunction is what is false, and the gate goes green over a population it never selected. Row 32 is the symptom, this is why it was written that way | separating the two capabilities in the sentence BEFORE gating on either — name the strongest thing the code actually needs, then grep the same repo for somewhere it already does that. The tell is a reason written in the language of evaluation ("raises a machine", "fingerprints a real system") sitting on a mechanism that is a build. Row 24's search aimed at the guard instead of the remedy: here the counter-example was one job away in the same file, doing the thing the guard called impossible | ## Why this family drifts more than a normal TODO list