Tillandsias

Tillandsia is the largest genus in the bromeliad family, with well over six hundred species.

A small private cloud on your own computer. Its workspaces are thrown away and rebuilt; the work you push out of them is kept.

tillandsias.org · v56.10.9.1 · website last updated 56.10.9

A small cloud on your own computer,
built to be thrown away and rebuilt.

Your own hardware. Free software. No account and no subscription. Below is what it is and how it works, told five times over — pick the version that fits the person reading.

The simplest way of putting it that is still true.

Tillandsias, explained like you are five

A real tillandsia needs no soil and no pot — it drinks from the air, and borrows nothing.

A tiny cloud on your own table

When you open a website, you borrow a computer in a huge building far away — the cloud. Tillandsias builds a tiny cloud inside your own computer, so you can work without sending your things to its makers. A service you choose to use can still receive what you send it.24

It is a doll house. Your computer is the table. Tillandsias puts a clean pretend-computer on it, and inside that, little rooms, one program each.

Anyone may have it, read it and change it, for free.14

Nobody goes outside except the doorman

The rooms have no doors outside. They share one private hallway, and exactly one room — the doorman — may step out to the internet. Everyone else asks it.2

Robot helpers may only play inside the doll house.11 If one makes a mess in its room, the room can be thrown away — but anything it sends out of the doll house is real, so look at what it sends.26

Broken things get thrown away, not glued

the usual way this way it breakspoke at it still oddpoke again drifts somewhere nobody can reproduce it breaksthrow it away build from the recipe identical every time, on every machine
Nothing at runtime is repaired. It is thrown away and built again from the recipe — which is why it comes back identical.

If a room breaks, nobody glues it. It goes in the bin and Tillandsias builds a fresh one from the same recipe. That is on purpose: gluing one layer tends to break the layer underneath next.3

Your own project folder lives outside the doll house, and sweeping a room never touches it.12

Getting work out of the doll house has its own rule. The doll house has a post box, and it does not say done until the shelf far away really has your parcel. If it cannot reach the shelf, it says so, instead of saying done and quietly keeping the parcel in a drawer.26

Getting better, honestly

Every time the makers fix something, a machine checks that nothing that used to work got broken.23

Less broken every time does not mean perfect one day. It only means creeping toward some resting place, which may not be zero. They say so themselves.4

Things that are still broken

known limitation The doorman has a secret key. The key itself is locked so only you can read it,15 and it lives in a folder inside your own home folder.1720 But that folder is made the ordinary way, without first being made private.25

what happens next Making the folder private before the key goes in is the written next job. It is not done yet.18

How we know — 14 sources

Each small number in the text opens the exact lines of the Tillandsias source that back that sentence, as they stand in release v56.10.9.1, the one the install commands give you. Hover a number to read the quoted lines without leaving the page. A source marked with its own release points at a newer one.

  1. 2Only the doorman may step outside openspec/specs/enclave-network/spec.md#L10-L12Only the proxy is dual-homed for external access; all other members communicate exclusively through the enclave.
  2. 3Throw away and rebuild, never patch by hand, and why methodology/philosophy.yaml#L84-L99Therefore the DEFAULT response to a borked host/VM/guest/podman/stack layer is to DESTROY and RECREATE it, never to hand-patch it forward.
  3. 4Getting better every time means nothing gets worse and what is left shrinks, not that it ends up perfect methodology/math-foundations.yaml#L108-L120Without that metric proof, "monotonic convergence" means ordered non-regression plus finite residual descent, not metric contraction.
  4. 11Robot helpers run only inside the doll house openspec/specs/forge-as-only-runtime/spec.md#L18-L19Every coding agent, maintenance shell, and runtime utility executes inside the project's forge container; there is no host-side execution surface.
  5. 12Sweeping a room never touches your project folder openspec/specs/app-lifecycle/spec.md#L79-L81the container is removed, project-specific cache data is deleted, but the project source directory in `~/src` is never touched
  6. 14The licence: anyone may have it, read it and change it LICENSE#L1-L16the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program
  7. 15The key is locked so only its owner can read it crates/tillandsias-headless/src/main.rs#L3400-L3400Clamp the CA private key to owner-only access (0600) — 755-qcxh.
  8. 17The key's folder sits inside the program's own folder crates/tillandsias-core/src/ca_path.rs#L36-L50format!("{}/ca", ca_template())
  9. 18The written next job: make the folder private before any key is written plan/index.yaml#L26013-L26013deliverable: CA material is created in a directory that is private by construction (XDG_RUNTIME_DIR, or DirBuilder::mode(0o700) before any key is written), and scripts/clamp-ca-material.sh gains a retirement condition or is deleted
  10. 20The program's own folder is inside your home folder images/default/ca-path.txt#L83-L83${HOME}/.local/state/tillandsias
  11. 23After every fix, a test that used to pass may not start failing build.sh#L2454-L2456a failure not named in scripts/test-known-red.txt is a new regression, and a listed test that PASSED is a stale entry that must be deleted
  12. 24A service you choose receives what you send it PRIVACY.md#L60-L63If you instead configure a remote provider, the content you send is transmitted to that provider under their terms.
  13. 25The key's folder is made the ordinary way, with no private setting crates/tillandsias-headless/src/main.rs#L3468-L3468std::fs::create_dir_all(&certs_dir)
  14. 26The post box says done only once the faraway copy really has the work images/git/pre-receive-hook.sh#L6-L8Validates ledger YAML, then synchronously relays the proposed ref transaction upstream before accepting it locally. A client success therefore means the configured upstream has durably accepted the same atomic ref set.

Straight answers to what you are actually wondering: privacy, cost, and what breaks.Picks up where “like I’m 5” left off.

Tillandsias, explained simply

A tillandsia is an epiphyte, not a parasite: it rests on its tree and takes nothing from it.

What it is for, in one paragraph

The reason a grown-up wants one is narrower than "the cloud, but at home". Software you did not write — increasingly, AI assistants that write and run code for you — has to work somewhere. The choice is between your actual machine and a sealed room containing only the project folder you deliberately opened.1 Tillandsias is that room, plus the plumbing to prepare it and start your tools.

the boundary the specification draws must hold must NOT hold positive cases negative controls
A test that only ever passes proves nothing. Each boundary is pinned from both sides.

Is my stuff private, and who could see it?

The people who make Tillandsias operate no servers your copy talks to: no account, no sign-up, no usage tracking, no crash reporting, no identifier attached to you or your machine.2 Your logs stay on your own disk where you can read them, and leave only if you send them somewhere yourself. Any login you hand over goes into a secret store on your own machine — the program's own vault, whose key sits in your operating system's keychain, or on some computers in a local file — never on anyone else's server.316

This does not depend on trusting anyone's intentions: there is no server of theirs to send anything to.2 It covers their side only. A service you sign into yourself still sees what you tell it.18

Does it cost money? Does it need the internet?

It costs nothing: free software under a licence that lets anyone read, run, modify and pass it on.4 You have already paid for the only hardware involved.

The internet is needed only for ordinary, visible reasons: fetching the program, fetching updates, reaching a service you chose.17 Local work can continue without sending it to an AI provider: language models can run on your own machine.18 That does not make every action offline-ready. Fetching a missing tool or model needs a connection, and publishing your work to GitHub fails when GitHub cannot be reached.31

What happens when I turn it off? Can it break my computer?

Turning it off is the expected motion, not an interruption. Anything you saved is a real file on your real disk, untouched by the rebuild. Even the internal passwords the pieces use to talk to each other are replaced at every start, so yesterday's messy shutdown cannot jam today's start.5

Installing a new release is a bigger change than turning it off: the Linux, Mac and Windows installers all reset the program's own working state and set it up again.363738 On Linux this reset is gentle: it rebuilds the program's parts but deletes none of your stored data, so your downloads survive, and so do your sign-ins as long as your keychain can still unlock them.39 Windows runs the same gentle reset without telling you first.3840 The Linux and Mac installers tell you before they reset; to skip the part that deletes things, set TILLANDSIAS_DESTRUCTIVE_RESET_OK=0 before installing.37

known limitation On a Mac the reset is not gentle yet. It still deletes your saved vault keys and the secret store, although the project's own rules say every platform's reset must keep them.4142

Publishing your work follows the same idea. Inside the sealed room you push to a copy that lives on your own machine, not to the internet. That copy passes your work on and waits: it reports success only once the copy outside has really accepted the same changes, so a success you can see is one that survives the room being thrown away.32 It is also the only part that holds your GitHub login — it uses it when it publishes, and never hands it to the room where your tools run.33

As for damage: a misbehaving tool inside the sealed room sees only the folder you gave it.1 Wiping the installation and rebuilding is documented and supported, not a last resort.6 The cost you do pay is ordinary and reversible. It is a real virtual machine, so it holds real memory while running and gives it back when it stops. The system image it downloaded stays on your disk so the next start is quick; uninstalling clears it, though on a Mac you have to ask for that explicitly, and the uninstaller makes you type a confirmation first.203043

what happens next A fix is being written. It is not in any release yet.

Will an update break what already works?

far close successive releases — each one re-raises the bar the floor — possibly not zero zero would mean “finished”. No proof it is reachable.
Schematic: if comparable residuals never rise, they approach a floor, and nobody has proven that floor is zero.

Each change to Tillandsias has to pass its tests, and a machine refuses a change that makes a test fail that was not already known to fail.23

Two honest limits come with that. Passing tests are evidence, not proof: they show no problem was found, not that none exists.8 And "never worse" means settling toward some floor, not that the floor is zero problems.9

Where it falls short today

known limitation On a Mac, the app has not yet passed Apple's inspection, so a copy downloaded with a web browser is blocked the first time you open it. The recommended one-line install avoids that only because it does not go through a browser.10

what happens next The fix needs a paid Apple developer membership, which is still pending.1011 The build is already able to do the inspection step once it has those credentials.24

known limitation The Windows downloads are not signed yet.27 One kind of Windows installer package cannot be installed at all without a signature, so it is left out of a release until signing is in place.1226

what happens next The signing service has been chosen but is not set up yet.13

known limitation The Linux program shows a version number but not the exact source it was built from, so two builds with the same number cannot be told apart.3435

None of these touch the privacy answer above.

what happens next It is recorded as an open problem in this release.35

How we know — 34 sources

Each small number in the text opens the exact lines of the Tillandsias source that back that sentence, as they stand in release v56.10.9.1, the one the install commands give you. Hover a number to read the quoted lines without leaving the page. A source marked with its own release points at a newer one.

  1. 1What a workspace can reach, and why that isolation exists to protect you from the tools PRIVACY.md#L32-L40Each workspace sees the project you opened, not your whole filesystem. That isolation exists to protect you from the tools, not to hide anything from you.
  2. 2"Tillandsias collects nothing" — no account, no telemetry, no analytics, no server PRIVACY.md#L5-L17**Tillandsias collects nothing.** There is no account to create, no telemetry, no analytics, and no server operated by us that your installation talks to.
  3. 3Credentials in a local secret store on your machine; logs kept on your own disk PRIVACY.md#L41-L44**Credentials you provide** (for example, a GitHub login you initiate) are stored in a local secret store on your machine.
  4. 4GNU General Public License, version 3 LICENSE#L1-L2GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007
  5. 5Internal passwords are removed and reissued at every start, so an unclean shutdown leaves nothing stale openspec/specs/ephemeral-secret-refresh/spec.md#L10-L25The system SHALL check for existing podman secrets before creation. If a secret exists from a prior unclean shutdown, it SHALL be removed and recreated with fresh content.
  6. 6"Designed to be wiped and rebuilt freely" PRIVACY.md#L46-L47You can remove all of it at any time by resetting or uninstalling; the application is designed to be wiped and rebuilt freely.
  7. 8Passing tests are evidence, not proof that the program is correct methodology/philosophy.yaml#L249-L252Traceability, version matching, litmus success, and CRDT metadata convergence are evidence. They are not proof of semantic correctness by themselves.
  8. 9The project does not claim it will reach zero problems methodology/math-foundations.yaml#L107-L120The methodology does not currently claim Banach-style contraction. It can report decreasing residuals, but it has not proven a contraction constant over a complete metric space of project states.
  9. 10The install instructions: a Mac download through a browser is blocked on first launch README.md#L27-L45Tillandsias is signed but not yet notarized (Apple Developer enrollment is pending), and macOS tags anything a *browser* downloads with `com.apple.quarantine` — so the .dmg route hits a Gatekeeper block on first launch, while `curl` does not tag at all and the app opens normally.
  10. 11Apple signing and inspection options, and which steps need a paid membership plan/issues/macos-gatekeeper-signing-options-2026-08-29.md#L39-L48| Developer ID Application certificate | **Yes** — no free-tier equivalent.
  11. 12An unsigned Windows installer package of this kind cannot be installed plan/issues/store-msix-submission-blockers-2026-08-31.md#L26-L40**Consequence for the pending signing decision.** The unsigned MSIX (`0x800B0100`, packet 722-w7a2) blocks the **GitHub-release** channel only. It does not block the Store channel at all.
  12. 13The signing service chosen for the Windows downloads plan/issues/windows-signing-research-2026-08-16.md#L1-L25**SignPath Foundation is the signing path for the GitHub-release channel.** Packet 722-w7a2 is reshaped, not closed: its deliverable changes from "an Azure Trusted Signing account" to the SignPath Foundation chain, with Azure **Artifact Signing** as the recorded fallback.
  13. 16Where no keychain is available, the vault's key falls back to a local file crates/tillandsias-headless/src/vault_bootstrap.rs#L2943-L2943Fallback: file (populated by keychain_set_blocking when keyring unavailable,
  14. 17Software sources — package repositories and release downloads, reached at your direction PRIVACY.md#L51-L56**Software sources** — package repositories and release downloads (for example GitHub, Linux distribution mirrors, and language package registries) to fetch the software it runs.
  15. 18AI providers only if you configure one; language models can run entirely on your own machine PRIVACY.md#L60-L63**AI providers, only if you configure one.** Tillandsias can run language models entirely on your own machine. If you instead configure a remote provider, the content you send is transmitted to that provider under their terms.
  16. 20The downloaded system image is cached on the host between runs openspec/specs/vm-provisioning-lifecycle/spec.md#L48-L51cached at `~/.local/share/tillandsias/rootfs-fedora-44-<sha256>.tar.xz` (on macOS: `~/Library/Application Support/tillandsias/rootfs-…`; on Windows: `%LOCALAPPDATA%\tillandsias\rootfs-…`).
  17. 23The test verdict: a failure not already on the known-failing list is refused as new build.sh#L2451-L2451THE VERDICT IS A RATCHET, NOT CARGO'S EXIT CODE.
  18. 24The Mac build inspects and seals the app when given Apple's credentials scripts/build-macos-tray.sh#L267-L290say "notarize: submitting (this waits for Apple's verdict)"
  19. 26That installer package is left out of a release when it is unsigned .github/workflows/release.yml#L751-L751::warning::withholding unsigned MSIX from release assets: $($_.Name) (uninstallable without a signature; set TILLANDSIAS_SIGNING_ACCOUNT to publish it)
  20. 27The other Windows downloads are published unsigned .github/workflows/release.yml#L740-L740::warning::TILLANDSIAS_SIGNING_ACCOUNT is unset — publishing UNSIGNED Windows artifacts (plan packet 722-w7a2)
  21. 30Uninstalling removes the cached image, except on macOS where it is preserved unless you ask for a full wipe scripts/uninstall.sh#L228-L228Preserving the VM image in $DATA_DIR (use --wipe to remove it).
  22. 31A publish that cannot reach GitHub fails instead of half-succeeding openspec/specs/git-mirror-service/spec.md#L225-L230the forge's `git push` SHALL return non-zero
  23. 32Success means the faraway copy really accepted the same changes images/git/pre-receive-hook.sh#L6-L8Validates ledger YAML, then synchronously relays the proposed ref transaction upstream before accepting it locally. A client success therefore means the configured upstream has durably accepted the same atomic ref set.
  24. 33The GitHub login is read by the publishing service and never enters a workspace openspec/specs/git-mirror-service/spec.md#L15-L16The git service reads the GitHub token from Vault at push time via Vault CLI; the token never crosses into a forge container.
  25. 34The version shown is the one written into the program when it was built crates/tillandsias-headless/src/main.rs#L161-L161pub(crate) const VERSION: &str = include_str!("../../../VERSION");
  26. 35The open problem: the Linux program records no build commit plan/index.d/20260914t185710z-1188-mm9y-installed-launcher-has-no-provenance-lenovinha.yaml#L27-L43the Linux launcher has no build.rs reading it.
  27. 36The Linux installer runs the reset scripts/install.sh#L340-L340"$INSTALL_PATH" --reset-state --debug
  28. 37The Mac installer runs the reset, and the setting that skips its deleting part scripts/install-macos.sh#L290-L290TILLANDSIAS_DESTRUCTIVE_RESET_OK=0 skips the destruction
  29. 38The Windows installer runs the reset without asking scripts/install-windows.ps1#L968-L968--reset-state < NUL
  30. 39The Linux reset rebuilds the program's parts and deletes no stored data crates/tillandsias-headless/src/main.rs#L10474-L10490A SOFT reset destroys DERIVED state only and deletes NO store under any
  31. 40The Windows installer's rule: the gentle reset, with no prompt scripts/install-windows.ps1#L951-L952So SOFT reset is the default only and forever.
  32. 41The Mac reset deletes the saved vault keys and the vault data crates/tillandsias-macos-tray/src/reset_state.rs#L286-L292remove_path(&cache_root.join("vault-data"))?;
  33. 42The project's rule: every platform's reset must keep your data openspec/specs/host-state-lifecycle/spec.md#L180-L182`--reset-state` on every platform SHALL be a SOFT reset: it destroys every item of derived state and SHALL NOT remove, move or rewrite any item of operator data.
  34. 43The uninstaller asks for a typed confirmation and refuses without a terminal scripts/uninstall.sh#L184-L185Type \"delete\" to uninstall, or press Enter to cancel:

The anatomy: what runs where, what survives a teardown, and where the sharp edges are.Assumes the two levels before it.

Tillandsias, for a power user

Its roots only grip; the leaves do the drinking — a plant that runs rootless.

What you get the moment a forge opens

tillandsias --headless /path/to/project --claude starts a container with your project in it and an agent at the prompt. Nothing below is configured by you or by the agent: the context file the agent reads first lists the plumbing under the heading "all transparent — zero configuration needed".16 Everything on this page describes the stable release, v56.10.9.1 — the one the install commands give you.

The forge container

A forge is a Fedora Minimal image in two layers: a heavy base carrying the toolchains — gcc, make, cmake, Rust with cargo and rust-analyzer, Go, Node, Java, Python, the debuggers — and a thin runtime layer with the entrypoints, cheatsheets and the agent's configuration.1718 The tag is a content hash of its sources, so an unchanged rebuild is a no-op.19 The coding agents are not baked in: every launch refreshes them into a per-project cache volume that outlives the container, rolling back to the last known-good version when upstream ships a broken one.2021 Cheatsheets, /tmp and the runtime directory sit on RAM-backed tmpfs with hard caps.22 The default clone-only lane now puts the source tree in RAM too — a tmpfs-backed volume the launcher creates for the lane, sized from the mirror’s pack; the opt-in host-mounted checkout keeps its host mount.30149 Allowlisted commands — tmux and lazygit among them — are shims that install userspace Homebrew on first use.23 On start the forge writes a context file into the checkout naming what is present, what is absent, and what needs no configuring.24

checked Works out of the box. The tool cache is a named volume, never a host path, so first-run installs survive the container's removal without opening a host-home surface.21

known limitation The storage spec names four RAM-backed roots and a host RAM gate with a matching container memory ceiling; the default lane now mounts all four roots, but the host RAM gate and the matching container memory ceiling remain library code nothing calls.2526 The Homebrew prefix is backed by no volume, so every on-demand tool is re-installed at every launch and its bootstrap needs live egress each time.27 And the context file tells the agent each such install verifies a signed attestation per bottle; the shim states that attestation is off, because fetching it would need a GitHub credential no forge may hold — what remains is integrity from one publisher.2829

what happens next The RAM gate and memory ceiling are a planned fix that has not been started: run the host RAM check before launch and set matching memory limits.26 The recorded remedy for the prefix asks for a decision between persisting it and baking the tools into the image.27 For the context file's wording there is a recorded remedy too: fetch and pin the attestation bundles when the image is built, so a lane can verify one with no GitHub credential present at all.145

The HTTPS proxy and its cache

Every container is born with its proxy variables set and the proxy's certificate composed into the system trust bundle before any network client starts; no per-client CA variable is set, and a regression test refuses a launcher that tries.34353637 The allowlist of developer hosts — registries, GitHub, cloud SDKs, model providers — is generous enough that npm, cargo, pip and flutter work unconfigured.38 It decrypts exactly one of them, GitHub's release-asset CDN, so a large asset is fetched once for the whole machine.3940 Everything else passes through encrypted, so the proxy never sees, and never caches, crates.io, npm, PyPI or api.github.com.41 What makes the second cargo build fast is not the proxy but a per-project cache volume for cargo, npm, pip and go that outlives the forge.42 A blocked host gets no 403: the connection is reset, and only the proxy log names the domain.43

checked The trust wiring is pinned by a test: the forge launcher mounts the single runtime CA input read-only and sets none of the per-client CA variables.37

known limitation The cache-hit claim for the one decrypted host has never been shown live. The fix for a rule order that made decryption unreachable shipped the config and static tests, and records that no real miss-then-hit trace was obtained.44

what happens next The next step is written down: on a Podman host, request one release-asset URL twice through the strict proxy and record a miss followed by a hit with equal checksums.44

Local inference

The launcher prepares a shared model server at http://inference:11434, started on launch if it is not already running and shared across projects.4546146 The first run pulls the engine and one small tool-capable chat model into a host directory that outlives the containers.47489 Readiness is best-effort: a failed readiness wait warns and the forge launches anyway; errors while preparing the service can still fail launch.46 The context file tells the agent, in a closed vocabulary, what the host's accelerators are and whether the endpoint is ready — ready means at least one model is cached, never "starting up".4950 Larger tiers are opt-in behind an environment variable; the CPU floor is unconditional by spec.5152

checked A failed model-readiness wait is non-fatal, so it does not hold up the agent session.46 The CPU floor is the required fallback, rather than evidence that every fresh host has successfully loaded a model.52

checked The first-run engine download now pins an Ollama release and checks an architecture-specific SHA-256 before execution; a missing checksum tool or mismatched digest stops it.4853

known limitation Two limits remain. GPUs reach the container only with a vendor stack — NVIDIA with a CDI spec, AMD when ROCm reports the card — and NPUs are named on every platform and used on none: no device argument for one exists in the launcher, and the spec itself says the engine cannot use them.54555657 The container's health check tests that the runner binary exists, not that a model loads.58

what happens next NPUs have two planned fixes, neither started — device passthrough for the container, and a host-native sidecar for Metal and the NPUs a Linux guest cannot see.5960

what happens next For the health check, no fix is planned yet. The entrypoint does run the real load check once at startup.58

The git mirror and push relay

For an agent, a refinement is a controlled data path: it receives selected source, rules, task context and tool results; it proposes a patch; local checks and review decide whether that patch becomes a commit. Git retains reachable commits, while the current tree is a snapshot that may replace or delete files. Calling the repository a "set of vectors" is therefore a useful description of the model's selected context, not Git's storage format and not a guarantee that every current file survives.616266

Several tasks can run this loop at once. Each needs its own declared target and measurement; the shared repository is the coordination point, not evidence that their measurements are already comparable. The relay makes an accepted push atomic,6263 which preserves the record of what was accepted, not the truth of the patch.

Why this is built rather than borrowed. The mirror has to satisfy two requirements at the same time. The credential for the upstream must stay on the server side, so a workspace can publish its own work without ever holding a token that could publish anything else — the git service reads that token when it pushes and it never crosses into a workspace container.152 And the push must be synchronously durable: the hook relays the proposed ref transaction upstream before accepting it locally, so a client's success means the upstream has durably accepted the same atomic ref set.153 The live relay performs that as a single atomic push.154

Tools in general offer one of those or the other. A managed push mirror holds the credential for you and copies asynchronously, reporting success before the copy lands — which is precisely the false success that strands an agent's work. A caching or redirecting git proxy relays synchronously but forwards the client's credentials, which is the isolation being bought in the first place. Needing both at once is the unusual requirement, and it is what the pre-receive relay exists to provide.

That no off-the-shelf component satisfies both requirements together is the project's survey, not a benchmark: read it as the reason the decision was taken, not as proof that no such tool exists.

Rolling the decision back is deliberately cheap: the relay is a bare repository plus a pre-receive hook, so undoing it means pointing the daemon back at the bare repository.

Git needs nothing from you inside a forge. A read-only global gitconfig redirects the project's GitHub URL to a bare mirror in its own named volume, so git remote -v still says github.com while clone, fetch and push hit the enclave.616 A push is not acknowledged until the mirror's hook has relayed exactly those refs upstream with one atomic push.6263 The token authorising that relay is read from the vault inside the git service at push time and never enters the forge.6465 Deletions and branch rewinds land nowhere;66 a project with no upstream keeps its pushes in the mirror.67

note The pairing is the argument, not novelty. The pattern of relaying a push

onward from a hook is documented elsewhere; what is specific here is refusing to

report success before the upstream has it, while the credential stays outside the

workspace.

checked Works out of the box once GitHub Login has run on the host; without a stored token the relay refuses the push and says so.68

known limitation The push path inside the enclave is anonymous: any peer that can reach the mirror may create branches or fast-forward them and have the privileged relay carry those updates upstream. Being on an internal-only network is placement, not client authentication.69 The authenticated SSH lane that would replace it is built but dark behind an environment variable that defaults to off.70 Offline, a push simply fails — nothing is queued.71

what happens next Turning the SSH lane on by default is planned and waits on a decision about the transport.72 For the offline push, no fix is planned yet.

The vault

Secrets live in a Vault container at vault:8200, reached over TLS with the enclave certificate already trusted.7475 A lane with a credentialed provider is handed a single-purpose token on tmpfs at /run/secrets/vault-token whose policy reaches only that provider's paths; the generic forge policy cannot read the GitHub token, which the git mirror, the tray and the login flow can.76777879 Across a stop, Vault unseals itself from one Shamir share held in your host keychain, with no passphrase prompt.8

checked The token is injected at container start with nothing for the agent to do,76 and a test pins that every credentialed lane mounts its scoped lease while a credential-free lane mounts none.147

known limitation The context file tells the agent the vault is at http://vault:8200. The shipped listener is TLS-only, so that address answers an HTTP error; only the in-image helper carries the right default.807581 The token defaults to a one-hour life and nothing in the lane renews it.82

what happens next No fix is planned yet.

The experts and the project index

The agent finds four tool servers already registered, for Claude Code and OpenCode alike: git tools, a project index, the host-browser bridge, and a plan expert.8384 The index answers project type, status, commands and layout for any checkout, citing what it indexed; the plan expert returns cited answers or a typed refusal, never a guess, and answers only on a checkout carrying Tillandsias' own plan crate — elsewhere it reports itself degraded, by design.858687 Both start in the background after the clone and never gate the session.88 The status now distinguishes the running expert’s capabilities from what a relaunch would build: waiting for a build, needing a relaunch, and losing capabilities on relaunch have different machine-readable verdicts.150 A ready index is separate from a usable answer engine. Deterministic project questions can work without local inference; synthesis still returns a typed refusal when the synthesis capability is absent, even if inference itself is ready.151 The "on-demand" spec describes tray-side spawning and health checks that do not exist; the harness launches each server itself.89

checked Works out of the box on any project, with no registration step.85

known limitation The context file tells the Claude lane about a conversational Local Experts mode, but only the OpenCode configuration carries an agent that reaches the grounded endpoint behind it.9091 And the pre-expert-binary trap is half closed: a forge seeded from a checkout without the expert sources now reports itself degraded instead of ready, but the fresh-forge criterion — an in-forge probe grading full marks with no manual step — is recorded open.92

what happens next No fix is planned yet for the Claude-lane mismatch. The fresh-forge check waits on a decision about how releases are branched.92

Sibling web containers

Ask the agent to publish the project and it does not start a server in the forge. It asks the host, over a socket that exists only for that lane, to launch a sibling web container beside it: read-only, capability-dropped, joined to the enclave network and given a router route.939495 The sibling sees the running forge's own worktree, resolved from the lane's live container and its RAM-backed source volume rather than from a path on the host, and it is given a selected public document tree rather than the repository: var/html, public or dist if one exists, otherwise the root index.html and the plainly static files and asset directories beside it; a Wrangler configuration at the project root selects a second, Wrangler-based runtime instead.160 The URL comes back as https://www.<project>.localhost:<port>, on a loopback TLS port the router listens on, with a leaf certificate issued for that name.96 The project is attributed from the socket the request arrived on, never from the request.95 From inside the forge the proxy forwards .localhost requests to the router;97 this page has not traced the HTTPS preview port through that rule.

checked A sibling never mounts .git, .env or the forge root, so the repository's history cannot be read through the router, as it could on the earlier publish path.16098 Publishing reads the running forge, so it no longer needs the project to live under ~/src on the host;160 the tray's project list still looks only there.100 And a sibling is not left running when its forge dies: a watcher stops it and removes its route once that forge is gone, and the next launch of the same lane clears a departed one; the stack-wide sweep still matches no sibling, so this cleanup is the preview's own.16199 All of this is read in the code, not shown live.

known limitation The feature's own spec still calls itself a draft whose verification is design-only, with executable coverage and live evidence pending.162 The only end-to-end test of publishing is still on the list of tests that no suite has ever run.104 Only www is routed, not the apex.96 The static web image is built by initialisation rather than by a forge launch, which ensures only four images;101102 a host that never initialised now gets an explicit error telling it to initialise the managed runtime image, where the earlier path hit a phantom registry pull that was fixed only for the status-check path.164103 The Wrangler runtime needs a second image that the list of images initialisation builds does not contain, so it stops with the same error.101164

what happens next Planned: prove the preview works live, then make its spec binding.163

what happens next For the apex route and the Wrangler image, no fix is planned yet.

Chromium

Two browser images are built at initialisation: a minimal headless core, and a framework layer on top of it with GUI Chromium, Node and Playwright.101105106 The runtime launches the framework image as a read-only, capability-dropped, incognito app window — but only for the OpenCode-web and observatory modes, and only on a machine with a display.107108 The browser tool the agent sees — open, screenshot, click, type, close; eval switched off — is a different thing: it drives a host Chrome from the user's cache directory that nothing shipped installs, so a fresh host answers "browser unavailable".109110111112

known limitation The headless core is built and never run,105 and the on-demand Chrome download the browser tool depends on still has no caller.111 The browser script now defaults to the enclave network,115 and the tool's allowlist accepts dotted project names while checking the project label exactly.113 Neither change installs the missing host Chrome or demonstrates proxy-only egress for every browser path.114

what happens next Planned and not started: give the Chrome installer a real caller, or remove it with its test and spec.112

what happens next For the remaining browser egress paths, no fix is planned yet.

The anatomy

The region is one Fedora guest — a VM, or a WSL2 distro on Windows — running a rootless Podman stack. Among the members:

  • a forge, the container your coding agent runs in, one per project;
  • a git mirror, a bare repo on the internal network;
  • a vault holding your tokens;
  • a forward proxy, the single outbound door;
  • a router, a Caddy reverse proxy giving web services stable names;
  • an inference container, the shared model server.

The roster is longer — a Nix cache, a catalog service, the observatory's web container, an SSH-lane sidecar — and most of those do not run in an ordinary session.1 Membership is not a hand-kept list: the spec names the attach sites by function symbol, and a guard compares the attach sites it finds in the source with that list, refusing the build if either side drifts. It reads code, not a live network.132

your computer · your disk · your electricity a clean Linux machine (made for you on Mac/Windows) the enclave — a private network with no way out appapp appagent the one guarded door out — everything else is refused
A region is layers, not a machine. Each ring only trusts what is inside it.

One rule carries the isolation: the containers share a Podman network created with --internal — no gateway, no route out.1 Only the proxy is dual-homed onto an egress network, and the launcher creates both networks on the first launch and reuses them after.131

The router publishes to one loopback address — port 80 by default, with fallbacks, or the port you pass with --port — and maps <service>.<project>.localhost onto internal ports. Nothing binds 0.0.0.0.2116

Starting one is a curl on Linux, an installer script on macOS, a tray executable on Windows. Then tillandsias --headless /path/to/project --claude.

checked Isolation is structural — one --internal network, exactly one dual-homed container — not a firewall ruleset you maintain. The launcher the tray runs passes the flag,131 and the membership guard runs in the build gate.32148

known limitation One thing the spec asks for is missing; another is enforced more narrowly than it reads. The spec requires the network to be removed at application exit when empty, and nothing implements that — the only removal is the destructive reset.133 And the membership guard, which does run, counts only builder and launcher functions by name, so an attach site written any other way is invisible to it.32

what happens next No fix is planned yet.

checked The stack script, the tray launcher, the per-project runner and the proxy diagnostic all create the network with --internal, and the stack script refuses to reuse a network that lacks it.311731120121 A build check finds each tracked shell network-create command and checks it; its Rust check covers less, one file at a time.118

What the walls are made of

Agent tooling exists only inside the forge image, never on your host $PATH; a missing image makes the launcher refuse rather than fall back to a host binary.4122 Mounts are enumerated — your project, a read-only CA certificate, the tmpfs roots, a per-launch temp dir, the persistent tool-cache volumes and a Tillandsias-written read-only gitconfig — so $HOME, ~/.config and your keychain are simply not addressable from inside, and a regression test asserts no user home is ever mounted.5

the usual way this way it breakspoke at it still oddpoke again drifts somewhere nobody can reproduce it breaksthrow it away build from the recipe identical every time, on every machine
Nothing at runtime is repaired. It is thrown away and built again from the recipe — which is why it comes back identical.

The forge does not touch your working tree by default: it clones fresh from the enclave mirror, and pushes travel back through the mirror on their way upstream.123124 An opt-in escape hatch bind-mounts your real checkout read-write and prints a reduced-isolation warning.125 Hence the blunt operating rule: a finding you did not push is a finding you destroyed.7

What survives, exactly

Idempotence is what makes destroy-and-recreate a repair procedure rather than a loss. Every layer comes up clean from scratch, so the boundary worth memorising is not "will it break" but "what is on which side of the wipe":

  • Survives a stop: the bare mirror in its per-project volume, with everything you pushed to it.6
  • Survives a stop: vault data, auto-unsealed from a single Shamir share held in your host keychain, with no passphrase prompt. The spec requires the share to reach the vault only through a tmpfs-mounted secret;8 the shipped guest also keeps a fallback copy of the share on its own disk when no keyring is reachable, and inside a VM guest that file is written on every initialisation.126127 The lifecycle spec says survival of the store requires an unlocking keyring and allows no persisted fallback share, so that code path is ahead of the spec in the other direction: the fallback writer is still present at this release.158
  • Survives even podman system reset, on Linux: the model cache, because it is a host bind mount and not a named volume. The spec spells that distinction out, having once cost someone a wrong answer.9 The spec now moves that directory to ~/.tillandsias/downloads/models, with a one-time migration from the old cache path; the launcher at this release still resolves the model directory under the cache root, so the move is specified and not yet in the code.9159
  • Does not survive --reset-guest: it removes every Tillandsias container, volume (the per-project git mirrors among them) and secret and the enclave and egress networks, then re-initialises. On Linux at this release it keeps the model cache and no longer deletes the Vault store, which is a host directory; whether your sign-ins then survive depends on the keyring still holding the unseal share. Push first.10128129 --reset-state goes further: it also destroys every Podman image, and still keeps the models and the keyring entries.156

checked On Linux the reset boundary is documented per artifact and matches the code for what a reset removes and keeps.128129 The one divergence is the model directory's path, which the spec has moved and the launcher has not.9159

known limitation The spec also promises that your host working copy is fast-forwarded after every successful push. No code implements it at this release: the file the spec names does not exist, and the tray-managed host checkout it fed was removed. After a forge push, your host checkout moves only when you pull.130143

what happens next No fix is planned yet.

known limitation On macOS the "cache survives" line is not yet proven. The VM at this release already boots with a second shared directory for the model cache, and the guest's first boot mounts it, so the durable path exists.131132 What remains open: guests provisioned before that change are not migrated, because the mount is written on first boot only, and survival across a VM rebuild has not been demonstrated end to end.11133 The shipped uninstaller now preserves the VM unless asked to wipe, and asks for a typed confirmation before it removes anything.134135157 A partially restored cache is worse than none — the inference service answers a version check and then fails every request.11

what happens next Planned and not started: prove survival end to end, and migrate the guests provisioned before the shared directory existed. How much a lost cache really costs to re-download has not been measured reliably yet.133136

known limitation On Windows, wiping the guest used to leave the old unseal share in Credential Manager, and the tray pushed that dead key into the fresh guest, permanently breaking GitHub login. The wipe paths now clear the stale share; a host that already holds one still delivers it, and the guest cannot tell the host it was rejected.12137

what happens next The reconcile half is blocked: the delivery reply carries no accept-or-reject signal, so a delivered share that fails to authenticate cannot yet lose to the guest's own secret.138 Until then, remove the stale credential by hand.

known limitation A host's container engine used to carry a global configuration line routing all registry traffic through the enclave proxy unconditionally, so with the stack down podman build and podman pull died on a raw DNS error that names nothing. Initialising a host now removes that line and hands the proxy setting to each container instead; a host never re-initialised still carries it, and a build that needs the absent proxy still fails without naming it.14014114213

what happens next The global-config half is done: re-running initialisation fixes the file.141 The rest is a planned fix nobody has started: a build must either not depend on a service that may be absent, or fail with a verdict that names the proxy.13

How Tillandsias itself is tested

the boundary the specification draws must hold must NOT hold positive cases negative controls
A test that only ever passes proves nothing. Each boundary is pinned from both sides.

The checking the earlier levels describe happens in a local gate, run on the developers' own machines: specs declare intent, litmus tests turn each requirement into an executable check, traces show the check ran, and the gate refuses a push that breaks the chain.

Read a passing gate with care. Eleven litmus files have never been run by any suite; they sit on a list that may only shrink, so that switching the gate on did not turn them all red at once.14

known limitation No server checks a change when it is pushed or proposed. The cloud runs one job, the release build, because the signing keys exist only there; everything else is checked only by the local gate.15

what happens next This is a deliberate choice to save cost, not a gap waiting for a fix.15

How we know — 159 sources

Each small number in the text opens the exact lines of the Tillandsias source that back that sentence, as they stand in release v56.10.9.1, the one the install commands give you. Hover a number to read the quoted lines without leaving the page. A source marked with its own release points at a newer one.

  1. 1Enclave network requirements — `--internal` creation, reuse, cleanup on exit, proxy-only egress, and the symbol-anchored membership list the guard enforces openspec/specs/enclave-network/spec.md#L19-L75A new enclave member MUST be added to this list in the same commit that attaches it; `scripts/lua/check-enclave-membership-documented.lua` refuses the divergence in both directions.
  2. 2Loopback-only publish and the host-port fallback chain openspec/specs/subdomain-routing-via-reverse-proxy/spec.md#L3-L43What did NOT change, and what the requirement is actually protecting, is the loopback-only invariant: the publish is `127.0.0.1:{host_port}:8080` in every branch.
  3. 3The stack orchestration path now creates an internal network scripts/orchestrate-enclave.sh#L90-L103--internal
  4. 4Forge-as-only-runtime: the agent binaries must resolve inside a fresh forge, and the host must not need them openspec/specs/forge-as-only-runtime/spec.md#L48-L68`command -v claude codex opencode bash` MUST print four valid paths from inside a freshly built forge container.
  5. 5Regression test asserting no user home is mounted into a forge, and the mounts it deliberately allows crates/tillandsias-headless/src/main.rs#L24432-L24432must not mount a host .config dir into the forge; got source in: {arg}
  6. 6Bare mirror per project in a named volume; forge pushes persist there openspec/specs/git-mirror-service/spec.md#L19-L39The system SHALL create and maintain a bare mirror repository for each project at `/srv/git/<project>` inside the git service container, backed by the named Podman volume `tillandsias-mirror-<project>`.
  7. 7The ephemerality rule, stated for agents AGENTS.md#L61-L61**IN A FORGE, A FINDING YOU DID NOT PUSH IS A FINDING YOU DESTROYED.**
  8. 8Auto-unseal from the host native keychain, no passphrase prompt, share on tmpfs only openspec/specs/tillandsias-vault/spec.md#L109-L128The unseal secret SHALL be loaded into a podman secret and mounted at `/run/secrets/vault-unseal` on tmpfs only.
  9. 9Model cache is a host bind mount, NOT a named volume — and why that decides what `podman system reset` destroys; the spec now names `~/.tillandsias/downloads/models/` openspec/specs/inference-container/spec.md#L30-L32Models SHALL be stored in a HOST DIRECTORY (a bind mount, NOT a named Podman volume)
  10. 10What `--reset-guest` wipes and what it keeps crates/tillandsias-headless/src/main.rs#L1706-L1706keeps the model cache) and re-initialize.
  11. 11macOS model cache and engine payload inside the VM disk image; the destroyers, the outage a lost engine caused, and the ~2.47 GB figure as filed (planned, not started) plan/index.yaml#L27667-L27667every VM-directory deletion forces a ~2.47 GB re-download — the macOS twin of 518
  12. 12Windows Credential Manager retains a stale unseal share across a guest wipe; the wipe half landed, the reconcile half still open (planned, not started) plan/index.yaml#L26930-L26930every Windows reset path (--reset-guest, installer -Purge, the e2e smoke) wipes the guest vault but never clears vault-shamir-share-v1 from Credential Manager
  13. 13Global container config hardcodes the enclave proxy, so builds fail on DNS when the stack is down; the open deliverable is a verdict naming the proxy (planned, not started) plan/index.yaml#L37615-L37615~/.config/containers/containers.conf hardcodes http_proxy=http://proxy:3128 unconditionally, so when the enclave is down EVERY image build fails on DNS — and with it the release gate
  14. 14The ratchet list of litmus files that have never run, and why they are exempt openspec/litmus-tests/unbound-grandfathered.txt#L1-L25THE ONLY LEGITIMATE EDIT TO THIS FILE IS A DELETION
  15. 15The cloud runs one job, the release; every other check runs locally methodology/ci.yaml#L286-L324GitHub Actions runs EXACTLY ONE workflow: the release. Nothing else may consume cloud minutes — no push CI, no PR CI, no scheduled jobs, no cache warming, no webhooks. Every other gate runs locally.
  16. 16The startup context's infrastructure section: everything transparent, nothing to configure images/default/lib-common.sh#L4978-L5007You never need to configure git remotes, tokens, SSH keys, proxy settings, or CA certs.
  17. 17The heavy base layer: Fedora Minimal with the toolchains in one package set images/default/Containerfile.base#L5-L35rust cargo clippy rustfmt rust-analyzer cargo-deny
  18. 18The thin runtime layer on top of the base images/default/Containerfile#L1-L6Builds upon the heavy base image to inject configuration,
  19. 19Image identity is a content hash of the sources openspec/specs/default-image/spec.md#L100-L104The default forge image SHALL use a content-hash canonical tag derived from the image source set.
  20. 20Harnesses refresh at every launch into the persistent per-project tool cache, and a freshly installed binary that fails its contracts is replaced by the last good one openspec/specs/default-image/spec.md#L196-L237A freshly installed OpenCode that violates any of those contracts SHALL be rejected and replaced by that last-good binary.
  21. 21The tool cache is a podman named volume, not a host bind-mount, so it cannot become a credential-leak path crates/tillandsias-headless/src/main.rs#L17589-L17589A named volume — not a host bind-mount —
  22. 22The three RAM-backed roots the launcher mounts at this release crates/tillandsias-headless/src/main.rs#L18125-L18125.tmpfs("/tmp:size=256m,mode=1777")
  23. 23The on-demand tool allowlist in full: every listed command becomes a PATH shim that installs on first use images/default/brew-tools-allowlist.txt#L1-L122Commands listed here get a PATH shim in the forge: running the command
  24. 24Where the startup context file is written images/default/lib-common.sh#L4679-L4679local ctx_file="$project_dir/.forge-startup-context.md"
  25. 25The hot/cold spec: exactly four RAM-backed roots, the source tree among them openspec/specs/forge-hot-cold-split/spec.md#L11-L24Only these four path roots are HOT. "Maybe a hot path" is a HARD NO.
  26. 26The planned fix for storage: source tmpfs, host RAM gate and memory ceiling still to wire (planned, not started) plan/index.yaml#L8207-L8207production launch runs check_host_ram, derives compute_memory_ceiling_mb, and emits equal --memory and --memory-swap limits
  27. 27The Homebrew prefix is ephemeral; every launch re-installs on-demand tools (planned, not started) plan/index.yaml#L12407-L12407The brew prefix is ephemeral, so every forge launch re-installs and re-attests every tool — that is the rate-limit multiplier
  28. 28The startup context's claim of a Sigstore attestation per bottle images/default/lib-common.sh#L5079-L5084it verifies a Sigstore attestation per bottle
  29. 29What the shim verifies today: integrity from one publisher, attestation off images/default/brew-shim-exec.sh#L12-L21Sigstore verification is OFF because it requires a GitHub credential to FETCH
  30. 30The source tmpfs budget is calculated from the mirror pack size crates/tillandsias-headless/src/main.rs#L17625-L17625format!("/home/forge/src:size={budget}m,mode=0777")
  31. 31The launcher creates the enclave network with `--internal` on first launch, after ensuring the egress network crates/tillandsias-headless/src/main.rs#L3218-L3221command.args([ "network", "create", "--internal",
  32. 32The membership guard (now a Lua decider): every attach site's enclosing function must be in the spec and vice versa; it counts only builder and launcher functions scripts/lua/check-enclave-membership-documented.lua#L60-L81local BUILD_LAUNCH = [[^(build|launch)_]]
  33. 33The only network removal in the launcher: the destructive reset crates/tillandsias-headless/src/main.rs#L10642-L10642command.args(["network", "rm", "-f", &name]);
  34. 34Proxy variables injected into every enclave container from one source crates/tillandsias-headless/src/main.rs#L2015-L2015"http_proxy=http://proxy:3128".into(),
  35. 35The forge composes vendor roots plus the proxy CA into the system-default bundle before any network client starts images/default/lib-common.sh#L12-L23Compose the per-install CA into that target atomically, before any network
  36. 36No per-client CA variables; trust flows through the distribution's standard path openspec/specs/transparent-https-caching/spec.md#L57-L62launchers and entrypoints SHALL NOT select CA files with `GIT_SSL_CAINFO`, `SSL_CERT_FILE`, `REQUESTS_CA_BUNDLE`, or `NODE_EXTRA_CA_CERTS`.
  37. 37A regression test pins the CA mount and the absence of CA overrides crates/tillandsias-headless/src/main.rs#L24542-L24542typed forge launcher must mount the single runtime CA input
  38. 38Package-manager workflows must work with no configuration openspec/specs/proxy-container/spec.md#L157-L157common development workflows (npm install, cargo build, pip install, flutter pub get) work out of the box without configuration.
  39. 39Proxy purpose: allowlisted caching forward proxy, bump only the release-asset CDN, splice all other TLS openspec/specs/proxy-container/spec.md#L8-L14HTTPS interception is deliberately exceptional: the proxy bumps only the exact GitHub release-asset CDN hostname and splices all other TLS traffic end to end.
  40. 40The three ssl_bump rules: peek once, bump one exact host, splice everything else images/proxy/squid.conf#L133-L139ssl_bump peek ssl_bump_step1 ssl_bump bump github_release_assets ssl_bump splice all
  41. 41Spliced HTTPS is never cached by the proxy openspec/specs/proxy-container/spec.md#L44-L49the HTTPS response SHALL NOT be cached (tunneled traffic is opaque to squid)
  42. 42cargo, Go, npm and pip caches all redirected into the per-project cache images/default/lib-common.sh#L2227-L2259export CARGO_HOME="$PROJECT_CACHE/cargo"
  43. 43Blocked hosts get a TCP reset, not a 403 images/proxy/squid.conf#L151-L156deny_info TCP_RESET strict_deny_acl
  44. 44The live miss-then-hit trace for the bumped host was never recorded, and the next action that would record it (planned, not started) plan/index.yaml#L7533-L7576no rebuilt-image parser result or real identical-key MISS-to-HIT trace is claimed
  45. 45The consumer contract: one endpoint at inference:11434, downloads through the proxy openspec/specs/inference-container/spec.md#L13-L14Forge containers SHALL access it via `OLLAMA_HOST=http://inference:11434`. The inference container SHALL use the proxy for model downloads.
  46. 46Inference readiness is best-effort, never a launch gate crates/tillandsias-headless/src/main.rs#L17226-L17226Local inference readiness (order 392) is BEST-EFFORT, not a launch
  47. 47The single default chat model pulled on first run images/inference/entrypoint.sh#L120-L120DEFAULT_MODELS="${TILLANDSIAS_DEFAULT_MODELS:-qwen2.5:0.5b}"
  48. 48The engine download pins a version and architecture-specific digests images/inference/entrypoint.sh#L329-L335OLLAMA_VERSION="v0.34.0"
  49. 49The closed-vocabulary accelerator line every forge receives crates/tillandsias-headless/src/accel_probe.rs#L4402-L4402accel_class=<workstation-gpu|mobile-npu|hybrid-gpu-npu|cpu-only>
  50. 50Ready means the endpoint answers and at least one model is cached; there is no "starting up" images/default/lib-inference-state.sh#L14-L24Reason vocabulary (CLOSED SET — there is deliberately no "starting up"):
  51. 51Larger tier models are opt-in, not default images/inference/entrypoint.sh#L795-L798tier pulls opt-in (set TILLANDSIAS_INFERENCE_TIER_PULLS=1) — skipping runtime tier pulls
  52. 52The CPU floor is unconditional by spec openspec/specs/inference-container/spec.md#L154-L159Tier-S SHALL be available on every host regardless of GPU, NPU, driver, or engine availability, and SHALL NOT be gated on any device probe.
  53. 53A missing verifier or wrong engine digest prevents execution images/inference/entrypoint.sh#L371-L383FATAL: sha256sum unavailable — cannot verify the engine payload, refusing to execute it
  54. 54NVIDIA delivery is gated on a CDI spec; without one the host degrades to CPU with a loud remedy crates/tillandsias-headless/src/main.rs#L6059-L6059a GPU host without CDI degrades to CPU with a LOUD remedy
  55. 55The AMD lane is selected only when ROCm reports a graphics agent crates/tillandsias-headless/src/main.rs#L5171-L5171let rocm = std::process::Command::new("rocminfo")
  56. 56The spec: NPU rows are never attempted on the shipped engine openspec/specs/inference-container/spec.md#L186-L193N rows SHALL NOT be attempted on the `ollama` engine kind. Ollama cannot use any NPU
  57. 57On Windows the NPU cannot be seen from inside the guest at all scripts/windows-host-capability-probe.sh#L12-L14The NPU is not merely undetected in the guest, it is STRUCTURALLY invisible:
  58. 58The health check tests that the runner binary exists; the real load check runs once at startup images/inference/Containerfile#L112-L121A real generate would be the strongest assertion but is far too expensive at a
  59. 59NPU passthrough for the container is planned (planned, not started) plan/index.yaml#L9263-L9263Impl: NPU /dev/accel passthrough plumbing for the inference container (AMD XDNA2 first lane)
  60. 60The host-native sidecar for Metal and the NPUs is planned (planned, not started) plan/index.yaml#L9419-L9419Impl: host-native inference sidecar registry (macOS Metal/MLX, AMD XDNA2 flm/Lemonade, Intel NPU OpenVINO) behind the enclave proxy
  61. 61Agents configure nothing and still see the original GitHub URL openspec/specs/git-mirror-service/spec.md#L470-L476`git push`, `git fetch`, and `git clone` SHALL work with zero agent-side configuration
  62. 62One atomic push of explicit refspecs images/git/relay-refs.sh#L298-L298git push --atomic "$PUSH_URL" "$@"
  63. 63The mirror acknowledges only after upstream durably accepts the ref transaction images/git/pre-receive-hook.sh#L765-L765Push rejected: configured upstream did not durably accept the ref transaction
  64. 64The GitHub token is read from Vault at push time inside the git service and never crosses into a forge openspec/specs/git-mirror-service/spec.md#L14-L16The git service reads the GitHub token from Vault at push time via Vault CLI; the token never crosses into a forge container.
  65. 65The credential helper fetches the token from Vault over git's credential protocol images/git/git-credential-tillandsias.sh#L93TOKEN="$(vault-cli read -field=token secret/github/token 2>/dev/null || true)"
  66. 66Every start re-applies receive hardening: no deletes, no rewinds, object checks openspec/specs/git-mirror-service/spec.md#L98-L106the git service SHALL set `receive.denyNonFastForwards=true`, `receive.denyDeletes=true`, and `receive.fsckObjects=true`.
  67. 67A project with no upstream keeps pushes durably in the local-only mirror images/git/relay-refs.sh#L157-L157No upstream configured; accepting as a durable local-only mirror update
  68. 68Without a stored token the relay refuses and names the remedy images/git/relay-refs.sh#L193-L193HTTPS upstream credential is unavailable; run GitHub Login before pushing
  69. 69The mirror daemon's push path is anonymous inside the enclave, and the repo says so in the same breath as the hardening that limits the damage images/git/entrypoint.sh#L504-L504The daemon remains anonymous inside the enclave: any reachable peer can still
  70. 70The authenticated SSH push lane is dark behind one flag until the default flip crates/tillandsias-headless/src/main.rs#L13186-L13186std::env::var("TILLANDSIAS_MIRROR_SSHD")
  71. 71Offline or credential-less, the forge's push returns non-zero and nothing is partially applied openspec/specs/git-mirror-service/spec.md#L224-L230the forge's `git push` SHALL return non-zero
  72. 72The SSH-lane default flip is planned (planned, not started) plan/index.yaml#L20538-L20538T11+T12 — flip one lane behind a flag once the §4a matrix is green
  73. 74Vault is the default and only Linux secrets backend; GitHub token and short-lived per-container tokens openspec/specs/tillandsias-vault/spec.md#L11-L14Run a HashiCorp Vault container as the default and ONLY Linux secrets backend for Tillandsias.
  74. 75The shipped Vault listener is TLS-only images/vault/vault.hcl#L16-L24tls_cert_file = "/run/secrets/tillandsias-vault-tls-cert"
  75. 76Every OAuth-credentialed agent lane mounts one scoped Vault token crates/tillandsias-headless/src/main.rs#L18186-L18186Every OAuth-credentialed agent lane mounts a scoped Vault token so its
  76. 77Forge containers get no broad Vault token, only a provider-scoped short-lived one openspec/specs/tillandsias-vault/spec.md#L348-L353Forge containers SHALL NOT receive a broad Vault token.
  77. 78The generic forge policy cannot read the GitHub token images/vault/policies/forge.hcl#L1-L8Explicitly NO github or token access — forge containers must remain
  78. 79The tray policy reads the whole secret tree images/vault/policies/tray.hcl#L1-L6path "secret/*" {
  79. 80The startup context advertises http for a TLS-only Vault images/default/lib-common.sh#L5005-L5005http://vault:8200
  80. 81The in-image helper defaults to https and the tmpfs token path images/default/vault-cli.sh#L23-L24VAULT_ADDR="${VAULT_ADDR:-https://vault:8200}"
  81. 82Client tokens default to a one-hour TTL openspec/specs/tillandsias-vault/spec.md#L252-L256Every client token, including tokens minted by Vault Agent, SHALL default to TTL 1h with a maximum TTL of 24h.
  82. 83Four MCP servers registered for Claude Code inside the forge image images/default/config-overlay/claude/mcp.json#L2-L23"command": "/home/forge/.config-overlay/mcp/forge-plan.sh"
  83. 84The same four servers registered for OpenCode images/default/config-overlay/opencode/config.json#L66-L87"command": ["/home/forge/.config-overlay/mcp/forge-plan.sh"],
  84. 85Spec: the generic project expert bootstraps for any project with no registration or configuration openspec/specs/forge-environment-discoverability/spec.md#L171-L173The forge MUST bootstrap a project expert surface for an ARBITRARY mounted project without manual registration, configuration, or repository-type knowledge from the caller.
  85. 86The plan expert's refusal rule: zero citations means unsupported images/default/config-overlay/mcp/forge-plan.sh#L1355An answer with zero citations is returned as confidence=unsupported — the expert refuses rather than guesses.
  86. 87The plan expert is degraded by design on a project without the plan crate images/default/lib-common.sh#L4996-L4996(this project has no plan expert — expected off-tillandsias)
  87. 88Discovery, expert build and the grounded endpoint all backgrounded and fail-soft after the clone images/default/lib-common.sh#L1652-L1658ensure_forge_experts >>/tmp/forge-lifecycle.log 2>&1 || true
  88. 89The on-demand spec obliges the tray to detect and spawn servers openspec/specs/mcp-on-demand/spec.md#L32-L37the tray MUST detect that the filesystem MCP server is not running
  89. 90The startup context every lane reads names Local Experts mode images/default/lib-common.sh#L5015-L5015Local Experts mode (the
  90. 91Only the OpenCode configuration carries an agent bound to the grounded endpoint images/default/config-overlay/opencode/config.json#L18-L30"model": "tillandsias-experts/all",
  91. 92The pre-expert-binary trap: criterion 2 closed, the fresh-forge criterion still open (planned, not started) plan/index.yaml#L6796-L6873criterion 1 (release/branch decision) still OPEN — packet intentionally stays `ready` (partial reduction)
  92. 93The catalog is an allowlist: WEB only, refused host-side otherwise openspec/specs/enclave-service-catalog/spec.md#L22-L26Requests outside the catalog are refused host-side; the guest cannot mint categories.
  93. 94The web image: busybox httpd on 8080, document root /var/www openspec/specs/web-image/spec.md#L24-L25The image MUST serve static files from `/var/www` on port 8080 using busybox httpd with no additional packages or configuration.
  94. 95One socket per lane, bind-mounted into the forge; attribution comes from the listener that accepted the connection, never from the request openspec/specs/mcp-tool-socket/spec.md#L31-L64The project (and instance) a request acts on SHALL be derived from WHICH LISTENER accepted the connection. The tray SHALL NOT read the project from the request body, from the peer process's environment, or from any other peer-supplied source.
  95. 96The live publish path hands back an https URL for the www name on the router's TLS port crates/tillandsias-headless/src/local_web_preview.rs#L730-L730Ok(format!("https://www.{project}.localhost:{}", tls_port()?))
  96. 97From inside the forge, .localhost requests are forwarded by the proxy to the router images/proxy/squid.conf#L183-L188cache_peer_access tillandsias-router allow localhost_subdomain
  97. 98What could be read through the router when the earlier path served the repository root plan/index.yaml#L44421-L44421/.git/config returned 200 through the router (full history + remote URLs reconstructible)
  98. 99The stack sweep's name patterns: a published sibling matches none of them crates/tillandsias-headless/src/main.rs#L7698-L7698name.starts_with("tillandsias-git-")
  99. 100Projects must live under ~/src on the host; discovery hardcodes it (planned, not started) plan/index.yaml#L42106-L42106tray discover_projects hardcodes ~/src and run_opencode_mode starts no lane listener
  100. 101The declarative image set initialisation builds: both Chromium images and the web image among them crates/tillandsias-headless/src/main.rs#L10190-L10192"forge-base", "forge", "web",
  101. 102An ordinary forge launch ensures four images, not the web or browser ones crates/tillandsias-headless/src/main.rs#L17144-L17144let images = ["router", "git", "inference", "forge"];
  102. 103The phantom pull the status-check path was fixed for crates/tillandsias-headless/src/main.rs#L11005-L11005phantom registry pull (125) on any version handover.
  103. 104The end-to-end publish litmus has never been run by any suite openspec/litmus-tests/unbound-grandfathered.txt#L21-L21litmus:publish-local-e2e
  104. 105The headless core image and its entrypoint images/chromium/Containerfile.core#L36-L37ENTRYPOINT ["/usr/lib64/chromium-browser/headless_shell", "--headless=new"]
  105. 106The framework image extends the core with GUI Chromium, Node and Playwright images/chromium/Containerfile.framework#L15-L35RUN npm install -g --prefix=/usr \ playwright \
  106. 107The hardened, ephemeral browser container the runtime launches crates/tillandsias-headless/src/main.rs#L15944-L15944Hardened browser boundary: read-only rootfs, CAP_DROP=ALL, no-new-privileges,
  107. 108Browser launch requires a graphical session crates/tillandsias-headless/src/main.rs#L15902-L15902OpenCode Web browser launch requires a graphical session (DISPLAY or WAYLAND_DISPLAY)
  108. 109The browser tool spawns a host Chromium from the cache directory, not a container crates/tillandsias-browser-mcp/src/launcher.rs#L78-L82root.join("current/chrome"),
  109. 110A missing bundled Chromium returns BROWSER_UNAVAILABLE crates/tillandsias-browser-mcp/src/server.rs#L431-L435BROWSER_UNAVAILABLE: bundled chromium not yet downloaded
  110. 111The only Chromium installer has no consumer scripts/install-chromium.sh#L10-L16INTEGRATION STATUS (2026-08-16 freshness audit, 774-cfw8): NO consumer
  111. 112Give the unused Chrome installer a caller, or remove it (planned, not started) plan/index.yaml#L22074-L22074scripts/install-chromium.sh has ZERO consumers while its header claimed two integrations; a litmus pins a shape nothing ships — wire it or tombstone it
  112. 113The allowlist accepts dotted project labels and compares them exactly crates/tillandsias-browser-mcp/src/allowlist.rs#L250-L257let host_project_label = labels[1..labels.len() - 1].join(".");
  113. 114Safe variant: proxy-only egress is the spec openspec/specs/chromium-safe-variant/spec.md#L16-L19Full network isolation inside the enclave, with allowlist enforcement via the proxy only; no host-gateway internet fallback
  114. 115The browser script defaults to the enclave network scripts/launch-chromium.sh#L93-L98"--network=${TILLANDSIAS_BROWSER_NETWORK:-${TILLANDSIAS_ENCLAVE_NET:-tillandsias-enclave}}"
  115. 116The router's host-port candidates: an explicit --port first, then 80 and the fallbacks crates/tillandsias-headless/src/main.rs#L6915-L6915let mut candidates = vec![80, 8080, 18080, 28080, 38080, 48080, 58080];
  116. 117Stack orchestration script now passes `--internal` and refuses to reuse an unisolated network scripts/orchestrate-enclave.sh#L82-L126Network $ENCLAVE_NET EXISTS BUT IS NOT INTERNAL — it was created without --internal, so every member has NAT egress and the proxy is not the only way out (order 972-a8vh, spec:enclave-network).
  117. 118The gate discovers tracked shell launchers and names its Rust limit scripts/check-enclave-network-internal.sh#L113-L135The shell side IS invocation-scoped: continuations are folded first, and the flag must appear on the same logical line as the create verb.
  118. 120The per-project runner creates an internal network scripts/run-forge-project.sh#L127-L130"$PODMAN_CTL" network create --driver bridge --internal --subnet "$ENCLAVE_SUBNET" "$ENCLAVE_NET" >/dev/null
  119. 121The proxy diagnostic creates an internal network scripts/diagnose-proxy.sh#L94-L97podman network create --driver bridge --internal --subnet "10.0.42.0/24" "$ENCLAVE_NET"
  120. 122A missing forge image makes the launcher refuse, never fall back to a host binary openspec/specs/forge-as-only-runtime/spec.md#L114-L115The tray MUST refuse to launch an agent if the forge image is missing — it MUST NOT silently fall back to a host binary.
  121. 123Clone-only by default: the host checkout is the opt-in path crates/tillandsias-headless/src/main.rs#L18039-L18039Order 437: clone-only by default.
  122. 124Forge containers clone from the mirror and push through it openspec/specs/git-mirror-service/spec.md#L54-L61Forge containers SHALL clone from `git://git-service/<project>`
  123. 125The reduced-isolation warning the host-mount escape hatch prints crates/tillandsias-headless/src/main.rs#L7435-L7435WARNING: WORKSPACE ENCLAVE ISOLATION IS REDUCED
  124. 126Inside a VM guest the share is written to a fallback file on every initialisation crates/tillandsias-headless/src/vault_bootstrap.rs#L2851-L2851Inside the VM there is no OS keychain, so these files are the only durable
  125. 127Where no OS keyring is reachable the share falls back to a file in the cache directory crates/tillandsias-headless/src/vault_bootstrap.rs#L4907-L4907using fallback file (expected in VM guest and headless environments)
  126. 128What the reset wipes on the host side: nothing; the function that used to name the vault data directory returns an empty list crates/tillandsias-headless/src/main.rs#L10397-L10399fn reset_guest_wipe_paths(_cache_dir: &Path) -> Vec<PathBuf> {
  127. 129The pin test: the model cache must never be in the reset wipe set crates/tillandsias-headless/src/main.rs#L33303-L33303the inference model cache must never be in the reset wipe set
  128. 130The host working-copy fast-forward is specified, and specified against a file that does not exist openspec/specs/git-mirror-service/spec.md#L344-L363The tray SHALL trigger a fast-forward attempt on the host working copy at `<watch_path>/<project>` for every successful push to the enclave bare mirror
  129. 131The VM boots with a second shared directory for the model cache crates/tillandsias-vm-layer/src/vz.rs#L2907-L2907tag: "model-cache".to_string(),
  130. 132The guest's first boot persists the model-cache mount crates/tillandsias-vm-layer/src/vz.rs#L1078-L1078model-cache /root/.cache/tillandsias/models virtiofs nofail 0 0
  131. 133The shared directory exists; migration and end-to-end survival are unproven plan/index.yaml#L27804-L27804guest wiring verified by source scan, MIGRATION UNVERIFIED.
  132. 134The shipped uninstaller preserves the VM unless asked to wipe scripts/uninstall.sh#L228-L228Preserving the VM image in $DATA_DIR (use --wipe to remove it).
  133. 135The uninstaller fix the macOS cache work waited on plan/archive/packets-2026-08.yaml#L23430-L23436shipped uninstall.sh deletes the 11.83 GiB VM directory with no --wipe, no root and no prompt
  134. 136The cold measurement and its limit for a populated model cache plan/index.yaml#L27838-L27838Re-fetched from the network: the Fedora Cloud qcow2 only, 514 MB.
  135. 137The wipe half landed; a host already holding a stale share is unaffected plan/index.yaml#L26924-L26924a host that ALREADY holds a stale credential is unaffected
  136. 138The delivery reply carries no accept-or-reject signal (planned, not started) plan/index.yaml#L37937-L37937DeliverCredentials reply says "I received it", never "I accepted it"
  137. 140Initialisation removes the orphaned global proxy block crates/tillandsias-headless/src/main.rs#L9222-L9222Remove the orphaned `[engine] env` proxy block.
  138. 141What initialisation prints when it converges the file crates/tillandsias-headless/src/main.rs#L9383-L9383removed the orphaned [engine] env proxy block from {} (923-rmtw); containers receive proxy env per-container
  139. 142The global-config fix, archived as completed plan/archive/packets-2026-08.yaml#L46813-L46818containers-conf-env-line-is-orphaned-and-never-converges
  140. 143The decision that removed the tray-managed host checkout plan/index.yaml#L16185-L16185It is REMOVED ENTIRELY.
  141. 145Fetch and pin the attestation bundles at image build time so no lane needs a GitHub identity (planned, not started) plan/index.yaml#L12320-L12320a forge lane installs an allowlisted tool with attestation verified and NO GitHub credential present anywhere in the lane
  142. 146The shared inference container is created only when it is not already running, so sibling forges share one crates/tillandsias-headless/src/main.rs#L17277-L17278inference is recreate-if-not-running (a --replace would drop loaded models and interrupt a sibling's inference mid-flight).
  143. 147The test pinning the scoped lease for every credentialed lane, and its absence from the credential-free ones crates/tillandsias-headless/src/main.rs#L29042-L29042Credential-free lanes never mount a provider lease.
  144. 148The check gate invokes the membership guard and fails the build when it refuses build.sh#L2922-L2922if ! _run_lua_decider "scripts/lua/check-enclave-membership-documented.lua" 2>&1; then
  145. 149Only the clone-only lane mounts the RAM-backed source volume; the host-mount arm binds the checkout instead crates/tillandsias-headless/src/main.rs#L18051-L18062forge_ram_workspace_volume(project_name),
  146. 150The context distinguishes waiting, relaunching and relaunch regression images/default/lib-common.sh#L4994-L5003\`relaunch-regresses\` means the running binary has MORE than the checkout and a relaunch would REMOVE capability.
  147. 151Ready inference alone does not provide the project index with a synthesis tier images/default/config-overlay/mcp/project-info.sh#L789-L789no synthesis tier is wired into project_answer yet
  148. 152The upstream token is read by the git service at push time and never enters a workspace container openspec/specs/git-mirror-service/spec.md#L15-L16The git service reads the GitHub token from Vault at push time via Vault CLI; the token never crosses into a forge container.
  149. 153The relay is synchronous: local acceptance follows upstream acceptance images/git/pre-receive-hook.sh#L6-L8Validates ledger YAML, then synchronously relays the proposed ref transaction upstream before accepting it locally. A client success therefore means the configured upstream has durably accepted the same atomic ref set.
  150. 154The live relay pushes the refs as one atomic transaction images/git/relay-refs.sh#L298-L298if OUTPUT="$(GIT_TERMINAL_PROMPT=0 git push --atomic "$PUSH_URL" "$@" 2>&1)"; then
  151. 156What a soft reset destroys and what it keeps crates/tillandsias-headless/src/main.rs#L10557-L10567<cache>/tillandsias/models and every other download in the cache
  152. 157The uninstaller asks for a typed confirmation before removing anything scripts/uninstall.sh#L184-L184Type \"delete\" to uninstall, or press Enter to cancel:
  153. 158The lifecycle spec's amendment: store survival requires an unlocking keyring, no persisted fallback share openspec/specs/host-state-lifecycle/spec.md#L13-L18survival of the Vault store REQUIRES an unlocking keyring — no persisted fallback share
  154. 159The launcher resolves the model directory under the cache root crates/tillandsias-headless/src/main.rs#L5954-L5957&tillandsias_core::cache_root::cache_root().join("models"),
  155. 160The preview never mounts .git, .env or the forge root; it selects a document tree or named assets crates/tillandsias-headless/src/local_web_preview.rs#L544-L551Static compatibility selects a public document tree, never mounts .git/.env/the forge root into a sibling.
  156. 161A departed lane's preview and route are removed crates/tillandsias-headless/src/local_web_preview.rs#L1289-L1291Called only after the launcher proves no forge remains for this lane.
  157. 162The preview spec's own status openspec/specs/local-web-preview/spec.md#L7-L10verification: S0 (design; executable coverage and live evidence pending)
  158. 163The plan item to prove the preview live and activate the scoped specs plan/index.d/20261007-local-web-preview-design-yoga.yaml#L79-L79title: Prove live tillandsias.org preview and activate scoped specs
  159. 164Publishing refuses when the managed runtime image is absent crates/tillandsias-headless/src/local_web_preview.rs#L1038-L1041initialize the managed runtime image before publication

The architecture interrogated rather than described — boundaries, egress, provenance, and what the tests do not actually test.Assumes the three levels before it.

Tillandsias, for an IT / security reader

Silvery leaf scales open to take water in, then trap air to keep it: every exchange across one surface.

Where the boundary actually is

The anatomy does not answer the security question: when an agent does something you did not intend, how far does it get?

First, a correction. The region was described as one Fedora guest — a VM, or WSL2 on Windows. True on two platforms, false on the third: Linux provisions no VM, and the orchestrator drives rootless containers directly on your host.1 The promise is that this costs nothing but Podman; the price is that the enclave is a hypervisor boundary on macOS and Windows and a namespace boundary on Linux — where an escape lands as your own uid, with your $HOME.2

Inside, every launch is meant to carry four flags — userns mapped to your uid, all capabilities dropped, no-new-privileges, and label-disable, which is to say SELinux labelling off on every container — audited by a policy module that rejects privileged mode, a non-identity userns, or a blanket capability add.498 Why this envelope: keep-id makes an escape land as you rather than root, and a checker that never runs Podman is one small place to ask whether a launch still carries it. The compromise is the fourth flag, bought so bind mounts work on an SELinux-enforcing host.9294 Read the envelope as defaults on a struct, not an invariant — and not quite every container: the vault adds one capability back, omits --rm, and never passes the checker at all.53

checked The threat model is written down, including the parts that make the design look bad — the architecture notes state plainly that the allowlist is generous by design and that an attacker can encode data in DNS queries or headers to an allowed domain.3

The litmus now derives its launch flags from the product's declared envelope.58 Its repaired user-namespace probe checks ownership of a host-owned mounted file, because inspecting the namespace label or comparing the process uid alone could give a misleading answer.100 That is evidence about the declared envelope and Podman on the tested host, not proof that every production launch uses it.

checked Production hardening checks now survive release compilation: the checked serializer returns an error for a stripped envelope, and both attached and delegated forge launch paths refuse it.6555657 The general capability-add escape hatch has also been removed from the typed container builder.27 Before this release the check was only a debug assertion, and the security litmus printed success on both branches.7

known limitation Enforcement is still incomplete across launch paths: proxy, mirror, observatorium and a host-browser launch assemble or serialize arguments without the policy check, and the Podman client accepts raw arguments.6490959697 The typed builder retains private boolean fields, although its public constructor turns all four on and exposes no setters to turn them off.63 A separate test defect remains: the capability probe treats empty output as success without checking whether the inspection ran, so a failed podman exec can still print CAPABILITIES_DROPPED.101

what happens next The checked serializer and forge callers are the enforcement mechanism that exists; extending it to every launch is not implemented.5565 The project's build rejects a litmus test whose success and failure print the same thing,6061 but that check does not catch the empty-output failure above,62 and the project's other test-quality checks are partial too.854

does not hold "The litmus proves every launch is hardened." It launches a probe from declared flags, leaving production call-site coverage to other checks; even its cleanup step reports success unconditionally.5859

known limitation SELinux confinement is disabled on every forge, so on a Fedora or RHEL host the agent's container is less confined than a default one. The project does confine elsewhere — the vault carries a loadable policy module,50 the Windows guest embeds policies for the control daemon and the vault51 — but on a rootless Linux host the vault too runs unconfined.52

what happens next MCS labelling of the forge is recorded as a planned later phase — intent, with no implementation and no litmus behind it.5 For the missing hypervisor on Linux no design tradeoff is recorded, only the blast radius of an escape — your uid, your $HOME2 — mitigated by defence in depth rather than a stronger boundary.

not yet shown Defence in depth is argued layer by layer below; nothing demonstrates that the layers compose to bound an escape.

Egress: intercepted versus passed through

The intended egress boundary is network placement plus proxy environment variables. The proxy configuration supplies hostname rules; it does not establish that the host or container networking stack uses no packet filters. An agent can unset HTTP_PROXY; what stops it is that the namespace has nowhere to send the packet. Then the question everyone asks on hearing "trusted CA in the container":

the boundary the specification draws must hold must NOT hold positive cases negative controls
A test that only ever passes proves nothing. Each boundary is pinned from both sides.

checked This is not a blanket MITM. The proxy peeks at the handshake and then splices — passes through undecrypted — everything but one exact hostname, GitHub's release-asset CDN, bumped so large binaries can be cached. Registries, GitHub's APIs and every auth endpoint stay end-to-end encrypted.9

shown The policy is three lines of a config file you can read, and the bump is scoped to one client-requested server name.9

The cost of that restraint: with one host decrypted the proxy does no payload inspection — hostnames, not content — and its default-deny allowlist admits anyone-can-publish namespaces, so an allowlist hit is no evidence of a benign destination.330 What it buys is a denial that looks like a network fault: a refused runtime request gets a TCP reset, not a page to negotiate with — the config's behaviour; the spec still promises a 403.3738

checked Where it does terminate TLS it still verifies upstream against the system trust store, and the config carries a written rule never to disable peer or domain verification — that would hide a real origin MITM behind a proxy-issued certificate.10

checked The permissive port now binds to loopback inside the proxy container, so an enclave peer cannot reach it by the proxy's network address.31 Its access rules remain permissive for a process inside that container,3233 and the decision to use it for builds or remove it remains open.3536

checked The stable release now puts the CA bundle under per-user persistent state instead of /tmp, so reboot cleanup no longer removes its declared location.284391 On Unix, new key material is clamped to owner-only before publication, and the existing-key path attempts the same repair; the non-Unix helper is a documented no-op.394041 This addresses the code side of a defect whose earlier fix had not reached material already created on hosts.11

known limitation Moving the directory does not make its permissions private by construction: the launcher still creates it with ordinary recursive directory creation.102

what happens next The planned fix is to create the directory privately before any key is written.45 It has not landed.

known limitation The published proxy spec describes something the code does not do: a fresh two-level EC P-256 chain generated per launch, held only on tmpfs, no CA key ever touching disk.12 The shipped launcher generates one self-signed RSA-2048 certificate with a 30-day life, stored in persistent per-user state and reused until refresh is needed.1328102

what happens next No fix is planned yet: the spec has not been marked stale, and nothing reconciles it with the code.

Secrets, and the channel that guards them

The GitHub token never enters the agent container: the mirror holds short-lived role credentials, relays pushes on the agent's behalf, and passes the credential on stdin rather than argv, closing a process-listing leak. The relay can invoke its helper for an HTTPS origin,48 but the helper now checks the credential request's protocol and exact host before returning a token.49

checked The mirror holds the upstream credential and mediates pushes; the agent-facing write path is separate from that credential-bearing relay.144849

does not hold "Exfiltrating a token requires compromising both the mirror and the proxy." The architecture says this,3 but the proxy forwards permitted destinations even though its permissive port now binds to loopback.3031 A compromised mirror could use an allowed destination without compromising the proxy process. Network placement restricts routes; it does not prove that an allowed request cannot carry a secret.

known limitation The write path the agent pushes to is an anonymous git daemon receive-pack listener. The spec is blunt: network placement SHALL NOT be described as client authentication — any process on the enclave network can write to any mirror.14

Why the relay is bespoke, in security terms. The design needs the upstream credential to stay server-side and the push to be synchronously durable, and those two requirements pull against the available tools. A managed push mirror holds the credential and copies asynchronously, so it reports success before the copy lands; a caching or redirecting git proxy relays synchronously but forwards the client's credentials, dissolving the separation this section is about. The hook takes the third option: it relays the proposed ref transaction upstream before accepting it locally, so a client's success means the upstream durably accepted the same atomic ref set.107

That is a security property and not only a convenience. A false success is indistinguishable, to the agent that received it, from work that is safely published — so an asynchronous mirror would put unpublished work behind a green light, on exactly the disposable machine the design expects to be thrown away.

what happens next The spec labels it interim and constrains what may be built on it, but records no dated authenticated replacement.14 That replacement is built and dark: an SSH-CA push lane exists in the mirror image and the launcher, behind an environment variable that defaults off and that nothing in shipped packaging sets.4647 Separately, the mirror's push hook can now refuse a push to the default branch, or to a new branch outside a ref grammar, when a project's branch-discipline seed says to enforce it; that is policy over anonymous writers, not authentication of them, and the seed is read from the repository's own integration branch.117

The host↔guest control channel now uses a Noise handshake by default. Its key is derived through HKDF from the guest binary's SHA-256, separated by build version, wire version and hop.15103 This binds compatibility to known release material; it is not remote attestation that the peer runs unmodified code.

note This says what the relay refuses to report, not that the credential

boundary is complete. The RED above about the anonymous receive-pack listener is

the live gap on the agent-facing side of the same path, and it is not closed by

anything in this paragraph.

A release exposed a second boundary: the tray and guest are different executables, so hashing each process's own binary produced different keys even at the same version. The host now derives its key from the guest digest embedded at build time, while the guest hashes itself.103104 The Windows caller refuses a missing embedded digest and includes tests for matching and mismatched guest digests.105106 A one-process round-trip test could never have detected the original two-binary mismatch; that limitation is now recorded beside the implementation.104

checked An absent secure-wire setting now selects encryption, blank or invalid values are refused, and the Windows tray uses the same parser as the other callers.16666769 A build check refuses any caller that reads the setting on its own.68 The macOS host writes the resulting value into the guest unit.93 Windows and the VM image need no explicit environment line to obtain the new default from the listener.7071

known limitation Encryption is on by default, but caller identity remains weaker than operator authentication: anyone with the published guest bytes and version inputs can derive the same key. The guest's debug path instead uses a public constant.1529103 Explicit off still enables plaintext.16 Neither possession of the digest nor a completed handshake proves the caller is the legitimate orchestrator.

what happens next The maturity ladder still records deletion of the insecure path as a later stage.17 Mixing in a per-boot host secret is approved and deferred; the present derivation does not include it.18103

Supply chain and provenance

checked Release artifacts are signed with Sigstore cosign keyless — no long-lived key, identity asserted by the CI OIDC token, inclusion recorded in a public transparency log — and the Linux and Windows asset sets are checked for transitive coverage as a property over whatever is staged, not over a remembered list.197274

known limitation The signature proves less than it looks like. Verification matches an identity regexp pinning only the repository name — not the owning organization, the workflow, or the ref — so a fork of the same name signing through the same CI provider satisfies it, in the published command and in the shipped verifier alike.1978 Beyond that there is no SBOM, no build-provenance attestation, no SLSA or in-toto predicate, no container-image signing; base images are pinned by mutable tag rather than digest, so the image you build tomorrow is not the one you built today.20

what happens next Recorded only as research — a study of another distribution channel shows such provenance verifies with no verifier identity at all, and recommends shipping bundles plus a pinned root.2180 Nothing has been adopted here.

known limitation The macOS lane is the exception: it signs an allow-list of three named assets and runs no integrity check, so its checksum manifest ships without a bundle73 — the allow-list shape the Windows lane's own comment blames for an installer once shipping bare.74

what happens next No fix is planned yet.

known limitation Nothing a user runs by default checks a signature; only the hand-run verifier does. The install scripts fetch the checksum manifest over the same channel as the binary and on Linux continue without it;757677 no install path verifies a cosign bundle, and the updater's spec asks for no verification.79 The Windows installer verifies a SHA-256 taken from the manifest it fetched beside the zip and nothing else.76

what happens next An open defect asks the Windows installer to verify the manifest's cosign bundle before trusting it and to refuse a bad signature; it is not fixed in this release.115 Another asks that the install entry point either verify its own bundle or state in writing that transport trust is the anchor.116 No fix is planned for the updater.

Where ephemerality stops being a control

Ephemerality resets compute, not identity. Of what survives teardown, the security-relevant item is the Vault unseal share, which lives in the host OS keychain — outside every boundary the enclave draws.22 The reason is fair — no passphrase prompt, never wipe a vault the host can still open — but destroying the region does not destroy the ability to open what it held.

known limitation The application's Linux --reset-state no longer clears host-held Vault credentials. It asks the keyring first, announces what it keeps, and leaves the Vault store and both keyring entries in place under every outcome.81111 That is deliberate: a reset renews compute and keeps identity; the only code that deletes the unseal material is a clearer reserved for an uninstall command that no code calls yet.110 A bare podman system reset still clears only Podman state. We have not re-checked the macOS and Windows resets for this release.

what happens next The clearer's own documentation names the uninstall command as its caller, and that command is not in the binary; until it lands, nothing shipped removes the share on request.110

known limitation The keychain is not the only copy. The vault spec now says the share lives only in an unlocking keyring and that there is no persisted fallback share file,109 yet where no OS keyring is available the launcher still writes the share to a plaintext file in its cache directory,83 and inside a VM guest it writes that file on initialization.84 Only a hand-run clean-room script clears those fallback files;23 the ordinary key lookup can still use a persistent file8586 while the spec says persistent on-disk copies are deleted immediately.87

what happens next Replace the persistent fallback with the ephemeral handoff the spec now describes, or revise the spec to state the actual lifetime; no completed remedy for ordinary operation is recorded here.

Blast radius, autonomy, auditability

An agent's reach: its forge, every mirror on the enclave network, any allowlisted hostname with nothing inspecting the payload. Not your keychain, $HOME, or real checkout — unless you took the host-mount escape hatch.

The project's record of what it checked is committed and candid. It is also written by its own agents and unsigned, with no commit signing in evidence; the local gate binds a one-shot pass token to the tree it checked.99 With nothing in the repository showing server-side validation of a push, that record's provenance rests on the honesty of the process that wrote it.

checked The one MCP surface that lets a contained agent drive a host browser ships with arbitrary JavaScript evaluation disabled — the tool is advertised but returns an explicit refusal, so it is visible without being live.24

What the assurance claim actually is

The convergence argument you already have is not a security argument, and the project does not offer it as one. What carries the weight is a pair of invariants — verification claims must be falsifiable, and evidence is not proof25 — plus the methodology's refusal to read its completion score as a probability.26 So: a passing suite is a bounded signal over the defects someone thought to write a litmus test for, and, as the hardening case shows, only over those whose tests distinguish the relevant failure from success. Finite litmus coverage is not proof of absence of defects; the repo says so before you do.

How we know — 107 sources

Each small number in the text opens the exact lines of the Tillandsias source that back that sentence, as they stand in release v56.10.9.1, the one the install commands give you. Hover a number to read the quoted lines without leaving the page. A source marked with its own release points at a newer one.

  1. 1Podman the only host dependency on Linux; macOS/Windows provision a VM README.md#L52-L53Podman is the only host dependency on Linux (auto-detected). macOS and Windows provision a lightweight Fedora-based utility VM; no host Podman required.
  2. 2Escape lands as the invoking user's UID on the host, with `$HOME` only openspec/specs/podman-idiomatic-patterns/spec.md#L209-L210- **THEN** the escaped process runs as the invoking user's UID on the host, not as root - **AND** it has access only to `$HOME` and user-owned resources
  3. 3Enclave architecture — attack scenarios and stated limits docs/cheatsheets/enclave-architecture.md#L217-L225The proxy allowlist is generous by design — a determined attacker could encode data in DNS queries or HTTP headers to an allowed domain.
  4. 4The mandatory hardening envelope and the argv policy checker crates/tillandsias-podman/src/policy.rs#L14-L22pub const MANDATORY_HARDENING_FLAGS: [&str; 4] = [ "--userns=keep-id", "--cap-drop=ALL", "--security-opt=no-new-privileges", "--security-opt=label=disable", ];
  5. 5SELinux MCS labelling recorded as a planned future phase openspec/specs/default-image/spec.md#L768-L769future release will apply SELinux MCS labels to the forge domain, adding mandatory access control at the boundary layer.
  6. 6Checked serialization refuses an invalid launch in every build profile crates/tillandsias-podman/src/container_spec.rs#L424-L425crate::policy::validate_launch_argv(&argv)?; Ok(argv)
  7. 7Historical false-pass defect and its corpus-wide guard openspec/litmus-tests/litmus-podman-idiomatic-security-flags.yaml#L7-L13Both branches printed what the harness was looking for, so the greps decided nothing
  8. 8The false-pass scanner: advisory, no caller gates on it, skips over half the corpus, keyed on a non-zero exit scripts/scan-litmus-false-pass.sh#L8-L48Advisory — no caller gates on it.
  9. 9Peek at step 1, bump one exact hostname, splice everything else images/proxy/squid.conf#L122-L139ssl_bump peek ssl_bump_step1 ssl_bump bump github_release_assets ssl_bump splice all
  10. 10Upstream verification is kept, with a written rule against disabling it images/proxy/squid.conf#L88-L97verification MUST remain enabled. Never add DONT_VERIFY_PEER or DONT_VERIFY_DOMAIN: doing so would hide an origin MITM behind a proxy-issued certificate that enclave clients trust.
  11. 11Host-side CA re-clamp, with the reasoning for why a code fix was only half the job, and the two hosts found world-readable scripts/clamp-ca-material.sh#L15-L43Two hosts were found with a world-readable CA private key days after the packet closed
  12. 12Spec: per-launch EC P-256 root-and-intermediate chain on tmpfs openspec/specs/proxy-container/spec.md#L63-L74The system SHALL generate a fresh two-level CA chain on every proxy launch. The chain SHALL consist of a self-signed Root CA and an Intermediate CA signed by the root, both using EC P-256 keys. All key material SHALL be stored on tmpfs
  13. 13Launcher generates self-signed RSA-2048 CA with a 30-day life crates/tillandsias-headless/src/main.rs#L3523-L3526"req", "-x509", "-newkey", "rsa:2048",
  14. 14Anonymous receive-pack listener; network placement is not authentication; owners named, no date openspec/specs/git-mirror-service/spec.md#L52-L72Network placement SHALL NOT be described as client authentication: any enclave peer can still create or fast-forward refs and cause the privileged relay to carry them upstream.
  15. 15Noise PSK derived from the binary's own hash crates/tillandsias-secure-channel/src/lib.rs#L112-L114let exe = std::env::current_exe().expect("current_exe for self-hash"); let bytes = std::fs::read(&exe).expect("read self binary for hash"); Sha256::digest(&bytes).to_vec()
  16. 16Absent means encrypted; plaintext requires explicit off crates/tillandsias-control-wire/src/secure_wire_mode.rs#L85-L131Err(std::env::VarError::NotPresent) => Ok(SecureWireMode::On),
  17. 17The maturity ladder from default-OFF to secure-by-default to removing the plaintext path — status active plan/issues/secure-channel-maturity-ladder-2026-07-04.md#L1-L12implement the e2e encrypted socket channel in ALL places, enable it at runtime with a flag, then advance through STABLE VERIFIABLE MATURITY GATES to secure-by-default and finally to removal of the insecure path.
  18. 18Per-boot key hardening — approved, deferred, unimplemented plan/issues/encrypted-channel-perboot-key-hardening-2026-07-01.md#L1-L22This packet is the operator-approved **later hardening**: mix in a **per-boot secret** the host controls, so a leaked release secret alone no longer lets an attacker with a matching-release binary attach across a different VM boot.
  19. 19Keyless signing recorded in the Rekor transparency log, and the verification identity regexp that pins the repo name only .github/workflows/release.yml#L355-L355--certificate-identity-regexp "https://github.com/.*/tillandsias/"
  20. 20Base images pinned by mutable tag images/git/Containerfile#L19-L21FROM docker.io/hashicorp/vault:1.18 AS vault-agent FROM docker.io/library/alpine:3.20
  21. 21Research: such provenance verifies with no verifier identity; it recommends bundles plus a pinned root (not adopted) plan/index.yaml#L12318-L12318verifying a Sigstore/SLSA attestation requires NO identity from the verifier. Reproduced with no token, no gh config, and inside a network namespace with no interface at all: exit 0, real certificate chain, correct signer; one flipped byte gives exit 1.
  22. 22Vault unseal share stored in the host OS keychain openspec/specs/tillandsias-vault/spec.md#L115-L118The unseal key (the single Shamir share generated by Vault during initialization) SHALL be stored directly in the host OS's native secure keychain (Secret Service/KWallet on Linux, Credential Manager on Windows, Keychain on macOS) under the versioned name `vault-shamir-share-v1`.
  23. 23Linux clean-room reset clears host credentials and probes the result scripts/e2e-step2-linux.sh#L15-L38"$SCRIPT_DIR/clear-vault-host-credentials.sh" 2>&1 | tee "$LOG_DIR/02-clear-credentials.log" || true
  24. 24Host-browser tool dispatcher refuses arbitrary JavaScript evaluation crates/tillandsias-browser-mcp/src/server.rs#L367-L379"browser.eval" => Self::tool_error( id, "EVAL_DISABLED: browser.eval is disabled in v1; see follow-up change", ),
  25. 25Falsifiability and evidence-is-not-proof invariants methodology/philosophy.yaml#L69-L71- verification_claims_must_be_falsifiable: true - convergence_requires_stability: true - evidence_is_not_proof: true
  26. 26Bounded ranking function, explicitly not a probability methodology/math-foundations.yaml#L91-L125CentiColons are a finite bounded ranking function.
  27. 27General capability-add field and builder method removed crates/tillandsias-podman/src/container_spec.rs#L85-L100there is deliberately no `cap_add`. The field existed as an unvalidated pass-through
  28. 28Shared CA path accessor composes the ca leaf under the state root crates/tillandsias-core/src/ca_path.rs#L37-L51format!("{}/ca", ca_template())
  29. 29Debug builds derive the channel key from a fixed, public seed crates/tillandsias-secure-channel/src/lib.rs#L63-L68Fixed, non-secret dev seed used in debug builds. It lets a locally-built host + guest of the *same* tree interoperate without a release build. It is intentionally NOT a secret
  30. 30The allowlist table: package registries and cloud namespaces anyone can publish to docs/cheatsheets/enclave-architecture.md#L144-L156| Package registries | `registry.npmjs.org`, `crates.io`, `pypi.org`, `rubygems.org` | Package installation |
  31. 31The permissive port binds only to loopback in the proxy container images/proxy/squid.conf#L70-L78http_port 127.0.0.1:3129 ssl-bump
  32. 32Permissive port: allow everything, no source restriction images/proxy/squid.conf#L146-L149http_access allow CONNECT SSL_ports build_port http_access allow build_port
  33. 33Spec: on the permissive port domain filtering does not apply openspec/specs/proxy-container/spec.md#L114-L114- **AND** domain filtering SHALL NOT apply
  34. 35The port is kept until a decision is made, not deleted images/proxy/squid.conf#L39-L44That is an operator decision, not a cleanup. Until it is taken, this comment is the honest state.
  35. 36Network audit: the permissive-port decision still open plan/issues/network-architecture-audit-2026-07-09.md#L531-L533**STILL OPEN as a decision**, with the false claim removed and the agreement now enforced
  36. 37Denied runtime traffic gets a TCP reset rather than a 403 images/proxy/squid.conf#L148-L153Send a TCP reset instead of an HTTP 403 error for strictly-denied runtime traffic.
  37. 38The spec still promises an HTTP 403 for denied domains openspec/specs/proxy-container/spec.md#L109-L109- **AND** all other domains SHALL be denied with HTTP 403
  38. 39The owner-only clamp on the CA key, in the stable release's source crates/tillandsias-headless/src/main.rs#L3408-L3410fn enforce_ca_key_mode(key: &Path) -> std::io::Result<()> { use std::os::unix::fs::PermissionsExt; std::fs::set_permissions(key, std::fs::Permissions::from_mode(0o600))
  39. 40On non-unix targets the clamp is a documented no-op crates/tillandsias-headless/src/main.rs#L3428-L3430This is deliberately NOT a security regression on Windows: the key is not protected by mode bits there in the first place, and the enclave's Windows path receives it as a podman secret rather than through this file.
  40. 41Pre-fix keys are healed down to owner-only on every pass crates/tillandsias-headless/src/main.rs#L3612-L3614Heal DOWN to 0600 every call so keys generated before this fix (deliberately world-readable 0644) are repaired without requiring a CA rotation.
  41. 43State root is persistent and explicitly shared between consumers images/default/ca-path.txt#L1-L83${HOME}/.local/state/tillandsias
  42. 45The planned fix: make the CA directory private by construction plan/index.yaml#L26013-L26013CA material is created in a directory that is private by construction
  43. 46The authenticated push lane is gated on an environment variable that defaults to off crates/tillandsias-headless/src/main.rs#L13186-L13188std::env::var("TILLANDSIAS_MIRROR_SSHD") .map(|v| v == "1") .unwrap_or(false)
  44. 47The mirror's sshd starts only behind that flag images/git/entrypoint.sh#L524-L525Behind TILLANDSIAS_MIRROR_SSHD=1 until the T11 staged migration flips the default.
  45. 48The relay wires the GitHub credential helper for any HTTPS origin images/git/relay-refs.sh#L154-L183case "$REMOTE_URL" in https://*)
  46. 49The helper checks exact GitHub hosts and refuses others images/git/git-credential-tillandsias.sh#L69-L83github.com|api.github.com) ;;
  47. 50The vault container carries an embedded SELinux policy module crates/tillandsias-headless/src/vault_bootstrap.rs#L4091const VAULT_SELINUX_CIL: &str = include_str!("../../../images/selinux/vault_container.cil");
  48. 51The Windows guest embeds policies for the control daemon and the vault crates/tillandsias-windows-tray/src/wsl_lifecycle.rs#L311const SELINUX_VAULT_TE: &str = include_str!("../../../images/selinux/tillandsias_vault.te");
  49. 52On a rootless Linux host the vault too runs unconfined crates/tillandsias-headless/src/vault_bootstrap.rs#L4190`label=disable` runs the vault container unconfined on the host
  50. 53The vault launch adds a capability back and omits `--rm` crates/tillandsias-headless/src/vault_bootstrap.rs#L4407-L4410"--cap-drop".into(), "ALL".into(), "--cap-add".into(), "IPC_LOCK".into(),
  51. 54A test-quality check that examines only new tests plan/index.yaml#L14455-L14455the check is DIFF-SCOPED BY CONSTRUCTION (examines added steps in the outgoing change only) and is structurally incapable of flagging the existing corpus
  52. 55Serialising a launch now refuses an argv that violates the envelope, in every build profile crates/tillandsias-podman/src/container_spec.rs#L424-L425crate::policy::validate_launch_argv(&argv)?; Ok(argv)
  53. 56The attached forge launch refuses on a hardening violation crates/tillandsias-headless/src/main.rs#L14990"refusing to launch {container_name}: hardening envelope violation: {err}"
  54. 57The delegated launch path refuses too crates/tillandsias-headless/src/main.rs#L14818[security] refusing to launch delegated {container_name}: {err}
  55. 58The rewritten litmus derives its launch flags from the product's declared envelope openspec/litmus-tests/litmus-podman-idiomatic-security-flags.yaml#L23-L25So the launch argv is now DERIVED from the product's own declared envelope, `MANDATORY_HARDENING_FLAGS` in crates/tillandsias-podman/src/policy.rs
  56. 59The ninth step, cleanup, still prints its token unconditionally openspec/litmus-tests/litmus-podman-idiomatic-security-flags.yaml#L131-L134expected_behavior: "CLEANED"
  57. 60A second scanner refuses litmus steps that print the same token on both branches scripts/check-litmus-steps-can-fail.sh#L5-L6Refuse a litmus step whose success and failure branches print the SAME token, because such a step passes whether the system works or not.
  58. 61The build gate fails on it build.sh#L5330a litmus step prints the same token on success and failure
  59. 62The second scanner refuses only the identical-token shape, by design scripts/check-litmus-steps-can-fail.sh#L26-L30Only identical tokens are refused, because only then is the branch a decoration.
  60. 63Private flags initialized on; serialization remains conditional crates/tillandsias-podman/src/container_spec.rs#L134-L137userns_keep_id: true, cap_drop_all: true, no_new_privileges: true, label_disable: true,
  61. 64A host-browser container still launches through the unchecked serialiser crates/tillandsias-headless/src/main.rs#L14408let args = spec.build_run_args();
  62. 65The repo's own admission that enforcement depends on call order crates/tillandsias-headless/src/main.rs#L14799-L14801An invariant that depends on call order is one refactor from being false, and this one decides whether a container runs unhardened.
  63. 66The one shared reader of the flag: a blank value is refused crates/tillandsias-control-wire/src/secure_wire_mode.rs#L85-L126is set but empty. Blank does NOT mean
  64. 67Windows tray uses shared mode reader and refuses bad values crates/tillandsias-windows-tray/src/hvsocket.rs#L74-L87let mode = match tillandsias_control_wire::secure_wire_mode::secure_wire_mode() { Ok(mode) => mode, Err(err) => return Err(std::io::Error::new(std::io::ErrorKind::InvalidInput, err)), };
  65. 68A build check allows no independent reader of the secure-wire setting scripts/lua/check-secure-wire-single-reader.lua#L6-L8BASELINE is 0 today: every reader was converted (972-umik commit B), so the ratchet is now effectively a refusal.
  66. 69Test pins the encrypted default through the shared parser crates/tillandsias-control-wire/src/secure_wire_mode.rs#L159-L165parse_secure_wire_mode(Err(VarError::NotPresent)).unwrap(), SecureWireMode::On,
  67. 70The Windows guest's service unit carries no secure-wire line crates/tillandsias-windows-tray/src/wsl_lifecycle.rs#L1938-L1939Environment=TILLANDSIAS_VAULT_API_BASE_URL=https://vault:8200 {low_power_env}ExecStart=/usr/local/bin/tillandsias-headless --listen-vsock 42420
  68. 71The VM image's default service unit carries no environment at all images/vm/bootstrap/20-tillandsias.sh#L92-L104ExecStart=/usr/local/bin/tillandsias-headless --listen-vsock 42420
  69. 72The Linux lane asserts transitive coverage as a property over what is staged .github/workflows/release.yml#L320-L322This asserts the PROPERTY over whatever is actually staged, so an asset added later cannot slip through unverifiable just because nobody added a line for it.
  70. 73The macOS lane signs an allow-list of three assets, and its checksum manifest is not among them .github/workflows/release.yml#L562for artifact in tillandsias-tray-*-macos-arm64.tar.gz Tillandsias.dmg install-macos.sh; do
  71. 74The Windows lane's coverage check, why it follows the transitive path, and the allow-list it blames for an installer shipping bare .github/workflows/release.yml#L863-L864The check follows the TRANSITIVE path rather than counting unsigned files
  72. 75The Linux installer fetches the manifest over the same channel and continues without it scripts/install.sh#L264say "SHA256SUMS not available; skipping checksum verification."
  73. 76The Windows installer compares a hash from the fetched manifest only scripts/install-windows.ps1#L746Say "Verifying SHA-256..."
  74. 77The macOS installer likewise scripts/install-macos.sh#L162-L162curl -fsSL "$SHA_URL" -o "$TMP/SHA256SUMS-macos"
  75. 78The shipped verifier uses the same repository-name-only regexp scripts/verify.sh#L24-L25CERTIFICATE_IDENTITY_REGEXP="https://github.com/.*/tillandsias/"
  76. 79The in-app updater's spec: artifact selection, no verification requirement openspec/specs/update-system/spec.md#L1-L53The updater SHALL select the correct artifact for the current platform and handle platform-specific execution constraints.
  77. 80Sigstore/in-toto provenance studied for another channel plan/issues/homebrew-harness-distribution-research-2026-07-11.md#L27-L31every bottle built by Homebrew CI carries a **Sigstore/in-toto build-provenance attestation** (SLSA Build L2)
  78. 81The Linux soft reset keeps the Vault store and both keyring entries crates/tillandsias-headless/src/main.rs#L10559-L10559the keyring entries vault-shamir-share-v1 and vault-root-token-v1 — never cleared by a reset
  79. 83Where no keyring is available the share is written to a fallback file in the cache directory crates/tillandsias-headless/src/vault_bootstrap.rs#L4907using fallback file (expected in VM guest and headless environments)
  80. 84Inside a VM guest both fallback writes are always attempted crates/tillandsias-headless/src/vault_bootstrap.rs#L2873-L2874BOTH writes are always ATTEMPTED — a token failure must never skip the share, because the share is the half that arms the wipe.
  81. 85The "share present in keychain" check falls through to the file crates/tillandsias-headless/src/vault_bootstrap.rs#L2943-L2944Fallback: file (populated by keychain_set_blocking when keyring unavailable, e.g. in a VM guest or headless environment without D-Bus)
  82. 86The preserve guard and the log line the smoke runs observed crates/tillandsias-headless/src/vault_bootstrap.rs#L4297[tillandsias-vault] preserving existing data volume (Shamir share present in keychain)
  83. 87Spec: every persistent on-disk copy of the share is deleted immediately openspec/specs/tillandsias-vault/spec.md#L132delete all persistent on-disk copies immediately
  84. 90The Podman client runs whatever argv it is handed, with no policy call crates/tillandsias-podman/src/client.rs#L1011-L1013let mut full_args = vec!["run".to_string()]; full_args.extend_from_slice(args); match self.execute(OperationKind::Container, &full_args).await {
  85. 91Why the CA directory moved: /tmp is volatile by design and swept on reboot images/default/ca-path.txt#L36-L38/tmp works everywhere and is the bug — volatile by design, cleared on reboot and swept by systemd-tmpfiles
  86. 92Why label=disable: no `:z`/`:Z` relabelling, so bind mounts work under SELinux hosts openspec/specs/podman-orchestration/spec.md#L110-L110no `:z` or `:Z` suffix is needed because `--security-opt=label=disable` disables SELinux confinement for the container process
  87. 93The macOS host writes the secure-wire flag into the guest's service unit crates/tillandsias-vm-layer/src/vz.rs#L1269Environment=TILLANDSIAS_SECURE_CONTROL_WIRE=__SECURE_CONTROL_WIRE__
  88. 94Why the flag is applied — SELinux relabelling would otherwise break the bind mount docs/audits/browser-isolation-audit-2026-04-30.md#L58| `--security-opt=label=disable` | ✅ | Required for Silverblue bind mounts |
  89. 95The observatorium web launch serialises without the policy call crates/tillandsias-headless/src/main.rs#L14359.build_run_args()
  90. 96The proxy launch is assembled as a raw argv the checker never sees crates/tillandsias-headless/src/main.rs#L3787fn build_proxy_run_args(certs_dir: &Path, image: &str) -> Vec<String> {
  91. 97The mirror launch is assembled the same way crates/tillandsias-headless/src/main.rs#L4933fn build_git_run_args(
  92. 98What the checker refuses beside a missing flag crates/tillandsias-podman/src/policy.rs#L189-L193if arg == "--privileged" || arg.starts_with("--privileged=") || flag_value(arg, next, "--userns").is_some_and(|value| value != "keep-id") || flag_value(arg, next, "--cap-add").is_some_and(|value| value == "ALL")
  93. 99The gate's pass token names the tree it checked plan/archive/packets-2026-08.yaml#L48763a green gate now issues a one-shot pass token naming the tree it validated
  94. 100Ownership probe distinguishes keep-id from misleading inspect labels openspec/litmus-tests/litmus-podman-idiomatic-security-flags.yaml#L41-L64The bind-mounted file discriminates — 1000 with keep-id, 0 without
  95. 101Capability probe ignores command exit status before declaring success openspec/litmus-tests/litmus-podman-idiomatic-security-flags.yaml#L111-L114expected_behavior: "CAPABILITIES_DROPPED"
  96. 102CA directory creation uses ordinary create_dir_all, followed by refresh decision crates/tillandsias-headless/src/main.rs#L3468-L3469std::fs::create_dir_all(&certs_dir) .map_err(|e| format!("Failed to create CA directory: {e}"))?;
  97. 103Host derives the control key from the build-time guest digest crates/tillandsias-secure-channel/src/lib.rs#L157-L182derive_psk(guest_binary_sha256, build_version, wire_version, hop)
  98. 104Why single-process tests could not reveal the host/guest digest mismatch crates/tillandsias-secure-channel/src/lib.rs#L205-L228The only integration proof is two DISTINCT binaries: a release-built tray and the release guest reaching Ready on a real cold provision.
  99. 105Windows refuses an absent embedded guest digest crates/tillandsias-windows-tray/src/hvsocket.rs#L25-L43EMBEDDED_GUEST_SHA256.as_ref().ok_or_else(|| {
  100. 106Windows handshake negative control rejects a different guest digest crates/tillandsias-windows-tray/src/hvsocket.rs#L525-L610a_guest_digest_mismatch_is_refused_not_carried
  101. 107Local acceptance follows upstream acceptance, so a reported success is a durable one images/git/pre-receive-hook.sh#L6-L8Validates ledger YAML, then synchronously relays the proposed ref transaction upstream before accepting it locally. A client success therefore means the configured upstream has durably accepted the same atomic ref set.
  102. 109Spec: the share lives only in an unlocking keyring, with no persisted fallback share file openspec/specs/tillandsias-vault/spec.md#L425-L428There is NO persisted fallback share file: where the keyring is unavailable the share stays in process memory or on tmpfs for the life of the process and is gone with it
  103. 110The credential clearer is uninstall-only and has no caller yet crates/tillandsias-headless/src/vault_bootstrap.rs#L3524-L3528UNINSTALL-ONLY (order 1437-qza3, operator directive 2026-09-27). No reset may call this: the store and the unseal material are operator data that survive every destructive reset
  104. 111The Linux soft reset runs no credential clearer and deletes no store crates/tillandsias-headless/src/main.rs#L10522-L10524No credential clearer and no store deletion: the store is a host directory the podman reset cannot reach.
  105. 115Open defect: the Windows installer never verifies a cosign signature plan/index.d/20261008t234534z-1562-leaked-test-creds-and-installer-cosign-yolanda-windows.yaml#L66-L66install-windows.ps1 checks the downloaded zip against SHA256SUMS-windows but never verifies a cosign signature
  106. 116Open defect: nothing verifies the install entry point itself plan/index.d/20260922t152446z-1361-pwjz-the-entry-point-verifies-everything-but-itself-pirria.yaml#L23-L24(a) the install path verifies its own published cosign bundle before executing, or
  107. 117The mirror hook can refuse pushes by a seed read from the integration branch, when the seed enforces images/git/pre-receive-hook.sh#L169-L177enforced the push is rejected BEFORE the relay, so nothing reaches upstream

And you would like me to be condescending about it. Very well.Assumes everything before it. Mathematics from here down.

Level 5 — For the MathWiz / Hacker

A monocot bromeliad flowers once and dies, leaving offsets behind — the pup is never the parent.

The lower levels gave you the object and its defects. What is left is the only part that can be attacked with a pencil: the formal content, stated as claims with hypotheses, sorted into (a) standard theorems being invoked, (b) what the repository actually asserts, (c) what follows, and (d) what does not. The security findings of level 4 are not repeated; they enter here only where a mathematical hypothesis depends on them.

The short verdict: the theory is small, finite, and unusually candid about the theorems it declines to claim. Its model now has generative property tests and stable requirement identifiers. Connecting those checks to live validation and justifying dependent iteration remain separate obligations.

The lattice, and the one step worth checking

An obligation is a named, auditable requirement, invariant, litmus signal, trace signal, provenance binding, or environment assumption carrying a stable identifier1. Its state ranges over a finite chain, seven long2:

\[\mathcal{O} = \{\,\texttt{absent} < \texttt{declared} < \texttt{traced} < \texttt{pos\_tested} < \texttt{neg\_tested} < \texttt{runtime\_obs} < \texttt{evidence\_bundled}\,\}\]
\[S_{\text{spec}} = \prod_{i \in \mathrm{Obl}} \mathcal{O}_i, \qquad S_{\text{proj}} = \prod_{s} S_{\text{spec}}(s), \qquad x \le y \iff \forall i,\; x_i \le_{\mathcal{O}} y_i\]

Fix the convention, since authors differ: the height of a finite poset is the maximum number of strict order steps in a chain — so a chain of \(n\) elements has height \(n-1\), and the one-element poset has height \(0\).

Proposition 1. \(S_{\text{proj}}\) is a finite complete lattice of height \(6|\mathrm{Obl}|\). Each \(\mathcal{O}_i\) is a finite chain, hence a complete lattice; a finite product of complete lattices is complete with \(\vee,\wedge\) computed componentwise; a chain of seven elements has height six and heights add over products3. Nothing here is in dispute — it is the least interesting true thing in the file, and the repository correctly declines to inflate it: "this proves monotonicity of the model, not truth of the modeled requirement or adequacy of the chosen obligation set"4.

Two hypotheses do quiet work. The order is componentwise after aligning stable IDs and tombstones1, so a renamed coordinate yields a different lattice, not a lower point in the same one; and \(|\mathrm{Obl}|\) must be finite and known, which is what makes the height bound a bound rather than an ordinal.

absentdeclared tracedtested + tested −observed less evidence → more evidence, one direction only going backwards is not a bug report, it is a rejected merge
Evidence only ever climbs. A claim may not descend a rung without an explicit, recorded scope change.
⊤ both fully evidenced ⊥ nothing evidenced joins
Two obligations, each a chain of three states, give a nine-element product lattice ordered componentwise. Real specs multiply many more chains than two.

The fixed point, in full

Let \(\mathrm{refine}: S \to S\) be validation followed by semantic distillation at a fixed bar \(B\). The repository's stated claim is idempotence5:

\[\mathrm{refine}(\mathrm{refine}(x)) = \mathrm{refine}(x)\]

Theorem 2 (Knaster–Tarski). A monotone map on a complete lattice has a complete lattice of fixed points, in particular a least one6. Applied here it gives existence, non-constructively, and says nothing about how to reach it.

Theorem 3 (finite monotone inflationary iteration). If \(f: L \to L\) is monotone and inflationary (\(x \le f(x)\)) on a finite lattice of height \(h\), then for every \(x\) the chain \(x \le f(x) \le f^2(x) \le \cdots\) stabilises after at most \(h\) strict increases, and its limit is the least fixed point \(\ge x\). The bound is immediate: each strict step rises at least one level in a poset of height \(h\). Leastness is a two-line argument worth doing, because it is the step people skip: if \(y = f(y)\) and \(y \ge x\), then monotonicity gives \(f^n(x) \le f^n(y) = y\) for every \(n\), so the limit is \(\le y\).

Note where Kleene's theorem proper — least fixed point as \(\bigsqcup_n f^n(\bot)\) for a Scott-continuous \(f\) on a CPO7 — would otherwise be invoked outside its hypotheses. Continuity is not assumed, it is free: in a finite poset every directed subset is finite and contains its own supremum, so every monotone map is Scott-continuous. That is the first place a referee probes, and it survives.

more less applications of the refinement operator lattice top — never reached, and need not be refine(refine(x)) = refine(x) the chain stops moving
Monotone refinement over a finite lattice cannot ascend forever. It stabilises — and the step where it stops is what “done” means. Provided refinement really is monotone.

So validator stability is a computable predicate: \(\mathrm{refine}(x) = x\). It is not completion: the identity operator is stable even with unmet obligations. The methodology makes this distinction explicitly40. The comparison bar \(B_v\) is fixed within a release, rising only by explicit operator decision, with the automation forbidden from self-escalating it8. A continuously rising bar makes \(\mathrm{refine}\) a moving operator and destroys stabilisation; discrete, externally gated raises preserve it. That is the standing hypothesis of Theorem 3, not a premise that forces every obligation to close.

And now the gap. Theorem 3 needs monotone and inflationary; the repository asserts idempotent. These are the three axioms of a closure operator, and the repository states one of them. Idempotence does not imply monotonicity: a map that sends \(a \mapsto \top\), \(b \mapsto b\) with \(a < b\) is idempotent and not monotone. Nor does it imply inflationarity. The validation program that would discharge the missing two is written down — enumerate the allowed transitions and property-test that each is order-preserving for every affected obligation9 — which is the right shape of obligation. That program now has executable tests over a committed rule set; it does not establish the same properties for every live validator41.

known limitation The refinement model has generative tests for monotonicity, inflationarity and idempotence, but they exercise a committed rule set rather than every live validator41. They establish a model boundary, not semantic correctness of the running system.

what happens next The staged validation program names the remaining obligation: enumerate permitted transitions and check every affected coordinate, treating tombstones and scope changes separately9.

checked Requirement identifiers now exist and a gate checks presence and uniqueness, including tombstoned specifications42. Whether an edit preserves meaning and should keep its identifier remains author judgement; the guard cannot decide that. The evidence-credit table still lists stable identifiers as a scoring input10, which by itself does not establish that credit is computed.

checked The negative results — no contraction, no Galois connection, no probabilities — are first-class claims carrying their own discharge conditions, not caveats appended to positive ones. Each names the object a future claim would have to construct.

The moving target, and what would force the floor to zero

Write \(T_v\) for the specifications at version \(v\), \(I_v\) for the implementation, \(d_v = d(I_v, T_v) \ge 0\). Because the specifications evolve, \(T\) moves and no final target exists11. The mechanically checkable release rule is non-increase.

Proposition 4 (monotone convergence). A non-increasing real sequence bounded below converges to its infimum12. Here:

\[d_{v+1} \le d_v \;\text{ for all } v, \quad d_v \ge 0 \qquad\Longrightarrow\qquad d_v \;\longrightarrow\; d_* \;=\; \inf_{v} d_v \;\ge\; 0\]

That is the whole result, and it is correct.

What does not follow is \(d_* = 0\), and the repository says so in those words11. A sufficient extra hypothesis is a uniform progress premise, in either of two forms:

\[\exists\,\varepsilon > 0 : \; d_v > 0 \implies d_{v+1} \le d_v - \varepsilon \qquad\text{or}\qquad \exists\,\theta < 1 : \; d_{v+1} \le \theta\, d_v\]

Either excludes positive residual fixed points and forces \(d_* = 0\) — the additive form in finitely many steps, the multiplicative one geometrically. The repository's phrase — "a validated progress premise that excludes positive residual fixed points"11 — can be discharged by either sufficient condition; neither is established here as a fact about the project.

What CentiColons make measurable

A concrete residual can replace an unspecified distance without becoming a metric in the sense needed by Banach. Fix a finite obligation set \(S\), positive integer weights \(w_i\), and a versioned closure predicate \(C_i(x) \in \{0,1\}\) requiring the declared evidence for obligation \(i\). A proposed binary accounting contract is

\[R_S(x) = \sum_{i \in S} w_i\bigl(1-C_i(x)\bigr).\]

This is a measurable residual of declared, unmet evidence obligations, not all uncertainty about the program. Its unit, scope and evidence policy must stay fixed across a comparison. Preserving each retained obligation's credit implies non-increase of \(R_S\); non-increase of the total does not imply preservation of each credit, because one gain can hide another loss. New obligations, retirements and evidence invalidation must be reported explicitly rather than called progress.

The termination statement now has a precise price: if there is no regression and, whenever \(R_S>0\), a decrease of at least one occurs within the next \(K\) cycles, then \(R_S=0\) within at most \(K R_S(x_0)\) cycles. There are at most \(R_S(x_0)\) such decreases to wait for. This is a conditional cycle bound, not a wall-clock bound or a proved property of the project. Non-increase alone permits \(R_S=10\) forever.

Lua makes extraction and grading executable. That closes part of the measurement gap, not the progress premise. At this release the Lua grader reports an unweighted count at the recorded positive-test bar47, while the methodology declares a richer weighted policy15. The regression reporter is advisory and stores an untracked, per-checkout baseline, not a durable accepted-release baseline48. The capability split between cacheable and observing predicates46 also cannot prove that an assertion tests the intended behavior or that a pass belongs to the current implementation and every required platform. A pure grader can give the same wrong credit on every evaluation.

known limitation CentiColons make declared residuals observable, but the stable release does not yet establish enforced monotonic reduction under a complete evidence contract. The observed grader selects results by test name and test digest, not required platform; its spec-digest check is conditional and it does not match implementation inputs47. Scenario candidates are shared at requirement level49.

what happens next Fix assertion, subject and platform attribution; align the scoring policy; then pilot a durable per-obligation non-regression check before enabling enforcement. Invariant extraction is separate from the current grading loop50. This is the proposed program, not a claim that it has shipped; strict progress remains a separate hypothesis even after these checks exist.

far close successive releases — each one re-raises the bar the floor — possibly not zero zero would mean “finished”. No proof it is reachable.
Schematic: if comparable residuals never rise, they approach a floor, and nobody has proven that floor is zero.

The Banach alternative, itemised. A contraction claim13 would owe a complete metric space \((X,d)\), an operator \(F: X \to X\), and a constant \(0 \le c < 1\) with

\[d\big(F(x),F(y)\big) \le c\,d(x,y) \quad \forall x,y \in X \qquad\Longrightarrow\qquad \exists!\,x^* = F(x^*), \quad d\big(F^n(x),x^*\big) \le \frac{c^n}{1-c}\,d\big(x,F(x)\big)\]

Missing: (i) a metric, (ii) completeness, (iii) any bound on \(c\). The repository lists all three14. The binding one is (i), and it is worse than "unproven". \(d\) as used is not a function on \(X \times X\) at all — it is only ever evaluated at the single pair \((I_v, T_v)\), giving a sequence of numbers indexed by release, not a binary function on a set. Ask which axiom fails and the honest answers are:

\[\underbrace{d(x,y) = d(y,x)}_{\text{undefined}}, \quad \underbrace{d(x,z) \le d(x,y) + d(y,z)}_{\text{undefined}}, \quad \underbrace{d(x,y) = 0 \implies x = y}_{\text{false on the natural candidate}}\]

The first two are undefined rather than false, because \(d(x,y)\) for two arbitrary project states is never given a value; identity of indiscernibles fails outright, since two materially different implementations can close the same obligation set and score residual zero. Absent (i), items (ii) and (iii) are not merely unproven — they are not well-posed. Until a metric exists, "monotonic convergence" means ordered non-regression plus finite residual descent, which is what the file claims and no more14.

Be careful about the scope of that verdict, because it is easy to overstate and I will not. Everything above concerns the \(d\) the repository actually documents. It is not an argument that no suitable metric exists on project states, and nothing here forecloses one: a genuine metric — with a completion, and a contraction constant — may well be constructible, and if it were, the Banach route would reopen exactly as written. The claim is the narrow one. The contraction argument cannot be discharged by the object currently on offer, and the work of building a better one is open rather than blocked.

There is a further boundary. Under a fixed denominator, a non-increasing integer residual is eventually constant; strict progress while positive would force zero. Changing the obligation scope breaks that comparison. A ratio alone cannot repair it: adding easy obligations can improve the percentage without closing an old gap.

known limitation The scorer marks a tombstone as a broken monotone regime, and the shell reports that regime4445. This is useful local instrumentation, but does not establish a cross-release comparison for arbitrary changes of weights or obligation scope.

what happens next The validation program requires denominator changes to be reported separately9. A complete cross-release comparison must align the obligation set and weights; no general normalisation rule is established by the cited implementation.

A repository refinement is a model, not a theorem

Here is the engineering picture in a form precise enough to attack. Let \(R_k\) be the repository state visible to a task, \(p_k\) its prompt and \(C_k\) the selected context built from source, rules, retrieved material and task state. A model proposes a delta \(Delta_k\); review and validators decide whether to commit it:

\[C_k = \operatorname{select}(R_k,p_k), \qquad \Delta_k = M(C_k), \qquad R_{k+1} = \operatorname{accept}(R_k,\Delta_k).\]

"Vectors" is a useful picture of the representations the model constructs from \(C_k\); it is not Git's data model. A commit can retain reachable history while its new tree replaces or deletes files. Nor is an accepted patch automatically an improvement. The acceptance step is where tests, review and evidence enter; the finite obligation model says exactly what those validators can record, and no more.140

local tasks: source + prompt + checks → committed delta accessibilityproduct copydashboard local targetlocal targetlocal target shared residual shared target: specifications, aligned before comparison a checked commit preserves history; it does not prove every line improved
A schematic engineering model: each checked change can move one local residual, while the shared record is judged against aligned specifications. It is not a probability measurement or a convergence proof.

For a workstream \(i\) with a fixed specification \(T_i\), write \(d_i(R_k,T_i)\) for its declared, measured residual. Accessibility and language, marketing, dashboards and customer support may each have their own \(d_i\). A shared residual is meaningful only after the obligation identities, weights and scope have been aligned; tombstoning already marks the shipped scorer's comparison as broken.4445 Thus the diagram's local traces are engineering telemetry, not samples from a common distribution.

not yet shown Repeated small, checked refinements can reduce observed local residuals faster than one large unreviewed change, while commits preserve an inspectable history of accepted evidence. This is a useful operating method, not a proof that the residual reaches zero.

The tempting probabilistic upgrade is to treat \(Delta_k\) as one random refinement with a finite-step skew, then invoke a weak or strong law over many refinements. That argument needs a stochastic model of the quality functional, a target mean and conditions on the joint process. Large context windows, high-dimensional embeddings and a fresh prompt do not supply independence. The next input contains \(R_{k+1}\), which was made using the previous output; this is feedback, not a negligible detail. The possible formal routes are the ones below: establish a dependence structure that supports an ergodic or martingale theorem, or leave the claim empirical.161819

what happens next Record each target, its scope and its negative controls; compare only aligned measurements; and retain counterexamples and retractions as first-class outcomes.940

Iteration: the law of large numbers at full strength

The methodology now withdraws its strong-law citation for dependent iterations16. The mathematics below explains why that correction matters. Set it up properly. Let \((X_k)_{k\ge1}\) be random elements on a probability space, \(X_k\) the value of a real quality functional on iteration \(k\), with \(\mathbb{E}|X_k| < \infty\) and \(\mathbb{E}[X_k] = \mu + b_k\), where \(b_k\) is the per-iteration skew. Write \(\bar{X}_N = N^{-1}\sum_{k \le N} X_k\) and \(\bar b = \lim_N N^{-1}\sum_{k\le N} b_k\) when that limit exists.

Theorem 5 (Kolmogorov's SLLN). Under independence, identical distribution, and integrability17:

\[X_k \text{ i.i.d.}, \;\; \mathbb{E}|X_1| < \infty \qquad\Longrightarrow\qquad \bar X_N \;\xrightarrow{\;\text{a.s.}\;}\; \mathbb{E}X_1\]

The iterations here are not identically distributed, so a candidate generalisation is Kolmogorov's variance criterion, which drops identical distribution and keeps independence18:

\[X_k \text{ independent}, \;\; \sum_{k\ge1} \frac{\mathrm{Var}(X_k)}{k^2} < \infty \qquad\Longrightarrow\qquad \bar X_N - \frac{1}{N}\sum_{k \le N} \mathbb{E}X_k \;\xrightarrow{\;\text{a.s.}\;}\; 0\]
\[\text{hence} \qquad \bar X_N \;\xrightarrow{\;\text{a.s.}\;}\; \mu + \bar b, \qquad \bar b = \lim_{N} \frac{1}{N}\sum_{k \le N} b_k\]

Same family as the three-series theorem; independence and the summable-variance condition are the whole price of admission.

Weak versus strong is convergence in probability versus almost-sure convergence; almost-sure convergence implies convergence in probability, but the converse need not hold. Under the criterion above and existence of the mean-bias limit, bounded bias bounds the limiting error. Bounded bias alone does not imply convergence, and unbounded skew does not by itself prove divergence. The methodology retains small, fast iterations as engineering guidance while withdrawing the almost-sure claim16.

one big slow iteration many small fast iterations truth truth lands off-target, and you cannot see by how much an illustrative running average no convergence theorem established here Bounded bias alone does not make the average reach the truth.
Schematic only: averaging can reduce variation under suitable hypotheses. Dependent iterations and persistent bias need a separate argument; this diagram is not evidence.

The objection the methodology now acknowledges is the load-bearing one. Every form of Theorem 5 quoted above requires independence. Iterations of an agent that reads its own prior output are not independent: \(X_{k+1}\) is a measurable function of \(X_k\) and the accumulated context. The architecture makes the dependence explicit and deliberate — retrieval is a cache of converged knowledge, updated on commits, with commits acting as a Lamport clock ordering the corpus against the code19. That is a feedback loop by construction, and it invalidates every independence hypothesis in the section above.

Two possible substitutes remain unestablished. Either the sequence is stationary and ergodic and one invokes Birkhoff's pointwise ergodic theorem20 — but stationarity is implausible for a process whose corpus grows monotonically, and ergodicity is exactly what fails if the chain can be absorbed. Or the increments form a martingale difference sequence with respect to the natural filtration \(\mathcal{F}_k = \sigma(X_1,\dots,X_k)\), giving an \(L^2\) or Azuma-type law — but that requires

\[\mathbb{E}\big[X_{k+1} \mid \mathcal{F}_k\big] = \mu \quad \text{a.s. for all } k\]

which is precisely the assertion that yesterday's output does not bias today's, and the cache design says it does. Under positive feedback an absorbing state is available: the stream converges almost surely to whatever the loop locked onto, and \(\bar X_N\) converges beautifully to a number with no relation to \(\mu\). Bounding per-prompt skew does not repair this; it is a hypothesis about marginals, and the failure is in the joint.

Be precise about what this objection kills. It does not show the iteration scheme is wrong, and it does not show the stream fails to converge — small fast iterations may well be the right engineering, and dependent sequences converge under plenty of other hypotheses. What it kills is the invocation of the strong law as stated: the theorem named is being applied to a sequence that does not satisfy its independence hypothesis, so the almost-sure conclusion is unearned rather than false. The repair is cheap in words and real in work — name the dependence structure, then cite the theorem that covers it.

checked The methodology explicitly withdraws the strong-law citation because the cache makes successive iterations dependent16. It names ergodic and martingale hypotheses as work needed for a future theorem, and presents the iteration practice as an empirical track record rather than an almost-sure guarantee.

What the repository renounces, and what that costs

No Galois connection. Specs and scores are described as abstractions that deliberately forget detail — the Cousot framing21 — and the file concedes that no adjunction is defined between program semantics and spec obligations22. State the missing object precisely. With \(C\) the lattice of sets of concrete executions and \(A\) the obligation lattice, a Galois connection \(\alpha \dashv \gamma\) requires monotone \(\alpha: C \to A\), \(\gamma: A \to C\) with

\[\alpha(c) \sqsubseteq a \iff c \sqsubseteq \gamma(a) \qquad\text{equivalently}\qquad c \sqsubseteq \gamma(\alpha(c)) \;\text{ and }\; \alpha(\gamma(a)) \sqsubseteq a\]
concrete: program behaviours abstract: obligation states α γ α(c) ⊑ a ⇔ c ⊑ γ(a) — not established
Without the adjunction, a closed obligation does not transport back to a statement about the program. The arrows exist; the law relating them is not proven.

What is forfeited is the transport theorem, and only that — but that is everything. Given the adjunction and a sound abstract operator (\(\alpha \circ F \sqsubseteq F^\# \circ \alpha\)), one gets \(\alpha(\mathrm{lfp}\,F) \sqsubseteq \mathrm{lfp}\,F^\#\): a fixed point computed in the abstract over-approximates the concrete, so a property proved upstairs holds of every concrete execution downstairs. Without it, closure in the obligation lattice does not inherit that transfer theorem. Tests can still provide empirical evidence about the executions they observe; they do not thereby establish a sound abstract interpretation of all program behavior.

Credit means reaching an obligation's configured evidence bar, not necessarily the top of the lattice: a passing CI check does not imply runtime observation or bundled evidence51. Applicable, discriminating tests add evidence about behavior; closure alone still proves neither that the obligation set is adequate4 nor that all concrete executions satisfy it22.

A ranking function, not a measure. The CentiColon map \(c: S_{\text{spec}} \to \mathbb{N}\) is bounded, with separately reported denominator and residual, and monotone only for transitions preserving obligation IDs and introducing no penalties, ambiguity, or denominator scope change1. Earned credit ascends; a residual derived from it can be a Floyd-style descent witness23 only with an additional strict-progress premise. Boundedness and non-increase alone do not prove termination.

It is not a measure. There is no \(\sigma\)-algebra on \(S_{\text{spec}}\); \(c\) is not additive, because obligations overlap and sixteen penalties plus six cap rules make the rollup non-linear in its parts15; and the normaliser is policy rather than a fixed total mass. A bounded ranking function into a finite ordinal therefore induces no probability, and the repository says so outright24. What that forecloses is concrete: two scores may not be combined by Bayes; there is no complement rule, so 89% closed is not an 11% chance of a defect; and no calibration statement is available, because there is no event space on which to calibrate. Shafer25 and Walley26 are cited as the layer a confidence model would have to occupy separately — correctly, since a belief function would at least supply the monotone-capacity structure that \(c\) lacks.

That is a statement about \(c\) as defined, and about nothing more. It does not show that no transformation, extension or added structure could turn part of this model into a measure or another probabilistic object — the citation of belief functions is precisely an admission that such a layer is available to be built. What is claimed is that \(c\) does not already carry that structure, so probabilistic language applied to today's scores is unlicensed. Building the layer that would license it is open work, and the repository files it as such.

known limitation The shell scorer now delegates arithmetic to the obligation model43, but still supplies weights over CI check names27. A passing check earns its weight at the positive-test bar; it is not runtime observation or bundled evidence. This wiring does not implement the full methodology table of multipliers, evidence credits, caps and penalties15. The committed dashboard remains a historical 890/990 snapshot28, not a measurement of this release.

what happens next The framework specification still names a separate scoring implementation29. The shipped path centralises the arithmetic; completing and evidencing the richer scoring contract remains distinct work. Lua adds scenario/requirement extraction and recorded positive-test grading, not yet integrated invariant credit or full evidence attribution474950. The advisory residual and the weighted CI score are distinct projections, not interchangeable units.

known limitation The methodology's own complexity constraint — methodology-to-codebase ratio below 0.15, with a red flag at 5000 lines of CI validators30 — has no enforcing check that we found, while the script that computes the score dispatches a large shell validator corpus.

what happens next The rule names two measurement procedures30; no enforcing implementation was found in the scripts searched.

CRDTs as algebra

The plan ledger is a genuine convergent replicated data type. State is \(\text{base} \oplus \mathrm{fold}(\text{fragments})\); the carriers are two join-semilattices31: grow-only sets of packets and of events keyed by \((\text{packet id}, \text{event identity})\), with \(x \sqcup y = x \cup y\); and last-writer-wins registers per \((\text{packet id}, \text{field})\), the winner chosen by the total order \((\text{timestamp}, \text{host})\).

\[\text{state} = \Big(\bigcup_i P_i,\; \bigcup_i E_i,\; \textstyle\bigsqcup_i R_i\Big), \qquad \bigsqcup \text{ componentwise}\]

The algebra: a commutative, associative, idempotent \(\sqcup\) induces a partial order \(x \le y \iff x \sqcup y = y\) under which \(x \sqcup y = \sup\{x,y\}\) — the join is the least upper bound, which is why the three properties are a definition rather than a checklist. Convergence follows because the fold is then a function of the set of delivered updates, not their sequence: idempotence kills duplicates, commutativity and associativity kill order.

agent A — offline agent B — offline a new factanother fact a third facta fourth fold — order does not matter set union preserves facts; registers choose a winning value
With the same delivered facts and valid merge rules, replicas agree regardless of arrival order. This illustration shows set union, not every field in the ledger.

State exactly what that buys, because it is strictly less than the folklore suggests32. It gives: replicas having delivered the same update set hold equal state. It does not give delivery — liveness is someone else's problem; it does not give correctness of the converged value; and it does not preserve concurrent intent, which is where LWW does its damage. Applying LWW to a list silently discards the loser's entries, which is why events are a set and not a register — a distinction the file calls the whole correctness argument31. Determinism is pinned by folding in \((\text{timestamp}, \text{filename})\) order rather than directory order33, and two of the three properties — commutativity and idempotence — are tested by name34.

The status field is not a plain register: it composes a monotone join over a closure rank with LWW as tiebreak at equal rank, treats terminal-but-lateral values as non-ladder moves, and permits descent only under an explicit falsification flag35 — a lexicographic join with a deliberate non-monotone escape hatch, so that monotonicity cannot force retention of a certainty later shown false.

checked The repository retracts its own CRDT claim where it fails, and names the property that failed: a deduplication routine's header states plainly that it is a seen-set dedup, not a CRDT, because first-wins keeps whichever duplicate arrives first, so commutativity is false36. Most projects keep the word.

checked Componentwise maximum gives version tuples a join-semilattice. The document uses that algebra to motivate mergeable version coordinates37. Its claim that SemVer has no natural total order is too broad: version precedence and preservation of branch causality are different properties.

checked Semantic merges are typed honestly as "semantic cache with CRDT preconditions" rather than as CRDTs, with the anti-pattern named: calling a lossy cache a CRDT manufactures false convergence claims32. The claim registry holds the lineage at strength "external analogy" until generative property tests exist38.

known limitation That same versioning document defines its two leading components as a contract version and a feature phase39. The live scheme is a temporal anchor throughout — years since epoch, month, day, build. The join algebra survives untouched, since componentwise max does not care what the coordinates mean; the documented semantics of two of four components are false.

what happens next The release-shape test was corrected; the versioning document still gives the old meanings.

Verdict

The formal core is a finite product lattice, a finite monotone-inflationary stabilisation under declared validators at an operator-gated bar, a candidate residual ranking function, and a moving-target result whose zero-floor corollary needs a progress premise. A metric is a separate missing hypothesis of the Banach argument. Contraction, Galois connections and probabilistic readings are named absent rather than assumed40. As a thesis position that is defensible, and I did not expect to write that sentence.

Two boundaries remain. Generative tests cover the committed refinement model, not the semantic correctness of all live validators. And dependent iteration still lacks a convergence theorem, although the methodology now retracts the unsupported strong-law claim. Stable identifiers and centralised score arithmetic make the bookkeeping more concrete; they do not turn closure into probability or a dashboard snapshot into release evidence. CentiColons supply an executable accounting direction; complete attribution, an aligned scoring policy and an enforced durable comparison are still needed for the non-regression guarantee. The stated progress premise suffices for eventual zero and the cycle bound.

How we know — 51 sources

Each small number in the text opens the exact lines of the Tillandsias source that back that sentence, as they stand in release v56.10.9.1, the one the install commands give you. Hover a number to read the quoted lines without leaving the page. A source marked with its own release points at a newer one.

  1. 1Formal objects — obligation, the state chain, product lattices, and the ranking function's monotonicity conditions methodology/math-foundations.yaml#L13-L43A named, auditable requirement, invariant, litmus signal, trace signal, provenance binding, or environment assumption with a stable ID.
  2. 2Stated purpose — separating defensible mathematics from analogy methodology/math-foundations.yaml#L4-L11Separate the math the methodology can defend from analogies it must not overclaim.
  3. 3Davey & Priestley, *Introduction to Lattices and Order*, 2nd ed. — product orders, heights, completeness doi.org/10.1017/CBO9780511809088
  4. 4The lattice-model claim and its stated limit methodology/math-foundations.yaml#L46-L59This proves monotonicity of the model, not truth of the modeled requirement or adequacy of the chosen obligation set.
  5. 5The fixed-point claim: idempotence of refine, and its limit methodology/math-foundations.yaml#L61-L74Define the refinement operator over a finite artifact snapshot and check idempotence: refine(refine(state)) == refine(state).
  6. 6Tarski, *A lattice-theoretical fixpoint theorem and its applications*, Pacific J. Math. 5 (1955) — the Knaster–Tarski theorem, generalising Knaster's 1928 powerset case doi.org/10.2140/pjm.1955.5.285
  7. 7Kleene, *Introduction to Metamathematics* (1952), cited for the iterative least-fixed-point construction archive.org/details/introductiontome00klee
  8. 8Bar-raise governance — the bar is fixed within a release, rises only by operator decision, and the automation must not self-escalate methodology/convergence.yaml#L502-L504is a one-off scope expansion that The Tlatoāni MUST approve every time. Recurring automation (the meta-orchestration loop) MUST NOT self-escalate the bar.
  9. 9The staged validation program: the monotonicity property tests and the denominator scope-change check methodology/math-foundations.yaml#L175-L198- allowed_evidence_transitions_are_monotone - tombstone_and_scope_change_transitions_are_explicitly_non_monotone
  10. 10The evidence-credit table containing `requirement_has_stable_id` methodology/proximity.yaml#L88-L97requirement_has_stable_id: 0.10
  11. 11Multi-version convergence: the moving target, the residual floor, and the refusal of the zero-floor claim methodology/philosophy.yaml#L134-L137Release-over-release non-increase of d_v guarantees convergence to some residual floor d_* >= 0; it does not by itself prove d_* = 0. A zero-floor claim would additionally require a validated progress premise that excludes positive residual fixed points
  12. 12Rudin, *Principles of Mathematical Analysis*, 3rd ed., Thm. 3.14 — monotone bounded sequences converge archive.org/details/principlesofmath0000rudi
  13. 13Banach, *Sur les opérations dans les ensembles abstraits*, Fund. Math. 3 (1922) doi.org/10.4064/fm-3-1-133-181
  14. 14Contraction explicitly not claimed, with the metric/operator/constant debt itemised methodology/math-foundations.yaml#L108-L120Without that metric proof, "monotonic convergence" means ordered non-regression plus finite residual descent, not metric contraction.
  15. 15The cap rules, the sixteen penalties, and the rollup that makes the score non-additive in its parts methodology/proximity.yaml#L100-L135requirement_cc = weighted_obligation * earned_credit + penalties bounded to [0, weighted_obligation].
  16. 16The strong-law citation withdrawn for dependent iteration methodology/philosophy.yaml#L29-L47So the almost-sure claim is not available as stated and is withdrawn rather than weakened.
  17. 17Kolmogorov, *Grundbegriffe der Wahrscheinlichkeitsrechnung* (1933) — the strong law and its variance criterion doi.org/10.1007/978-3-642-49888-6
  18. 18Durrett, *Probability: Theory and Examples*, 5th ed. — SLLN, Kolmogorov's three-series theorem, and the ergodic and martingale substitutes doi.org/10.1017/9781108591034
  19. 19Retrieval as a cache; commits as the Lamport clock versioning it — the mechanism that makes iterations dependent methodology/philosophy.yaml#L54-L57That cache is updated on COMMITS — a commit hitting the relevant bits retrains the RAG. Commits are therefore the LAMPORT CLOCK of the RAG models: they causally order and version the cached knowledge against the code
  20. 20Birkhoff, *Proof of the ergodic theorem*, PNAS 17 (1931) doi.org/10.1073/pnas.17.2.656
  21. 21Cousot & Cousot, *Abstract Interpretation*, POPL 1977 doi.org/10.1145/512950.512973
  22. 22The concession: no Galois connection is defined — "an abstraction discipline, not a formal abstract interpreter" methodology/math-foundations.yaml#L76-L89Tillandsias does not yet define a full Galois connection between program semantics and spec obligations. Until then, this is an abstraction discipline, not a formal abstract interpreter.
  23. 23Floyd, *Assigning Meanings to Programs* (1967), cited for ranking-style progress reasoning doi.org/10.1090/psapm/019/0235771
  24. 24Scores are not probabilities; finite coverage is not proof of absence methodology/math-foundations.yaml#L122-L135Evidence bundles and CentiColon scores should not be interpreted as probabilities. If probabilistic or belief-function confidence is later added, it must be a separate layer from obligation closure.
  25. 25Shafer, *A Mathematical Theory of Evidence* (1976), cited as a possible separate confidence layer press.princeton.edu/books/paperback/9780691100425/a-mathematical-theory-of-evidence
  26. 26Walley, *Statistical Reasoning with Imprecise Probabilities* (1991) doi.org/10.1007/978-1-4899-3472-7
  27. 27The shell still supplies weights over CI check names scripts/local-ci.sh#L413-L418check_weight() { case "$1" in spec-cheatsheet-binding) echo 100 ;; spec-code-drift) echo 120 ;; spec-trace-coverage) echo 90 ;; version-monotonicity) echo 40 ;;
  28. 28The committed dashboard's earned and total figures docs/convergence/centicolon-dashboard.json#L75-L76"total_cc": 990, "earned_cc": 890,
  29. 29The framework specification names its intended scoring implementation methodology/litmus-framework.yaml#L88-L96files: - crates/tillandsias-litmus/src/convergence/mod.rs - crates/tillandsias-litmus/src/convergence/centicolon.rs
  30. 30The uninstrumented complexity constraint and its 5000-line red flag methodology/convergence.yaml#L420-L424ratio_constraint: "methodology_complexity / codebase_complexity < 0.15" anti_pattern: > A validation system that requires more code to understand than the code being validated. Red flag: CI validators exceed 5000 lines or require specialized training to understand.
  31. 31The three CRDT primitives, and why each field uses the one it does crates/tillandsias-plan/src/fragments.rs#L28-L39Applying LWW to a LIST would silently discard the loser's entries, which is why events are a set and not a register.
  32. 32Semantic merge typed as a cache with CRDT preconditions, plus the anti-pattern methodology/cheatsheets.yaml#L88-L114Calling a lossy semantic cache a CRDT without stable IDs, tombstones, deterministic merge, and property tests creates false convergence claims.
  33. 33Determinism rules: fold order and idempotence crates/tillandsias-plan/src/fragments.rs#L41-L49Fragments fold in `(ts, filename)` order, never directory order — the filesystem does not promise an order, and two hosts folding differently would compute different states from identical inputs, which presents as corruption rather than as a sorting bug.
  34. 34Commutativity and idempotence of the fold pinned as named tests crates/tillandsias-plan/src/fragments.rs#L3229-L3230fn the_fold_is_commutative_the_defining_crdt_property() { Order of arrival must not change the result.
  35. 35Rank-aware status join with a falsification escape hatch crates/tillandsias-plan/src/fragments.rs#L386-L400The closure ladder implemented<completed<verified<done is a monotone lattice: you climb UP freely and move DOWN only through a `falsified` event.
  36. 36A retracted CRDT claim, with the failed property named crates/tillandsias-plan/lua/collect.lua#L7-L11This is a SEEN-SET DEDUP, not a CRDT — the earlier header's CRDT claim (commutativity in particular) was false: first-wins keeps whichever duplicate arrives first, so order matters.
  37. 37The join-semilattice argument for the version scheme, and the SemVer critique methodology/versioning.yaml#L104-L124SemVer (Major.Minor.Patch) allows arbitrary resets: Patch can go 1→2→1 if someone hotfixes a branch. There is no natural total order.
  38. 38CRDT preconditions filed at claim strength "external analogy" methodology/provenance.yaml#L167-L180claim_strength: external_analogy
  39. 39The documented component semantics the project retired methodology/versioning.yaml#L9-L20Major: meaning: "Contract version — breaking changes only"
  40. 40Thesis defence position: finite ordered convergence under declared validators, with unknown-event intake as the escape hatch methodology/math-foundations.yaml#L200-L206The defensible claim is finite ordered convergence under declared validators: stable obligations form a finite lattice; evidence-improving transitions are checked for monotonicity; CentiColons are a bounded ranking function over that model; fixed points mean validator stability; and unknown-event intake is the escape hatch
  41. 41Generative property tests for monotonicity, inflationarity and idempotence of the refinement operator, over a committed rule set crates/tillandsias-plan/src/obligation_props.rs#L172-L219fn refine_is_monotone_on_the_real_rules((x, y) in comparable_pair()) {
  42. 42The gate that refuses a missing or duplicated requirement identifier, tombstoned specifications included scripts/lua/check-requirement-ids.lua#L5-L10and no identifier appears twice anywhere in the corpus. Tombstoned specs are checked too
  43. 43The shell scorer hands its weights to the model instead of summing them itself scripts/local-ci.sh#L571-L573What LEFT is the arithmetic: earned/denominator/residual are now computed by `tillandsias-plan score-checks`, which runs obligation::centicolon_function over a SpecState.
  44. 44The ranking function in code names which side of the monotone band a score is on crates/tillandsias-plan/src/obligation.rs#L675-L708Regime::Broken("denominator scope changed: an obligation was tombstoned")
  45. 45The shell prints non-comparability when the scorer reports a broken regime scripts/local-ci.sh#L624-L624this score is NOT comparable with the previous run
  46. 46Cacheable and observing predicate capabilities crates/tillandsias-plan/src/lua_predicate.rs#L74-L95Pure: no shell, no clock. Results MAY be cached.
  47. 47Observed grading: latest test/digest record, conditional spec freshness and raw positive-test residual scripts/lua/centicolon-grade-observed.lua#L38-L105
  48. 48Advisory regression comparison and local snapshot, advanced even after a loss scripts/lua/check-centicolon-ratchet.lua#L4-L138the advisory CentiColon R line, ported from check-centicolon-ratchet.sh without changing its protocol stdout or status.
  49. 49Static grading assigns requirement-level candidates to each scenario scripts/lua/centicolon-grade-static.lua#L180-L201
  50. 50Invariants are emitted separately from scenario/requirement obligations scripts/lua/centicolon-extract.lua#L258-L266
  51. 51A configured positive-test bar does not witness the whole evidence chain crates/tillandsias-plan/src/obligation.rs#L640-L646a CI check that passes establishes `PositivelyTested` and says nothing about `RuntimeObserved` or `EvidenceBundled`.

I want it!

One line in a terminal, one photo, one GitHub app, one prompt.

Linux
macOS
Windows

Each line fetches a short script from this site, which resolves the latest stable release on GitHub and runs that release’s own installer. Stable moves only when a daily build is promoted, so it normally trails the newest code. The scripts here are not rebuilt when the app releases; they look the release up every time they run. Current installers reset local application state and reprovision by default; set TILLANDSIAS_DESTRUCTIVE_RESET_OK=0 before running one to skip the destructive reset. On Linux and Windows that reset keeps your local vault and sign-ins; on macOS the stable release’s reset still clears them, and a fix is pending.

  1. Paste

    Run the line above. When it finishes, a Tillandsias icon sits in your tray.

  2. Scan

    Choose GitHub login from that icon, point your phone at the QR code in the terminal and confirm the code at github.com/login/device.

  3. Authorize

    Install the Tillandsias GitHub App and choose which repositories it can use.

  4. Prompt

    Pick a project from the same icon and start typing.

Live progress

The accountability ledger: findings, the full pinned runtime spec inventory, and recorded change checklists. Every entry starts collapsed; open it for evidence or source. This is a build-time snapshot, not live telemetry.

62 findings11 resolved184 runtime specs219 archived changes115 checklists complete

“Complete” means all tasks in that archived checklist are checked. It does not certify current runtime behavior. The canonical spec status (“active”, “obsolete”, etc.) describes document lifecycle. See the stable audit for checked capability limits.

How much work is still open

The whole history of the project up to release v56.10.9.1, from its first day. Time is squeezed toward the past: recent weeks get the most room, and older months are drawn narrower but never cut off. Dotted lines mark releases. Point at any mark to read its value. In the top chart, each grey dot is one test run. The line is the lowest value reached so far: it can only go down, while the runs themselves went up and down.

First v0.0 release, 2026-03-22v0.0First v0.1 release, 2026-03-25First v0.2 release, 2026-05-19v0.2First v0.3 release, 2026-06-05The project raised its own quality bar, 2026-07-10stricterFirst v0.4 release, 2026-07-28First v56.8 release, 2026-08-31v56.8First v56.9 release, 2026-09-02First v56.10 release, 2026-10-08v56.10This release, v56.10.9.1, 2026-10-09 Open obligations, as measured by each test run (CentiColons) dots: each run · line: lowest reached 0 285 570 2026-05-07: 380 still open out of 700 2026-05-07: 380 still open out of 700 2026-05-07: 380 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 340 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 340 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 340 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 340 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 340 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 340 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 340 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 340 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 340 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 340 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 340 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 340 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 340 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 340 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 340 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 340 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 340 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 340 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 340 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 340 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 340 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 340 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 340 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 340 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 340 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 340 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 340 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 340 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 340 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 340 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 340 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 340 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 340 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 340 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 340 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 520 still open out of 700 2026-05-07: 340 still open out of 700 2026-05-07: 340 still open out of 700 2026-05-07: 340 still open out of 700 2026-05-07: 340 still open out of 700 2026-05-07: 340 still open out of 700 2026-05-07: 340 still open out of 700 2026-05-07: 340 still open out of 700 2026-05-07: 340 still open out of 700 2026-05-07: 340 still open out of 700 2026-05-07: 340 still open out of 700 2026-05-07: 340 still open out of 700 2026-05-07: 340 still open out of 700 2026-05-07: 340 still open out of 700 2026-05-07: 340 still open out of 700 2026-05-07: 340 still open out of 700 2026-05-07: 260 still open out of 700 2026-05-07: 260 still open out of 700 2026-05-07: 260 still open out of 700 2026-05-07: 140 still open out of 700 2026-05-07: 140 still open out of 700 2026-05-07: 140 still open out of 700 2026-05-07: 140 still open out of 700 2026-05-08: 0 still open out of 660 2026-05-08: 0 still open out of 120 2026-05-08: 0 still open out of 140 2026-05-08: 0 still open out of 660 2026-05-08: 0 still open out of 120 2026-05-08: 0 still open out of 140 2026-05-08: 0 still open out of 560 2026-05-08: 0 still open out of 660 2026-05-08: 0 still open out of 660 2026-05-08: 0 still open out of 120 2026-05-12: 40 still open out of 560 2026-05-12: 40 still open out of 560 2026-05-12: 0 still open out of 560 2026-05-12: 0 still open out of 660 2026-05-12: 0 still open out of 120 2026-05-12: 0 still open out of 560 2026-05-12: 0 still open out of 560 2026-05-12: 0 still open out of 660 2026-05-12: 0 still open out of 660 2026-05-12: 0 still open out of 120 2026-05-12: 0 still open out of 660 2026-05-12: 0 still open out of 120 2026-05-12: 0 still open out of 660 2026-05-12: 0 still open out of 120 2026-05-12: 0 still open out of 140 2026-05-12: 0 still open out of 660 2026-05-12: 0 still open out of 120 2026-05-12: 0 still open out of 660 2026-05-12: 0 still open out of 120 2026-05-12: 0 still open out of 140 2026-05-12: 40 still open out of 660 2026-05-12: 0 still open out of 660 2026-05-12: 0 still open out of 660 2026-05-12: 0 still open out of 120 2026-05-12: 180 still open out of 660 2026-05-12: 180 still open out of 660 2026-05-12: 0 still open out of 660 2026-05-12: 0 still open out of 120 2026-05-12: 0 still open out of 140 2026-05-13: 100 still open out of 560 2026-05-13: 0 still open out of 560 2026-05-13: 0 still open out of 560 2026-05-13: 0 still open out of 660 2026-05-13: 0 still open out of 120 2026-05-13: 0 still open out of 140 2026-05-13: 0 still open out of 560 2026-05-14: 0 still open out of 560 2026-05-14: 0 still open out of 560 2026-05-14: 0 still open out of 560 2026-05-14: 0 still open out of 560 2026-05-14: 0 still open out of 660 2026-05-14: 0 still open out of 120 2026-05-14: 0 still open out of 140 2026-05-14: 0 still open out of 560 2026-05-14: 0 still open out of 660 2026-05-14: 0 still open out of 120 2026-05-14: 0 still open out of 560 2026-05-14: 0 still open out of 560 2026-05-14: 0 still open out of 660 2026-05-14: 0 still open out of 120 2026-05-14: 0 still open out of 140 2026-05-14: 0 still open out of 560 2026-05-14: 0 still open out of 660 2026-05-14: 0 still open out of 120 2026-05-14: 0 still open out of 140 2026-05-14: 0 still open out of 560 2026-05-14: 0 still open out of 660 2026-05-14: 0 still open out of 120 2026-05-14: 0 still open out of 560 2026-05-14: 40 still open out of 560 2026-05-14: 0 still open out of 560 2026-05-14: 200 still open out of 660 2026-05-14: 140 still open out of 660 2026-05-14: 0 still open out of 660 2026-05-14: 100 still open out of 660 2026-05-14: 180 still open out of 920 2026-05-14: 40 still open out of 560 2026-05-14: 0 still open out of 560 2026-05-14: 100 still open out of 660 2026-05-14: 100 still open out of 560 2026-05-14: 270 still open out of 650 2026-05-14: 270 still open out of 650 2026-05-14: 270 still open out of 650 2026-05-14: 370 still open out of 750 2026-05-14: 370 still open out of 750 2026-05-14: 570 still open out of 1010 2026-05-14: 570 still open out of 1010 2026-05-14: 370 still open out of 750 2026-05-14: 230 still open out of 750 2026-05-14: 230 still open out of 750 2026-05-14: 290 still open out of 750 2026-05-14: 290 still open out of 750 2026-05-14: 290 still open out of 750 2026-05-14: 410 still open out of 750 2026-05-14: 250 still open out of 750 2026-05-15: 290 still open out of 750 2026-05-15: 190 still open out of 750 2026-05-15: 150 still open out of 750 2026-05-15: 390 still open out of 1010 2026-05-15: 270 still open out of 750 2026-05-15: 390 still open out of 1010 2026-05-15: 60 still open out of 650 2026-05-15: 60 still open out of 650 2026-05-15: 60 still open out of 650 2026-05-15: 60 still open out of 650 2026-05-15: 100 still open out of 650 2026-05-15: 60 still open out of 650 2026-05-15: 160 still open out of 750 2026-05-15: 100 still open out of 650 2026-05-15: 100 still open out of 650 2026-05-15: 100 still open out of 650 2026-05-15: 100 still open out of 650 2026-05-15: 140 still open out of 650 2026-05-15: 60 still open out of 650 2026-05-15: 60 still open out of 650 2026-05-15: 60 still open out of 650 2026-05-15: 60 still open out of 650 2026-05-15: 100 still open out of 650 2026-05-15: 60 still open out of 650 2026-05-15: 0 still open out of 650 2026-05-15: 40 still open out of 650 2026-05-15: 40 still open out of 750 2026-05-15: 0 still open out of 750 2026-05-15: 100 still open out of 750 2026-05-15: 100 still open out of 750 2026-05-15: 0 still open out of 650 2026-05-15: 120 still open out of 1010 2026-05-15: 120 still open out of 120 2026-05-15: 0 still open out of 120 2026-05-15: 100 still open out of 750 2026-05-15: 0 still open out of 120 2026-05-15: 100 still open out of 750 2026-05-15: 100 still open out of 750 2026-05-15: 100 still open out of 750 2026-05-15: 100 still open out of 750 2026-05-16: 0 still open out of 1010 2026-05-16: 0 still open out of 750 2026-05-16: 0 still open out of 120 2026-05-16: 0 still open out of 750 2026-05-16: 0 still open out of 120 2026-05-16: 0 still open out of 750 2026-05-16: 0 still open out of 120 2026-05-16: 0 still open out of 140 2026-05-16: 40 still open out of 750 2026-05-16: 0 still open out of 750 2026-05-16: 0 still open out of 120 2026-05-16: 0 still open out of 140 2026-05-16: 0 still open out of 750 2026-05-16: 0 still open out of 120 2026-05-16: 0 still open out of 750 2026-05-16: 0 still open out of 120 2026-05-16: 0 still open out of 750 2026-05-16: 0 still open out of 120 2026-05-16: 0 still open out of 750 2026-05-16: 0 still open out of 750 2026-05-16: 0 still open out of 120 2026-05-16: 0 still open out of 750 2026-05-16: 0 still open out of 120 2026-05-16: 0 still open out of 750 2026-05-16: 0 still open out of 120 2026-05-16: 0 still open out of 750 2026-05-16: 0 still open out of 120 2026-05-16: 140 still open out of 140 2026-05-16: 0 still open out of 750 2026-05-16: 0 still open out of 120 2026-05-16: 140 still open out of 140 2026-05-16: 140 still open out of 140 2026-05-16: 140 still open out of 140 2026-05-16: 140 still open out of 140 2026-05-16: 140 still open out of 140 2026-05-16: 140 still open out of 140 2026-05-16: 140 still open out of 140 2026-05-16: 140 still open out of 140 2026-05-16: 140 still open out of 140 2026-05-16: 140 still open out of 140 2026-05-16: 140 still open out of 140 2026-05-16: 0 still open out of 140 2026-05-16: 0 still open out of 750 2026-05-16: 0 still open out of 120 2026-05-16: 0 still open out of 140 2026-05-16: 140 still open out of 750 2026-05-16: 140 still open out of 750 2026-05-16: 100 still open out of 750 2026-05-16: 100 still open out of 750 2026-05-16: 0 still open out of 750 2026-05-16: 0 still open out of 120 2026-05-16: 0 still open out of 750 2026-05-16: 0 still open out of 120 2026-05-16: 0 still open out of 140 2026-05-17: 0 still open out of 750 2026-05-17: 0 still open out of 120 2026-05-17: 0 still open out of 140 2026-05-17: 40 still open out of 750 2026-05-17: 0 still open out of 750 2026-05-17: 0 still open out of 750 2026-05-17: 0 still open out of 750 2026-05-17: 0 still open out of 750 2026-05-17: 0 still open out of 120 2026-05-17: 0 still open out of 750 2026-05-17: 0 still open out of 120 2026-05-17: 0 still open out of 140 2026-05-17: 40 still open out of 750 2026-05-17: 0 still open out of 750 2026-05-17: 0 still open out of 120 2026-05-17: 0 still open out of 140 2026-05-17: 80 still open out of 750 2026-05-17: 80 still open out of 750 2026-05-17: 80 still open out of 750 2026-05-17: 0 still open out of 750 2026-05-17: 0 still open out of 750 2026-05-17: 0 still open out of 120 2026-05-17: 0 still open out of 750 2026-05-17: 100 still open out of 750 2026-05-17: 0 still open out of 120 2026-05-17: 0 still open out of 140 2026-05-17: 0 still open out of 750 2026-05-17: 0 still open out of 120 2026-05-17: 0 still open out of 140 2026-05-18: 0 still open out of 750 2026-05-18: 0 still open out of 120 2026-05-18: 0 still open out of 140 2026-05-18: 0 still open out of 750 2026-05-18: 0 still open out of 120 2026-05-18: 0 still open out of 140 2026-05-18: 0 still open out of 750 2026-05-18: 0 still open out of 120 2026-05-18: 0 still open out of 140 2026-05-18: 220 still open out of 750 2026-05-18: 220 still open out of 750 2026-05-18: 0 still open out of 750 2026-05-18: 0 still open out of 750 2026-05-19: 0 still open out of 750 2026-05-19: 0 still open out of 120 2026-05-19: 0 still open out of 140 2026-05-19: 0 still open out of 1010 2026-05-19: 100 still open out of 750 2026-05-19: 0 still open out of 750 2026-05-19: 0 still open out of 120 2026-05-19: 0 still open out of 140 2026-05-19: 0 still open out of 750 2026-05-19: 0 still open out of 120 2026-05-19: 0 still open out of 140 2026-05-19: 160 still open out of 750 2026-05-19: 0 still open out of 750 2026-05-19: 0 still open out of 120 2026-05-19: 0 still open out of 140 2026-05-19: 0 still open out of 750 2026-05-19: 0 still open out of 120 2026-05-19: 0 still open out of 140 2026-05-19: 80 still open out of 750 2026-05-19: 0 still open out of 750 2026-05-19: 0 still open out of 120 2026-05-19: 0 still open out of 140 2026-05-20: 0 still open out of 750 2026-05-20: 0 still open out of 750 2026-05-20: 0 still open out of 120 2026-05-20: 0 still open out of 750 2026-05-20: 0 still open out of 120 2026-05-20: 140 still open out of 140 2026-05-20: 0 still open out of 140 2026-05-20: 140 still open out of 140 2026-05-20: 140 still open out of 140 2026-05-20: 0 still open out of 140 2026-05-21: 0 still open out of 750 2026-05-21: 0 still open out of 120 2026-05-21: 0 still open out of 140 2026-05-21: 0 still open out of 140 2026-05-21: 0 still open out of 750 2026-05-21: 0 still open out of 750 2026-05-21: 0 still open out of 120 2026-05-21: 0 still open out of 140 2026-05-21: 0 still open out of 140 2026-05-21: 0 still open out of 750 2026-05-21: 0 still open out of 120 2026-05-21: 0 still open out of 140 2026-05-21: 0 still open out of 140 2026-05-21: 60 still open out of 750 2026-05-21: 0 still open out of 750 2026-05-21: 0 still open out of 120 2026-05-21: 0 still open out of 140 2026-05-21: 0 still open out of 140 2026-05-21: 0 still open out of 750 2026-05-21: 0 still open out of 120 2026-05-21: 60 still open out of 750 2026-05-21: 60 still open out of 750 2026-05-21: 60 still open out of 750 2026-05-21: 0 still open out of 750 2026-05-21: 0 still open out of 120 2026-05-21: 0 still open out of 140 2026-05-21: 0 still open out of 750 2026-05-21: 0 still open out of 120 2026-05-21: 0 still open out of 140 2026-05-21: 0 still open out of 750 2026-05-21: 0 still open out of 120 2026-05-21: 0 still open out of 140 2026-05-22: 40 still open out of 750 2026-05-22: 0 still open out of 750 2026-05-22: 0 still open out of 120 2026-05-22: 0 still open out of 140 2026-05-22: 0 still open out of 750 2026-05-22: 0 still open out of 120 2026-05-22: 0 still open out of 140 2026-05-25: 100 still open out of 750 2026-05-25: 100 still open out of 750 2026-05-25: 40 still open out of 750 2026-05-25: 0 still open out of 750 2026-05-25: 0 still open out of 120 2026-05-25: 0 still open out of 140 2026-05-25: 0 still open out of 750 2026-05-25: 0 still open out of 120 2026-05-25: 0 still open out of 140 2026-05-26: 0 still open out of 750 2026-05-26: 0 still open out of 120 2026-05-26: 0 still open out of 140 2026-05-26: 60 still open out of 750 2026-05-26: 0 still open out of 750 2026-05-26: 0 still open out of 120 2026-05-26: 0 still open out of 140 2026-05-26: 0 still open out of 750 2026-05-26: 0 still open out of 120 2026-05-26: 0 still open out of 140 2026-05-26: 0 still open out of 750 2026-05-26: 0 still open out of 120 2026-05-26: 0 still open out of 140 2026-05-26: 0 still open out of 750 2026-05-26: 0 still open out of 120 2026-05-26: 0 still open out of 140 2026-05-26: 0 still open out of 750 2026-05-26: 0 still open out of 120 2026-05-26: 0 still open out of 140 2026-05-26: 0 still open out of 750 2026-05-26: 0 still open out of 120 2026-05-26: 0 still open out of 140 2026-05-26: 0 still open out of 750 2026-05-26: 0 still open out of 120 2026-05-26: 0 still open out of 140 2026-05-26: 0 still open out of 750 2026-05-26: 0 still open out of 750 2026-05-26: 0 still open out of 120 2026-05-26: 0 still open out of 140 2026-05-26: 0 still open out of 750 2026-05-26: 0 still open out of 120 2026-05-26: 0 still open out of 140 2026-05-26: 0 still open out of 750 2026-05-26: 0 still open out of 120 2026-05-26: 0 still open out of 140 2026-05-26: 60 still open out of 750 2026-05-26: 60 still open out of 750 2026-05-26: 60 still open out of 750 2026-05-26: 0 still open out of 750 2026-05-26: 0 still open out of 120 2026-05-26: 0 still open out of 140 2026-05-26: 40 still open out of 750 2026-05-26: 40 still open out of 750 2026-05-27: 0 still open out of 750 2026-05-27: 0 still open out of 120 2026-05-27: 0 still open out of 140 2026-05-28: 40 still open out of 750 2026-05-28: 0 still open out of 750 2026-05-28: 120 still open out of 120 2026-05-28: 60 still open out of 750 2026-05-28: 0 still open out of 750 2026-05-28: 0 still open out of 750 2026-05-28: 0 still open out of 120 2026-05-28: 0 still open out of 140 2026-05-28: 0 still open out of 750 2026-05-28: 0 still open out of 120 2026-05-28: 0 still open out of 140 2026-05-28: 60 still open out of 650 2026-05-28: 0 still open out of 650 2026-05-28: 0 still open out of 650 2026-05-28: 0 still open out of 750 2026-05-28: 0 still open out of 120 2026-05-28: 0 still open out of 140 2026-05-28: 0 still open out of 750 2026-05-28: 0 still open out of 120 2026-05-28: 0 still open out of 140 2026-05-28: 0 still open out of 750 2026-05-29: 80 still open out of 750 2026-05-29: 0 still open out of 1010 2026-05-29: 0 still open out of 1010 2026-05-29: 0 still open out of 1010 2026-05-29: 0 still open out of 1010 2026-05-29: 0 still open out of 1010 2026-05-29: 120 still open out of 1010 2026-05-29: 120 still open out of 1010 2026-05-29: 0 still open out of 1010 2026-05-29: 0 still open out of 650 2026-05-29: 0 still open out of 650 2026-05-29: 0 still open out of 750 2026-05-29: 120 still open out of 120 2026-05-29: 0 still open out of 650 2026-05-29: 0 still open out of 750 2026-05-29: 120 still open out of 120 2026-05-29: 0 still open out of 750 2026-05-29: 120 still open out of 120 2026-05-29: 0 still open out of 650 2026-05-29: 0 still open out of 750 2026-05-29: 0 still open out of 120 2026-05-29: 0 still open out of 140 2026-05-29: 0 still open out of 650 2026-05-29: 0 still open out of 650 2026-05-29: 0 still open out of 750 2026-05-29: 120 still open out of 120 2026-05-31: 60 still open out of 750 2026-06-01: 60 still open out of 750 2026-06-01: 0 still open out of 750 2026-06-01: 120 still open out of 120 2026-06-01: 0 still open out of 750 2026-06-01: 120 still open out of 120 2026-06-01: 0 still open out of 750 2026-06-01: 0 still open out of 120 2026-06-01: 0 still open out of 140 2026-06-02: 0 still open out of 750 2026-06-02: 0 still open out of 120 2026-06-02: 0 still open out of 140 2026-06-02: 340 still open out of 1010 2026-06-02: 320 still open out of 750 2026-06-02: 220 still open out of 750 2026-06-02: 320 still open out of 750 2026-06-02: 280 still open out of 750 2026-06-03: 320 still open out of 750 2026-06-03: 40 still open out of 750 2026-06-03: 0 still open out of 750 2026-06-03: 0 still open out of 120 2026-06-03: 0 still open out of 140 2026-06-03: 0 still open out of 750 2026-06-03: 120 still open out of 120 2026-06-03: 0 still open out of 750 2026-06-03: 120 still open out of 120 2026-06-03: 0 still open out of 750 2026-06-03: 120 still open out of 120 2026-06-03: 0 still open out of 750 2026-06-03: 40 still open out of 750 2026-06-03: 0 still open out of 750 2026-06-03: 0 still open out of 750 2026-06-03: 0 still open out of 750 2026-06-03: 0 still open out of 120 2026-06-03: 0 still open out of 140 2026-06-03: 320 still open out of 750 2026-06-03: 480 still open out of 1010 2026-06-03: 280 still open out of 1010 2026-06-03: 160 still open out of 1010 2026-06-03: 120 still open out of 1010 2026-06-03: 120 still open out of 1010 2026-06-03: 120 still open out of 1010 2026-06-03: 120 still open out of 1010 2026-06-03: 0 still open out of 1010 2026-06-03: 0 still open out of 750 2026-06-03: 0 still open out of 750 2026-06-03: 0 still open out of 1010 2026-06-03: 0 still open out of 750 2026-06-03: 0 still open out of 120 2026-06-03: 0 still open out of 120 2026-06-03: 0 still open out of 140 2026-06-03: 0 still open out of 140 2026-06-04: 60 still open out of 750 2026-06-04: 0 still open out of 750 2026-06-04: 0 still open out of 120 2026-06-04: 0 still open out of 140 2026-06-04: 140 still open out of 750 2026-06-04: 0 still open out of 750 2026-06-04: 40 still open out of 750 2026-06-04: 0 still open out of 750 2026-06-05: 0 still open out of 750 2026-06-06: 0 still open out of 750 2026-06-07: 160 still open out of 750 2026-06-07: 200 still open out of 750 2026-06-07: 100 still open out of 1010 2026-06-07: 60 still open out of 1010 2026-06-07: 0 still open out of 1010 2026-06-07: 200 still open out of 750 2026-06-07: 280 still open out of 750 2026-06-07: 200 still open out of 750 2026-06-07: 200 still open out of 750 2026-06-07: 200 still open out of 750 2026-06-07: 200 still open out of 750 2026-06-07: 200 still open out of 750 2026-06-07: 100 still open out of 750 2026-06-07: 0 still open out of 750 2026-06-07: 0 still open out of 750 2026-06-07: 0 still open out of 750 2026-06-07: 0 still open out of 750 2026-06-07: 0 still open out of 750 2026-06-07: 60 still open out of 750 2026-06-07: 0 still open out of 750 2026-06-07: 0 still open out of 750 2026-06-07: 140 still open out of 750 2026-06-07: 0 still open out of 750 2026-06-07: 0 still open out of 750 2026-06-07: 0 still open out of 750 2026-06-07: 0 still open out of 750 2026-06-07: 60 still open out of 750 2026-06-07: 0 still open out of 750 2026-06-07: 0 still open out of 750 2026-06-07: 0 still open out of 750 2026-06-07: 0 still open out of 750 2026-06-07: 0 still open out of 750 2026-06-07: 0 still open out of 750 2026-06-07: 0 still open out of 750 2026-06-07: 0 still open out of 750 2026-06-07: 0 still open out of 750 2026-06-07: 100 still open out of 750 2026-06-07: 0 still open out of 750 2026-06-07: 0 still open out of 750 2026-06-07: 0 still open out of 750 2026-06-07: 0 still open out of 750 2026-06-07: 0 still open out of 750 2026-06-07: 0 still open out of 750 2026-06-07: 0 still open out of 750 2026-06-07: 0 still open out of 750 2026-06-07: 0 still open out of 750 2026-06-07: 0 still open out of 750 2026-06-07: 0 still open out of 750 2026-06-07: 0 still open out of 1010 2026-06-07: 0 still open out of 750 2026-06-07: 0 still open out of 750 2026-06-08: 100 still open out of 750 2026-06-08: 40 still open out of 750 2026-06-08: 0 still open out of 750 2026-06-08: 0 still open out of 750 2026-06-08: 200 still open out of 750 2026-06-08: 0 still open out of 750 2026-06-08: 40 still open out of 750 2026-06-08: 240 still open out of 750 2026-06-08: 140 still open out of 650 2026-06-08: 240 still open out of 750 2026-06-08: 100 still open out of 750 2026-06-08: 100 still open out of 750 2026-06-08: 100 still open out of 750 2026-06-08: 0 still open out of 750 2026-06-08: 0 still open out of 750 2026-06-08: 0 still open out of 750 2026-06-08: 0 still open out of 750 2026-06-08: 0 still open out of 750 2026-06-08: 0 still open out of 750 2026-06-08: 0 still open out of 750 2026-06-08: 0 still open out of 750 2026-06-09: 0 still open out of 750 2026-06-09: 0 still open out of 750 2026-06-09: 40 still open out of 750 2026-06-09: 40 still open out of 750 2026-06-09: 0 still open out of 750 2026-06-09: 0 still open out of 750 2026-06-09: 0 still open out of 750 2026-06-09: 0 still open out of 750 2026-06-09: 0 still open out of 750 2026-06-09: 0 still open out of 750 2026-06-09: 0 still open out of 750 2026-06-09: 0 still open out of 750 2026-06-09: 40 still open out of 750 2026-06-09: 0 still open out of 750 2026-06-09: 0 still open out of 750 2026-06-09: 0 still open out of 750 2026-06-09: 0 still open out of 750 2026-06-12: 0 still open out of 750 2026-06-12: 0 still open out of 750 2026-06-14: 200 still open out of 750 2026-06-14: 200 still open out of 750 2026-06-14: 100 still open out of 750 2026-06-14: 0 still open out of 140 2026-06-14: 0 still open out of 750 2026-06-14: 0 still open out of 650 2026-06-14: 0 still open out of 750 2026-06-14: 0 still open out of 1010 2026-06-14: 0 still open out of 750 2026-06-14: 0 still open out of 750 2026-06-14: 100 still open out of 650 2026-06-14: 0 still open out of 650 2026-06-14: 40 still open out of 650 2026-06-14: 0 still open out of 650 2026-06-14: 0 still open out of 650 2026-06-14: 0 still open out of 750 2026-06-14: 0 still open out of 750 2026-06-14: 0 still open out of 750 2026-06-14: 0 still open out of 750 2026-06-14: 0 still open out of 750 2026-06-14: 0 still open out of 750 2026-06-14: 40 still open out of 750 2026-06-14: 0 still open out of 750 2026-06-15: 0 still open out of 750 2026-06-16: 0 still open out of 750 2026-06-16: 0 still open out of 750 2026-06-16: 0 still open out of 750 2026-06-16: 0 still open out of 750 2026-06-16: 0 still open out of 750 2026-06-17: 180 still open out of 750 2026-06-17: 0 still open out of 750 2026-06-17: 0 still open out of 750 2026-06-17: 0 still open out of 750 2026-06-17: 0 still open out of 750 2026-06-17: 0 still open out of 140 2026-06-18: 100 still open out of 750 2026-06-18: 0 still open out of 750 2026-06-19: 140 still open out of 750 2026-06-19: 100 still open out of 750 2026-06-19: 100 still open out of 750 2026-06-19: 0 still open out of 750 2026-06-19: 0 still open out of 750 2026-06-19: 40 still open out of 750 2026-06-19: 0 still open out of 750 2026-06-19: 0 still open out of 750 2026-06-19: 0 still open out of 750 2026-06-20: 160 still open out of 750 2026-06-20: 0 still open out of 750 2026-06-20: 100 still open out of 750 2026-06-20: 0 still open out of 750 2026-06-20: 0 still open out of 750 2026-06-20: 0 still open out of 750 2026-06-20: 0 still open out of 750 2026-06-20: 100 still open out of 750 2026-06-20: 0 still open out of 750 2026-06-20: 0 still open out of 750 2026-06-21: 140 still open out of 750 2026-06-21: 100 still open out of 750 2026-06-21: 0 still open out of 750 2026-06-21: 0 still open out of 650 2026-06-23: 100 still open out of 750 2026-06-23: 100 still open out of 750 2026-06-23: 160 still open out of 750 2026-06-23: 100 still open out of 750 2026-06-23: 100 still open out of 750 2026-06-23: 0 still open out of 750 2026-06-24: 0 still open out of 750 2026-06-25: 0 still open out of 750 2026-06-26: 40 still open out of 750 2026-06-26: 0 still open out of 750 2026-06-26: 0 still open out of 750 2026-06-26: 0 still open out of 750 2026-06-26: 0 still open out of 750 2026-07-02: 0 still open out of 650 2026-07-02: 160 still open out of 650 2026-07-02: 160 still open out of 650 2026-07-02: 120 still open out of 650 2026-07-02: 0 still open out of 650 2026-07-03: 180 still open out of 750 2026-07-03: 100 still open out of 750 2026-07-04: 0 still open out of 710 2026-07-04: 100 still open out of 810 2026-07-04: 100 still open out of 810 2026-07-04: 100 still open out of 810 2026-07-06: 100 still open out of 810 2026-07-06: 100 still open out of 810 2026-07-06: 120 still open out of 1070 2026-07-06: 120 still open out of 1070 2026-07-08: 180 still open out of 810 2026-07-08: 40 still open out of 810 2026-07-09: 0 still open out of 810 2026-07-09: 0 still open out of 810 2026-07-10: 0 still open out of 810 2026-07-10: 0 still open out of 990 2026-07-10: 0 still open out of 990 2026-07-10: 0 still open out of 990 2026-07-10: 0 still open out of 990 2026-07-10: 0 still open out of 990 2026-07-10: 0 still open out of 990 2026-07-10: 140 still open out of 990 2026-07-10: 0 still open out of 990 2026-07-10: 0 still open out of 990 2026-07-11: 0 still open out of 990 2026-07-11: 0 still open out of 990 2026-07-11: 0 still open out of 990 2026-07-11: 0 still open out of 990 2026-07-13: 180 still open out of 990 2026-07-13: 0 still open out of 990 2026-07-14: 130 still open out of 990 2026-07-14: 200 still open out of 990 2026-07-15: 280 still open out of 990 2026-07-15: 100 still open out of 990 2026-07-15: 100 still open out of 990 2026-07-15: 100 still open out of 990 2026-07-16: 0 still open out of 990 2026-07-16: 0 still open out of 990 2026-07-24: 360 still open out of 990 2026-07-24: 180 still open out of 990 2026-07-24: 0 still open out of 990 2026-07-28: 120 still open out of 1250 2026-07-30: 260 still open out of 990 2026-07-30: 40 still open out of 710 2026-07-30: 40 still open out of 710 2026-07-31: 110 still open out of 710 2026-07-31: 80 still open out of 710 2026-08-01: 100 still open out of 990 2026-08-01: 280 still open out of 990 2026-08-01: 0 still open out of 990 2026-08-01: 100 still open out of 990 2026-08-01: 100 still open out of 990 2026-08-01: 0 still open out of 990 2026-08-01: 100 still open out of 990 2026-08-01: 100 still open out of 990 2026-08-01: 100 still open out of 990 2026-08-02: 0 still open out of 990 2026-08-02: 100 still open out of 990 2026-08-04: 100 still open out of 990 2026-08-04: 100 still open out of 990 2026-08-04: 0 still open out of 990 2026-08-04: 0 still open out of 990 2026-08-04: 0 still open out of 990 2026-08-06: 0 still open out of 990 2026-08-06: 0 still open out of 990 2026-08-09: 100 still open out of 990 2026-08-09: 100 still open out of 990 2026-08-09: 100 still open out of 990 2026-08-09: 0 still open out of 990 2026-08-09: 100 still open out of 990 0 lowest (Aug 9) Clean-up work the build will not let grow: change since tracking began lower is better -13 -6 +1 scripts to rewrite · 2026-10-01 04:37: 677 scripts to rewrite · 2026-10-01 21:50: 672 scripts to rewrite · 2026-10-02 05:43: 671 scripts to rewrite · 2026-10-02 05:52: 668 scripts to rewrite · 2026-10-02 15:44: 667 scripts to rewrite · 2026-10-02 15:48: 668 scripts to rewrite · 2026-10-02 16:24: 664 fragile pipelines · 2026-10-01 04:37: 951 fragile pipelines · 2026-10-01 21:50: 946 fragile pipelines · 2026-10-02 05:43: 945 fragile pipelines · 2026-10-02 05:52: 943 fragile pipelines · 2026-10-02 15:44: 942 fragile pipelines · 2026-10-02 15:48: 942 fragile pipelines · 2026-10-02 16:24: 939 JSON tool calls · 2026-09-26 06:30: 90 JSON tool calls · 2026-09-26 09:46: 91 JSON tool calls · 2026-09-26 11:00: 86 86 JSON tool calls 939 fragile pipelines 664 scripts to rewrite Planned work still open in the design documents rises when new work is planned 0 230 461 open tasks · 2026-03-21 21:46: 0 open tasks · 2026-03-22 22:22: 14 open tasks · 2026-03-23 21:33: 14 open tasks · 2026-03-24 23:59: 12 open tasks · 2026-03-25 23:07: 10 open tasks · 2026-03-26 20:11: 9 open tasks · 2026-03-27 08:01: 48 open tasks · 2026-03-28 04:35: 48 open tasks · 2026-03-29 16:50: 48 open tasks · 2026-03-30 23:21: 51 open tasks · 2026-03-31 23:37: 76 open tasks · 2026-04-01 22:46: 76 open tasks · 2026-04-02 21:44: 78 open tasks · 2026-04-03 23:53: 163 open tasks · 2026-04-04 20:55: 180 open tasks · 2026-04-05 23:55: 180 open tasks · 2026-04-06 06:06: 172 open tasks · 2026-04-07 23:53: 192 open tasks · 2026-04-08 22:32: 192 open tasks · 2026-04-09 16:02: 85 open tasks · 2026-04-14 21:54: 85 open tasks · 2026-04-15 21:30: 85 open tasks · 2026-04-23 23:12: 85 open tasks · 2026-04-29 23:48: 85 open tasks · 2026-04-30 08:35: 85 open tasks · 2026-05-01 17:28: 86 open tasks · 2026-05-02 18:42: 135 open tasks · 2026-05-05 23:59: 170 open tasks · 2026-05-06 10:09: 170 open tasks · 2026-05-07 04:32: 170 open tasks · 2026-05-12 20:05: 170 open tasks · 2026-05-13 15:53: 170 open tasks · 2026-05-14 23:53: 109 open tasks · 2026-05-15 19:37: 109 open tasks · 2026-05-16 23:05: 109 open tasks · 2026-05-19 22:31: 109 open tasks · 2026-05-20 21:18: 109 open tasks · 2026-05-22 20:47: 109 open tasks · 2026-05-23 23:03: 109 open tasks · 2026-05-24 02:29: 109 open tasks · 2026-05-26 23:19: 246 open tasks · 2026-05-27 20:11: 246 open tasks · 2026-05-29 18:18: 246 open tasks · 2026-05-30 18:11: 246 open tasks · 2026-05-31 18:04: 246 open tasks · 2026-06-01 18:05: 246 open tasks · 2026-06-02 21:36: 246 open tasks · 2026-06-03 18:04: 246 open tasks · 2026-06-05 23:01: 246 open tasks · 2026-06-06 18:05: 246 open tasks · 2026-06-08 17:40: 246 open tasks · 2026-06-09 02:27: 246 open tasks · 2026-06-12 21:50: 246 open tasks · 2026-06-13 06:59: 246 open tasks · 2026-06-14 00:40: 246 open tasks · 2026-06-15 20:52: 246 open tasks · 2026-06-16 23:41: 246 open tasks · 2026-06-18 17:27: 259 open tasks · 2026-06-20 19:45: 249 open tasks · 2026-06-21 18:34: 249 open tasks · 2026-06-22 23:16: 249 open tasks · 2026-06-23 20:38: 249 open tasks · 2026-06-25 00:54: 249 open tasks · 2026-06-26 21:24: 249 open tasks · 2026-06-27 23:19: 249 open tasks · 2026-06-28 01:56: 249 open tasks · 2026-06-29 23:10: 249 open tasks · 2026-06-30 18:28: 249 open tasks · 2026-07-01 23:02: 249 open tasks · 2026-07-02 23:22: 249 open tasks · 2026-07-03 22:24: 249 open tasks · 2026-07-04 03:37: 249 open tasks · 2026-07-07 08:02: 249 open tasks · 2026-07-08 21:23: 249 open tasks · 2026-07-11 19:25: 268 open tasks · 2026-07-12 19:15: 268 open tasks · 2026-07-14 18:50: 268 open tasks · 2026-07-15 06:37: 268 open tasks · 2026-07-16 10:07: 268 open tasks · 2026-07-19 01:32: 268 open tasks · 2026-07-21 02:56: 268 open tasks · 2026-07-22 04:03: 268 open tasks · 2026-07-23 03:35: 268 open tasks · 2026-07-24 21:12: 268 open tasks · 2026-07-25 02:36: 268 open tasks · 2026-07-28 23:06: 289 open tasks · 2026-08-03 18:43: 289 open tasks · 2026-08-04 01:17: 289 open tasks · 2026-08-09 04:21: 289 open tasks · 2026-08-10 01:42: 288 open tasks · 2026-08-13 05:19: 288 open tasks · 2026-08-15 05:45: 288 open tasks · 2026-08-17 12:28: 288 open tasks · 2026-08-26 01:53: 288 open tasks · 2026-08-31 22:19: 295 open tasks · 2026-09-02 16:19: 295 open tasks · 2026-09-04 21:22: 294 open tasks · 2026-09-05 02:22: 294 open tasks · 2026-09-11 21:28: 294 open tasks · 2026-09-12 09:29: 294 open tasks · 2026-09-14 00:54: 294 open tasks · 2026-09-19 17:48: 293 open tasks · 2026-09-20 23:41: 293 open tasks · 2026-09-21 04:04: 293 open tasks · 2026-09-22 14:01: 300 open tasks · 2026-09-23 21:22: 300 open tasks · 2026-09-25 23:57: 300 open tasks · 2026-09-27 07:15: 314 open tasks · 2026-10-08 22:17: 461 open tasks · 2026-10-09 15:36: 461 461 open tasks pin1 day ago1 week ago1 month ago3 months ago2026-03-212026-10-09

The test runs stopped publishing this measure on 9 August 2026, 61 days before this release. The chart leaves that stretch empty rather than guess. The clean-up counts are the only measures that continue past it.

The numbers behind the chart
MeasureFromToFirstLastPointsDirectionSource
Open obligations per test run2026-05-072026-08-093801001063went up 144 times from one run to the next; the lowest-reached line only goes downfirst · last
Shell scripts still to be rewritten in Lua2026-10-012026-10-026776647went up oncefirst · last
Fragile shell pipelines still to be replaced2026-10-012026-10-029519397never went upfirst · last
Calls to an external JSON tool still to be replaced2026-09-262026-09-2690863went up oncefirst · last
Planned work still open2026-03-212026-10-090461110grew by 461 as work was plannedfirst · last

Every number is read from the project's public history and links to the exact version it came from.

How this section has improved
  • A chart now shows the whole history of open work and updates with each release. The most recent weeks are drawn largest.
  • Each problem found in the project now has its own card. The card says whether the problem is waiting, being worked on or fixed, and links to the source it rests on.
  • Every design document in the current release is listed, so you can see what is planned as well as what is broken.

Waiting 50

Found findings needing a supported remedy.

52c8f180Nothing runs the checked build before a push, so a broken footnote can be publishedhightillandsias.org · skills/update-website/scripts/checked-build.sh, .git/hooks · 2026-09-05
Claim

The update-website skill requires the checked build to pass before publishing, and the matrix rules say a footnote that 404s is worse than no footnote and a drifted quote is worse than a 404.

Code / observed behavior

checked-build.sh does exactly what it promises and nothing runs it. There is no .githooks directory and .git/hooks holds only the stock samples, there is no .github/ in this repository, and wrangler deploys committed assets with no build step — so a push to main publishes whatever var/html/index.html contains. Both qualities are a matter of the author remembering.

Why it matters

This is the whole class the trial build measures: if the five pins were moved to the newest daily without re-verification, 125 of the 342 footnote targets would resolve to a range that no longer contains their quote. One rule the gate still could not check is that every RED is followed by a PATH, which no script enforces today; a lint would be ten lines, and must read "followed by" as "before the next RED or GREEN", since on level 4 one REFUTED sits between a RED and its PATH — the only such case on the site, and every one of the thirty-nine REDs is answered today.

Remedy / check

A tracked .githooks/pre-push that runs checked-build.sh and refuses on a non-zero exit, plus a one-line installer setting core.hooksPath and a line in the skill telling a fresh forge to run it. Fail closed when the checkouts are absent: an unchecked level is not a checked build.

Evidence
History
  1. · found — Migrated from docs/audit/2026-09-05-issues-tillandsias.org.md section G1; re-checked open against the tree at c4039e1 — no .githooks directory exists and .git/hooks holds only samples. The forecast was re-run rather than copied: 125 of 342 targets break against the daily, and all thirty-nine REDs are answered by a PATH today.
Classification and release

defect · c4039e1 · c4039e18f5533054f368b9836867655880d55c09

62cacffeThe Vault unseal share persists to a plain file that satisfies the "share in keychain" check, contradicting the tmpfs-only invariant, and `--reset-guest` leaves it behindhightillandsias · crates/tillandsias-headless/src/vault_bootstrap.rs, crates/tillandsias-headless/src/main.rs, spec tillandsias-vault · 2026-09-05
Claim

`openspec/specs/tillandsias-vault/spec.md#L391-L393` — "### Invariant: Unseal key is tmpfs-only … **Expression**: `/run/secrets/vault-unseal IS_ON tmpfs AND NEVER persisted to disk`". `#L88-L91` adds that "the host launcher captures the random Vault-generated share on first boot, persists it in the host keychain, and deletes all local on-disk copies".

Code / observed behavior

`crates/tillandsias-headless/src/vault_bootstrap.rs#L1295-L1322` (`fn write_vm_credential_fallbacks`) writes the base64 Shamir share with `fs::write(cache_dir.join(format!("fallback_{VAULT_SHAMIR_SHARE_V1}")), share_b64)`. `cache_dir` is `crate::init_cache_dir()` (`main.rs#L2011-L2028`), which resolves to `$XDG_CACHE_HOME/tillandsias` or `$HOME/.cache/tillandsias` — ordinary durable disk. The write is unconditional whenever the host delivers a share into the guest (`vault_bootstrap.rs#L108-L124`), and its doc comment says so plainly: the file "is the ONLY evidence inside the guest that `operator init` ever completed".

That file is also what answers the "is the share in the keychain?" question. `has_shamir_share_in_keyring` (`#L1345-L1372`) tries the OS keychain, then falls through to `fs::read_to_string(cache_dir.join(format!("fallback_{}", VAULT_SHAMIR_SHARE_V1)))` and returns true if it base64-decodes to 32 bytes — so the predicate that guards the partial-init wipe is satisfied by a file, not by a keychain.

Nothing deletes it. There is no `remove_file` / `delete` of any `fallback_*` path anywhere under `crates/`. And `--reset-guest`, documented at `main.rs#L1430` as an "EPHEMERAL RESET: wipe the guest substrate", removes containers, volumes, podman secrets and networks (`main.rs#L8803-L8848`) and then exactly one host path — `reset_guest_wipe_paths` (`#L8746-L8748`) returns `vec![cache_dir.join("vault-data")]`. The share file sits in the same directory and survives. `--reset-guest` also never touches the OS keychain: the only `delete_credential` call in the crate is `vault_bootstrap.rs#L1924`, inside `fn sanitize_keychain`, and it deletes only the legacy `vault-unseal-v1` entry.

Why it matters

The unseal share is the key to the vault holding the user's provider tokens and GitHub credential. The spec's headline invariant is that it never reaches disk; in the VM guest it always does, in a directory that survives the command whose entire purpose is discarding guest state.

Remedy / check

Add the fallback share (and root-token) files to `reset_guest_wipe_paths`, delete the OS-keychain entry in the same reset, and either restrict the fallback file to a tmpfs path or amend the `unseal-key-tmpfs-only` invariant to state the guest exception explicitly.

Evidence

No upstream issue filed.

History
  1. · found — From docs/audit/2026-09-05-issues-tillandsias.md, section under Vault and secrets, the tillandsias.org explanation-page audit of 2026-09-05, level 5, re-read at v56.9.5.1.
Classification and release

spec-vs-code · v56.9.5.1 · b528680d2b938b09279e31faee981a8f6e370b3b

6767e64dThe wording rules do not reach footnote labels, and the disagreement remedy prefers the wrong halfhightillandsias.org · openspec/changes/add-level-page-specs/specs/site/level-2, docs/matrix/level-2-phone.md · 2026-09-06
Claim

The level-2 spec's wording rules govern "a reassuring sentence", "the page" and "the visible sentence", and its "Prose and tooltip disagree" scenario resolves a conflict between the two by correcting the sentence to the footnote's terms or re-anchoring the footnote.

Code / observed behavior

A footnote's label is author-written prose the reader sees on hover and in the rendered list, and the checked build verifies only path, range and quote — never the label. So the one surface the build cannot check is also the one the spec does not govern, and the consequence is live: the verification pass withdrew the false reassurance from the page's prose, which now says secrets are re-registered at every start, while [^5]'s label still tells the reader they are removed and reissued so that an unclean shutdown leaves nothing stale — the invented staleness property this spec's own "Reassurance is bounded by the cited source" forbids. Both remedies in the disagreement scenario treat the footnote as the correct half, so an editor applying it would restore the withdrawn claim into the sentence.

Why it matters

This is the one defect of the eight specs that touches published text: a false reassurance is on the live page today, in the surface the build does not check, and the spec written to prevent exactly that failure leaves the surface ungoverned and then points the remedy at the wrong half. Finding 1e141597 records the same label from the annotation-file angle; this one is about the rule that was supposed to catch it.

Remedy / check

Say once, in this spec, that a footnote's label is page prose bound by every wording and sourcing rule here, as PATH, GREEN and RED text are. Add the third remedy — the label is rewritten to what the cited range states — and order the remedies by which half the pinned source supports rather than by which half is the footnote. Add a scenario: when a claim is withdrawn from a sentence, the label of every footnote attached to it is checked for the same claim.

Evidence
History
  1. · found — From the specification review of the eight site page specs; the two high defects of the level-2 review, which share one live consequence.
Classification and release

process · main · cb07e89c411e6c63abb8995928ed9a6da34ddc8c

778a9057The only page build is Python, which the runtime methodology forbids for committed automationhightillandsias.org · scripts/build-matrix.py, scripts/figures.py, scripts/issues.py, skills/update-website/scripts · 2026-09-05
Claim

The runtime methodology, identical at both tags, forbids Python for runtime, harness or repository scripts, allows Rust and POSIX shell that dispatches existing binaries, and requires explicit approval from the operator before an exception is committed or used by recurring automation.

Code / observed behavior

This repository's page generator is 1,185 lines of Python plus 372 more of inline-SVG figures, and the ledger fold the page now renders adds a third file of 392; package.json wires the generator to npm run build; and two committed skill scripts, checked-build.sh and drift-report.sh, invoke it on every run of the update loop. That is recurring automation. The convention is written down in skills/README.md as an inheritance argument rather than an approval, and the rule's own checker resolves the repository root with git rev-parse and so has only ever scanned the runtime repository. No recorded approval was found in either repository.

Why it matters

The site is the project's public argument that it says what is true about itself, and the security level's whole case is that promises are machine-checked. A methodology rule this repository quietly does not follow is something a reader of that page is entitled to know about, and it is a live risk: the day the checker learns about sibling repositories, the build breaks and the page cannot be regenerated. A Rust rewrite is the approved path and is not small — the markdown dialect, seven callout kinds, the footnote parser with its release-override suffix, the checked-build resolver, the ledger fold and the whole template, with byte-identical output as the acceptance test — and a POSIX-shell port is not permitted by the rule's own wording, since it would not be dispatching an existing binary.

Remedy / check

Ask the operator for the explicit exception the rule provides for, and record it beside the build command in docs/matrix/README.md, naming the three files it covers and the condition — no new Python, replacement in Rust when the page generator is next touched substantially.

Evidence
History
  1. · found — Migrated from docs/audit/2026-09-05-issues-tillandsias.org.md section C1; re-checked open against the tree at c4039e1, with the line counts re-measured there — package.json still runs the Python build, a third Python file now folds the ledger inside it, and no approval is recorded.
Classification and release

spec-vs-code · cb07e89 · cb07e89c411e6c63abb8995928ed9a6da34ddc8c

865e6b30`--publish-local` serves the whole repository, `.git` included, on a route with no auth gate, with none of the hardening flags the spec's builder carrieshightillandsias · crates/tillandsias-headless/src/main.rs, images/web/entrypoint.sh, spec enclave-service-catalog · 2026-09-05
Claim

`openspec/specs/enclave-service-catalog/spec.md#L61-L65` names `build_catalog_service_run_args` as a source of truth for how a catalog service is launched, and `openspec/specs/enclave-network/spec.md#L53` lists it as the enclave attach site for this service.

Code / observed behavior

`build_catalog_service_run_args` (`crates/tillandsias-headless/src/main.rs#L3368-L3394`) is hardened and digest-pinned — `--cap-drop=ALL`, `--security-opt=no-new-privileges`, `--security-opt=label=disable`, `--userns=keep-id`, `--pids-limit=32`, and an image resolved to `entry.digest`. **It has no production caller**: the only reference in the tree is the test at `#L18836`.

The live path is `fn publish_local_service` (`#L16812`), reached from `--publish-local` (`#L1082-L1084`) and from the MCP `publish_local` tool (`crates/tillandsias-headless/src/tray/mod.rs#L1012-L1017`). It builds its own argv at `#L16841-L16853`:

"--detach", "--rm", "--name", <container>, "--hostname", …, "--network", "tillandsias-enclave", "-v", format!("{}:/var/www:ro,Z", worktree.display()), image /* "tillandsias-web", a mutable tag */

No capability drop, no `no-new-privileges`, no `userns`, no pids limit, no digest pin. `worktree` is `host_project_root().join(project_name)` (`#L16836`) — the project's repository root, `.git` and all — and the image serves it with `httpd -f -p 8080 -h /var/www` (`images/web/entrypoint.sh#L17`) with no config file, so no path is excluded. The route is then registered with `new_route.public = true` (`#L16883-L16885`), which `generate_dynamic_caddyfile` renders as a bare `reverse_proxy` with no `forward_auth` gate — the project's own test at `#L18872-L18886` asserts exactly that.

Why it matters

Any forge agent (the MCP tool is in-forge reachable) can publish `http://www.<project>.localhost/`, and from that moment `.git/config`, `.git/objects/**` and any untracked secret in the working tree are served without authentication to everything that can reach the router. The hardened, digest-pinned builder that the two specs point at as the implementation is dead code, so reviewing it tells you nothing about what runs.

Remedy / check

Have `publish_local_service` call `build_catalog_service_run_args` (deleting its inline argv), and give the web image an httpd config that refuses dot-directories — or mount a build output directory rather than the repository root.

Evidence

No upstream issue filed.

History
  1. · found — From docs/audit/2026-09-05-issues-tillandsias.md, section under Published local web service, the tillandsias.org explanation-page audit of 2026-09-05, level 4, re-read at v56.9.5.1.
Classification and release

defect · v56.9.5.1 · b528680d2b938b09279e31faee981a8f6e370b3b

b28aba62No default install path verifies a Cosign bundle, and the Linux installer continues when the checksum file is missinghightillandsias · scripts/install.sh, scripts/install-macos.sh, scripts/install-windows.ps1, spec update-system · 2026-09-05
Claim

`openspec/specs/binary-signing/spec.md#L27-L30` — "All signable release artifacts MUST be signed with Cosign keyless mode and MUST be verifiable locally using the bundle format." Verification itself is placed on the user: `#L45-L52` requires only that the *instructions* and `scripts/verify.sh` use `cosign verify-blob --bundle`.

Code / observed behavior

`cosign` appears in no installer. `scripts/verify.sh` is the only consumer (`#L46`, `#L78`), and it is an opt-in helper the install path never calls. The three install scripts stop at SHA-256: `scripts/install.sh#L226-L239`, `scripts/install-macos.sh#L106-L128`, `scripts/install-windows.ps1#L361-L390`.

`scripts/install.sh` additionally proceeds on every failure of that weaker check — `#L232` "sha256sum not found; skipping checksum verification.", `#L235` "SHA256SUMS did not contain $ASSET; skipping checksum verification.", `#L238` "SHA256SUMS not available; skipping checksum verification." — and then installs the downloaded bytes at `#L243`. (`install-macos.sh` and `install-windows.ps1` do die on a missing manifest; only the Linux path degrades open.)

`openspec/specs/update-system/spec.md` — the spec governing how a running install obtains a new artifact — contains one requirement, "Platform-appropriate artifact selection", and the strings `cosign`, `verify`, `signature` and `checksum` do not appear in it at all.

Why it matters

The signing story the spec tells is only ever exercised by an opt-in helper, so the default path a user actually runs verifies nothing stronger than a checksum — and on Linux not even that, because every failure of the checksum step is a warning followed by an install of the downloaded bytes. The spec that governs how a running install fetches a new artifact does not mention verification at all.

Remedy / check

Make `install.sh` fail closed when `SHA256SUMS` is missing or does not name the asset, and add a signature-verification requirement to `openspec/specs/update-system/spec.md` (verify the bundle when `cosign` is present, refuse or warn loudly when it is not).

Evidence

No upstream issue filed.

History
  1. · found — From docs/audit/2026-09-05-issues-tillandsias.md, section under Release integrity and install, the tillandsias.org explanation-page audit of 2026-09-05, level 5, re-read at v56.9.5.1.
Classification and release

defect · v56.9.5.1 · b528680d2b938b09279e31faee981a8f6e370b3b

da34e5eeFive production container launches never reach `validate_launch_argv`, and one of them builds an argv the policy forbidshightillandsias · crates/tillandsias-headless/src/main.rs, crates/tillandsias-podman/src/container_spec.rs · 2026-09-05
Claim

`crates/tillandsias-headless/src/main.rs#L11681-L11688` — "VERIFIED 2026-09-04: this function's only production caller is `run_agent_container_attached`, which validates first; its four other callers are inside `mod tests` … **Every production container launch therefore already passes `validate_launch_argv`.**" A second doc comment at `#L14177` states "Every option flows through the policy-validated `build_run_argv()` path".

Code / observed behavior

`ContainerSpec` has two serializers: `build_run_argv()` validates (`container_spec.rs#L394-L397`), `build_run_args()` does not (`#L274`). These production paths use the unvalidated one, or build argv by hand and hand it straight to `PodmanClient::run_container_observed` (`crates/tillandsias-podman/src/client.rs#L1022`), which performs no validation:

- the egress proxy — `main.rs#L3414` `fn build_proxy_run_args` returns a raw `Vec<String>`, launched at `#L3526`; - the git mirror — `main.rs#L4284` `fn build_git_run_args`, launched at `#L9087`; - the observatorium web container — `main.rs#L11246` `.build_run_args()`; - the project browser — `main.rs#L11295` and `#L13047` `spec.build_run_args()`; - the interactive forge — `build_forge_agent_run_args_with_vault` ends at `#L14736` with `spec.build_run_args()`, and `build_forge_agent_run_argv` (`#L14741`) merely prefixes `podman run` before `launch_forge_agent` (`#L15089`) spawns it in a host terminal emulator. This is the path the `#L14177` doc comment describes, and it never calls the validator.

And the gap is not theoretical. `fn run_provider_login` (`main.rs#L9607`) launches a container at `#L9722-L9740` with `--cap-drop=ALL`, `--security-opt=no-new-privileges` and `--userns=keep-id` — but **not** `--security-opt=label=disable`, which `policy.rs#L17-L22` (`MANDATORY_HARDENING_FLAGS`) declares mandatory. `validate_launch_argv` would return `MissingMandatoryHardening(["--security-opt=label=disable"])` for that argv. Nothing calls it there, so the launch proceeds.

Why it matters

The project's own framing (`container_spec.rs#L385-L392`) is that an argv missing a hardening flag "is not a degraded launch to be reported, it is a container that must not be created". At this tag that guarantee holds for exactly the delegated-agent lanes, while the proxy, the mirror, the observatorium, the browser and the interactive forge — the containers a user actually runs — are outside it, and there is already one production argv the policy would reject.

Remedy / check

Route every builder through `build_run_argv()` (or call `validate_launch_argv` in `run_container_observed` / `run_container_attached_observed` so no launcher can bypass it), fix `run_provider_login`'s missing flag, and correct the two doc comments.

Evidence

No upstream issue filed.

History
  1. · found — From docs/audit/2026-09-05-issues-tillandsias.md, section under Container launch hardening, the tillandsias.org explanation-page audit of 2026-09-05, level 4, re-read at v56.9.5.1.
Classification and release

defect · v56.9.5.1 · b528680d2b938b09279e31faee981a8f6e370b3b

077e5ab2Level 3's capability inventory has no requirement in its specmediumtillandsias.org · openspec/changes/add-level-page-specs/specs/site/level-3/spec.md · 2026-09-05
Claim

The level-3 spec says the page is the anatomy — what runs where, what survives a teardown, and where the sharp edges are — and at c4039e1 carries four requirements: enclave anatomy, survivorship claims, every RED true at the pin, and gate and CI statements.

Code / observed behavior

The page opens with a section the spec does not mention, an eight-part inventory of what a forge gives you on launch, each part ending in a GREEN or a RED with its PATH. It is 2,119 of the page's 3,681 prose words and the part a reader arrives for.

Why it matters

A requirement is what survives the next rewrite. The four existing requirements hold the next editor to four sections; nothing holds anyone to keeping the inventory complete, to matching the audit record's capability table, or to marking a capability spec-only when the code does not carry it. The page and its contract disagree about what the page is for.

Remedy / check

Add one requirement to the level-3 spec: the page enumerates the capabilities a forge provides on launch, one flag per capability, with the status at the pin matching the current audit record's capability table.

Evidence
History
  1. · found — Migrated from docs/audit/2026-09-05-issues-tillandsias.org.md section B2; re-checked open against the tree at c4039e1, where the level-3 spec carries four requirements and none is about the inventory. An uncommitted expansion of that spec in the working tree adds a required-sections requirement that would close this; it is not in any commit, so the finding stands.
Classification and release

spec-vs-code · c4039e1 · c4039e18f5533054f368b9836867655880d55c09

0d88cfc7The level specs restate site/level-common's shared rules instead of referencing themmediumtillandsias.org · openspec/changes/add-level-page-specs/specs/site · 2026-09-06
Claim

site/level-common exists so that the shared rules are "stated once so the per-level specs can say only what is particular to their page", and each level spec defers to it by name where a rule is shared.

Code / observed behavior

Eight defects across five reviews are one shape: the level spec restates the shared rule rather than referencing it. Level 1 re-asserts level-common's RED lifecycle in a scenario under a requirement that has just deferred to it, and its footnote-past-the-pin scenario is level-common's. Level 2 restates the RED lifecycle and narrows it — a replaced defect "MUST be written in the past tense", where level-common allows a past-tense flag or a GREEN — and then says its lifecycle is as level-common states. The cross-level same-source sentence is verbatim in both level 1 and level 2. The page-furniture ban appears in levels 1, 2, 3 and 4. Level 3 copies level 1's footnote-target sentence verbatim, and reproduces the no-path sentence level-common already carries. Level 5 restates level-common in four requirements (own pin, verdicts re-derived, sourcing, audit annotations), and states the newer-build/@vTAG rule three times inside its own file, a fourth in level-common and a fifth in level-5-stable-pin-deltas.

Why it matters

Normative copies drift independently: an editor correcting one leaves the others contradicting it, and level 2's copy already narrows what level-common permits, so two editors reading two specs reach opposite verdicts on the same fixed RED. Two of the duplicated rules are also cross-level by construction — a level cannot satisfy "cite the same source as the other level" alone — so no level spec is the right owner. Nothing here misleads a reader today: the specs are unpublished and govern no page yet. They are what the next update loop reads.

Remedy / check

Reduce each restating half to a reference to level-common, and move the two genuinely shared rules the level specs invented — levels agree on a source for a shared claim; page furniture lives in the LEVELS table — into site/level-common, so each level spec carries only its particular.

Evidence
History
  1. · found — From the specification review of the eight site page specs; eight defects across five reviews share this shape.
Classification and release

process · main · cb07e89c411e6c63abb8995928ed9a6da34ddc8c

1e3f8c3dThe navigation spec describes a progress board the build does not producemediumtillandsias.org · openspec/changes/add-level-page-specs/specs/site/navigation, scripts/build-matrix.py · 2026-09-06
Claim

The navigation spec requires the board to place every finding in exactly one of three columns whose blurbs a reader who has never opened the ledger can apply, to show per finding whether it has been filed where it has to go, to show when each was first recorded and last moved, and to order each column by a rule the build states.

Code / observed behavior

Three of those do not hold. The ledger's lateral terminals, obsoleted and wontfix, are not ladder rungs and are placed in the middle column by the fold, but the spec's middle-column blurb requires that a plan of work exists and the left requires found-and-not-yet-tracked, so such a card matches no blurb the page states. The filing state is computed and then discarded: build-matrix.py builds the filed-or-unfiled markup into `up` and appends it behind a literal `if False`, and would emit it as a sibling list item outside the card even if the guard were flipped, so no card on the published board carries a filing link or says "not filed upstream". A finding whose events carry no timestamp renders as a blank meta field rather than as explicitly undated, and the ordering rule (severity, then id) exists only as a Python sort key.

Why it matters

The requirement the board exists for — an unfiled finding MUST NOT be displayed in a way that lets it read as filed — is unmet by omission for every runtime finding on the live board, mitigated only by the aggregate "0 filed upstream" tally beside it. The column defect is in the opposite direction: the spec is unpublished and the fold already does something defensible, but the blurb a reader is told to apply and the placement the code performs disagree, so the first wontfix will be misfiled by the page's own definition.

Remedy / check

In the build, drop the `if False` and emit the filing state inside the card. In the spec, name where a lateral terminal sits in the ledger's own terms — on the board, never in the right-hand column, its card saying the attempt ended without completion — widen the middle column's stated meaning to cover it, require a finding with no chronology to render as undated rather than blank, and say where the ordering rule is stated.

Evidence
History
  1. · found — From the specification review of the eight site page specs; three defects of the navigation review, one of which is a live build defect.
Classification and release

doc-drift · main · cb07e89c411e6c63abb8995928ed9a6da34ddc8c

2448903cThe startup context says on-demand installs verify a Sigstore attestation; the shim disables attestation, and the Homebrew prefix is backed by no volumemediumtillandsias · images/default/lib-common.sh, images/default/brew-shim-exec.sh, images/default/Containerfile · 2026-09-05
Claim

`images/default/lib-common.sh#L4514-L4516` — "**On-demand tools** (`tmux`, `lazygit`, `gum`, `fd`, `rg`, …): typing the command triggers a userspace Homebrew install through a shim, gated on an allowlist. That install is NOT free — **it verifies a Sigstore attestation per bottle** and is time-bounded". The same file repeats it in the shim-generation comment at `#L3336`: "(homebrew-core formulae only, Sigstore attestation verification REQUIRED)".

Code / observed behavior

`images/default/brew-shim-exec.sh#L89` — `export HOMEBREW_NO_VERIFY_ATTESTATIONS=1`, with a long, correct explanation at `#L62-L88` of why it had to be turned off (the attestation path calls `gh attestation verify`, which needs a GitHub credential the forge deliberately does not have) and what replaced it (Homebrew's own signed formula index, which the comment is careful to call "authenticated integrity from a single publisher … NOT provenance").

Separately, the prefix that install lands in is `BREW_PREFIX="${TILLANDSIAS_BREW_PREFIX:-/home/linuxbrew/.linuxbrew}"` (`brew-shim-exec.sh#L32`). `/home/linuxbrew` exists only as a directory created in the image (`images/default/Containerfile#L27`); no builder mounts a volume there. The per-project tool-cache volume mounts `/home/forge/.cache/tillandsias-project` (`crates/tillandsias-headless/src/main.rs#L14414-L14416`), a different path. Forge containers run `--rm` (`ContainerSpec::new` sets `remove: true`, `crates/tillandsias-podman/src/container_spec.rs#L121`), so the Homebrew clone and every installed bottle are discarded when the container exits and re-fetched on the next launch.

Why it matters

The startup context is the security story an agent is given about the tools it installs, and it asserts a provenance check that the shim explicitly turns off — the one place where the code comment is more honest than the user-facing text. The unbacked prefix is a second, quieter cost: an "on-demand install" that is redone from scratch every launch, against an egress allowlist, rather than cached.

Remedy / check

Rewrite both lib-common.sh strings to describe what actually holds (signed formula index → bottle checksum; no attestation, no provenance), and mount a named volume at `/home/linuxbrew` in the forge builders.

Evidence

No upstream issue filed.

History
  1. · found — From docs/audit/2026-09-05-issues-tillandsias.md, section under Forge on-demand tooling, the tillandsias.org explanation-page audit of 2026-09-05, level 2, re-read at v56.9.5.1.
Classification and release

doc-drift · v56.9.5.1 · b528680d2b938b09279e31faee981a8f6e370b3b

247ed660Ten of level 5's forty-four footnotes need re-anchoring when its pin moves, and one quote no longer exists at the dailymediumtillandsias.org · docs/matrix/level-5-phd.md · 2026-09-05
Claim

The matrix rules say that when a level's tag is bumped its line ranges are re-verified, and the level-5 spec adds that a pin-bump change lists one delta per footnote whose target or quote drifted between the tags.

Code / observed behavior

A trial build pinned to the newest daily reports six broken level-5 targets, every one a quote no longer inside its cited range: [^10] and [^15] into methodology/proximity.yaml, [^11], [^16] and [^19] into methodology/philosophy.yaml, and [^34] into crates/tillandsias-plan/src/fragments.rs. The four @v56.9.5.1 footnotes [^41]-[^44] are the other half: each resolves at the daily today, but once the pin is the daily the suffix is redundant and the REDs their PATH lines hang from are fixed at that tag, so the flags have to be re-derived rather than the anchors merely moved. [^16] is the hard one: its quote about the strong law is deleted at the daily, not moved — it is one of the nine quotes in the whole site that the daily no longer contains anywhere in the file they cite — and the surrounding text records the citation as withdrawn.

Why it matters

Ten of forty-four footnotes is close to a quarter of the page, and the level-5 rule forbids doing that work as an ordinary edit: each is a delta with evidence and a verbatim quote at the target tag, in a change the operator reviews. Written down now, at the tags where both sides were read, the next pin bump is mechanical; discovered at bump time it is a re-derivation of the strong-law argument under time pressure.

Remedy / check

Put the six-row drift table and the four suffix footnotes into the next level-5 change's tasks.md as its work list, with [^16] flagged as needing new prose rather than a new anchor.

Evidence
History
  1. · found — Migrated from docs/audit/2026-09-05-issues-tillandsias.org.md section A2; re-checked open against the tree at c4039e1 — level 5 still pins v56.9.2.1, a trial build against the daily still breaks exactly those six targets, and no re-anchoring change exists.
Classification and release

doc-drift · c4039e1 · c4039e18f5533054f368b9836867655880d55c09

296676d4Every broken footnote is re-anchored by hand although the search is mechanicalmediumtillandsias.org · skills/update-website/scripts · 2026-09-05
Claim

The skill's step 5 tells the editor to correct targets, quotes and prose in the level's source, following the audit-site-claims procedure per level.

Code / observed behavior

The tooling reports that a quote left its range and never says where the quote went, yet for almost all of it the answer is one command. The day's record measured 112 moved and 9 gone out of 121 breakages; re-measured against the same daily at c4039e1, after the pages grew, it is 125 breakages of which 116 are the same characters elsewhere in the same file and need only a new line range, and 9 are genuinely gone and need a human to write new prose. No such helper exists in skills/update-website/scripts, which holds five scripts.

Why it matters

It removes toil, and toil is what makes a pin bump get postponed. It is also the precondition for a delta generator: a generator can only list one delta per drifted footnote if something can compute the new target.

Remedy / check

A re-anchor.sh taking a level and a tag, about sixty lines of shell over grep, sed and git, parsing footnote definitions the way the build does and printing one of three verdicts per broken target — moved with the new range, relocated to another file, or gone — as a paste-ready block.

Evidence
History
  1. · found — Migrated from docs/audit/2026-09-05-issues-tillandsias.org.md section G3; re-checked open against the tree at c4039e1 — skills/update-website/scripts holds five scripts and no re-anchor helper. The moved/gone split was re-derived against the v56.9.5.1 checkout: 116 moved, 9 gone.
Classification and release

defect · c4039e1 · c4039e18f5533054f368b9836867655880d55c09

30127aadThe macOS release lane signs a hardcoded three-asset list and runs no transitive integrity checkmediumtillandsias · .github/workflows/release.yml · 2026-09-05
Claim

`openspec/specs/binary-signing/spec.md#L17-L18` — "**WHEN** a release contains the Linux musl binary and installer helper scripts **THEN** each signable artifact MUST have its own `.cosign.bundle` file".

Code / observed behavior

The Linux lane discovers what to sign and then proves the property over whatever was staged: `.github/workflows/release.yml#L264-L268` loops `for artifact in *` (skipping only `SHA256SUMS` and existing bundles), and `#L293` runs `bash ../scripts/check-release-asset-integrity.sh .`, whose own header (`scripts/check-release-asset-integrity.sh#L14-L20`) exists precisely because "the Windows job's signing loop was an ALLOW-LIST".

The macOS lane at `#L507-L518` is that allow-list:

for artifact in tillandsias-tray-*-macos-arm64.tar.gz Tillandsias.dmg install-macos.sh; do

`SHA256SUMS-macos` — staged at `#L501` — is not in the list and gets no `.cosign.bundle`. And `check-release-asset-integrity.sh` is invoked in only two jobs (`#L293` Linux, `#L772` Windows); the macOS job does not run it.

Why it matters

The integrity guard was written after an asset shipped unverifiable for exactly this reason, and the lane that reproduces the pattern is the one it is not wired into. `scripts/install-macos.sh#L106-L128` downloads `SHA256SUMS-macos` and trusts it as the sole integrity source for the tarball — a manifest with no signature of its own. Any asset added to the macOS lane later inherits the same silence.

Remedy / check

Replace the three-name glob with the Linux lane's `for artifact in *` form and add `bash scripts/check-release-asset-integrity.sh release-artifacts` to the macOS job.

Evidence

No upstream issue filed.

History
  1. · found — From docs/audit/2026-09-05-issues-tillandsias.md, section under Release integrity and install, the tillandsias.org explanation-page audit of 2026-09-05, level 5, re-read at v56.9.5.1.
Classification and release

defect · v56.9.5.1 · b528680d2b938b09279e31faee981a8f6e370b3b

3d0ef7e2The drift report computes no forecast when every level already pins stablemediumtillandsias.org · skills/update-website/scripts/drift-report.sh · 2026-09-05
Claim

The skill says that if every level already pins the stable tag and nothing is broken the report prints ok:up-to-date, and that the daily-channel counts still tell you whether any PATH lines can be updated.

Code / observed behavior

The report runs its trial build only when some level's pin differs from stable. Every level pins stable today, so it prints per-level file counts and ok:up-to-date, and the one number that matters for planning — how much work the next promotion will be — is never computed. The script already knows how; it just points its pin override at stable rather than at the daily.

Why it matters

The forecast is what turns "we should look at the site" into a number. Run against the daily today it is 125 broken targets out of 342, a week before anyone proposes moving a pin.

Remedy / check

Roughly fifteen lines: a second trial build pinned to the daily whenever the daily differs from stable, reported as a forecast and never as a failure, with a verdict carrying both numbers. The scheduling question is the real cost and is shared with finding d0c34ed6.

Evidence
History
  1. · found — Migrated from docs/audit/2026-09-05-issues-tillandsias.org.md section G2; re-checked open against the tree at c4039e1 — the trial build is still gated on repin_needed.
Classification and release

defect · c4039e1 · c4039e18f5533054f368b9836867655880d55c09

3d8dffdeThe pages are 48% longer in prose than yesterday, and the length expectation is written down nowheremediumtillandsias.org · docs/matrix, openspec/changes/add-level-page-specs/specs/site/level-common · 2026-09-05
Claim

The editorial rules that are written down constrain shape, not size: one merged narrative per level, implementation trivia is not substance, a page that is all flags has stopped being prose. None of them constrains length, and a search for length, shorter, longer, concise and brev across docs, openspec, skills and scripts returns no rule.

Code / observed behavior

Prose words outside the footnote definitions and their quote lines went from 8,277 at bf5bde3, the last commit of 2026-09-04, to 12,240 across the five levels — a factor of 1.48 — and footnote definitions from 105 to 342. Level 3 more than tripled, 1,034 words to 3,681, and its new capability inventory is 2,119 of those 3,681. The spread between the shortest and the longest level is essentially unchanged, 6.6x before and 6.5x after.

Why it matters

The operator asked for the capability inventory, and an inventory is a completeness claim: it is worth having because it enumerates, and cannot be halved without becoming a sample. The unwritten length norm has no recorded justification and nothing checks it, so the next editing agent is guessing at a rule it cannot read.

Remedy / check

One requirement in the level-common spec stating the argument/reference split — levels 1 and 2 stay readable in one sitting, levels 3 to 5 are as long as their subject provided every section earns its place under the rules that already exist — so the next editor is bound by a written rule.

Evidence
History
  1. · found — Migrated from docs/audit/2026-09-05-issues-tillandsias.org.md section B1; re-checked open against the tree at c4039e1, with every word and footnote count re-derived there and at bf5bde3 rather than copied from the audit — no length or shape rule exists in docs/matrix/README.md or the level-common spec.
Classification and release

doc-drift · c4039e1 · c4039e18f5533054f368b9836867655880d55c09

3ed0ddf7Requirement text carries dated findings, counts and rosters that level-common forbidsmediumtillandsias.org · openspec/changes/add-level-page-specs/specs/site/level-{2,3,4},navigation · 2026-09-06
Claim

The shared contract forbids a page spec's requirement text from carrying a dated finding, a file count or a mandated verdict on a specific flag, and says such findings are work items in the change's tasks.md while the spec states the property the finding is an instance of.

Code / observed behavior

Four specs do exactly what that requirement forbids. Level 4's "RED precision and scope" hard-codes three audit findings — a litmus corpus count down to the literal replacement phrase, a "(server-side branch protection)" scope that the page has already stopped using, and a mandated verdict on one named footnote — all three already tracked as work items in this change's own tasks.md. Level 3's survivorship requirement hard-codes a roster of artifacts two requirements after the same spec rejects prose rosters on principle, and the roster does not match the page: the host working copy it names appears on the page only inside a RED. Level 2 counts today's sources and headings into MUST text ("the two honest limits", "the page's four questions"). The navigation spec conditions a MUST NOT on a dated measurement of the runtime's local-ci.sh — a weight table over CI check names — which the review found half-superseded at the newest daily, where the arithmetic is run by a score-checks subcommand.

Why it matters

A requirement that names an instance duplicates the ledger and rots against it: it mandates a false number the moment the philosophy adds a limit, the page answers a fifth question or the litmus corpus grows, and the navigation clause is unappliable because the progress page has no pin and the answer differs by release. These are the specs' own stated rule being broken by the specs, not a claim to a reader: nothing is published, so no reader is misled today.

Remedy / check

Reduce each to the durable property it is an instance of — a number in prose is re-derived at the pin or replaced by the unquantified noun; a claim about state outside the repository is scoped to what the repository evidences; the page states what survives for every artifact whose fate it asserts — and leave the instances in tasks.md and the dated audit record.

Evidence
History
  1. · found — From the specification review of the eight site page specs; four defects, one per spec that breaks level-common's own rule.
Classification and release

process · main · cb07e89c411e6c63abb8995928ed9a6da34ddc8c

3f11cee0`litmus:git-mirror-no-anonymous-daemon-write` asserts the opposite of its own namemediumtillandsias · openspec/litmus-tests/litmus-git-mirror-no-anonymous-daemon-write.yaml · 2026-09-05
Claim

The fixture is registered as `name: litmus:git-mirror-no-anonymous-daemon-write` (`openspec/litmus-tests/litmus-git-mirror-no-anonymous-daemon-write.yaml#L2`), `severity: critical`.

Code / observed behavior

`#L7-L17` — "Historical name retained, but the current daemon **DOES** expose anonymous enclave receive-pack (order 450). Any reachable peer can create or fast-forward refs and cause the privileged service relay to carry those updates upstream … This litmus pins only honest interim containment … It MUST NOT be cited as zero-trust closure."

Why it matters

The body is exemplary; the name is what appears in `openspec/litmus-bindings.yaml`, in gate output, and in any summary a reader or an auditing agent assembles from test names. A green `critical` test called "no-anonymous-daemon-write" reads as proof that anonymous daemon writes are impossible, which is the exact opposite of what the file establishes.

Remedy / check

Rename it to what it pins — e.g. `litmus:git-mirror-anonymous-write-relay-containment` — and leave a one-line alias note for the old name.

Evidence

No upstream issue filed.

History
  1. · found — From docs/audit/2026-09-05-issues-tillandsias.md, section under Git mirror, the tillandsias.org explanation-page audit of 2026-09-05, level 4, re-read at v56.9.5.1.
Classification and release

doc-drift · v56.9.5.1 · b528680d2b938b09279e31faee981a8f6e370b3b

467f07baThe proxy CA is one persistent RSA-2048 30-day certificate; the spec promises a per-launch two-level EC P-256 chain on tmpfsmediumtillandsias · spec proxy-container, crates/tillandsias-headless/src/main.rs, images/default/ca-path.txt · 2026-09-05
Claim

`openspec/specs/proxy-container/spec.md#L66` — "The system SHALL generate a fresh two-level CA chain on every proxy launch. The chain SHALL consist of a self-signed Root CA and an Intermediate CA signed by the root, both using EC P-256 keys. All key material SHALL be stored on tmpfs (`$XDG_RUNTIME_DIR/tillandsias/proxy-certs/`) and SHALL be destroyed when the session ends (logout/reboot). No CA keys SHALL persist to disk." The four-file table below it names `root.crt`, `intermediate.crt`, `intermediate.key`, `ca-chain.crt`, each with lifetime "Session (tmpfs)", and a scenario requires generation "in under 10ms (EC P-256 key generation)". The spec's Status is `active` with no stale or superseded marker.

Code / observed behavior

`crates/tillandsias-headless/src/main.rs#L3170-L3189` (`fn ensure_ca_bundle`) shells out to `openssl req -x509 -newkey rsa:2048 … -days 30 -nodes -subj "/C=US/ST=Privacy/L=Local/O=Tillandsias/CN=Tillandsias CA"` — one self-signed RSA-2048 certificate valid 30 days, not a two-level EC P-256 chain. It is not per-launch: `ca_bundle_needs_refresh` (`#L3055-L3068`) returns false while both files are under 25 days old, so a launch reuses the existing key. And it is deliberately not on tmpfs: the directory comes from `tillandsias_core::ca_path::ca_dir()`, whose single-source manifest `images/default/ca-path.txt` declares `${HOME}/.local/state/tillandsias` and explains at length that a durable location was chosen by measurement because "/tmp works everywhere and is the bug" — a reboot on a volatile path made the proxy permanently unrestartable (order 975-rsgm).

Why it matters

The code is right and the spec is wrong, which is the worse direction: the spec is the artifact a reader (or an auditing agent, or this site) treats as the contract, and it currently promises ephemerality that the implementation deliberately abandoned for a documented reason. "No CA keys SHALL persist to disk" is a security claim, and the CA private key persists to `$HOME/.local/state/tillandsias/ca/intermediate.key` for up to 25 days.

Remedy / check

Rewrite the "Ephemeral CA chain (per-launch generation)" requirement to describe the durable single-certificate design and its 25-day refresh, citing `images/default/ca-path.txt` for why the path is not tmpfs.

Evidence

No upstream issue filed.

History
  1. · found — From docs/audit/2026-09-05-issues-tillandsias.md, section under Egress proxy, the tillandsias.org explanation-page audit of 2026-09-05, level 3, re-read at v56.9.5.1.
Classification and release

spec-vs-code · v56.9.5.1 · b528680d2b938b09279e31faee981a8f6e370b3b

4aea5a83Linux clean-room step continues after a failed credential-cold probemediumtillandsias · scripts/e2e-step2-linux.sh · 2026-09-22
Claim

The Linux clean-room run is meant to exercise first-time Vault initialization with no retained host credentials.

Code / observed behavior

scripts/e2e-step2-linux.sh#L34-L38 logs both the host-credential clear and the cold-state probe but allows either to fail and continues.

Why it matters

A later green smoke result does not establish a credential-cold starting state unless its logged probe result is inspected separately.

Remedy / check

Stop the clean-room run when the clear or cold-state probe fails, or make the final verdict explicitly conditional on the recorded cold-state result.

Evidence

No upstream issue filed.

History
  1. · found — Observed in stable v56.9.21.1 by source inspection; no live Linux clean-room run was performed by this audit.
Classification and release

defect · v56.9.21.1 · 2f5f2a90a7b43ecdee766e38561b8e5790c603aa

56435d54`browser.open` cannot resolve a browser binary: the on-demand chromium installer has no consumer, and the chromium images are built but never runmediumtillandsias · crates/tillandsias-browser-mcp/src/launcher.rs, scripts/install-chromium.sh, images/chromium/ · 2026-09-05
Claim

`openspec/litmus-tests/litmus-host-chromium-on-demand-shape.yaml` is green, and its name asserts that host chromium is installed on demand.

Code / observed behavior

`crates/tillandsias-browser-mcp/src/launcher.rs#L67-L91` (`fn resolve_browser_binary`) accepts an explicit override, then `TILLANDSIAS_BROWSER_BIN`, then `TILLANDSIAS_CHROMIUM_BIN`, then looks only under the cache root (`~/.cache/tillandsias/chromium/current/chrome` and four siblings) and otherwise returns `LaunchError::BrowserUnavailable` — declared at `#L21` as "bundled chromium not yet downloaded". There is no `PATH` fallback to a system chrome/chromium.

The only thing that would populate that directory is `scripts/install-chromium.sh`, and nothing invokes it. Outside the script itself, a repository-wide search for `install-chromium.sh` returns no caller anywhere under `crates/`, `scripts/` or `images/`; the only reference that runs is `openspec/litmus-tests/litmus-host-chromium-on-demand-shape.yaml`, whose steps (`#L33-L75`) are `grep` assertions over the script's own text. The shape test is green while the installer has no caller. (The repository already knows: `plan/index.yaml#L22893` carries a packet titled "scripts/install-chromium.sh has ZERO consumers … wire it or tombstone it", so check that packet's state before filing.)

Separately, `images/chromium/Containerfile.core` and `Containerfile.framework` are wired into the build (`scripts/build-image.sh#L214-L215`, `crates/tillandsias-core/src/image_builder.rs#L329-L330`), but no Rust code launches either image — the only non-build references under `crates/` are build plumbing and status maps.

Why it matters

On a fresh host the whole host-browser MCP surface fails at its first call with an error that names a download step no code performs, and the litmus that is supposed to cover "host chromium on demand" cannot detect that because it only greps the installer's source. Meanwhile two multi-hundred-megabyte container images are built on every image pass and never started.

Remedy / check

Call `scripts/install-chromium.sh` from `resolve_browser_binary`'s miss path (or from the tray's first `browser.open`), add a `PATH` fallback for a system chromium, and add one step to the litmus that asserts a *caller* exists rather than only the script's text.

Evidence

No upstream issue filed.

History
  1. · found — From docs/audit/2026-09-05-issues-tillandsias.md, section under Browser, the tillandsias.org explanation-page audit of 2026-09-05, level 4, re-read at v56.9.5.1.
Classification and release

defect · v56.9.5.1 · b528680d2b938b09279e31faee981a8f6e370b3b

576200f9The level-5 deltas are live on the site while their OpenSpec change is unapproved, and openspec/specs/ holds no site specmediumtillandsias.org · openspec/changes/level-5-stable-pin-deltas, openspec/specs · 2026-09-05
Claim

The level-5 delta discipline says an editor may apply the deltas a change lists, and that the operator reviews the change before it is archived. The record of the work says the remaining deltas are waiting on the operator.

Code / observed behavior

The seven applied deltas D1-D7 are live: eleven of the thirteen task boxes in openspec/changes/level-5-stable-pin-deltas/tasks.md are [x], and the two that are not are the proposed deltas D8 and D9, which the same file says wait for the operator rather than for an editor. The prose and the four @v56.9.5.1 footnotes [^41]-[^44] are in docs/matrix/level-5-phd.md, and the result is in var/html/index.html, which Cloudflare serves on commit. Meanwhile openspec/specs/ and openspec/changes/archive/ each hold only .gitkeep, so no site capability has ever been promoted out of changes/ and the current contract for every level lives in one of the three unarchived changes there.

Why it matters

Applying before approval is permitted, so this is an unclosed loop rather than a violation. It matters because the reviewable artifact and the published artifact have drifted apart in status: a reader of openspec/ cannot tell which of the three open changes describe the live site and which are proposals, and openspec/specs/site/level-5/ — where the next editor would look for the page's contract — does not exist. The longer the change sits, the more the next audit's deltas pile onto an unreviewed base.

Remedy / check

The operator reviews level-5-stable-pin-deltas and archives it, together with add-level-page-specs, whose task 3.3 names the same sync, so that openspec/specs/site/level-{common,1..5}/ become the current contract and changes/ holds only what is genuinely proposed.

Evidence
History
  1. · found — Migrated from docs/audit/2026-09-05-issues-tillandsias.org.md section A1; re-checked open against the tree at c4039e1 — openspec/specs/ and openspec/changes/archive/ hold only .gitkeep, and eleven of the change's thirteen boxes are ticked.
Classification and release

doc-drift · c4039e1 · c4039e18f5533054f368b9836867655880d55c09

5e0a299e`run_provider_login` attaches a container to the enclave without being a documented member, and the membership guard cannot see itmediumtillandsias · crates/tillandsias-headless/src/main.rs, scripts/check-enclave-membership-documented.sh, spec enclave-network · 2026-09-05
Claim

`openspec/specs/enclave-network/spec.md#L45-L60` — "The attach sites are these run-argument builders, named by SYMBOL so the list survives every edit that does not rename one … A new enclave member MUST be added to this list in the same commit that attaches it; `scripts/check-enclave-membership-documented.sh` refuses the divergence in both directions."

Code / observed behavior

`crates/tillandsias-headless/src/main.rs#L9728` and `#L9764` — inside `fn run_provider_login` (defined at `#L9607`), two `podman run` invocations pass `"--network", ENCLAVE_ONLY_NET`. `run_provider_login` is not in the spec's twelve-symbol list.

The guard cannot report it. `scripts/check-enclave-membership-documented.sh#L71` — `if (fname ~ /^(build|launch)_/) { print fname }`. Only enclosing functions whose names begin `build_` or `launch_` are counted, so a production attach site in any other function is filtered out before the comparison runs. The same name filter is also silently doing the job the header at L59-L61 attributes to something else: that header says "The `#[cfg(test)]` region is cut before scanning", and no such cut exists anywhere in the script.

Why it matters

The spec's stated guarantee is that the membership list cannot drift silently, and its own text explains that a hand-maintained prose list had already gone stale by six members. The replacement mechanism reproduces the failure one level down: this attach site has been undetectable since the guard landed, and the spec's "Only the proxy MUST additionally be attached to the egress network" is stated against a list that is missing a member. The guard's own header even acknowledges a second dual-homed member ("the dual-homed pair (proxy, and the login helper)") that the spec text denies.

Remedy / check

Drop the `^(build|launch)_` name filter (cutting the real `#[cfg(test)]` region instead, as the header already claims), then add `run_provider_login` to the spec's membership list or route it through a named builder.

Evidence

No upstream issue filed.

History
  1. · found — From docs/audit/2026-09-05-issues-tillandsias.md, section under Enclave network isolation, the tillandsias.org explanation-page audit of 2026-09-05, level 4, re-read at v56.9.5.1.
Classification and release

spec-vs-code · v56.9.5.1 · b528680d2b938b09279e31faee981a8f6e370b3b

5e3a98f3The enclave-network spec's cleanup-on-exit and lifecycle-telemetry requirements have no implementationmediumtillandsias · spec enclave-network, crates/tillandsias-headless/src/main.rs · 2026-09-05
Claim

`openspec/specs/enclave-network/spec.md#L31-L34` — "**WHEN** the Tillandsias application exits **AND** no containers are attached … **THEN** the system MUST remove the network with `podman network rm tillandsias-enclave`". And `#L80-L86` — "**THEN** the system MUST log `[enclave] Network created: tillandsias-enclave`" / "`[enclave] Container attached: <name>`", both with `@trace spec:enclave-network`.

Code / observed behavior

`graceful_shutdown_async` at `crates/tillandsias-headless/src/main.rs#L16652` stops `tillandsias-`-prefixed containers and never touches a network. The only `network rm` in the crate is `#L8845`, inside `run_reset_guest` — the explicit `--reset-guest` wipe, not an exit path. `PodmanClient::remove_network` (`crates/tillandsias-podman/src/client.rs#L975`) has no production caller: the only references are `crates/tillandsias-podman-cli/src/lib.rs#L479` (the scenario tool) and a `let _ =` liveness reference at `client.rs#L3374`. The two mandated log strings — `Network created` and `Container attached` — do not appear anywhere under `crates/`, `scripts/` or `images/`.

Why it matters

Two of the four scenarios under the spec's primary requirement, and both scenarios of its telemetry requirement, describe behaviour that does not exist. The network persists across every exit, which is also what makes the `--internal` installed-base gap permanent rather than self-healing, and the `--log-enclave` accountability window the spec promises is empty for enclave lifecycle events.

Remedy / check

Either implement the exit-time `podman network rm` (guarded on zero attached containers) and the two log lines, or mark both requirements `deferred` with the reason, so the spec stops asserting them.

Evidence

No upstream issue filed.

History
  1. · found — From docs/audit/2026-09-05-issues-tillandsias.md, section under Enclave network isolation, the tillandsias.org explanation-page audit of 2026-09-05, level 4, re-read at v56.9.5.1.
Classification and release

spec-vs-code · v56.9.5.1 · b528680d2b938b09279e31faee981a8f6e370b3b

5f323a37The Windows tray reads the secure-wire variable directly, the single-reader ratchet has two slots of slack, and the WSL guest unit never receives the flagmediumtillandsias · crates/tillandsias-windows-tray/src/hvsocket.rs, scripts/check-secure-wire-single-reader.sh, crates/tillandsias-vm-layer/src/wsl.rs · 2026-09-05
Claim

`scripts/check-secure-wire-single-reader.sh#L3-L5` — "`TILLANDSIAS_SECURE_CONTROL_WIRE` has ONE reader, and this ratchets the number of files that name it down, never up." `#L39` sets `BASELINE="${TILLANDSIAS_SECURE_WIRE_READER_BASELINE:-3}"`, and `#L37-L38` says "Lower this as each is converted; it must never rise. 0 means the flip may land."

Code / observed behavior

At this tag exactly one file outside the owning module still reads the variable — `crates/tillandsias-windows-tray/src/hvsocket.rs#L29`, `if std::env::var("TILLANDSIAS_SECURE_CONTROL_WIRE").as_deref() == Ok("on")`, i.e. the case-sensitive byte comparison the guard's own header (`#L15-L20`) identifies as the behaviour that made `On` silently plaintext. So the count is 1 against a baseline of 3: two more direct readers could be added, with two more private interpretations of the value, and the guard would still print `ok`.

The other half is that the flag never reaches the Windows-side server. `crates/tillandsias-vm-layer/src/vz.rs#L859` injects `Environment=TILLANDSIAS_SECURE_CONTROL_WIRE=__SECURE_CONTROL_WIRE__` into the macOS guest's systemd unit; the WSL equivalent, `fn headless_unit` (`crates/tillandsias-vm-layer/src/wsl.rs#L90-L110`), writes only `Environment=HOME=/root` and `Environment=XDG_RUNTIME_DIR=/run/user/0`. The guest headless therefore always runs with the variable unset, so even a tray told `on` would hand a Noise handshake to a server that is not expecting one — which is precisely the one-sided flip the guard's header records as having already happened once (`#L23-L27`, flipped at e6a80609f, reverted at 08a7d3cc7).

Worth recording alongside: the PSK root is `release_root_secret()` (`crates/tillandsias-secure-channel/src/lib.rs#L86-L101`), which on debug builds is the literal `b"tillandsias-dev-root-not-a-secret"` (`#L68`) and on release builds is the SHA-256 of the shipped binary. Both are values an attacker holding a public artifact can compute, so the channel provides version binding, not confidentiality against anyone who has the release. The source comments say this plainly; the word "secret" in the function name does not.

Why it matters

A ratchet with slack is not a ratchet: the guard prints `ok` while two more private readers of the flag may still be added. And the flip the whole mechanism exists to make safe cannot succeed today, because the WSL guest unit never carries the variable, so a tray told `on` would speak Noise at a server that is not.

Remedy / check

Lower `BASELINE` to 1 now (it can only be lowered honestly if it tracks reality), convert `hvsocket.rs` to `tillandsias_control_wire::secure_wire_mode`, and add the `TILLANDSIAS_SECURE_CONTROL_WIRE` line to `wsl.rs`'s `headless_unit` before the default is flipped.

Evidence

No upstream issue filed.

History
  1. · found — From docs/audit/2026-09-05-issues-tillandsias.md, section under Secure control wire, the tillandsias.org explanation-page audit of 2026-09-05, level 5, re-read at v56.9.5.1.
Classification and release

defect · v56.9.5.1 · b528680d2b938b09279e31faee981a8f6e370b3b

64d733d3The Home positioning line makes a claim no level page footnotesmediumtillandsias.org · openspec/changes/add-level-page-specs/specs/site/navigation, scripts/build-matrix.py · 2026-09-06
Claim

The navigation spec's scenario for the positioning line says that where it makes a claim about what the software does, it says only what a level page already carries with a footnote, in words no stronger, and short enough that the two can be compared at a glance.

Code / observed behavior

The line the build ships is a verbatim copy of the "What is it?" hero lede, which is page furniture carrying no footnote. The review reports that a grep across the level pages finds none of "Local hardware", "rented", "metered" or "left behind"; two of the line's clauses map loosely onto footnoted level-2 sentences, and "nothing left behind" maps onto nothing at all. The scenario names no level sentence to compare against and no party to do the comparing, so neither the build nor a reviewer can fail it.

Why it matters

This is the requirement in the navigation spec that governs a claim on the live Home page, and as written it neither identifies its source nor is checkable — the clause it exists to keep honest has no footnoted counterpart anywhere on the site. Unlike the rest of this review's findings, the sentence it fails to govern is published.

Remedy / check

Bind the positioning line to a named source — it repeats one sentence a level page carries with its footnote, and an editor can point at that sentence — or govern the hero lede in this spec as well, so the two copies have one owner. Then move the unbacked clause onto a level page with a footnote, or drop it.

Evidence
History
  1. · found — From the specification review of the eight site page specs; the navigation review's positioning-line defect.
Classification and release

doc-drift · main · cb07e89c411e6c63abb8995928ed9a6da34ddc8c

6efcbb8bThe next level-5 delta change is unwritten while two of its REDs are already outrun in the daily channelmediumtillandsias.org · openspec/changes, docs/matrix/level-5-phd.md · 2026-09-05
Claim

Task 4.18 of add-level-page-specs names the work for the next pin bump — the REDs on property tests and on requirement identifiers, the footnotes that drift between the tags, the measured numbers to re-measure — as its own OpenSpec change, one delta per item.

Code / observed behavior

The box is unchecked and no such change directory exists: openspec/changes/ holds add-container-framework, add-level-page-specs, level-5-stable-pin-deltas and an empty archive. The fixes it names are real at the daily and were read there — the generative property tests over the refinement operator, and the gate for stable requirement identifiers — and of level 5's fifteen cited files, seven differ between the two tags: fragments.rs, methodology/philosophy.yaml, methodology/proximity.yaml and scripts/local-ci.sh changed, and obligation.rs, obligation_props.rs and scripts/check-requirement-ids.sh exist only in the daily. Only the four the D2-D5 footnotes name are acknowledged on the page today.

Why it matters

Level 5 is the page that may lag, and lag is legitimate while it is recorded. Task 4.18 is the only record, it is a paragraph inside another change's task list, and it will be read by whoever bumps the pin rather than by whoever reviews the site next week.

Remedy / check

Open openspec/changes/level-5-pin-bump-<tag>/ now, empty except a proposal.md carrying task 4.18's list and the drift table of finding 247ed660, so the deltas accumulate against a change instead of a checkbox.

Evidence
Depends on

247ed660

History
  1. · found — Migrated from docs/audit/2026-09-05-issues-tillandsias.org.md section A3; re-checked open against the tree at c4039e1 — task 4.18 unchecked, no pin-bump change directory, and seven of level 5's fifteen cited files differ between the tags.
Classification and release

stale-record · cb07e89 · cb07e89c411e6c63abb8995928ed9a6da34ddc8c

764caa09Requirements freeze today's verdicts, so a pin bump would make the spec mandate a false sentencemediumtillandsias.org · openspec/changes/add-level-page-specs/specs/site/level-{1,4,5} · 2026-09-06
Claim

The level-5 spec states its own governing principle — this spec requires the form of the verdicts and never a particular verdict — and level-common orders that a RED the new pin's code has fixed becomes a past-tense flag or a GREEN.

Code / observed behavior

Three requirements do the opposite, and the reviews show two of them already falsified one release forward. Level 4 makes a REFUTED permanent and unconditional, with no pin condition, while the page's only REFUTED says the hardening litmus cannot fail at this release — a claim the change's own tasks.md 4.16 records as no longer holding on the newer build. Level 5's section enumeration mandates today's conclusions rather than the object each section covers: item 4 requires the gap between what the repository asserts of the operator and what the theorem needs, item 6 requires the law-of-large-numbers family the doctrine invokes and the hypothesis the architecture violates by design; the review found both falsified at the only candidate pin, where the workspace property-tests monotonicity of refine and the methodology withdraws its strong-LLN claim. Level 1 requires the method section to carry an honest limit that exists only because the pinned methodology currently withholds a metric proof.

Why it matters

At the next pin bump each of these compels the page to keep a sentence its source no longer supports — the precise failure level-common's RED lifecycle exists to prevent — or leaves the page unable to comply without inventing a defect. The level-5 case also reverses this change's own completed task 2.6, which stripped mandated verdicts out of every requirement. Nothing is published, so no reader is misled today; the cost falls on the first editor to move a pin.

Remedy / check

Condition each on the pin, as level 5's "Verdicts are re-derived, never frozen" already does: name the section's object and the job it does and let the conclusion be a fact of the pin; keep a REFUTED while it holds at the pin and restate it in the past tense, naming the release that changed it, when the pin moves; and require the limit the cited methodology records at the pin rather than today's wording of it.

Evidence
History
  1. · found — From the specification review of the eight site page specs; three defects, two of them already falsified one release forward.
Classification and release

process · main · cb07e89c411e6c63abb8995928ed9a6da34ddc8c

88d8d509The forge startup context tells the agent Vault is at `http://vault:8200`; the listener is TLS-onlymediumtillandsias · images/default/lib-common.sh, images/vault/vault.hcl · 2026-09-05
Claim

`images/default/lib-common.sh#L4441` — the text printed into every forge agent's opening context reads: "**Vault**: secrets are available at `http://vault:8200`; token is injected automatically."

Code / observed behavior

`images/vault/vault.hcl#L16-L21` configures the only listener with `tls_cert_file`, `tls_key_file` and `tls_client_ca_file` and no `tls_disable`, and `#L23` sets `api_addr = "https://vault:8200"`. The Rust side agrees: `crates/tillandsias-headless/src/vault_bootstrap.rs#L900` — `const VAULT_EXEC_ADDR: &str = "https://127.0.0.1:8200"`, and its own test at `#L4864` asserts `linux_vault_api_base_url(true) == "https://vault:8200"`.

Why it matters

This string is the first thing an autonomous agent reads about how to reach Vault, and following it produces a TLS-handshake failure against a plaintext request. It also mis-teaches the security posture: the in-enclave hop is authenticated TLS, and the context says it is not.

Remedy / check

Change the line to `https://vault:8200`.

Evidence

No upstream issue filed.

History
  1. · found — From docs/audit/2026-09-05-issues-tillandsias.md, section under Vault and secrets, the tillandsias.org explanation-page audit of 2026-09-05, level 2, re-read at v56.9.5.1.
Classification and release

doc-drift · v56.9.5.1 · b528680d2b938b09279e31faee981a8f6e370b3b

8dd0e5c5Nine requirements contradict their own scenario or a sibling requirementmediumtillandsias.org · openspec/changes/add-level-page-specs/specs/site/level-{1,2,3,4,5} · 2026-09-06
Claim

Each of these requirements is written to be applied by an editor who was not present when it was written, and openspec validate passes on all three changes, so nothing structural flags them.

Code / observed behavior

Nine requirements instruct and forbid the same thing. Level 4's required sections open "in this order" and close by permitting the surface sections to be reordered, silently dropping the assurance-last constraint stated two sentences earlier. Level 4 lists the argument markers as optional, with dropping one explicitly not a regression, while "Rejected arguments stay on the page" makes a REFUTED mandatory. Level 4's hardening requirement governs a "hardening section" its own section list never defines. Level 3 makes it a MUST that each subsystem subsection say what falls short at the pin, and its own third scenario says a subsystem with nothing wrong keeps its subsection. Level 3 requires each unanswered item of a multi-item RED to be named in a scoped no-path clause and then makes the scoping optional in the next sentence. Level 5 requires each argument section to end with the flags that judge it while the same requirement declares markers optional and the page's first argument section carries none. Level 1's app-vocabulary requirement asserts the pinned source records a machine check of the promise it has just shown, and its third scenario forbids presenting that check as proof of that promise. Level 2 requires a cost sentence that its own platform requirement would forbid on Linux, and gives no precedence rule where "verbs match what the code does" and "the footnote target carries the claim" pull in opposite directions — the case that produced both rules.

Why it matters

A requirement that contradicts itself cannot be applied: an editor either picks a half and calls it compliance, or reads the whole spec as approximate. Three of these would license deleting true page content, and one would license writing a claim false on a supported platform. Nothing is published, so no reader is misled today; the damage is to the next editor, who is the reader these specs are written for.

Remedy / check

Resolve each pair in favour of the scenario, which is the half derived from the page: state ordering once and positively; carve the required markers out of the optional list by reference; anchor the hardening rule to the boundary section's hardening paragraphs; make the shortcoming clause conditional; say which no-path form governs and when scoping becomes required; make the flag rule a placement rule; reword the app-vocabulary MUST to permit a machine check only when the written promise it enforces is named; and add the missing precedence clause.

Evidence
History
  1. · found — From the specification review of the eight site page specs; nine defects, in five of the six specs reviewed.
Classification and release

process · main · cb07e89c411e6c63abb8995928ed9a6da34ddc8c

a988d3c4The forge startup context hands agents the pre-flip project.service.localhost URL shapemediumtillandsias · images/default/lib-common.sh, spec subdomain-naming-flip · 2026-09-05
Claim

The subdomain-naming-flip requirement stands at both tags and mandates service.project.localhost for every URL that names a forge service.

Code / observed behavior

The startup-context generator in images/default/lib-common.sh still emits the pre-flip shape — project.service.localhost — in the dev-server paragraph, at L4223 in v56.9.2.1 and at L4535 in v56.9.5.1, so every agent in every forge reads it. The browser MCP enforces the flipped shape and refuses any other host. The publishing paragraph a few lines below the stale one is correct, and so is the copy that lands in a project's .forge-startup-context.md, which is this file's output.

Why it matters

An agent that follows the context file hands its user a hostname the router has no route for and the browser tool refuses by shape, then debugs a dev server that looks dead. This is the fourth documented case of the startup context misinforming an agent — the vault address, the on-demand attestation claim and the Local Experts mode are already flagged on the site's level-3 page — and the only one that hands the agent a wrong artifact to give the user rather than a wrong belief about a service.

Remedy / check

Swap the two labels in images/default/lib-common.sh so the line reads service.project.localhost. The file is generated into each project and gitignored there, so nothing in the website repository can fix it: it is filed here alongside the vault-address and attestation entries, and the site's level-3 context-file RED picks it up at the next re-verification.

Evidence

No upstream issue filed.

History
  1. · found — Migrated from docs/audit/2026-09-05-issues-tillandsias.org.md section D1. Recorded against the runtime rather than this website: the generator that emits the stale line is images/default/lib-common.sh, present with the same wording at L4223 in v56.9.2.1 and L4535 in v56.9.5.1, and nothing in this repository can fix it.
Classification and release

doc-drift · v56.9.5.1 · b528680d2b938b09279e31faee981a8f6e370b3b

cf6b4dffEvery claim MUST carry a footnote resolving at the level's pin is false of both pages that state itmediumtillandsias.org · openspec/changes/add-level-page-specs/specs/site/level-{1,4} · 2026-09-06
Claim

Level 1 and level 4 both open their "Claims are no stronger than their source" requirement with the same sentence: every claim MUST carry a footnote resolving at the level's pin. Level 1's Purpose says the same of every sentence on the page.

Code / observed behavior

The sentence is false of both pages and contradicted by the shared contract it sits under. level-common permits a footnote to point past the pin with an explicit @vTAG suffix, and both pages use it — the review counted roughly twenty @v56.9.5.1 footnotes on level 4 alone, none of which resolves at v56.9.2.1. Both pages also carry unfootnoted claims: level-1-five.md states its cloud framing and its doll-house mapping with no footnote marker at all, and the review lists three more lines there and three unfootnoted absolutes on level 4, none of them scheduled for repair in the change's tasks.md.

Why it matters

This is the first sentence of the requirement that governs every claim on the page, and it is contradicted by the shared contract, by the pages it describes, and — on level 4 — by another requirement in the same file that mandates PATH evidence at a newer tag. An editor either ignores it or deletes true prose to satisfy it. The specs are unpublished, so no reader is misled today; the pages themselves are unaffected by this finding, which is about what the spec says of them.

Remedy / check

Scope it: every factual claim about the runtime carries a footnote; a claim about the pinned release resolves at the pin, and a PATH clause reporting a newer build resolves at the tag its @vTAG names. Leave where and how footnotes resolve to level-common, and either schedule the unfootnoted lines or say that a section's framing sentence inherits the citations of the claims it introduces.

Evidence
History
  1. · found — From the specification review of the eight site page specs; the same opening sentence flagged in both the level-1 and level-4 reviews.
Classification and release

doc-drift · main · cb07e89c411e6c63abb8995928ed9a6da34ddc8c

d0c34ed6Nothing tells this repository that the runtime published a releasemediumtillandsias.org · skills/update-website/scripts/latest-release.sh · 2026-09-05
Claim

The skill's second automation candidate is the same drift report, fired when the runtime publishes a release, either by the release workflow pinging this repository or by a cron that polls tags.

Code / observed behavior

latest-release.sh resolves both channels correctly and cheaply, stable from the releases endpoint with cache-busting and a cross-check against the git tag, unstable over git ls-remote with no API call at all. It is polled by a human who remembers to run the loop.

Why it matters

Dailies land daily, the pages pin stable, and stable moves only on promotion — so the event worth waking up for is rare and easy to miss. During that window the site's PATH lines say a fix is not yet promoted about things that are promoted, which is the one lie this design exists to avoid.

Remedy / check

A cron entry on a host session running drift-report.sh and reporting to the operator whenever its last line is not ok:up-to-date; plan/host-notes.md is the channel a host session already has. A ping from the runtime's release workflow is better and belongs on the runtime's side of the fence, which makes it a request rather than a task.

Evidence
Depends on

3d0ef7e2

History
  1. · found — Migrated from docs/audit/2026-09-05-issues-tillandsias.org.md section G4; re-checked open against the tree at c4039e1 — nothing schedules latest-release.sh or the drift report.
Classification and release

defect · c4039e1 · c4039e18f5533054f368b9836867655880d55c09

e2e95e62Two register MUSTs name no criterion an editor could applymediumtillandsias.org · openspec/changes/add-level-page-specs/specs/site/level-{1,4} · 2026-09-06
Claim

Level 1's register requirement and level 4's layer requirement are stated as MUSTs, which the change's tasks.md treats as checkable by an editor reviewing the page.

Code / observed behavior

Three clauses give nothing to check against. "One metaphor family" leaves family undefined, and the page mixes doll-house imagery, household and postal imagery and a literal UI element, so the MUST is either already breached or unenforceable. "Machine vocabulary" names no list, on a page whose own subject is that two different banned-word lists exist and differ. Level 4's requirement that a mitigation be criticised "without the criticism being softened" offers no test at all.

Why it matters

An unappliable MUST is either ignored, which teaches that the rest are optional, or used to justify an arbitrary edit; the metaphor clause's first use would be to delete prose that works. Nothing is published, so no reader is misled today.

Remedy / check

Keep the enforceable rule level 1 already states — one metaphor per object, and no second metaphor for an object that has one — and drop "one metaphor family". Define the vocabulary ban by reference to the banned-word lists the pinned source records, noting that more than one exists and the prose must avoid the union. Delete the "softened" clause, whose intent the scenario about what a cost bought already carries.

Evidence
History
  1. · found — From the specification review of the eight site page specs; the level-1 register defect and the untestable clause the level-4 review flagged alongside it.
Classification and release

process · main · cb07e89c411e6c63abb8995928ed9a6da34ddc8c

ecbe8175Universal MUSTs assert of the pages and of the audit record what is not true of themmediumtillandsias.org · openspec/changes/add-level-page-specs/specs/site/level-{3,4,5} · 2026-09-06
Claim

These requirements are written as descriptions of the pages they govern and of how the project works, in the register of a contract the page already satisfies.

Code / observed behavior

Eight of them are false of their subject on the day they ship. Level 4 states that audits of the page are run against a build newer than the pin; the page's own annotation file is scoped to the pin itself, and the current dated record says it re-verified every claim at the pin. Level 4 requires promise, reason, price and purchase before the flags of every defended surface; the supply-chain heading is followed immediately by its first GREEN with no prose at all. Level 3 defines its required subsections as one per subsystem a launch brings up unconfigured, which by the page's own account excludes two of its eight subsections, and its add-or-remove scenario then licenses deleting them. Level 3's GREEN attribution MUST is broken by five of nine GREENs, and its "any list" MUST catches an illustrative naming of toolchains that nobody thinks is wrong. Level 5's closed section enumeration has no home for the live RED on the methodology's complexity constraint; its superlative ban forbids the theorem statements the page exists to make; and its Verdict rule requires every sentence there to restate an established claim, which the section's closing line does not.

Why it matters

A requirement falsified by its own subject teaches an editor that this spec's MUSTs are approximate — and several of these, applied literally, would have the editor delete true prose or a live RED to comply. The audit-premise sentence is worse than wrong: it reads as licence to distrust an at-the-pin audit, which is the steady state the update loop aims at. The specs are unpublished, so nothing here misleads a reader today.

Remedy / check

Scope each MUST to the case its own scenarios test, and make the audit premise conditional ("where an audit is run against a build newer than the pin"). Bind the Verdict rule to claims rather than sentences, the superlative ban to claims about the tree, the list rule to lists offered as the inventory of a bounded set, and widen level 3's subsection class to any subsystem a session reaches without the reader configuring it.

Evidence
History
  1. · found — From the specification review of the eight site page specs; eight defects across the level-3, level-4 and level-5 reviews.
Classification and release

doc-drift · main · cb07e89c411e6c63abb8995928ed9a6da34ddc8c

05229912The level-5 delta change is assembled by hand every timelowtillandsias.org · openspec/changes/level-5-stable-pin-deltas · 2026-09-05
Claim

The skill's fifth candidate is to produce the OpenSpec change for level 5 with the exact deltas and evidence, never the edit. The requirement it serves is one delta per requirement, flag or footnote touched, each with a line-range citation and a verbatim quote at the target tag.

Code / observed behavior

The change filed that day was assembled by hand: seven applied deltas D1-D7, four new footnotes [^41]-[^44], twenty-eight verbatim quotes added under the in-repo footnotes, and a task list whose every line had to be verified twice. Most of that is generable — the drift set, the new anchors, the quote text at the target tag, and the skeletons of proposal.md and tasks.md.

Why it matters

The failure the level-5 spec names — the page's diff containing a hunk that no delta lists — is detectable only if the change lists everything the diff should contain, and a generator that starts from the drift set does that by construction.

Remedy / check

About 120 lines on top of the re-anchor helper: take a target tag, diff the cited-file set, read level 5's rows out of a trial build at that tag, and emit the change directory with a proposal, one unchecked box per delta and the minimal delta spec validation insists on. It must never touch docs/matrix/level-5-phd.md; stopping short is the point.

Evidence
Depends on

296676d4

History
  1. · found — Migrated from docs/audit/2026-09-05-issues-tillandsias.org.md section G5; re-checked open against the tree at c4039e1 — no generator exists and the one filed change was hand-written; its seven deltas, four footnotes and twenty-eight quotes were counted in the change itself.
Classification and release

defect · c4039e1 · c4039e18f5533054f368b9836867655880d55c09

1e141597The 2026-09-04 annotation files were left unamended beside pages the verification pass rewrotelowtillandsias.org · docs/matrix/level-1-five.audit.md and its three 2026-09-04 siblings · 2026-09-05
Claim

Each annotation file is a per-footnote verdict table for the page it sits beside, written 2026-09-04 against the pinned release.

Code / observed behavior

The four 2026-09-04 files were last touched at 607b5cf, before the verification pass rewrote the pages they annotate at 4a56048, 8155885, ef65ab0 and de2759b. Their verdicts now describe text that is gone: the level-2 table still marks footnote 5 OK against ephemeral-secret-refresh/spec.md#L1-L20 with the gloss "reissued at every start", while the page's footnote 5 was re-anchored to #L9-L25 and the reassurance it carried was withdrawn as false in the reassuring direction.

Why it matters

An annotation file that contradicts the page beside it is worse than none: it is the artifact a next editor would consult to learn which claims were checked, and it will hand them a verdict that was overturned the same day.

Remedy / check

Amend each file with the corrections the verification pass applied, or — better, and jointly with finding a0be2422 — move them under docs/audit/ with their date in the name, where a superseded record is expected to be read as history.

Evidence
Depends on

a0be2422

History
  1. · found — Confirmed by the final completeness audit, left open, and listed in the "What could not be verified here" section of docs/audit/2026-09-05-v56.9.2.1.md; re-checked at c4039e1 against the level-2 annotation table and the corrected page.
Classification and release

stale-record · c4039e1 · c4039e18f5533054f368b9836867655880d55c09

34460f90The hardening litmus header says the argv validator has zero production call sites; it has threelowtillandsias · openspec/litmus-tests/litmus-podman-idiomatic-security-flags.yaml · 2026-09-05
Claim

`openspec/litmus-tests/litmus-podman-idiomatic-security-flags.yaml#L66-L71` — "WHAT IT STILL DOES NOT COVER … that every production launch site actually APPLIES the envelope. The validator that would answer this — `validate_launch_argv` in the same module — has zero production call sites, which is order 972-6vaj's subject, not this file's."

Code / observed behavior

At this tag `validate_launch_argv` (`crates/tillandsias-podman/src/policy.rs#L110`) has three non-test callers: `crates/tillandsias-podman/src/container_spec.rs#L396` inside `fn build_run_argv`, and `crates/tillandsias-headless/src/main.rs#L11711` and `#L11877`. `container_spec.rs#L379-L384` documents the change explicitly ("THIS WAS A `debug_assert!` AND THEREFORE NOTHING … the validator had zero production call sites"), i.e. the header describes the state 972-6vaj fixed.

Why it matters

This is the file a reader consults to learn what the hardening gate does and does not prove, and it is currently more pessimistic than the truth in a way that hides the real remaining gap (see finding da34e5ee). A stale disclaimer in a security fixture is read as current fact.

Remedy / check

Replace the paragraph with the current state: the validator now guards `ContainerSpec::build_run_argv` and the two delegated-run entry points, and name the launch paths that still bypass it.

Evidence
Depends on

da34e5ee

No upstream issue filed.

History
  1. · found — From docs/audit/2026-09-05-issues-tillandsias.md, section under Container launch hardening, the tillandsias.org explanation-page audit of 2026-09-05, level 4, re-read at v56.9.5.1.
Classification and release

doc-drift · v56.9.5.1 · b528680d2b938b09279e31faee981a8f6e370b3b

373585f1The published site cannot be reached from a forge, so no claim about it was verified as served HTMLlowtillandsias.org · docs/audit, and the proxy allowlist in the runtime · 2026-09-05
Claim

The audit verifies what a reader of tillandsias.org sees, and the update loop ends by publishing to it.

Code / observed behavior

The site is not reachable from the forge that builds it: the connection is reset, because the proxy allowlist is baked into the proxy image and the project's own domain reached it only in the daily channel. So every claim in the day's records about what a reader sees rests on the committed var/html/index.html and its rebuild hash, not on served HTML.

Why it matters

The last link in the chain is unverified. A build can be byte-identical to the committed artifact and the served page still differ — a stale cache, a misrouted deployment, a worker that never took the commit — and nothing in this repository would notice. It is also the only claim class the checked build cannot reach, so it will stay unverified until a session outside the stable image looks.

Remedy / check

Until a forge on a release carrying the allowlist entry can fetch the site, have a host session or the operator fetch the published page and compare its hash with the committed artifact, and record the comparison in the run's record beside the rebuild hash.

Evidence
History
  1. · found — Migrated from the closing "Open questions this record does not answer" of docs/audit/2026-09-05-issues-tillandsias.org.md, where it is the one item that is a finding rather than a restatement; the same limitation is listed in the run record's "What could not be verified here". Re-checked against the tree at c4039e1 and against both checkouts - the stable allowlist has no entry for the domain, the daily has one.
Classification and release

process · c4039e1 · c4039e18f5533054f368b9836867655880d55c09

53650fa1Cross-references in the audit records name line ranges that moved during the daylowtillandsias.org · docs/audit/2026-09-05-issues-tillandsias.org.md, docs/audit/2026-09-05-v56.9.2.1.md · 2026-09-05
Claim

The records cite this repository's own files by path and line range, and state that line references into this repository were resolved at the site commit each record names.

Code / observed behavior

The commits that followed moved the lines. The site issues record cites skills/update-website/scripts/checked-build.sh#L22 as the line that invokes the Python build; at c4039e1 line 22 is `done` and the invocation is at L28, because the gate repair inserted a checkout loop above it. Where a reference was qualified with the commit it was read at — the two into scripts/build-matrix.py — it still resolves; the unqualified ones do not.

Why it matters

These records are the durable form of the day's findings, and a citation that lands on the wrong line is the same defect the pages are gated against. The fix pattern already exists in the same files: name the commit beside the range.

Remedy / check

Qualify every intra-repository line reference in the records with the commit it was read at, as the two build-matrix references already are, or re-anchor them at the commit the record now names.

Evidence
History
  1. · found — Confirmed by the final completeness audit, left open, and listed in the "What could not be verified here" section of docs/audit/2026-09-05-v56.9.2.1.md; re-checked at c4039e1 against the checked-build.sh reference, which now lands on the wrong line.
Classification and release

doc-drift · c4039e1 · c4039e18f5533054f368b9836867655880d55c09

548baf18The audit record's Method paragraph still reports the superseded footnote countslowtillandsias.org · docs/audit/2026-09-05-v56.9.2.1.md · 2026-09-05
Claim

The record says every count in it was re-derived at the commit it describes, and that an earlier version describing the tree as it stood mid-run is superseded.

Code / observed behavior

The correction reached the header and the five per-level tables but not the Method section, whose closing paragraph still reports 22, 29, 141, 93 and 44 footnotes for levels one to five and 317 of 329 carrying a verbatim quote. Counted in the tree at c4039e1 the levels carry 23, 29, 147, 99 and 44, totalling 342 with 330 quoted — the same numbers the header now gives. The record therefore contradicts itself by thirteen footnotes, and by thirteen again on the quoted count.

Why it matters

The Method section is where the record explains why its numbers can be trusted, so a stale count there undermines the paragraph it sits in. It is also the residue of a fix believed complete, which is how a corrected record quietly goes stale again.

Remedy / check

Re-derive the three numbers in that paragraph at the commit the record names, as the header and the per-level tables already were.

Evidence
Depends on

67b4a3aa

History
  1. · found — Found while verifying that 67b4a3aa was closed — the Method paragraph of docs/audit/2026-09-05-v56.9.2.1.md still carries the pre-correction counts at c4039e1, so the record contradicts its own header.
Classification and release

stale-record · c4039e1 · c4039e18f5533054f368b9836867655880d55c09

5811a59dThe byte-identical rebuild check is recorded as unrun, and it no longer passes now that the page embeds the ledgerlowtillandsias.org · openspec/changes/add-level-page-specs/tasks.md · 2026-09-05
Claim

Task 3.2 records the 2026-09-05 attempt as inconclusive — two different hashes, not comparable because three levels were being edited in the same working tree — and leaves the box unchecked, to be re-run on a clean tree.

Code / observed behavior

The check did start passing: rebuilt from the tree at c00622b, the committed page and a private build both hash to baab0e512b737f8467646dfc2688b4da609bc680b1b13c631dae2e7913cc3714, and the same run printed that all checked footnote targets resolve and exited zero. The box is still [ ] and still carries the superseded pair of hashes. It would now be wrong to tick it: since 30c6e5e the page also renders the folded findings ledger, and the committed var/html/index.html at c4039e1 was built against ledger fragments that are not in that commit, so rebuilding c4039e1 from its own tree gives a different hash — six findings on the committed board against the one fragment the commit carries.

Why it matters

That hash is the guarantee that the artifact Cloudflare serves is the one the sources produce, which is the assumption the deployment notes rest on when they leave the build command empty. An unchecked box says the guarantee is untested; worse, the guarantee has since stopped holding and nothing in the record says so.

Remedy / check

Commit the ledger fragments the published page was built from, re-run the check on the clean tree, and tick task 3.2 with the hash and the date beside it — so that the page, its sources and the ledger it renders are one commit.

Evidence
History
  1. · found — Migrated from docs/audit/2026-09-05-issues-tillandsias.org.md section B3; re-checked open against the tree at c4039e1 — task 3.2 is still unchecked and still records the superseded hashes, and the rebuild the audit reported as passing was re-run here, passing at c00622b and no longer reproducing at c4039e1.
Classification and release

stale-record · c4039e1 · c4039e18f5533054f368b9836867655880d55c09

71e14b04Denied runtime traffic gets a TCP reset; the proxy spec says HTTP 403 twicelowtillandsias · spec proxy-container, images/proxy/squid.conf · 2026-09-05
Claim

`openspec/specs/proxy-container/spec.md#L53` — "**THEN** the proxy SHALL deny the request with HTTP 403"; and again at `#L109` — "**AND** all other domains SHALL be denied with HTTP 403". A third scenario (`#L186-L188`) requires that "the proxy response SHALL include the blocked domain name for debugging".

Code / observed behavior

`images/proxy/squid.conf#L137-L141` — `acl strict_deny_acl localport 3128` followed by `deny_info TCP_RESET strict_deny_acl`, with the inline rationale "Send a TCP reset instead of an HTTP 403 error for strictly-denied runtime traffic. This prevents sophisticated tunneling/exfiltration over denied connections."

Why it matters

The behaviours are not interchangeable for a client: a 403 carries a body naming the blocked domain (which a third scenario in the same spec requires), a TCP reset carries nothing. Anyone debugging a blocked domain from inside a forge, or writing a test against the spec, is told to expect a status code that never arrives. The deliberate choice recorded in the config is the right one; the spec has simply not been updated to it.

Remedy / check

Change the two `HTTP 403` scenarios to `TCP_RESET` on port 3128, and either drop or re-scope the "response SHALL include the blocked domain" scenario, which a reset cannot satisfy.

Evidence

No upstream issue filed.

History
  1. · found — From docs/audit/2026-09-05-issues-tillandsias.md, section under Egress proxy, the tillandsias.org explanation-page audit of 2026-09-05, level 3, re-read at v56.9.5.1.
Classification and release

spec-vs-code · v56.9.5.1 · b528680d2b938b09279e31faee981a8f6e370b3b

7d737704A footnote referenced more than once emits a duplicate element identifierlowtillandsias.org · scripts/build-matrix.py, var/html/index.html · 2026-09-05
Claim

The generated page gives each footnote reference an identifier so the footnote list can point back at the place in the prose that cited it.

Code / observed behavior

The identifier is built from the level slug and the footnote number alone, and is emitted at every occurrence of the reference, so a footnote cited twice on one page produces the same id twice. Of the 775 ids in the published page, 56 are emitted more than once — 69 duplicate emissions in all, every one of them a footnote back-reference. For example id="rlevel-1-five-5" appears twice in var/html/index.html.

Why it matters

Duplicate ids are invalid HTML, and the back-link they exist for lands on whichever occurrence the browser reaches first, which is not necessarily the one the reader came from. Assistive technology and in-page anchors both rely on ids being unique.

Remedy / check

Number the occurrences — append an occurrence index to the id for the second and later references — and have the footnote list link to the first.

Evidence
History
  1. · found — Confirmed by the final completeness audit, left open, and listed in the "What could not be verified here" section of docs/audit/2026-09-05-v56.9.2.1.md; re-checked at c4039e1, where 56 of the published page's identifiers are emitted more than once, 69 duplicate emissions in all — recounted here rather than copied.
Classification and release

defect · c4039e1 · c4039e18f5533054f368b9836867655880d55c09

83309118README's release ledger has no row for v56.9.5.1lowtillandsias · README.md · 2026-09-05
Claim

`README.md#L107` — "The release skill appends a row per release; STABLE marks channel promotions." The ledger table header is at `#L112`.

Code / observed behavior

The newest row is `v56.9.4.1` (`#L114`), then `v56.9.2.1` (`#L115`), then `v56.8.31.3` (`#L116`). The string `56.9.5.1` does not appear anywhere in `README.md`, while `VERSION` at this tag reads `56.9.5.1`.

The source section records this as class `stale-plan-entry`; the ledger's nearest class is `stale-record`.

Why it matters

The ledger is explicitly addressed to agents ("Agents doing smoke curl-installs or jumpstarting work read the recent rows first", `README.md#L103-L104`), so an agent on the newest daily reads a ledger whose newest entry is a different release and has no way to see what changed under it. It is also the mechanism by which this site's explanation pages decide what a release shipped.

Remedy / check

Append the `v56.9.5.1` row, or make the release skill's ledger append a gated step rather than a convention.

Evidence
  • README.md#L107 — “The release skill appends a row per release; STABLE marks channel promotions.”
  • README.md#L110 — “<summary>Release ledger (newest first)</summary>”
  • README.md#L114 — “v56.9.4.1 (daily — cut 2026-09-04 by the coordinator's 20:09Z meta cycle, PR #105 merge + bump PR;”

No upstream issue filed.

History
  1. · found — From docs/audit/2026-09-05-issues-tillandsias.md, section under Documentation and release ledger, the tillandsias.org explanation-page audit of 2026-09-05, level 1, re-read at v56.9.5.1.
Classification and release

stale-record · v56.9.5.1 · b528680d2b938b09279e31faee981a8f6e370b3b

89ab166aThe loop figure is drawn and placed nowhere, and the new capability sections carry no figurelowtillandsias.org · scripts/figures.py, docs/matrix/level-3-power.md, docs/matrix/level-4-security.md · 2026-09-05
Claim

The matrix rules list eleven figures defined as inline SVG in scripts/figures.py, drawn with the page's own CSS variables so that they carry one idea each.

Code / observed behavior

All eleven exist and are registered. Ten are placed, fourteen times in all, seven of them on level 5. The loop figure — the closed loop from specs through evidence back to specs — appears in no level file, and neither its title nor its caption appears in any byte of var/html/index.html. The two sections written that day are figureless: level 3's eight-capability inventory has none, and level 4's layer-by-layer case has only the gate figure it already had.

Why it matters

A figure defined and never placed is dead weight in a file whose whole point is that the figures carry one idea each, and the longest, newest, most list-shaped section on the site has no visual anchor at all.

Remedy / check

Place @fig:loop in level 3's gate section, where the argument is exactly that specs, evidence and the gate close on each other, and move @fig:layers above the capability inventory instead of below it. Both are one-line edits under the existing dialect and no new figure needs drawing.

Evidence
History
  1. · found — Migrated from docs/audit/2026-09-05-issues-tillandsias.org.md section E1; re-checked open against the tree at c4039e1 — eleven figures registered, ten placed fourteen times, and neither the loop figure's title nor its caption appears in var/html/index.html.
Classification and release

doc-drift · c4039e1 · c4039e18f5533054f368b9836867655880d55c09

956f6baaThe drift report over-counts because it ignores per-footnote release overrideslowtillandsias.org · skills/update-website/scripts/drift-report.sh · 2026-09-05
Claim

For each level the report lists the cited files that changed between the level's pin and the stable tag, on the ground that a footnote into a changed file needs re-verification even when its line range still resolves.

Code / observed behavior

Its path extraction takes the target out of every footnote definition and drops the trailing release override, so a footnote already anchored at the daily channel with an @vTAG suffix is counted as if it were resolved against the level's pin. Those footnotes are unaffected by a pin bump — the checked build resolves them against the release they name — yet they inflate both the changed-file count and the per-level totals the report prints.

Why it matters

The report exists to size the next promotion. An inflated number is the same kind of lie as an inflated claim on the page: it makes the work look larger than it is, and the one thing that makes a pin bump get postponed is the belief that it is large.

Remedy / check

Keep the @vTAG suffix when extracting targets, and exclude from the pin-versus-stable diff every footnote that names its own release.

Evidence
History
  1. · found — Confirmed by the final completeness audit, left open, and listed in the "What could not be verified here" section of docs/audit/2026-09-05-v56.9.2.1.md; re-checked at c4039e1 by reading the report's path extraction.
Classification and release

defect · c4039e1 · c4039e18f5533054f368b9836867655880d55c09

a0be2422The per-level annotation files are superseded and still sit in docs/matrix beside page contentlowtillandsias.org · docs/matrix/level-1-five.audit.md and its four siblings · 2026-09-05
Claim

Each annotation file says it is valid for the v56.9.2.1 pin only and superseded once the day's record under docs/audit/ lands; the audit directory's README agrees, and the level-common contract makes it a rule that they are never a source of requirement text.

Code / observed behavior

That record landed, so the annotation files are history — and they still sit in docs/matrix/, the directory whose README opens with the instruction to edit the words there and whose every other file is page content. Each opens with a comment header saying it is not page content. There are five of them now, level 5's having been added the same day at 4ba2294, so the audit's own remark that the set is incomplete is itself stale.

Why it matters

Two kinds of file in one directory, distinguished only by an infix, is a trap for the next editing agent working from a glob. The build protects itself by rendering only the slugs in its LEVELS table, so only a human reader is harmed, quietly.

Remedy / check

Move each to docs/audit/2026-09-04-level-<n>-<slug>.audit.md, then update the three places that name the old path: the audit README, the design note of add-level-page-specs, and the level-common requirement quoted above.

Evidence
History
  1. · found — Migrated from docs/audit/2026-09-05-issues-tillandsias.org.md section F1; re-checked open against the tree at c4039e1 — five annotation files are still under docs/matrix/, one more than the audit counted.
Classification and release

doc-drift · c4039e1 · c4039e18f5533054f368b9836867655880d55c09

c1223430Phone-level provenance claim exceeded its Linux source evidencelowtillandsias.org · docs/matrix/level-2-phone.md · 2026-09-24
Claim

The published phone-level copy said no installed Tillandsias program records its build snapshot.

Code / observed behavior

The cited runtime finding concerns the Linux launcher; the macOS tray embeds its build commit at crates/tillandsias-macos-tray/src/main.rs#L58-L64.

Why it matters

The universal wording hid the platform boundary and overstated what the Linux finding established.

Remedy / check

Scope the shortcoming and its path to the Linux launcher and cite the finding at the stable release.

Evidence
  • docs/matrix/level-2-phone.md#L66 — “> RED: After installing, you cannot tell from the program what was installed. It prints a version number that was baked in when it was compiled, and nothing on it records which snapshot of the code it was built from.”
History
  1. · found — Review against stable v56.9.21.1 showed the Linux-only finding did not support the page's claim about every platform.
  2. · retracted — The universal claim was withdrawn and replaced with wording scoped to the Linux launcher; the macOS tray's embedded build commit was confirmed in the stable source.
Classification and release

site · main · d6e95bebe749e6077f375d21a503f16a0d6a00a6

e87c4f45Per-level re-verification is unautomated, and is the part that must stay reviewedlowtillandsias.org · skills/audit-site-claims/SKILL.md · 2026-09-05
Claim

The skill's fourth candidate is an agent per level running the audit-site-claims procedure, then a skeptic per level, then a human or a second agent reading the diff — needing a model, not free, and the part that should stay reviewed.

Code / observed behavior

The 2026-09-05 run did it by hand and at scale: an auditing agent per slice of the work and a skeptic per slice whose refutations overrode the audit, then a writer, a reviewer and a corrector per level. The record says why the skeptic pass is not optional — several corrections came from it rather than from the first pass, including the two sibling scripts that still create the enclave network without the isolation flag and the withdrawal of a commit-count figure.

Why it matters

This is the class of claim no script can check: a RED still literally true but no longer honest, prose that outran its footnote, a PATH line promising something the ledger has closed. It should be the last candidate automated and the first to keep a human on.

Remedy / check

Not a fix but a sequencing decision: automate the pre-push gate, the forecast and the re-anchor helper first, so that when agents do run, the objective failures are already impossible; then fan out one agent per level with a skeptic each, and gate publication on a human reading the diff.

Evidence
Depends on

52c8f180, 3d0ef7e2, 296676d4

History
  1. · found — Migrated from docs/audit/2026-09-05-issues-tillandsias.org.md section G6; re-checked open against the tree at c4039e1 — re-verification remains a hand-run multi-agent pass.
Classification and release

defect · c4039e1 · c4039e18f5533054f368b9836867655880d55c09

Tracked 1

Triaged, filed, active, blocked or terminal without a verified fix.

984bf6ccThe published `stable` git tag points at a stale commitlowtillandsias · git tags, release promotion · 2026-09-05
Claim

The `stable` git tag is the channel pointer a reader (and this site's pins) treats as naming the current stable release.

Code / observed behavior

What IS observable here: the runtime mirror this site audits from carries no `stable` ref at all. `git for-each-ref` over the v56.9.5.1 worktree lists `refs/tags/latest`, `refs/tags/unstable`, `refs/tags/v56.9.2.1` and `refs/tags/v56.9.5.1`, and `git rev-parse stable` fails with `unknown revision`. The release ledger names v56.9.2.1 as the most recent STABLE promotion (`README.md#L115`), so that is the release a correct channel pointer resolves to.

What is NOT observable here: where the published `stable` tag on the origin actually points, and why. That half is a report from the fleet coordinator, carried in the evidence notes rather than asserted as a verified finding, and it is recorded so a reader of the source audit does not open a duplicate.

Why it matters

A channel tag that names the wrong commit misdirects anyone resolving `stable` — including this site's release pins — and the correction is an operator action rather than a code change.

Remedy / check

Operator decision: move the published `stable` tag, and make promotion tag the pushed release commit rather than the promoting checkout's local main.

Evidence
  • README.md#L115 — “v56.9.2.1 (**STABLE** — cut and promoted 2026-09-02 unattended, operator-authorized while asleep; PRs #103 merge + #104 bump;”

No upstream issue filed.

History
  1. · found — From docs/audit/2026-09-05-issues-tillandsias.md, the section marked "Already filed — do not re-file", carried across so the ledger records it rather than a reader re-filing it.
  2. · filed — The fleet coordinator holds this as a p3 packet, reported 2026-09-05; not verifiable from this checkout, whose mirror carries no stable ref, so no upstream URL is recorded here.
Classification and release

stale-record · v56.9.5.1 · b528680d2b938b09279e31faee981a8f6e370b3b

Resolved 11

Remedies checked in this website’s audit record.

56415013The checked build passed on footnotes whose release had no checkouthightillandsias.org · scripts/build-matrix.py, skills/update-website/scripts/checked-build.sh · 2026-09-05
Claim

The checked build is the gate this repository calls the thing that stops a lie from deploying: it resolves every footnote target against the checkout for the tag that footnote names, and fails on any unresolved target or drifted quote.

Code / observed behavior

With only the stable checkout present the build printed that all checked footnote targets resolved and exited zero while all 38 footnotes naming the daily channel went unopened — ten of them citing one of eight files that do not exist at the stable tag at all. Three causes: the check returned early when a release had no checkout and recorded nothing; the per-level line said "checked at <pin>" whatever it had actually opened; and the wrapper demanded checkouts only for the tags the levels pin, so the daily tag was never required.

Why it matters

The gate was silent about exactly the citations most likely to drift — the ones a PATH line uses to acknowledge a fix that is in the daily channel and not yet promoted. A gate whose green means only that it opened the easy half is worse than no gate, because the publishing step trusts it.

Remedy / check

Make an unchecked target a first-class result, report it per level and in a summary naming each level and release it could not answer for, require a checkout for every release any footnote names, and refuse the push on any unchecked target.

Evidence
History
  1. · found — Confirmed by the final completeness audit and recorded in the "What could not be verified here" section of docs/audit/2026-09-05-v56.9.2.1.md as one of the two fixed the same day.
  2. · resolved — Fixed at ceb944b and verified in the tree at c4039e1 — unchecked targets are tracked per level and reported, the wrapper requires a checkout for every release any footnote names, and it exits blocked:targets-unchecked on any that went unopened.
Classification and release

defect · c4039e1 · c4039e18f5533054f368b9836867655880d55c09

572f7388The safe-browser launcher defaults to host networking; its spec promises enclave-only egress through the proxyhightillandsias · scripts/launch-chromium.sh, spec chromium-safe-variant · 2026-09-22
Claim

`openspec/specs/chromium-safe-variant/spec.md#L18-L19` — "Full network isolation inside the enclave, with allowlist enforcement via the proxy only; no host-gateway internet fallback"; and `#L137-L138` — "**THEN** it SHALL connect through the enclave network **AND** all web egress SHALL traverse the proxy/allowlist path".

Code / observed behavior

`run-safe-browser.sh` execs `scripts/run-safe-browser.sh`, which execs `scripts/launch-chromium.sh safe-browser "$URL" 9222 open_safe_window` (`scripts/run-safe-browser.sh#L42`). That script's podman argv is built at `scripts/launch-chromium.sh#L84-L98`, and line 90 reads:

"--network=${TILLANDSIAS_BROWSER_NETWORK:-host}"

Neither wrapper sets `TILLANDSIAS_BROWSER_NETWORK` — the string does not appear in either file — so the documented entry point for the "safe" variant launches the browser in the host network namespace, with direct internet access and no proxy in the path. The surrounding flags (`--cap-drop=ALL`, `--security-opt=no-new-privileges`, `--read-only`, tmpfs mounts) are all correctly applied; only the network default contradicts the spec.

Why it matters

The variant's name and its spec both say the containment is network containment, and that is the one control the default turns off. A user running `./run-safe-browser.sh --url …` gets a hardened-but-unisolated browser while every other property the spec lists is genuinely enforced, which is the hardest kind of gap to notice.

Remedy / check

Default to `tillandsias-enclave` (`:-tillandsias-enclave`) and keep `host` reachable only by explicitly setting `TILLANDSIAS_BROWSER_NETWORK`.

Evidence

No upstream issue filed.

History
  1. · found — From docs/audit/2026-09-05-issues-tillandsias.md, section under Browser, the tillandsias.org explanation-page audit of 2026-09-05, level 4, re-read at v56.9.5.1.
  2. · resolved — At stable v56.9.21.1, scripts/launch-chromium.sh#L93-L98 defaults the browser network to the enclave, with an explicit operator override still available. This closes the reported host-network default; no live egress test was run in this site audit.
Classification and release

spec-vs-code · v56.9.5.1 · b528680d2b938b09279e31faee981a8f6e370b3b

6cd2f616The permissive proxy port grants unfiltered egress to every enclave member with no source restrictionhightillandsias · images/proxy/squid.conf, spec proxy-container · 2026-09-22
Claim

`openspec/specs/proxy-container/spec.md#L104` — the dual-port table lists port 3129 as "Permissive | All domains allowed", and `#L112-L114`: "**WHEN** a request is sent through port 3129 **THEN** all domains on SSL ports (443, 8443) SHALL be permitted **AND** domain filtering SHALL NOT apply."

Code / observed behavior

`images/proxy/squid.conf#L60` — `http_port 3129 ssl-bump \` with no bind address, so squid listens on `0.0.0.0:3129` on every interface the container has. `#L133-L134` — `http_access allow CONNECT SSL_ports build_port` / `http_access allow build_port`, with `build_port` defined at `#L93` as `acl build_port localport 3129` and no source ACL anywhere in the file. The container is dual-homed: `crates/tillandsias-headless/src/main.rs#L3425-L3426` passes `--network ENCLAVE_EGRESS_NETS` (= `"tillandsias-enclave,tillandsias-egress"`, `#L1651`), so the listener is reachable from both networks.

Why it matters

Every enclave container is handed `http_proxy=http://proxy:3128` (`main.rs#L1711-L1717`), but nothing prevents any of them from using `http://proxy:3129` instead and reaching the internet with the allowlist skipped entirely. The port's own header comment (`squid.conf#L6-L45`) is careful and honest about the port having no consumer today, and `scripts/check-proxy-permissive-port-routing.sh` pins that honesty — but "no consumer" is a statement about Tillandsias's own code, not about what an agent inside a forge can dial. Filtering that a client can opt out of by changing a port number is not a boundary, and the explanation pages describe the allowlist as one.

Remedy / check

Bind the permissive listener to loopback (`http_port 127.0.0.1:3129 …`) so it is unreachable from the enclave, or add a source ACL restricting it, until a real consumer exists.

Evidence

No upstream issue filed.

History
  1. · found — From docs/audit/2026-09-05-issues-tillandsias.md, section under Egress proxy, the tillandsias.org explanation-page audit of 2026-09-05, level 3, re-read at v56.9.5.1.
  2. · resolved — At stable v56.9.21.1, images/proxy/squid.conf#L75 binds the permissive listener to 127.0.0.1:3129 inside the proxy container. The permissive access rule still exists for processes in that container; the reported enclave-peer bypass is closed by the bind.
Classification and release

defect · v56.9.5.1 · b528680d2b938b09279e31faee981a8f6e370b3b

6e61831eThe mirror relay wires the GitHub credential helper for any `https://` origin, and the helper answers unconditionallyhightillandsias · images/git/relay-refs.sh, images/git/git-credential-tillandsias.sh · 2026-09-22
Claim

`images/git/git-credential-tillandsias.sh#L34-L37` states the assumption the design rests on: "Drain stdin (git sends protocol/host/path). We do not branch on it: this helper is wired per-invocation by the relay for one specific remote, so answering unconditionally is correct".

Code / observed behavior

`images/git/relay-refs.sh#L182-L221` — the credential wiring is inside `case "$REMOTE_URL" in https://*)`, i.e. it triggers for any HTTPS upstream, not for GitHub specifically. `REMOTE_URL` is the project's own `remote.origin.url`, read from the host checkout and passed into the mirror (`crates/tillandsias-headless/src/main.rs` `fn read_host_project_origin_url`, `#L3653-L3660`, and the `remote.origin.url` read at `#L3588`). The helper it installs is `images/git/git-credential-tillandsias.sh`, which does not branch on the host git asks about and returns the Vault-held GitHub token (`#L48-L57`) for whatever host that is.

Why it matters

The helper's assumption ("wired for one specific remote") is enforced nowhere: the relay wires it on the basis of the URL *scheme* alone. A project whose `origin` is any non-GitHub HTTPS remote — or a redirect chain ending at one — gets the user's GitHub token offered to that host on the first push through the mirror. The mirror is the privileged component in this design precisely because it holds the credential the forge is denied; the condition that decides when it hands that credential out should be the host, not the scheme.

Remedy / check

Match on the origin host (`https://github.com/*`, plus any explicitly configured enterprise host) rather than `https://*`, and have the helper read `host=` from stdin and refuse anything it does not recognise.

Evidence

No upstream issue filed.

History
  1. · found — From docs/audit/2026-09-05-issues-tillandsias.md, section under Git mirror, the tillandsias.org explanation-page audit of 2026-09-05, level 4, re-read at v56.9.5.1.
  2. · resolved — At stable v56.9.21.1, images/git/git-credential-tillandsias.sh#L58-L83 refuses non-HTTPS requests and host values other than exact github.com or api.github.com before returning a token.
Classification and release

defect · v56.9.5.1 · b528680d2b938b09279e31faee981a8f6e370b3b

9f823c95Two scripts create the enclave network without --internal, and the new guard cannot see themhightillandsias · scripts/orchestrate-enclave.sh, scripts/run-forge-project.sh, scripts/diagnose-proxy.sh · 2026-09-22
Claim

The enclave is isolated because its network is created with --internal, and a build-gate check now reads the create sites.

Code / observed behavior

scripts/run-forge-project.sh#L116-L119 and scripts/diagnose-proxy.sh#L90-L93 still create tillandsias-enclave with no --internal; scripts/check-enclave-network-internal.sh#L87-L115 checks a restated three-file list rather than discovering create sites, so both pass the gate.

Why it matters

A stack brought up by either script is not isolated, and the guard's green means only that the three files it names were clean. A check whose coverage is a hardcoded set reports the set, not the property.

Remedy / check

Make the guard discover every create site from source rather than restating a file list, and add the flag to both scripts.

Evidence

No upstream issue filed.

History
  1. · found — Found by the tillandsias.org spec-versus-code audit at v56.9.5.1, confirmed by an adversarial second pass.
  2. · resolved — At stable v56.9.21.1, scripts/run-forge-project.sh#L130 and scripts/diagnose-proxy.sh#L97 both pass --internal. scripts/check-enclave-network-internal.sh#L130-L181 now discovers tracked shell and Rust sources and checks shell network-create invocations. Its Rust verdict remains explicitly file-scoped.
Classification and release

spec-vs-code · v56.9.5.1 · b528680d2b938b09279e31faee981a8f6e370b3b

f664ad4aThe inference engine is fetched at first run from an unpinned `latest` release with no integrity check, the specs promise baked models the image does not carry, and the health check tests the runner binaryhightillandsias · images/inference/entrypoint.sh, images/inference/Containerfile, specs inference-container and zen-default-with-ollama-analysis-pool · 2026-09-22
Claim

`openspec/specs/zen-default-with-ollama-analysis-pool/spec.md#L47-L54` — T0 `qwen2.5:0.5b` and T1 `llama3.2:3b` are both marked "Baked", and "T0 and T1 are baked into the inference image at build time so the first attach has a usable analysis model with zero network overhead"; `#L59` repeats it as a scenario outcome. `openspec/specs/inference-container/spec.md#L285` lists "T0 models baked into image" as a gating point (while `#L110` of the same file says they are pulled at startup — the two specs already disagree with each other).

Code / observed behavior

Three separate gaps in one container.

*Unpinned, unverified engine.* `images/inference/entrypoint.sh#L293` — `OLLAMA_URL="https://github.com/ollama/ollama/releases/latest/download/ollama-linux-${OLLAMA_ARCH}.tar.zst"`. `latest` is a moving target, and the download at `#L309-L312` is followed by `zstd -dc | tar -xf` and `install -m 0755 … "$OLLAMA_BIN"` (`#L345-L348`) with no checksum, no signature and no pinned digest anywhere in the file. The fetch goes through the enclave proxy, which bumps `release-assets.githubusercontent.com` with a certificate the container is configured to trust (`openspec/specs/proxy-container/spec.md#L36-L42`), so TLS is not standing in for provenance here either.

*Promised baked models that are absent.* `images/inference/Containerfile#L70` only runs `mkdir -p /opt/baked-models && chown`; no `COPY` ever puts a model there, so the seeding block at `entrypoint.sh#L399-L406` has nothing to seed from. T1 is absent altogether: `entrypoint.sh#L120` sets `DEFAULT_MODELS="${TILLANDSIAS_DEFAULT_MODELS:-qwen2.5:0.5b}"`, and the comment at `#L114-L115` records that `llama3.2:3b` was replaced. So `llama3.2:3b` is named by both specs and is neither baked nor pulled.

*A health check that cannot see the failure it was written for.* `images/inference/Containerfile#L118-L121` — `curl -sf http://127.0.0.1:11434/api/version && test -x /home/ollama/.ollama/models/.tools/lib/ollama/llama-server`. The header above it (`#L100-L117`) explains the live incident it exists for: the container sat at `Up (healthy)` while every `/api/generate` returned HTTP 500. The repair upgrades the probe from "the port is bound" to "the port is bound and a file is executable" — it still never loads a model, so a present-but-broken engine or an empty model cache still reads healthy.

Why it matters

The engine binary is the largest single untrusted input the runtime installs, and it arrives from a mutable URL into a container that then serves every forge on the enclave. The baked-model promise is what the specs offer as the offline/first-attach guarantee, and it is not there. And the health signal that the launcher's readiness gate believes is still a proxy for serving, not serving.

Remedy / check

Pin the ollama release to a version and verify its published checksum before extracting; then either bake T0/T1 (`COPY` into `/opt/baked-models`) or correct both specs to say the models are pulled and drop `llama3.2:3b`; and make the health check issue one tiny `/api/generate` (or `/api/show`) against T0 rather than stat a file.

Evidence

No upstream issue filed.

History
  1. · found — From docs/audit/2026-09-05-issues-tillandsias.md, section under Inference, the tillandsias.org explanation-page audit of 2026-09-05, level 3, re-read at v56.9.5.1.
  2. · resolved — At stable v56.9.21.1, images/inference/entrypoint.sh#L329-L335 pins Ollama v0.34.0 with per-architecture SHA-256 values; entrypoint.sh#L371-L383 refuses an absent verifier or digest mismatch before extraction and execution.
Classification and release

defect · v56.9.5.1 · b528680d2b938b09279e31faee981a8f6e370b3b

0fdb0cb3The browser allowlist cannot open any window for a project whose directory name contains a dotmediumtillandsias · crates/tillandsias-browser-mcp/src/allowlist.rs, crates/tillandsias-headless/src/local_projects.rs · 2026-09-22
Claim

`browser.open` is offered as a working MCP surface for any local project, and the allowlist is the component that decides which host a project may open.

Code / observed behavior

`crates/tillandsias-browser-mcp/src/allowlist.rs#L233-L246` — `validate()` splits the host on `.` and refuses anything that is not exactly three labels:

let labels = split_host_labels(&host); if labels.len() != 3 || labels[2] != "localhost" { return Err(AllowlistDeny::HostShape); } … if host_project_label != project_label { return Err(AllowlistDeny::ProjectMismatch { … }); }

The project label it compares against is the raw directory name. `crates/tillandsias-browser-mcp/src/server.rs#L62-L63` reads it from `TILLANDSIAS_PROJECT`, which the forge builders set to `project_name` (`crates/tillandsias-headless/src/main.rs#L11236`, `#L14437`), and `project_name` comes from `scan_project_root`'s `path.file_name()` (`crates/tillandsias-headless/src/local_projects.rs#L76-L78`) with no dot-substitution — only a leading-dot skip.

Why it matters

For a project directory such as `tillandsias.org`, the label is `tillandsias.org`, so every candidate host is four labels (`web.tillandsias.org.localhost`) and hits `HostShape`; and a three-label host would fail `ProjectMismatch` because `labels[1]` is `tillandsias`. There is no path through `validate()` that succeeds, so `browser.open` is unusable for the whole class of projects named after a domain — a common naming convention, and the one this very site's repository uses.

Remedy / check

Compare the trailing labels against the project label rather than requiring `len() == 3`, or normalise dots to `-` when deriving the label (matching `project_label_from_path` at `main.rs#L11189`, which already does this for container hostnames) and use the same normalised label on both sides.

Evidence

No upstream issue filed.

History
  1. · found — From docs/audit/2026-09-05-issues-tillandsias.md, section under Browser, the tillandsias.org explanation-page audit of 2026-09-05, level 4, re-read at v56.9.5.1.
  2. · resolved — At stable v56.9.21.1, crates/tillandsias-browser-mcp/src/allowlist.rs#L250-L257 accepts at least three labels, rejoins the middle labels as the project name, and checks exact equality. A dotted project name is no longer rejected by host shape.
Classification and release

defect · v56.9.5.1 · b528680d2b938b09279e31faee981a8f6e370b3b

1b27ef15CentiColon page overstates enforcement and fixed-point completionmediumtillandsias.org · CentiColons and Level 5 · 2026-09-28
Claim

The site presents CentiColons as an enforceable metric, a validator fixed point as completion, and the Lua path as grading systemic invariants hermetically in milliseconds.

Code / observed behavior

At runtime v56.9.27.2, scripts/check-centicolon-ratchet.sh#L4-L29 reports advisory results against an untracked local baseline. scripts/lua/centicolon-grade-observed.lua#L38-L105 selects observations by test/digest, not required platform, and counts positive-test credit. methodology/math-foundations.yaml#L200-L206 calls fixed points validator stability, not completion.

Why it matters

A measurable score does not prove evidence applies or that residuals strictly decrease; readers could infer an enforced convergence guarantee that the stable release does not provide.

Remedy / check

Distinguish the simplified example, shipped advisory measurement, proposed evidence contract and conditional finite-time progress theorem.

Evidence
History
  1. · found — Scoped source review after BigPickle's stable refresh; not a new runtime execution.
  2. · retracted — Withdraw the published claims of current enforcement, fixed-point completion, integrated invariant grading and an unmeasured milliseconds guarantee. Stable-source inspection and an independent skeptical pass distinguish the shipped advisory raw residual from the proposed evidence contract and a conditional progress theorem. Tests retain empirical value without implying a Galois connection or universal correctness.
  3. · resolved — Scoped copy correction applied to CentiColons and Level 5 with an unarchived OpenSpec proposal and release-pinned citations. This resolves the website overclaim, not the runtime attribution gaps or the implementation program.
Classification and release

site · main · b3f3091cb6d21b47349e2b407c46981d9d34b515

66b46da4The runtime issue drafts cited annotated-tag object hashes as commitsmediumtillandsias.org · docs/audit/2026-09-05-issues-tillandsias.md, skills/file-findings/SKILL.md · 2026-09-05
Claim

Each section of the runtime issue drafts is meant to be pasted into the runtime's tracker with the commit its evidence was read at, so a reader can follow the citation to the code.

Code / observed behavior

The header and the Tag line of all 22 issue sections carried the hash of the annotated tag object — 77958f55… for v56.9.5.1, 49458e96… for v56.9.2.1 — rather than the commit it points at. Pasted into the tracker each would have cited a hash that resolves to no commit there. The mistake was in the instruction rather than in the transcription: the issue template in the file-findings skill said only "commit …" beside the tag.

Why it matters

A finding relayed to another project is only as good as its citation. A hash that resolves nowhere makes the whole relay unusable, and the template would have reproduced the error in every future run.

Remedy / check

Peel every hash to the commit, and make the template say to peel — git rev-parse <tag>^{commit} — because a bare rev-parse of an annotated tag gives the tag object.

Evidence
History
  1. · found — Found in the same end-of-day pass over the day's artifacts as the other completeness findings, and recorded in the commit message of 8007f39.
  2. · resolved — Fixed at 8007f39 and verified in the tree at c4039e1 — the header cites the full peeled commits b528680d2b938b09279e31faee981a8f6e370b3b and d6d3e3ed910feb24579304d08ef464a229eae21f, each section's Tag line cites the peeled short hash b528680, no tag-object hash survives anywhere in the file, and the skill's template now says to peel.
Classification and release

defect · c4039e1 · c4039e18f5533054f368b9836867655880d55c09

67b4a3aaThe audit record's own counts went stale before it was publishedmediumtillandsias.org · docs/audit/2026-09-05-v56.9.2.1.md · 2026-09-05
Claim

The run's record carries the per-level tables and the headline totals for the site the run produced.

Code / observed behavior

It was written mid-run and never updated, so it described a site that was never published: 329 footnotes with 317 quoted against 342 and 330 in the tree it claimed to describe, per-level counts of 22, 141 and 93 against 23, 147 and 99, and a level-2 section still counting a verdict the verification pass deleted and a verified claim the same pass withdrew as false.

Why it matters

This record is the artifact a reader consults to learn what the day's work established. Counts that belong to no tree make every other number in it unfalsifiable, and the level-2 error pointed the wrong way — it reported a reassurance that had been withdrawn precisely because it was false in the reassuring direction.

Remedy / check

Re-derive every count at the commit the record describes, name that commit in the record, and say what actually happened to both level-2 flags.

Evidence
History
  1. · found — Confirmed by the final completeness audit and recorded in the "What could not be verified here" section of docs/audit/2026-09-05-v56.9.2.1.md as the second of the two fixed the same day.
  2. · resolved — Fixed at 8007f39 and verified in the tree at c4039e1 — the header and the five per-level tables carry counts re-derived at the commit the record now names, and the level-2 section says what happened to both flags. The residue in the Method paragraph is filed separately as 548baf18.
Classification and release

stale-record · c4039e1 · c4039e18f5533054f368b9836867655880d55c09

60a07324Checkout preparation omitted runtime evidence releases needed by ledger validationlowtillandsias.org · skills/update-website/scripts/fetch-checkouts.sh · 2026-09-24
Claim

The update skill's checkout step prepares releases used by the checked build.

Code / observed behavior

Before the fix, fetch-checkouts.sh gathered pins, stable, daily, daily footnote overrides, and explicit arguments, but not historical runtime tags carried only by issues.d findings.

Why it matters

A fresh checked build then left historical ledger evidence unchecked, so the required gate failed despite every page pin checkout being present.

Remedy / check

Include each folded runtime finding's cited release tag in the checkout set.

Evidence
History
  1. · found — The first checked build named 67 historical quotes as unchecked because their tagged runtime worktrees had not been prepared by the checkout helper.
  2. · resolved — The helper now discovers quoted evidence tags from folded runtime findings; a no-argument run prepared v56.9.5.1 and the checked build then verified all 67 historical quotes.
Classification and release

process · main · d6e95bebe749e6077f375d21a503f16a0d6a00a6

Runtime specifications 184

All canonical specs at v56.10.9.1, including retired documents. Status here is the document’s own lifecycle label.

specaccel-capability-probestatus: active authored-by: order 479 (`heterogeneous-inference-specs`), spec-before-implementation derived-from: `plan/issues/heterogeneous-inference-cpu-gpu-npu-research-2026-07-24.md` gates: order 480 (`accel-capability-probe`), order 481 (`npu-device-passthrough`), order 483 (`host-native-sidecar-endpoints`), order 484 (`inference-policy-router`)9 requirements

Replace the single-string inference tier (`detect_inference_tier()` → `metal|gpu-cuda|gpu-rocm|cpu`) with a structured, versioned, host-side capability document describing every execution device the host actually has, which engine lanes can reach each one, and how fast each combination measured.

Document status: status: active authored-by: order 479 (`heterogeneous-inference-specs`), spec-before-implementation derived-from: `plan/issues/heterogeneous-inference-cpu-gpu-npu-research-2026-07-24.md` gates: order 480 (`accel-capability-probe`), order 481 (`npu-device-passthrough`), order 483 (`host-native-sidecar-endpoints`), order 484 (`inference-policy-router`). This describes the spec’s lifecycle, not verified implementation.

Requirements (9)
  1. PROBE-1 — Structured capability document replaces the string tier
  2. PROBE-2 — Device enumeration is explicit and vendor-resolved
  3. PROBE-3 — Engine inventory gates usability
  4. PROBE-4 — Memory bandwidth is recorded as a first-class quantity
  5. PROBE-5 — Microbenchmarks are bounded, one-time, and degrade gracefully
  6. PROBE-6 — Roofline sanity check flags degraded lanes
  7. PROBE-7 — The probe runs on the host and records per-lane reachability
  8. PROBE-8 — The capability cache is derived, ephemeral, and self-invalidating
  9. PROBE-9 — The document names the host it describes

Read full pinned spec ↗

specagent-cheatsheetsobsolete0 requirements

This umbrella spec has been retired. Its remaining live obligations now live in:

Document status: obsolete. This describes the spec’s lifecycle, not verified implementation.

Read full pinned spec ↗

specapp-lifecycleactive14 requirements

TBD - created by archiving change tillandsias-bootstrap. Update Purpose after archive.

Document status: active. This describes the spec’s lifecycle, not verified implementation.

Requirements (14)
  1. User-facing app semantics
  2. Running environment tracking with lifecycle iconography
  3. Graceful stop
  4. Destroy with safety hold
  5. Tray status display
  6. shutdown_all terminates web containers and closes webviews
  7. Orphan web containers are swept on shutdown
  8. Webview close is not an exit signal
  9. ExitRequested discriminates on code
  10. Tray Quit dispatches MenuCommand::Quit (no direct process exit)
  11. Event loop owns the sole shutdown_all invocation
  12. shutdown_all removes containers AND destroys the enclave network
  13. shutdown_all verifies post-sweep state and escalates stragglers
  14. kill_container accepts an explicit signal

Read full pinned spec ↗

specappimage-build-pipelinestatus: obsolete0 requirements

No purpose recorded in this spec.

Document status: status: obsolete. This describes the spec’s lifecycle, not verified implementation.

Read full pinned spec ↗

specartifact-detectionobsolete0 requirements

This umbrella spec has been retired. Its live obligations are now covered by:

Document status: obsolete. This describes the spec’s lifecycle, not verified implementation.

Read full pinned spec ↗

specasync-inference-launchactive promoted-from: openspec/changes/archive/2026-04-16-async-inference-launch/ annotation-count: 93 requirements

Defer inference container startup to a background task, unblocking forge launch within 2-5 seconds while inference initializes asynchronously. Inference is a soft requirement — failure logs `DEGRADED` but does not block coding session start.

Document status: active promoted-from: openspec/changes/archive/2026-04-16-async-inference-launch/ annotation-count: 9. This describes the spec’s lifecycle, not verified implementation.

Requirements (3)
  1. Detached inference startup task
  2. Startup-time empirical validation
  3. Enclave readiness message clarity

Read full pinned spec ↗

specbinary-signingactive3 requirements

No purpose recorded in this spec.

Document status: active. This describes the spec’s lifecycle, not verified implementation.

Requirements (3)
  1. Cosign bundle artifacts
  2. Cosign signing produces verifiable bundles
  3. Release verification instructions stay bundle-based

Read full pinned spec ↗

specbranch-disciplinestatus: draft7 requirements

A project's branch discipline — the protected default branch, the integration branch per platform, the work-ref and salvage grammar, the plan-only lane, the freeze namespace and the operator-worded denial message — is DATA in the project (`.tillandsias/branch-discipline.yaml`), not a constant in a tool — and it is checked against what the project's history a

Document status: status: draft. This describes the spec’s lifecycle, not verified implementation.

Requirements (7)
  1. The seed is per project and has a built-in default
  2. The runtime answers discipline questions
  3. The mirror enforces the seed and publishes the enforced level
  4. The level is derived from observed facts and checked against the seed
  5. Hooks are templates installed on demand for the project's level
  6. The mirror dispatches the project's hooks per push event
  7. The land tool asks before it fetches, gates or pushes

Read full pinned spec ↗

specbrowser-daemon-trackingobsolete0 requirements

No purpose recorded in this spec.

Document status: obsolete. This describes the spec’s lifecycle, not verified implementation.

Read full pinned spec ↗

specbrowser-debounceobsolete0 requirements

No purpose recorded in this spec.

Document status: obsolete. This describes the spec’s lifecycle, not verified implementation.

Read full pinned spec ↗

specbrowser-isolation-coreactive3 requirements

No purpose recorded in this spec.

Document status: active. This describes the spec’s lifecycle, not verified implementation.

Requirements (3)
  1. Security model for chromium-core image
  2. Content-hash image identity with human aliases
  3. Build invocation via build-image.sh

Read full pinned spec ↗

specbrowser-isolation-frameworkactive4 requirements

No purpose recorded in this spec.

Document status: active. This describes the spec’s lifecycle, not verified implementation.

Requirements (4)
  1. Content-hash image identity with human aliases
  2. Image hierarchy locked to chromium-core
  3. Security model inheritance and framework-specific isolation
  4. Build invocation via build-image.sh

Read full pinned spec ↗

specbrowser-isolation-launcherobsolete0 requirements

This placeholder spec has been retired. The real browser-launch contract is now owned by:

Document status: obsolete. This describes the spec’s lifecycle, not verified implementation.

Read full pinned spec ↗

specbrowser-isolation-tray-integrationactive7 requirements

Define how the tray menu integrates with browser isolation containers to launch safe GUI browser windows for OpenCode Web and other web-based tools.

Document status: active. This describes the spec’s lifecycle, not verified implementation.

Requirements (7)
  1. Tray launches run in the desktop user session
  2. OpenCode Web launches in browser isolation
  3. Versioned identity without `latest`
  4. Safe window type by default
  5. Browser launcher uses the secure OpenCode Web session
  6. Browser window lifecycle
  7. No Tauri webview path

Read full pinned spec ↗

specbrowser-mcp-serverobsolete0 requirements

Historical stub retained for traceability only. The live browser MCP server contract now lives in `host-browser-mcp`, with transport and tray ownership split across `tray-host-control-socket` and `browser-isolation-tray-integration`.

Document status: obsolete. This describes the spec’s lifecycle, not verified implementation.

Read full pinned spec ↗

specbrowser-routing-allowlistactive1 requirements

No purpose recorded in this spec.

Document status: active. This describes the spec’s lifecycle, not verified implementation.

Requirements (1)
  1. Browser routes are allowlisted by project and service

Read full pinned spec ↗

specbrowser-tray-notificationsobsolete0 requirements

No purpose recorded in this spec.

Document status: obsolete. This describes the spec’s lifecycle, not verified implementation.

Read full pinned spec ↗

specbrowser-window-lifecycleactive1 requirements

No purpose recorded in this spec.

Document status: active. This describes the spec’s lifecycle, not verified implementation.

Requirements (1)
  1. Browser windows have explicit lifecycle state

Read full pinned spec ↗

specbrowser-window-rate-limitingobsolete0 requirements

Historical duplicate of the browser launch throttle. The live debounce ownership now lives in `host-browser-mcp`.

Document status: obsolete. This describes the spec’s lifecycle, not verified implementation.

Read full pinned spec ↗

specbrowser-window-timeoutactive1 requirements

No purpose recorded in this spec.

Document status: active. This describes the spec’s lifecycle, not verified implementation.

Requirements (1)
  1. Browser windows expire after inactivity

Read full pinned spec ↗

specbuild-lockobsolete0 requirements

Historical build-serialization helper. The live build coordination behavior is now owned by `init-command` and the higher-level build orchestration specs.

Document status: obsolete. This describes the spec’s lifecycle, not verified implementation.

Read full pinned spec ↗

specbuild-script-architecturestatus: obsolete0 requirements

No purpose recorded in this spec.

Document status: status: obsolete. This describes the spec’s lifecycle, not verified implementation.

Read full pinned spec ↗

speccache-isolationactive1 requirements

No purpose recorded in this spec.

Document status: active. This describes the spec’s lifecycle, not verified implementation.

Requirements (1)
  1. Durable cache and project state remain separated

Read full pinned spec ↗

speccache-recovery-mechanismactive5 requirements

Formalize the behavioral contract for fresh-start cache initialization. An absent `cache_version` file indicates a valid fresh system state (first run, post-reset, ephemeral deployment), not a version mismatch error. This spec prevents regression of the unwrap_or(false) fix at main.rs:549 and documents the cache lifecycle from initialization through corrupti

Document status: active. This describes the spec’s lifecycle, not verified implementation.

Requirements (5)
  1. Fresh Start is NOT a Version Mismatch
  2. Actual Mismatch Triggers Error (with Recovery Path)
  3. Corruption Recovery
  4. Cache Directory Lifecycle and Paths
  5. Version File Format and Semantics

Read full pinned spec ↗

speccdp-robustnessactive1 requirements

No purpose recorded in this spec.

Document status: active. This describes the spec’s lifecycle, not verified implementation.

Requirements (1)
  1. CDP operations handle transport failure explicitly

Read full pinned spec ↗

speccertificate-authorityobsolete0 requirements

Historical umbrella for the ephemeral CA lifecycle. The live requirements are now owned by `reverse-proxy-internal`, `podman-secrets-integration`, and `secrets-management`.

Document status: obsolete. This describes the spec’s lifecycle, not verified implementation.

Read full pinned spec ↗

speccertificate-validationactive1 requirements

No purpose recorded in this spec.

Document status: active. This describes the spec’s lifecycle, not verified implementation.

Requirements (1)
  1. Enclave TLS validation is explicit

Read full pinned spec ↗

speccheatsheet-mcp-serverobsolete0 requirements

This spec is a historical tombstone.

Document status: obsolete. This describes the spec’s lifecycle, not verified implementation.

Read full pinned spec ↗

speccheatsheet-methodology-evolutionobsolete0 requirements

This placeholder spec has been retired. Its intended live behavior is now covered by methodology:

Document status: obsolete. This describes the spec’s lifecycle, not verified implementation.

Read full pinned spec ↗

speccheatsheet-source-layeractive6 requirements

Define the repository-side source binding, validation, and refresh tooling for cheatsheet provenance.

Document status: active. This describes the spec’s lifecycle, not verified implementation.

Requirements (6)
  1. Verbatim source storage
  2. License allowlist gates bundling
  3. Provenance binding
  4. Validator invariants
  5. Hot/cold separation
  6. Refresh behaviour

Read full pinned spec ↗

speccheatsheet-toolingactive4 requirements

Define the repository-side cheatsheet structure, template, and generated index contract. This spec owns the cheatsheet tree layout and the authoring/indexing tooling that keeps the cache queryable.

Document status: active. This describes the spec’s lifecycle, not verified implementation.

Requirements (4)
  1. Cheatsheet tree structure is fixed
  2. Cheatsheet template is standardized
  3. INDEX.md is generated, not hand-edited
  4. Cheatsheet authoring stays toolable

Read full pinned spec ↗

speccheatsheets-license-tieredactive0 requirements

No purpose recorded in this spec.

Document status: active. This describes the spec’s lifecycle, not verified implementation.

Read full pinned spec ↗

specchromium-debug-variantactive8 requirements

Unrestricted, ephemeral Chromium variant for testing and debugging. Runs with all domains accessible, DevTools enabled, extensions allowed, and verbose logging. Profile and cache are tmpfs-backed and destroyed on shutdown. Designed for engineers testing web features in development environments without production safety constraints.

Document status: active. This describes the spec’s lifecycle, not verified implementation.

Requirements (8)
  1. Unrestricted domain access
  2. DevTools and console access
  3. Extensions and plugins allowed
  4. Verbose network logging
  5. Ephemeral profile and cache
  6. Verbose logging to host
  7. Safe isolation (still ephemeral)
  8. Litmus test — chromium-debug-variant lifecycle

Read full pinned spec ↗

specchromium-safe-variantactive7 requirements

Ephemeral, restricted Chromium variant for production web access. Runs in a tmpfs-backed container with minimal capabilities, domain allowlist enforcement, zero credential persistence, zero cache, and zero sync. Profile and tabs are destroyed on shutdown. Designed for safe, auditable web access in development environments.

Document status: active. This describes the spec’s lifecycle, not verified implementation.

Requirements (7)
  1. Ephemeral profile on tmpfs
  2. Domain allowlist enforcement
  3. Credential isolation — no persistence
  4. Cache is ephemeral and minimal
  5. Minimal capabilities — cap-drop=ALL
  6. No extensions or plugins
  7. Litmus test — chromium-safe-variant lifecycle

Read full pinned spec ↗

specci-releaseactive0 requirements

No purpose recorded in this spec.

Document status: active. This describes the spec’s lifecycle, not verified implementation.

Read full pinned spec ↗

speccli-bash-modestatus: obsolete0 requirements

No purpose recorded in this spec.

Document status: status: obsolete. This describes the spec’s lifecycle, not verified implementation.

Read full pinned spec ↗

speccli-diagnosticsobsolete0 requirements

This specification documents the retired Tauri-era `--diagnostics` live-tail flow. That design mixed log streaming, container discovery, and debugging output into a single CLI command and is no longer part of the current architecture.

Document status: obsolete. This describes the spec’s lifecycle, not verified implementation.

Read full pinned spec ↗

speccli-modeactive10 requirements

Define the interactive CLI contract for the shipped Tillandsias binary. The binary is a compiled runtime orchestrator: it may embed static assets and metadata, but it MUST NOT depend on repository shell scripts for user-facing runtime behavior.

Document status: active. This describes the spec’s lifecycle, not verified implementation.

Requirements (10)
  1. CLI mode launches container from terminal
  2. Runtime paths are compiled Rust
  3. Observatorium project view
  4. OpenCode Web launch reuses the router when already present
  5. Image selection flag
  6. Debug flag
  7. User-friendly output
  8. Security flags are non-negotiable
  9. CLI modes are tray-aware
  10. SIGINT triggers clean shutdown on every CLI path

Read full pinned spec ↗

specclickable-trace-indexactive1 requirements

Generate a searchable, clickable index of all `@trace spec:<name>` annotations in the codebase, enabling developers and agents to navigate from any spec to all code locations that implement it. The index lives in `TRACES.md` at the repository root (GitHub-renderable Markdown with clickable links) and per-spec back-link files at `openspec/specs/<name>/TRACES.

Document status: active. This describes the spec’s lifecycle, not verified implementation.

Requirements (1)
  1. Project observatorium view

Read full pinned spec ↗

speccodex-tray-launcheractive1 requirements

No purpose recorded in this spec.

Document status: active. This describes the spec’s lifecycle, not verified implementation.

Requirements (1)
  1. Tray-launched Codex uses the forge entrypoint contract

Read full pinned spec ↗

speccommand-policiesstatus: draft6 requirements

A declarative COMMAND POLICY decides, for one argv request, whether the runtime may run it: allow, deny, or consent-required. Policies are versioned in the repository, evaluated inside every door onto the runtime (the Lua `proc`/`sh`/`fs` verbs, the `run` verb, the MCP tool, the temporary PreToolUse bridge), keyed by command family, host kind and regime, and

Document status: status: draft. This describes the spec’s lifecycle, not verified implementation.

Requirements (6)
  1. A built-in floor that a project seed can only tighten
  2. Evaluation is keyed by command family, host kind and regime
  3. Every refusal names why and what would clear it
  4. Filesystem scope under the fixture regime
  5. Consent is per run; soft reset is pre-authorised in forges, hard reset never is
  6. Every decision is audited with secrets redacted

Read full pinned spec ↗

speccommand-runtimestatus: draft8 requirements

Agents and committed automation execute commands through ONE Rust-hosted runtime (`tillandsias_exec` behind the plan binary's Lua environment and its `run` verb) instead of through shell strings. A command is an argv with named parameters; stdout, stderr and the completion are three separate values; time and output are bounded; the child starts from a fixed

Document status: status: draft. This describes the spec’s lifecycle, not verified implementation.

Requirements (8)
  1. A command is an argv with named parameters, never a shell string
  2. The run verb is the agent's door and reads argv as JSON when asked
  3. A result is typed and carries its run identity
  4. Time and output are bounded and a clipped capture cannot look whole
  5. Every Lua door judges and runs in one directory, from one environment, under one deadline rule
  6. Script code cannot outrun its verdict or load bytecode
  7. A terminated runner leaves no script-owned process behind
  8. The runtime runs identically on every locus

Read full pinned spec ↗

speccontainerfile-stalenessactive1 requirements

No purpose recorded in this spec.

Document status: active. This describes the spec’s lifecycle, not verified implementation.

Requirements (1)
  1. Runtime image source digest detects stale images

Read full pinned spec ↗

speccross-platformdeprecated0 requirements

No purpose recorded in this spec.

Document status: deprecated. This describes the spec’s lifecycle, not verified implementation.

Read full pinned spec ↗

specdefault-imageactive27 requirements

TBD - created by archiving change attach-here-mvp. Update Purpose after archive.

Document status: active. This describes the spec’s lifecycle, not verified implementation.

Requirements (27)
  1. Fedora Minimal base image with dev tools
  2. Non-root user with UID 1000
  3. Entrypoint launches OpenCode
  4. Prompted CLI entrypoints emit structured results only on request
  5. Declarative image definition via embedded Containerfiles
  6. Image identity is content-hash based with human aliases
  7. Forge image ships an OpenCode Web entrypoint
  8. OpenCode Web entrypoint runs opencode serve
  9. Default opencode model is a tool-capable Zen provider
  10. Cooperative split documented in agent instructions
  11. Coding-agent harnesses refresh at launch with persistent fallback
  12. Forge worker identities are collision-resistant across launch and teardown
  13. Codex state persistence excludes provider authentication and is worker-namespaced
  14. OpenCode consumes Vault authentication without credential files
  15. Agent instructions document subdomain routing convention
  16. Forge ships headless Chromium and headless Firefox with WebDriver bridges
  17. Forge image bakes the cheatsheets directory at /opt/cheatsheets/
  18. Forge entrypoint surfaces TILLANDSIAS_CHEATSHEETS to agents
  19. forge-welcome.sh prints the cheatsheet location once per session
  20. Forge image ships cheatsheets at /opt/cheatsheets-image (image-baked canonical)
  21. OpenCode config includes 4 new instruction files
  22. config-overlay installs 4 new instruction files
  23. methodology.md becomes an index
  24. Forge image ships the host-browser MCP stub
  25. Agent permission defaults — pre-grant container-local filesystem; enforce at the boundary
  26. Forge validation profile is non-destructive and machine-readable
  27. Forge harness permission bypass is pre-accepted at launch, never prompted

Read full pinned spec ↗

specdev-buildactive15 requirements

Define the build pipeline for local development, install, and CI gating. The pipeline MUST keep cheap pre-build validation separate from the expensive post-build smoke so measurable debt is visible instead of folded into one blob.

Document status: active. This describes the spec’s lifecycle, not verified implementation.

Requirements (15)
  1. Transparent build-substrate re-exec
  2. Debug build by default
  3. Release build
  4. Test execution
  5. Clean build
  6. Build churn directories opt out of copy-on-write on btrfs
  7. Install to local path
  8. CI full runs in explicit phases
  9. Post-build status check smoke
  10. Remove installed binary
  11. Wipe caches and artifacts
  12. Installer triggers init
  13. Cross-platform build documentation
  14. Install exits with deterministic exit codes
  15. Transparent HTTPS caching via dev proxy

Read full pinned spec ↗

specdirect-podman-callsobsolete superseded_by: podman-container-spec + podman-container-handle + podman-orchestration0 requirements

Historical tombstone for the earlier direct-shell-launch runtime model. The current runtime shape is the typed Podman layer (`podman-container-spec`, `podman-container-handle`, and `podman-orchestration`). This tombstone is kept for traceability only and should not receive new behavior.

Document status: obsolete superseded_by: podman-container-spec + podman-container-handle + podman-orchestration. This describes the spec’s lifecycle, not verified implementation.

Read full pinned spec ↗

specdisk-usage-detectionactive1 requirements

No purpose recorded in this spec.

Document status: active. This describes the spec’s lifecycle, not verified implementation.

Requirements (1)
  1. Cache tooling reports disk pressure before eviction

Read full pinned spec ↗

specdistributed-tracingactive1 requirements

No purpose recorded in this spec.

Document status: active. This describes the spec’s lifecycle, not verified implementation.

Requirements (1)
  1. Runtime events can share trace context

Read full pinned spec ↗

specdownload-telemetrydeprecated0 requirements

No purpose recorded in this spec.

Document status: deprecated. This describes the spec’s lifecycle, not verified implementation.

Read full pinned spec ↗

specembedded-scriptsobsolete superseded_by: podman-container-spec + podman-container-handle + podman-orchestration0 requirements

Historical tombstone for the retired script-embedding model. The shipped Tillandsias binary no longer extracts or executes repository shell scripts at runtime. Static assets such as icons, version metadata, and Containerfile-adjacent build inputs may still be embedded where needed, but executable runtime behavior is owned by compiled Rust and direct CLI call

Document status: obsolete superseded_by: podman-container-spec + podman-container-handle + podman-orchestration. This describes the spec’s lifecycle, not verified implementation.

Read full pinned spec ↗

specenclave-networkactive3 requirements

Internal podman network that isolates every Tillandsias-managed container. Only the proxy is dual-homed for external access; all other members communicate exclusively through the enclave.

Document status: active. This describes the spec’s lifecycle, not verified implementation.

Requirements (3)
  1. Internal podman network for container isolation
  2. Container attachment to enclave network
  3. Enclave lifecycle telemetry

Read full pinned spec ↗

specenclave-service-catalogactive3 requirements

The catalog of publishable enclave services: how a project's container (today the single fixed category `WEB`) is brought up on the enclave network and exposed through the reverse proxy under a friendly local URL. Orders 357/363/ 364 built the surface (`--publish-local`, the MCP `publish_local` tool, and `build_catalog_service_run_args`); this spec pins what

Document status: active. This describes the spec’s lifecycle, not verified implementation.

Requirements (3)
  1. The catalog is an allowlist, not a namespace
  2. Published services join the enclave by fixed name
  3. Public routes drop auth deliberately, never by default

Read full pinned spec ↗

specenforce-trace-presenceobsolete0 requirements

No purpose recorded in this spec.

Document status: obsolete. This describes the spec’s lifecycle, not verified implementation.

Read full pinned spec ↗

specenvironment-runtimestatus: active7 requirements

No purpose recorded in this spec.

Document status: status: active. This describes the spec’s lifecycle, not verified implementation.

Requirements (7)
  1. Global and per-project configuration
  2. User-facing files must be verbose and non-technical
  3. Accountable uninstall
  4. Desktop user-session runtime
  5. Dedicated service account runtime for headless installs
  6. TILLANDSIAS_AGENT accepts opencode-web
  7. Forge launchers export the exact active harness identity

Read full pinned spec ↗

specephemeral-guaranteeactive6 requirements

Prevent state pollution, cross-session leakage, and unverified persistence by formalizing the ephemerality invariants governing forge experts, tmpfs allocations, container lifecycles, and diagnostic log lifecycles.

Document status: active. This describes the spec’s lifecycle, not verified implementation.

Requirements (6)
  1. Ephemeral Expert State and Binary Surfaces
  2. Persistent Volume Disjointness
  3. Containment of Build-Cache Carve-Out
  4. Read-Only Named Volume for Content-Addressed Spec Index
  5. Zero Artifact Persistence Post-Teardown
  6. Ephemeral Initialization and Diagnostic Log Lifecycle

Read full pinned spec ↗

specephemeral-secret-refreshunspecified2 requirements

Tillandsias tray ensures podman secrets (CA certificates, tokens) are refreshed on each startup, preventing "secret name in use" errors from unclean shutdowns.

Document status: unspecified. This describes the spec’s lifecycle, not verified implementation.

Requirements (2)
  1. Stale secrets are automatically refreshed on startup
  2. Secret removal failures are reported and propagate

Read full pinned spec ↗

specexpert-serve-grounded-pipelineactive6 requirements

No purpose recorded in this spec.

Document status: active. This describes the spec’s lifecycle, not verified implementation.

Requirements (6)
  1. R1 — one pipeline, two front-ends
  2. R2 — retrieval only from the published index
  3. R3 — answer or typed refusal, no third state
  4. R4 — citations survive only if used
  5. R5 — freshness from the entry's own frame
  6. R6 — OpenAI-compatible loopback endpoint

Read full pinned spec ↗

specexternal-logs-layeractive6 requirements

Define the host-side external log tier for producer/consumer container pairs, including the shared parent directory layout, producer manifests, and auditor invariants that keep cross-container log visibility explicit and bounded.

Document status: active. This describes the spec’s lifecycle, not verified implementation.

Requirements (6)
  1. Two-tier observability model
  2. Producer mount + manifest contract
  3. Consumer mount
  4. Auditor invariants
  5. Reverse-breach refusal
  6. Migration of git-push.log

Read full pinned spec ↗

specfilesystem-scannerstatus: active2 requirements

No purpose recorded in this spec.

Document status: status: active. This describes the spec’s lifecycle, not verified implementation.

Requirements (2)
  1. OS-native event-driven watching
  2. Debounced event batching

Read full pinned spec ↗

specfix-podman-machine-host-aliasesobsolete0 requirements

No purpose recorded in this spec.

Document status: obsolete. This describes the spec’s lifecycle, not verified implementation.

Read full pinned spec ↗

specfix-windows-extended-pathobsolete0 requirements

No purpose recorded in this spec.

Document status: obsolete. This describes the spec’s lifecycle, not verified implementation.

Read full pinned spec ↗

specfix-windows-image-routingstatus: active promoted-from: openspec/changes/archive/2026-04-16-fix-windows-image-routing/ annotation-count: 1 implementation-complete: true5 requirements

Fix Windows container image building by routing each image type (forge, proxy, git, inference, web) to its own Containerfile and build context. Previously, Windows hardcoded all builds to `images/default/Containerfile` (the forge), resulting in four image tags pointing to the same image ID.

Document status: status: active promoted-from: openspec/changes/archive/2026-04-16-fix-windows-image-routing/ annotation-count: 1 implementation-complete: true. This describes the spec’s lifecycle, not verified implementation.

Requirements (5)
  1. Image Build Path Routing by Type
  2. Correct Image Entrypoints
  3. Image Build Centralized in Helper
  4. Defensive Integration Test or Self-Check
  5. Build Number Bump

Read full pinned spec ↗

specforge-as-only-runtimeactive7 requirements

Lock in a single, falsifiable runtime contract for Tillandsias' interactive surface so that:

Document status: active. This describes the spec’s lifecycle, not verified implementation.

Requirements (7)
  1. Forge Image Bakes the Full Agent Set
  2. All Podman Run Goes Through the Idiomatic Layer
  3. No Host-Side Agent Binaries
  4. Mount Categories Are Exhaustive
  5. Attached forge exit cleans idle stacks
  6. Delegated agent results come from the current scoped run
  7. Host Terminal Is the Only Host-Side Process

Read full pinned spec ↗

specforge-cache-architectureobsolete0 requirements

No purpose recorded in this spec.

Document status: obsolete. This describes the spec’s lifecycle, not verified implementation.

Read full pinned spec ↗

specforge-cache-dualstatus: active5 requirements

Define the live cache discipline for forge containers: shared Nix store populated by build-time Nix-managed processes, per-project cache, and per-language env var routing.

Document status: status: active. This describes the spec’s lifecycle, not verified implementation.

Requirements (5)
  1. Forge containers see exactly four path categories
  2. Per-language env vars resolve into the per-project cache
  3. Shared cache uses nix as the single entry point
  4. Project workspace is the user's git repo, not a cache
  5. cache_version file lifecycle

Read full pinned spec ↗

specforge-environment-discoverabilityactive15 requirements

No purpose recorded in this spec.

Document status: active. This describes the spec’s lifecycle, not verified implementation.

Requirements (15)
  1. Inventory CLI lists installed toolchains
  2. Services CLI lists running containers and services
  3. Models CLI queries available LLM models
  4. Welcome banner on terminal entry
  5. Discovery commands are discoverable via `--help`
  6. MCP host-services tool surface is organically discoverable
  7. Plan expert preserves release constraints and dependency direction
  8. Plan expert ranks claimable next work deterministically
  9. Generic project expert bootstrap is zero-intervention
  10. Project index is ephemeral and rebuilt from the active checkout
  11. Host-session experts refresh on commit, env-gated
  12. Generic expert readiness is machine-readable and never blocks launch
  13. project_answer is one uniform cited surface with a deterministic fallback
  14. Every citation names the frame it was read in, and the reader's position is derived rather than assumed
  15. local-experts agent is grounded-endpoint only

Read full pinned spec ↗

specforge-forward-compatdeprecated0 requirements

No purpose recorded in this spec.

Document status: deprecated. This describes the spec’s lifecycle, not verified implementation.

Read full pinned spec ↗

specforge-git-identity-anonymizationactive5 requirements

A forge guest commits as a project-scoped identity, never as the operator, and every agentic commit carries machine-readable attribution. Seven guest entrypoints (`images/default/entrypoint-forge-*.sh`, `entrypoint-terminal.sh`) and `lib-common.sh` traced this contract for months while the spec file did not exist — order 877 closed that ghost. The 2026-08-24

Document status: active. This describes the spec’s lifecycle, not verified implementation.

Requirements (5)
  1. The guest never inherits the host's git identity
  2. Guest identity is config, not exported environment
  3. Committer host stays derivable
  4. Agentic commits carry attribution trailers
  5. Hook installation cannot break a hostile environment

Read full pinned spec ↗

specforge-hot-cold-splitstatus: active7 requirements

Define the forge storage split between HOT tmpfs mounts, COLD disk-backed state, and the runtime banner that makes those boundaries visible to agents and humans.

Document status: status: active. This describes the spec’s lifecycle, not verified implementation.

Requirements (7)
  1. HOT tier — RAM-backed tmpfs for finely curated paths
  2. Per-mount size caps
  3. --memory ceiling pairs with tmpfs caps
  4. Pre-flight RAM check refuses launch on insufficient host RAM
  5. Per-launch project source budget
  6. Agent transparency
  7. Tmpfs-overlay lane for per-project ephemeral cache

Read full pinned spec ↗

specforge-offlinestatus: active3 requirements

Forge containers operate offline -- no credentials, no project mounts, no direct internet. Code comes from git clone, packages come through the proxy, secrets live exclusively in the git service.

Document status: status: active. This describes the spec’s lifecycle, not verified implementation.

Requirements (3)
  1. Forge containers have zero credentials
  2. Forge containers have no direct project mount
  3. Forge containers are enclave-only

Read full pinned spec ↗

specforge-opencode-onboardingstatus: active3 requirements

The OpenCode onboarding bundle SHALL define the live launch-time contract for newly attached forge sessions: discovery, modular instructions, OpenSpec bootstrap, and the synthetic `/startup` prompt.

Document status: status: active. This describes the spec’s lifecycle, not verified implementation.

Requirements (3)
  1. First-turn environment discovery
  2. Modularized instruction files
  3. OpenCode bootstrap seeds /startup and OpenSpec init

Read full pinned spec ↗

specforge-shell-toolsstatus: active3 requirements

Define the live shell-tooling surface in the forge image: alternative shells, modern terminal utilities, and shell startup config that make interactive sessions useful by default.

Document status: status: active. This describes the spec’s lifecycle, not verified implementation.

Requirements (3)
  1. Alternative shells available
  2. Terminal tools installed
  3. Shell configurations

Read full pinned spec ↗

specforge-stalenessstatus: active promoted-from: openspec/changes/archive/2026-04-08-fix-forge-staleness-and-pruning/ annotation-count: 136 requirements

Ensure forge image staleness detection is driven by the source hash, always invokes the build script for freshness checks, refreshes human aliases on rebuild, auto-prunes old images to save disk space, and forward-compatible detects newer forge images from a different binary version.

Document status: status: active promoted-from: openspec/changes/archive/2026-04-08-fix-forge-staleness-and-pruning/ annotation-count: 13. This describes the spec’s lifecycle, not verified implementation.

Requirements (6)
  1. Source-hash staleness file
  2. Tray always invokes build script for staleness check
  3. Prune old forge images after successful build
  4. Forward-compatible newer image detection
  5. Staleness detection in init command
  6. Absent cache version file means fresh start, not a staleness signal

Read full pinned spec ↗

specforge-standaloneactive5 requirements

Define the single-container forge troubleshooting path. This artifact lets an operator launch only the forge image, mount one host project directory, and enter an interactive bash session for local diagnosis or in-container development. This is the canonical name for the standalone forge launcher and supersedes the older `forge-standalone-runner` artifact se

Document status: active. This describes the spec’s lifecycle, not verified implementation.

Requirements (5)
  1. Standalone forge runner uses the forge image only
  2. Source mount is project-scoped
  3. Interactive bash session
  4. Full network access without enclave wiring
  5. Runner is fail-fast and explicit

Read full pinned spec ↗

specforge-standalone-runnerobsolete0 requirements

This document is retained for history only. The canonical standalone forge launcher contract now lives in `openspec/specs/forge-standalone/spec.md`.

Document status: obsolete. This describes the spec’s lifecycle, not verified implementation.

Read full pinned spec ↗

specforge-welcomestatus: active2 requirements

Define the interactive terminal welcome banner and its once-per-session startup gating so new forge sessions expose project context, OS context, and discovery tips without repeating themselves.

Document status: status: active. This describes the spec’s lifecycle, not verified implementation.

Requirements (2)
  1. Welcome message on terminal launch
  2. Fish as default interactive shell

Read full pinned spec ↗

specgh-auth-scriptstatus: active7 requirements

The interactive GitHub Login user experience. Both the CLI entry point (`tillandsias --github-login`) and the tray menu item ("GitHub Login") drive the same single Rust implementation: spin up an ephemeral container from the git service image, run and verify `gh auth login` interactively, write the resulting OAuth token to Vault from inside the container, an

Document status: status: active. This describes the spec’s lifecycle, not verified implementation.

Requirements (7)
  1. Single implementation behind tray and CLI entry points
  2. Non-interactive login is explicit and cannot hang on `/dev/tty`
  3. Interactive login uses an ephemeral git-service-image container
  4. Container verifies the session, writes the token to Vault, never reaches the host
  5. Drop guard tears down the login container on every exit path
  6. Mobile QR Code Device Flow
  7. Token Rotation and Expiration Management

Read full pinned spec ↗

specgit-mirror-servicestatus: active14 requirements

Per-project bare mirror repositories live inside a named Podman volume mounted at `/srv/git` in the git service container. Git daemon serves clones and pushes over the enclave network. A pre-receive relay forwards exactly the proposed ref transaction to the configured upstream with `git push --atomic` before the mirror acknowledges it locally. GitHub credent

Document status: status: active. This describes the spec’s lifecycle, not verified implementation.

Requirements (14)
  1. Bare mirror repository management
  2. Git daemon serves mirrors on enclave network
  3. Existing-volume receive hardening
  4. Pre-receive relay verifies acknowledgement durability
  5. Reconciliation fetch never clobbers exported refs
  6. GitHub token delivery uses Vault AppRole
  7. Git service container lifecycle
  8. Git accountability window
  9. Mirror → host working-copy auto-sync on push
  10. Mirror sync never clobbers user work
  11. Tray startup sweeps all mirrors
  12. Tray Quit triggers a final sync
  13. Per-project transparency — no hardcoded project names
  14. Repository-local Git metadata is bidirectionally quarantined

Read full pinned spec ↗

specgithub-credential-healthstatus: active5 requirements

TBD - created by archiving change tray-responsiveness-and-startup-gating. Update Purpose after archive.

Document status: status: active. This describes the spec’s lifecycle, not verified implementation.

Requirements (5)
  1. Credential health probe distinguishes "down" from "unauthenticated"
  2. Probe runs off the event loop with a bounded timeout
  3. Result is cached per process lifetime but invalidated on explicit sign-in / sign-out
  4. A credential that goes bad IN PLACE is observed without a presence transition
  5. The implemented vocabulary is named, and the tray-loop polling ban is scoped

Read full pinned spec ↗

specgraceful-shutdownstatus: active promoted-from: direct annotation-count: 87 requirements

Define the graceful shutdown contract for Tillandsias headless binary: signal handling (SIGTERM/SIGINT), container lifecycle management with timeout, resource cleanup, exit codes, and verification that no ephemeral artifacts remain after shutdown.

Document status: status: active promoted-from: direct annotation-count: 8. This describes the spec’s lifecycle, not verified implementation.

Requirements (7)
  1. SIGTERM and SIGINT trigger graceful shutdown immediately
  2. All managed containers receive stop signals before force-kill
  3. Foreground forge exits clean idle stacks
  4. Graceful shutdown has a configurable timeout with force-kill fallback
  5. Exit code reflects shutdown status
  6. No stale sockets remain after shutdown
  7. No stale mounts remain after shutdown

Read full pinned spec ↗

specguest-crashloop-detectionactive3 requirements

The host tier must tell a guest that is LOOPING — restarting over and over, never converging — apart from one that is merely progressing slowly, and say so in a pinned, regex-testable grammar. One pure-Rust state machine (`tillandsias-control-wire::crashloop`) is consumed by every tray (Windows NotifyIcon, macOS AppKit, Linux), so the platforms cannot drift

Document status: active. This describes the spec’s lifecycle, not verified implementation.

Requirements (3)
  1. A loop is detected within the window
  2. Slow progress is never a crash loop
  3. The detector is proven where it ships

Read full pinned spec ↗

specheadless-modestatus: active promoted_from: direct annotation_count: 45 requirements

Define the contract for running Tillandsias without a graphical user interface. Headless mode emits structured JSON events to stdout and is suitable for CI/CD automation, server deployments, and containerized environments. Headless mode must not depend on GTK or display subsystems, ensuring portability and composability.

Document status: status: active promoted_from: direct annotation_count: 4. This describes the spec’s lifecycle, not verified implementation.

Requirements (5)
  1. Headless binary invocation with --headless flag
  2. Container status events and metrics reporting
  3. No GTK dependency in headless path
  4. Status-check mode for initialization verification
  5. Per-operation image ensure lists, and the Build-context vault

Read full pinned spec ↗

spechost-browser-mcpstatus: active12 requirements

Define the host-resident MCP browser bridge that lets forge agents request allowlisted project-local browser windows over the tray control socket. The live implementation is the Rust `tillandsias-browser-mcp` crate plus the forge-side `host-browser.sh` stdio bridge, and it exists to launch isolated Chromium windows for mapped `<service>.<project>.localhost`

Document status: status: active. This describes the spec’s lifecycle, not verified implementation.

Requirements (12)
  1. Host MCP server exposes browser-control tools to forge agents
  2. MCP reaches this server over the per-lane tool socket
  3. Browser provider is bundled Chromium only
  4. Process-per-window with ephemeral profile
  5. Window survives MCP connection drop
  6. URL allowlist denies opencode-self and non-project hosts
  7. Authorisation by Unix-socket peer credential
  8. Per-(project,host) debounce on browser.open
  9. browser.eval is disabled by default in v1
  10. CDP method usage is pinned to stable 1.3 and the bundled-Chromium version
  11. Per-call accountability log with redacted payloads
  12. Per-message size cap and DoS protection

Read full pinned spec ↗

spechost-chromiumobsolete0 requirements

Historical wrapper retained for traceability only. The live Chromium installation and detection contract now lives in `openspec/specs/host-chromium-on-demand/spec.md`.

Document status: obsolete. This describes the spec’s lifecycle, not verified implementation.

Read full pinned spec ↗

spechost-chromium-on-demandstatus: active promoted-from: openspec/changes/archive/2026-04-26-host-chromium-on-demand/ annotation-count: 254 requirements

Provide an isolated, on-demand Chromium runtime for tray-launched browser windows and the secure OpenCode Web attach flow. Downloads Chrome for Testing into a shared host cache on first launch, verifies integrity via SHA-256, and launches windows with per-session incognito profiles isolated from the user's daily browser. Enables CDP (remote debugging protoco

Document status: status: active promoted-from: openspec/changes/archive/2026-04-26-host-chromium-on-demand/ annotation-count: 25. This describes the spec’s lifecycle, not verified implementation.

Requirements (4)
  1. Download and Verify Bundled Chromium
  2. Launch Windows with Ephemeral Profiles
  3. Browser Detection
  4. Download Telemetry

Read full pinned spec ↗

spechost-guest-transportactive phase: 1 (facade contract landed; per-platform backends in progress)4 requirements

Normalize every host→guest interaction onto a single platform-agnostic facade so the Linux, macOS, and Windows trays stop drifting into separate "connect from host to guest" implementations. Two primitives cover every need the operator named:

Document status: active phase: 1 (facade contract landed; per-platform backends in progress). This describes the spec’s lifecycle, not verified implementation.

Requirements (4)
  1. Two canonical primitives
  2. Callers are backend-agnostic
  3. One wire protocol for both primitives on every backend
  4. Cross-platform conformance

Read full pinned spec ↗

spechost-shell-architectureproposed phase: 15 requirements

Define the contract for the shared host-side library `tillandsias-host-shell` that backs both the Windows (Win32 NotifyIcon) and macOS (AppKit NSStatusItem) native tray binaries. The host shell owns the portable, OS-agnostic responsibilities that previously lived inside `tillandsias-headless`'s Linux tray module: project discovery, menu state modelling, cont

Document status: proposed phase: 1. This describes the spec’s lifecycle, not verified implementation.

Requirements (5)
  1. Host filesystem scanner enumerates `~/src/` projects
  2. Portable `MenuStructure` is the single source of truth for menu paint
  3. vsock client lifecycle is owned by the host shell
  4. VM orchestration is delegated to `tillandsias-vm-layer`
  5. Host shell never holds long-lived credentials

Read full pinned spec ↗

spechost-state-lifecycleactive4 requirements

State what a destructive reset destroys, what it preserves, and what only a manual uninstall removes — on every platform, by named path — so that "reset is the baseline" and "credentials survive wipes" hold at the same time.

Document status: active. This describes the spec’s lifecycle, not verified implementation.

Requirements (4)
  1. Everything Tillandsias owns on a host lives under `~/.tillandsias/`, and a first launch migrates the legacy roots there
  2. Destructive reset destroys derived state and preserves operator data; guest regimes have a SOFT and a HARD reset
  3. Every download is recorded in one manifest under `~/.tillandsias/downloads/`
  4. `--uninstall` is the preferred removal, leaves ZERO traces, and asks before removing `~/.tillandsias/`

Read full pinned spec ↗

specinference-containerstatus: active9 requirements

Shared ollama inference container on the enclave network. Forge containers query it via OLLAMA_HOST. Models persist in a host-mounted cache volume. Downloads route through the proxy.

Document status: status: active. This describes the spec’s lifecycle, not verified implementation.

Requirements (9)
  1. Local LLM inference via ollama
  2. Shared model cache
  3. Inference container lifecycle
  4. Inference NO_PROXY covers loopback + enclave peers
  5. Tier-tagged tool-capable model pre-pulls
  6. CPU tier-S is the always-available terminal
  7. NPU tier rows are additive and engine-gated
  8. Tier classification logged once at boot
  9. The engine behind the endpoint is a slot

Read full pinned spec ↗

specinference-engine-slotsstatus: active authored-by: order 479 (`heterogeneous-inference-specs`), spec-before-implementation derived-from: `plan/issues/heterogeneous-inference-cpu-gpu-npu-research-2026-07-24.md` gates: order 481 (`npu-device-passthrough`), order 482 (`llama-server-engine-slot`), order 483 (`host-native-sidecar-endpoints`) answers: the order-478 enclave-boundary ques8 requirements

Make the inference *engine* pluggable while the *endpoint* stays frozen.

Document status: status: active authored-by: order 479 (`heterogeneous-inference-specs`), spec-before-implementation derived-from: `plan/issues/heterogeneous-inference-cpu-gpu-npu-research-2026-07-24.md` gates: order 481 (`npu-device-passthrough`), order 482 (`llama-server-engine-slot`), order 483 (`host-native-sidecar-endpoints`) answers: the order-478 enclave-boundary ques. This describes the spec’s lifecycle, not verified implementation.

Requirements (8)
  1. SLOT-1 — One stable enclave endpoint, selectable engine behind it
  2. SLOT-2 — Slot contract: OpenAI-compatible surface plus the existing readiness probe
  3. SLOT-3 — Engine registry, default engine, and the NPU integration point
  4. SLOT-4 — Vulkan via /dev/dri is the default Linux GPU lane
  5. SLOT-5 — Device passthrough for container-resident slots is graceful and unprivileged
  6. SLOT-6 — Host-native sidecar slots are reachable only behind the enclave proxy
  7. SLOT-7 — The model tier table is data consumed by every engine
  8. SLOT-8 — Slot lifecycle is idempotent, ephemeral, and soft-failing

Read full pinned spec ↗

specinference-host-side-pullobsolete0 requirements

Historical wrapper retained for traceability only. The live inference startup and model-pull contract now lives in `inference-container` and `zen-default-with-ollama-analysis-pool`, with runtime behavior owned by the inference image entrypoint.

Document status: obsolete. This describes the spec’s lifecycle, not verified implementation.

Read full pinned spec ↗

specinference-policy-routerstatus: active authored-by: order 479 (`heterogeneous-inference-specs`), spec-before-implementation derived-from: `plan/issues/heterogeneous-inference-cpu-gpu-npu-research-2026-07-24.md` gates: order 484 (`inference-policy-router`)12 requirements

Decide, transparently and without operator configuration, which device, which engine slot, and which model tier serve a given inference request — and revise that decision when the machine's power and thermal situation changes.

Document status: status: active authored-by: order 479 (`heterogeneous-inference-specs`), spec-before-implementation derived-from: `plan/issues/heterogeneous-inference-cpu-gpu-npu-research-2026-07-24.md` gates: order 484 (`inference-policy-router`). This describes the spec’s lifecycle, not verified implementation.

Requirements (12)
  1. CLASS-1 — Four workload classes with declared objectives
  2. ROUTE-1 — The routing table is data and route() is a pure function
  3. ROUTE-2 — Placements are ordered fallback chains terminating in CPU tier-S
  4. ROUTE-3 — Failures advance the chain; three strikes quarantine the device
  5. ROUTE-4 — A model must fit device-visible memory with at least 10% headroom
  6. ROUTE-5 — First request is served from a resident small model while the preferred model loads
  7. ADAPT-1 — Power and thermal signals are subscribed, not polled-only
  8. ADAPT-2 — On battery or power-saver, background work suspends within 5 seconds
  9. ADAPT-3 — Background work resumes only on AC and thermal-nominal and idle
  10. ADAPT-4 — Status surface and pin override with auto-release
  11. NEG-1 — Non-goals are binding prohibitions
  12. NEG-2 — Degraded lanes never outrank healthy ones

Read full pinned spec ↗

specinit-commandstatus: active8 requirements

Define behavior of `tillandsias --init` command including incremental builds, debug mode, and the runtime contract that the shipped binary performs image orchestration directly from Rust without shell-script wrappers or a source checkout.

Document status: status: active. This describes the spec’s lifecycle, not verified implementation.

Requirements (8)
  1. Init CLI command
  2. Init path does not invoke shell wrappers
  3. Init does not require a checkout
  4. Init build uses host user namespace
  5. Init failure diagnostics are host-specific
  6. Exit code contract for init command
  7. Debug mode log capture
  8. All images built

Read full pinned spec ↗

specinit-incremental-buildsstatus: active4 requirements

Track and resume partial `tillandsias --init` builds, with debug logging for failed images and content-based runtime asset staleness.

Document status: status: active. This describes the spec’s lifecycle, not verified implementation.

Requirements (4)
  1. Incremental build state tracking
  2. Final Containerfile instruction layers are squashed
  3. Debug flag for init command
  4. Failed build log display

Read full pinned spec ↗

specinstall-progressactive0 requirements

No purpose recorded in this spec.

Document status: active. This describes the spec’s lifecycle, not verified implementation.

Read full pinned spec ↗

specknowledge-source-of-truthstatus: active13 requirements

This spec is the **epistemology** of Tillandsias: it defines what counts as authoritative knowledge, how artefacts converge over time, and how spec-vs-code divergence is resolved. Every other spec is interpreted *through* this spec. Observability surfaces (dashboards, signatures, evidence bundles) are downstream projections of the truth contract defined here

Document status: status: active. This describes the spec’s lifecycle, not verified implementation.

Requirements (13)
  1. Knowledge directory structure
  2. Cheatsheet format
  3. XML collection index
  4. Version tracking and freshness
  5. Project agnosticism
  6. External debug source fetching
  7. Authority hierarchy across artefact classes
  8. CRDT-inspired monotonic convergence
  9. Spec-vs-code divergence resolution
  10. Staleness detection via timestamp and content hash
  11. Convergence evidence bundles
  12. Provenance is mandatory for every authoritative artefact
  13. Integration with downstream observability surfaces

Read full pinned spec ↗

speclayered-tools-overlayobsolete0 requirements

Historical overlay capability retained for traceability only. The live tool lifecycle is now owned by `default-image`, `forge-hot-cold-split`, and `tools-overlay-fast-reuse`.

Document status: obsolete. This describes the spec’s lifecycle, not verified implementation.

Read full pinned spec ↗

speclinux-native-portable-executableactive4 requirements

No purpose recorded in this spec.

Document status: active. This describes the spec’s lifecycle, not verified implementation.

Requirements (4)
  1. Headless Linux launcher is portable across common distros
  2. Linux binary carries runtime image contexts
  3. Linux release artifact is the musl binary
  4. Native tray builds may use host UI libraries

Read full pinned spec ↗

speclitmus-frameworkactive1 requirements

No purpose recorded in this spec.

Document status: active. This describes the spec’s lifecycle, not verified implementation.

Requirements (1)
  1. Litmus bindings remain falsifiable

Read full pinned spec ↗

speclocal-web-previewstatus: draft version: v1 id: enclave.web.local-preview@v1 verification: S0 (design; executable coverage and live evidence pending) previous: none; additive runtime profile over enclave-service-catalog5 requirements

No purpose recorded in this spec.

Document status: status: draft version: v1 id: enclave.web.local-preview@v1 verification: S0 (design; executable coverage and live evidence pending) previous: none; additive runtime profile over enclave-service-catalog. This describes the spec’s lifecycle, not verified implementation.

Requirements (5)
  1. Session-bound live source
  2. Shared source preserves the bounded HOT storage contract
  3. Allowlisted local runtime profiles
  4. Existing MCP lifecycle contract
  5. HTTPS routing and honest trust

Read full pinned spec ↗

speclog-aggregationactive1 requirements

No purpose recorded in this spec.

Document status: active. This describes the spec’s lifecycle, not verified implementation.

Requirements (1)
  1. Runtime logs aggregate without dropping source metadata

Read full pinned spec ↗

speclog-schema-versioningactive1 requirements

No purpose recorded in this spec.

Document status: active. This describes the spec’s lifecycle, not verified implementation.

Requirements (1)
  1. Log entries carry schema version

Read full pinned spec ↗

speclogging-accountabilityobsolete0 requirements

Historical logging-accountability umbrella retained for traceability only. The live logging contracts now live in `runtime-logging`, `logging-levels`, and the diagnostics/runtime specs.

Document status: obsolete. This describes the spec’s lifecycle, not verified implementation.

Read full pinned spec ↗

speclogging-levelsobsolete0 requirements

This specification is a historical tombstone for the retired embedded `external-logs.yaml` configuration model. The live behavior is now owned by:

Document status: obsolete. This describes the spec’s lifecycle, not verified implementation.

Read full pinned spec ↗

specmacos-native-trayactive phase: 56 requirements

Define the contract for the macOS-native tray binary `tillandsias-macos-tray` (produces `tillandsias-tray.app`). The binary runs on the host as an OS-native AppKit application, owns an `NSStatusItem` (system menu bar icon) constructed via `objc2-app-kit`, and delegates portable logic to `tillandsias-host-shell`. The binary is responsible for driving the macO

Document status: active phase: 5. This describes the spec’s lifecycle, not verified implementation.

Requirements (6)
  1. AppKit `NSStatusItem` is the only tray surface
  2. Menu items match the host-shell parity contract
  3. Virtualization.framework guest lifecycle is owned by this binary
  4. Terminal attach runs the tray's own attach client in the window
  5. GUI passthrough is explicitly deferred to v2
  6. `--diagnose` CLI mode emits a stable bundled health report

Read full pinned spec ↗

specmcp-on-demandactive7 requirements

MCP servers (filesystem, resource tools, inference) are started on-demand when first invoked by an agent, not eagerly at container startup. Reduces boot time, memory footprint, and startup complexity. Servers are destroyed on container shutdown (ephemeral lifecycle).

Document status: active. This describes the spec’s lifecycle, not verified implementation.

Requirements (7)
  1. On-demand MCP server startup
  2. Multiple MCP servers coexist
  3. Health check and readiness
  4. Ephemeral lifecycle — servers destroyed on shutdown
  5. Browser tool variant — on-demand CLI instead of daemon
  6. MCP server communication channel
  7. Litmus test — on-demand MCP lifecycle

Read full pinned spec ↗

specmcp-tool-socketstatus: active5 requirements

Define the per-lane Unix-domain socket that carries MCP JSON-RPC between a forge lane and the host tray, and the attribution model that makes a request answerable *on behalf of a specific project*.

Document status: status: active. This describes the spec’s lifecycle, not verified implementation.

Requirements (5)
  1. Each lane is served by its own socket
  2. Attribution is derived from the accepting listener
  3. NDJSON framing with a bounded per-line payload
  4. One request in flight per connection
  5. The socket carries both host-services and browser tools

Read full pinned spec ↗

specmenu-action-error-handlingactive1 requirements

No purpose recorded in this spec.

Document status: active. This describes the spec’s lifecycle, not verified implementation.

Requirements (1)
  1. Tray menu action failures are visible and bounded

Read full pinned spec ↗

specmeta-orchestrationactive5 requirements

The unattended top-level loop: how a host runs one work cycle — heartbeat, drain, verify, attest — without a human watching. The runbook lives in `skills/meta-orchestration/SKILL.md`; THIS document pins the invariants that runbook may never lose, each falsifiable by the litmus tests bound in `openspec/litmus-bindings.yaml` (spec_id `meta-orchestration`). Thi

Document status: active. This describes the spec’s lifecycle, not verified implementation.

Requirements (5)
  1. A full cycle proves its own exit
  2. The startup boundary is inviolable
  3. One checkout, one cycle
  4. Guards run before work, and fail loud
  5. Every mode names its verdict

Read full pinned spec ↗

specmethodology-accountabilityactive5 requirements

Make the methodology itself auditable. The project already requires specs, cheatsheets, traces, litmus tests, and evidence bundles for implementation work. This spec extends the same discipline to methodology claims, unknown-event intake, and correctness-proximity scoring.

Document status: active. This describes the spec’s lifecycle, not verified implementation.

Requirements (5)
  1. Methodology claims cite provenance
  2. Unknown events are first-class artifacts
  3. Correctness proximity uses CentiColons
  4. Existing convergence score remains coarse
  5. Mathematical convergence claims are bounded

Read full pinned spec ↗

specnative-secrets-storeactive3 requirements

Store and retrieve the GitHub OAuth token in the host OS's platform-native secret service. The host Rust process is the sole consumer of the keyring; containers never call any keyring API. This is the source of truth for GitHub credentials — no plaintext credential files live on persistent disk.

Document status: active. This describes the spec’s lifecycle, not verified implementation.

Requirements (3)
  1. Platform-native keyring backend
  2. Single keyring entry for the GitHub token
  3. Host-only keyring API surface

Read full pinned spec ↗

specnetwork-scenariosactive2 requirements

Name every network posture a Tillandsias-managed container can run in, so an operation declares which one it is using rather than assembling `--network` arguments ad hoc. Published from order 245 §5 P5; every posture below was verified against the tree on 2026-08-30 rather than carried over from the draft catalog.

Document status: active. This describes the spec’s lifecycle, not verified implementation.

Requirements (2)
  1. Every container declares a network posture
  2. S0 is not a scenario, and S4 is retired

Read full pinned spec ↗

specnix-builderactive3 requirements

No purpose recorded in this spec.

Document status: active. This describes the spec’s lifecycle, not verified implementation.

Requirements (3)
  1. Build-time Nix only
  2. Git-tracked files for flake builds
  3. dockerTools builder/attribute pairing

Read full pinned spec ↗

specnix-cache-serviceactive5 requirements

Serve the persistent host nix store (order 795-h8er) to the enclave as a nix binary cache, so an ephemeral forge lands on a warm cache without any host path being mounted into it.

Document status: active. This describes the spec’s lifecycle, not verified implementation.

Requirements (5)
  1. The cache is a first-class enclave service
  2. The cache hostname bypasses the enclave proxy
  3. Transport trust and content trust are BOTH enforced
  4. An unreachable cache degrades to silence, not to failure
  5. A running but untrusted cache is a violation, not a skip

Read full pinned spec ↗

specno-terminal-flickeractive0 requirements

No purpose recorded in this spec.

Document status: active. This describes the spec’s lifecycle, not verified implementation.

Read full pinned spec ↗

specobservability-convergenceactive11 requirements

Track alignment between OpenSpec specs and runtime implementation via instrumented metrics. Measure code coverage of spec requirements, spec→code→trace traceability, and convergence toward spec intent. Enable data-driven decisions about spec completeness and implementation gaps.

Document status: active. This describes the spec’s lifecycle, not verified implementation.

Requirements (11)
  1. Spec coverage metrics
  2. Trace cardinality and completeness
  3. Requirement↔Litmus Test binding
  4. Implementation latency — time from spec to code
  5. Spec debt and staleness
  6. Convergence scoring
  7. Dashboard is a read-only projection
  8. Dashboard alert thresholds
  9. Dashboard trend metrics
  10. Litmus test — observability instrumentation
  11. Test requirement

Read full pinned spec ↗

specobservability-metricsactive1 requirements

No purpose recorded in this spec.

Document status: active. This describes the spec’s lifecycle, not verified implementation.

Requirements (1)
  1. Runtime metrics expose current system health

Read full pinned spec ↗

specopencode-web-sessionobsolete9 requirements

How Tillandsias runs a persistent OpenCode Web server per project, maps it to a local-only host port, and renders it in an embedded Tauri webview — including multi-session reattach semantics and shutdown guarantees.

Document status: obsolete. This describes the spec’s lifecycle, not verified implementation.

Requirements (9)
  1. OpenCode Web is the default session agent
  2. Per-project persistent web container
  3. Host port bound to 127.0.0.1 only
  4. Unique host port per concurrent web container
  5. WebviewWindow launch contract
  6. Stop tears down the web container
  7. Shutdown stops all web containers and closes all webviews
  8. OpenCode Web defaults to dark theme
  9. Webview close does not terminate the tray

Read full pinned spec ↗

specopencode-web-session-otpactive promoted-from: openspec/changes/archive/2026-04-27-opencode-web-session-otp/ annotation-count: 296 requirements

Provide application-layer authentication for OpenCode Web sessions launched from the tray. Generates a one-time-password (OTP) per browser window, communicates it to the router via a control channel, and enforces session-cookie validation on all requests. This defence-in-depth mechanism complements the loopback-only network bind to close per-host-user attack

Document status: active promoted-from: openspec/changes/archive/2026-04-27-opencode-web-session-otp/ annotation-count: 29. This describes the spec’s lifecycle, not verified implementation.

Requirements (6)
  1. Per-Attach OTP Generation and Delivery
  2. Router-Side OTP Validation and Cookie Issuance
  3. Cookie Shape and Lifetime
  4. Multi-Window Reattachment
  5. Secrets Management Integration
  6. Authenticated Readiness Probe

Read full pinned spec ↗

specoverlay-mount-cacheobsolete0 requirements

No purpose recorded in this spec.

Document status: obsolete. This describes the spec’s lifecycle, not verified implementation.

Read full pinned spec ↗

specpersistent-git-serviceobsolete0 requirements

This per-project git-service lifetime contract has been retired. The live lifecycle is now described by `git-mirror-service`, which starts the git service on first attach for a project and stops it when the project's last forge exits.

Document status: obsolete. This describes the spec’s lifecycle, not verified implementation.

Read full pinned spec ↗

specpodman-container-handleactive3 requirements

Define the lightweight handle returned by the Podman wrapper after a launch request succeeds. The handle SHALL carry the launch spec snapshot and the runtime identity needed for stop, inspect, and event correlation.

Document status: active. This describes the spec’s lifecycle, not verified implementation.

Requirements (3)
  1. Handle retains launch identity
  2. Handle creation remains unit-testable
  3. Event stream maps back to handle identity

Read full pinned spec ↗

specpodman-container-specactive3 requirements

Define the typed Rust container-spec layer used by Tillandsias to build Podman launch arguments without shell strings. This layer is the isolated, unit-testable boundary between launch intent and runtime execution.

Document status: active. This describes the spec’s lifecycle, not verified implementation.

Requirements (3)
  1. Launch specs are typed and directly serialized
  2. Security defaults are immutable
  3. Spec layer supports launch-shape composition

Read full pinned spec ↗

specpodman-idiomatic-patternsactive9 requirements

Canonical idiomatic patterns for Podman usage in Tillandsias: event-driven container observation, security-mandatory flags, per-project storage isolation, ephemeral secret mounting, categorized error handling, and rootless-first execution. These patterns govern every layer that touches Podman — Rust crates, shell scripts, and Containerfiles.

Document status: active. This describes the spec’s lifecycle, not verified implementation.

Requirements (9)
  1. Event-driven container observation, never polling
  2. Non-negotiable security flags on every container
  3. Per-project storage isolation (enclave model)
  4. Ephemeral secret mounting, never env vars or image layers
  5. Categorized error handling with retry discrimination
  6. Rootless-first execution with keep-id mapping
  7. Enclave network with internal DNS and dynamic IPAM
  8. Observed launch helpers produce actionable failures
  9. Tillandsias-owned podman configuration is explicit on every invocation and wins over system defaults

Read full pinned spec ↗

specpodman-image-evictionactive1 requirements

No purpose recorded in this spec.

Document status: active. This describes the spec’s lifecycle, not verified implementation.

Requirements (1)
  1. Image eviction is targeted and explainable

Read full pinned spec ↗

specpodman-orchestrationactive8 requirements

No purpose recorded in this spec.

Document status: active. This describes the spec’s lifecycle, not verified implementation.

Requirements (8)
  1. Rootless user-session ownership
  2. Security-hardened container defaults
  3. FUSE file descriptor sanitization before container launch
  4. Rootless networking backend
  5. Volume mount strategy
  6. Detached web-mode launch profile
  7. Deterministic forge-container name
  8. Launchers build argv directly

Read full pinned spec ↗

specpodman-registries-configunspecified1 requirements

No purpose recorded in this spec.

Document status: unspecified. This describes the spec’s lifecycle, not verified implementation.

Requirements (1)
  1. The user runtime reads a Tillandsias-owned registries.conf

Read full pinned spec ↗

specpodman-secrets-integrationactive8 requirements

Tillandsias SHALL use podman's native secret mechanism (`podman secret`) as the transport for credential material that must cross the host-container boundary (CA certificates, Vault unseal key, per-container Vault tokens, and the git mirror's launch-scoped AppRole material). Secrets are created at tray startup as ephemeral tmpfs-backed artifacts, mounted int

Document status: active. This describes the spec’s lifecycle, not verified implementation.

Requirements (8)
  1. Ephemeral secrets created at tray startup
  2. Secrets mounted via `--secret` flag, never bind mounts
  3. Secrets readable at `/run/secrets/<name>` inside containers
  4. Secrets hidden from `podman inspect` and process environment
  5. All secrets cleaned up on tray shutdown
  6. Secrets creation uses podman filedriver (default)
  7. Accountability logging for all secret operations
  8. CA certificate generation is ephemeral and per-session

Read full pinned spec ↗

specproject-bootstrap-readmeactive6 requirements

No purpose recorded in this spec.

Document status: active. This describes the spec’s lifecycle, not verified implementation.

Requirements (6)
  1. README MUST have two sections: FOR HUMANS and FOR ROBOTS
  2. README MUST be auto-generated from source manifests
  3. README validation ensures structure and freshness
  4. Pre-push hook auto-regenerates README
  5. Traces ledger records agent observations
  6. Bootstrap skills guide workflow

Read full pinned spec ↗

specproject-managementactive1 requirements

No purpose recorded in this spec.

Document status: active. This describes the spec’s lifecycle, not verified implementation.

Requirements (1)
  1. Project selection is stable across tray operations

Read full pinned spec ↗

specproject-summarizersactive4 requirements

No purpose recorded in this spec.

Document status: active. This describes the spec’s lifecycle, not verified implementation.

Requirements (4)
  1. Summarizers MUST parse manifest files deterministically
  2. Summarizers MUST output structured, consistent data
  3. Summarizers MUST be embeddable as shell scripts
  4. README dispatcher orchestrates all summarizers

Read full pinned spec ↗

specproxy-containeractive17 requirements

Caching HTTP/HTTPS forward proxy with a domain allowlist that mediates external traffic from forge containers. It is Alpine-based with Squid and a bounded 4GiB disk cache. HTTPS interception is deliberately exceptional: the proxy bumps only the exact GitHub release-asset CDN hostname and splices all other TLS traffic end to end.

Document status: active. This describes the spec’s lifecycle, not verified implementation.

Requirements (17)
  1. Caching HTTP/HTTPS proxy with ssl-bump MITM architecture
  2. Ephemeral CA chain (per-launch generation)
  3. Dual-port architecture
  4. Image builds bypass the proxy
  5. CA chain injection into forge containers
  6. Generous developer-focused domain allowlist
  7. SSL bump policy is step-aware and narrowly selective
  8. Release-asset caching preserves origin semantics
  9. Upstream TLS verification
  10. Proxy container image versioned and built via pipeline
  11. Proxy container lifecycle management
  12. Proxy request telemetry
  13. Privacy and identity
  14. Allowlist covers OpenCode's default egress footprint
  15. Allowlist entries follow Squid 6.x single-entry rule
  16. Forward proxy recognises `*.localhost` as enclave-internal
  17. NO_PROXY exemptions and the Node egress contract

Read full pinned spec ↗

specremote-projectsactive5 requirements

TBD - created by archiving change remote-project-clone. Update Purpose after archive.

Document status: active. This describes the spec’s lifecycle, not verified implementation.

Requirements (5)
  1. Fetch remote repository list
  2. Filter against local projects
  3. Cache remote repository list
  4. Clone remote project
  5. Loading state

Read full pinned spec ↗

specresource-metric-collectionactive1 requirements

No purpose recorded in this spec.

Document status: active. This describes the spec’s lifecycle, not verified implementation.

Requirements (1)
  1. Resource samplers collect bounded measurements

Read full pinned spec ↗

specreverse-proxy-internalactive7 requirements

Internal reverse proxy for routing enclave traffic with SSL termination, request/response logging, and caching. Sits between agents/web clients and enclave-internal services (forge containers, inference, git mirror). Certificates are ephemeral (tmpfs-based CA). Zero persistence, all state destroyed on shutdown.

Document status: active. This describes the spec’s lifecycle, not verified implementation.

Requirements (7)
  1. Enclave-internal routing
  2. SSL termination with ephemeral CA
  3. Request/response logging
  4. Caching for performance
  5. Ephemeral cache and state
  6. Access control and authentication
  7. Litmus test — reverse-proxy-internal lifecycle

Read full pinned spec ↗

specruntime-diagnosticsactive6 requirements

Capture and surface container failure diagnostics (exit code, stderr, resource exhaustion, timeout) in a structured format that enables root-cause analysis without exposing raw container internals to the user. Emphasize ephemeral failure data: diagnostics are collected only for the container's lifetime, then discarded.

Document status: active. This describes the spec’s lifecycle, not verified implementation.

Requirements (6)
  1. Capture exit code and termination signal
  2. Capture container stderr during execution
  3. Detect resource exhaustion conditions
  4. Analyze stderr for common failure patterns
  5. Ephemeral diagnostics lifecycle
  6. Litmus test — runtime diagnostics capture and lifecycle

Read full pinned spec ↗

specruntime-diagnostics-streamactive6 requirements

Live streaming of container events and diagnostics to the user's terminal when running with `--debug` flag. Events flow from container (exit, signal, resource events) → podman → tray → terminal in near-real-time. Stream is ephemeral and scoped to the current session; no persistence across runs.

Document status: active. This describes the spec’s lifecycle, not verified implementation.

Requirements (6)
  1. Stream activation on --debug flag
  2. Event structure and formatting
  3. Event filtering and control
  4. Ephemeral stream lifecycle
  5. Backpressure and flow control
  6. Litmus test — diagnostic streaming activation and lifecycle

Read full pinned spec ↗

specruntime-loggingactive17 requirements

Structured logging system with compact formatting, accountability windows for sensitive operations, and spec traceability via `@trace` links.

Document status: active. This describes the spec’s lifecycle, not verified implementation.

Requirements (17)
  1. Logging is observational only
  2. Logging transport is non-blocking and failure-tolerant
  3. Terminal log output when launched from CLI
  4. File log output always
  5. Modular log filtering via environment variable
  6. Container lifecycle logging
  7. Spec trace links in all accountability events
  8. Accountability metadata excluded from inline fields
  9. No PII in logs
  10. Proxy accountability window
  11. Enclave accountability window
  12. Git accountability window
  13. All enclave accountability windows emit real events
  14. External-tier logging
  15. Schema versioning for log entry evolution
  16. Secret rotation event logging (OBS-021)
  17. Image build event logging (OBS-022)

Read full pinned spec ↗

specsecret-rotationobsolete0 requirements

This spec used to describe tmpfs token files and `GIT_ASKPASS` delivery. That model has been superseded by `podman-secrets-integration`, which delivers the GitHub token as a podman secret mounted at `/run/secrets/tillandsias-github-token` inside the git service container.

Document status: obsolete. This describes the spec’s lifecycle, not verified implementation.

Read full pinned spec ↗

specsecrets-managementobsolete (removed in v0.3 — see `tillandsias-vault` spec)0 requirements

(This spec is obsolete. See `tillandsias-vault` for the current credential architecture.)

Document status: obsolete (removed in v0.3 — see `tillandsias-vault` spec). This describes the spec’s lifecycle, not verified implementation.

Read full pinned spec ↗

specsecurity-privacy-isolationactive5 requirements

Define the zero-tolerance security, privacy, and isolation boundaries that govern all current Tillandsias runtime paths. This spec is the top-level entrypoint for agent work that needs a current answer to: "what must never leak, escape, or silently weaken?"

Document status: active. This describes the spec’s lifecycle, not verified implementation.

Requirements (5)
  1. Zero-tolerance credential boundary
  2. Zero-tolerance runtime leakage boundary
  3. Zero-tolerance network boundary
  4. Zero-tolerance shell-wrapper boundary
  5. Security hardening defaults are immutable

Read full pinned spec ↗

specsignal-handlingactive1 requirements

No purpose recorded in this spec.

Document status: active. This describes the spec’s lifecycle, not verified implementation.

Requirements (1)
  1. Headless runtime shuts down on process signals

Read full pinned spec ↗

specsimplified-tray-uxactive promoted-from: openspec/changes/archive/2026-04-25-simplified-tray-ux/ annotation-count: 177 requirements

Replace the accreted, multi-submenu tray design with a minimal, static menu structure that mirrors the user's mental model: launch a project, optionally drop into a terminal, quit. Eliminates per-project action duplication, visible menu flicker, and the "Seedlings" agent-picker submenu (converged on OpenCode Web as the sole runtime). Pre-builds all menu item

Document status: active promoted-from: openspec/changes/archive/2026-04-25-simplified-tray-ux/ annotation-count: 17. This describes the spec’s lifecycle, not verified implementation.

Requirements (7)
  1. Startup Menu Structure
  2. Five-Stage Static Menu Structure
  3. Home Menu ([<home> ~/src/ >])
  4. Cloud Menu ([<cloud> Cloud >])
  5. GitHub Login Menu ([<key> GitHub Login])
  6. Single Container Per Project
  7. CLI Behavior Unchanged

Read full pinned spec ↗

specsingleton-guardactive4 requirements

TBD - created by archiving change singleton-instance. Update Purpose after archive.

Document status: active. This describes the spec’s lifecycle, not verified implementation.

Requirements (4)
  1. Single instance enforcement
  2. Lock file location
  3. Lock file cleanup on exit
  4. Stale lock detection

Read full pinned spec ↗

specsocket-container-orchestrationactive1 requirements

No purpose recorded in this spec.

Document status: active. This describes the spec’s lifecycle, not verified implementation.

Requirements (1)
  1. Socket-based orchestration uses explicit readiness

Read full pinned spec ↗

specspec-trace-coverage-thresholdactive1 requirements

No purpose recorded in this spec.

Document status: active. This describes the spec’s lifecycle, not verified implementation.

Requirements (1)
  1. Trace coverage checks report threshold status

Read full pinned spec ↗

specspec-traceabilityactive7 requirements

Enable bidirectional navigation between code and specs through `@trace` annotations and structured logging fields. When a spec is applied, the implementation SHALL reference the spec with `// @trace spec:<name>` comments in code and `spec = "<name>"` fields in log events. This creates a queryable graph: code → spec, spec → code, log → code → spec. Developers

Document status: active. This describes the spec’s lifecycle, not verified implementation.

Requirements (7)
  1. Code annotations link to specs and knowledge
  2. Structured spec field in tracing logs
  3. CRDT-like reference semantics
  4. Specs SHOULD prefer non-DRAFT cheatsheets in Sources of Truth
  5. @cheatsheet path annotation in code is a peer of @trace spec:
  6. Litmus chain references are explicit in updated specs
  7. Rust source relevance queries gate opt-in litmus tests

Read full pinned spec ↗

specsquid-proxy-integrationactive1 requirements

No purpose recorded in this spec.

Document status: active. This describes the spec’s lifecycle, not verified implementation.

Requirements (1)
  1. Runtime traffic uses the Squid proxy policy

Read full pinned spec ↗

specstructured-query-languageactive1 requirements

No purpose recorded in this spec.

Document status: active. This describes the spec’s lifecycle, not verified implementation.

Requirements (1)
  1. Trace queries use structured predicates

Read full pinned spec ↗

specsubdomain-naming-flipactive promoted-from: openspec/changes/archive/2026-04-26-subdomain-naming-flip/ annotation-count: 53 requirements

Reorganize subdomain URL structure from `<project>.opencode.localhost` to `opencode.<project>.localhost`, enabling hierarchical service discovery (`*.opencode.localhost`, `*.flutter.localhost`, etc.) and simplifying browser-MCP allowlists.

Document status: active promoted-from: openspec/changes/archive/2026-04-26-subdomain-naming-flip/ annotation-count: 5. This describes the spec’s lifecycle, not verified implementation.

Requirements (3)
  1. Subdomain ordering inversion
  2. Router configuration update
  3. Loopback resolution unchanged

Read full pinned spec ↗

specsubdomain-routing-via-reverse-proxyactive promoted-from: openspec/changes/archive/2026-04-25-subdomain-routing-via-reverse-proxy/ annotation-count: 116 requirements

Enable stable, port-agnostic URLs for all web services spawned in the enclave via a reverse-proxy listener that maps `<service>.<project>.localhost` hostnames to internal container ports, while maintaining RFC 6761 loopback-only binding.

Document status: active promoted-from: openspec/changes/archive/2026-04-25-subdomain-routing-via-reverse-proxy/ annotation-count: 11. This describes the spec’s lifecycle, not verified implementation.

Requirements (6)
  1. Reverse-proxy container and binding
  2. Dynamic routing table from Caddyfile
  3. Forward-proxy integration
  4. No container port publication
  5. Caddyfile reload via the in-container reload script
  6. Agent instructions for service binding

Read full pinned spec ↗

spectestingactive1 requirements

No purpose recorded in this spec.

Document status: active. This describes the spec’s lifecycle, not verified implementation.

Requirements (1)
  1. Local CI maps checks to traceable specs

Read full pinned spec ↗

spectillandsias-vaultactive phase: 6.610 requirements

Run a HashiCorp Vault container as the default and ONLY Linux secrets backend for Tillandsias. Vault stores long-lived external credentials, starting with the GitHub token at `secret/github/token`, and issues short-lived per-container tokens scoped by fine-grained ACL policies.

Document status: active phase: 6.6. This describes the spec’s lifecycle, not verified implementation.

Requirements (10)
  1. Vault container runs inside the secrets boundary with persistent storage
  2. Auto-unseal key securely stored in host native keychain with versioning
  3. Vault is the ONLY secret store (Legacy Fallback Removed)
  4. Policy taxonomy enforces least privilege per container kind
  5. Per-container Vault authentication is short-lived and revocable
  6. Forge Vault access is optional and provider-scoped
  7. Default-image Vault requests verify TLS and keep live tokens off argv
  8. The Vault store and its unseal material survive every destructive reset; only uninstall removes them
  9. Every harness credential document has a Vault home and is restored without a prompt
  10. Codex device sign-in offers a code-free terminal QR

Read full pinned spec ↗

spectools-overlay-fast-reuseobsolete0 requirements

No purpose recorded in this spec.

Document status: obsolete. This describes the spec’s lifecycle, not verified implementation.

Read full pinned spec ↗

spectransparent-https-cachingactive4 requirements

No purpose recorded in this spec.

Document status: active. This describes the spec’s lifecycle, not verified implementation.

Requirements (4)
  1. HTTPS cache interception uses generated CA material
  2. The intermediate CA private key is never world-readable
  3. Runtime containers trust the Tillandsias proxy CA
  4. HTTPS interception is exact-host, step-aware, and fail-closed

Read full pinned spec ↗

spectransparent-mode-detectionactive1 requirements

No purpose recorded in this spec.

Document status: active. This describes the spec’s lifecycle, not verified implementation.

Requirements (1)
  1. Launcher mode is detected from invocation context

Read full pinned spec ↗

spectray-appactive5 requirements

No purpose recorded in this spec.

Document status: active. This describes the spec’s lifecycle, not verified implementation.

Requirements (5)
  1. First-launch readiness feedback
  2. Native tray behavior by platform wrapper
  3. Seedlings submenu exposes OpenCode Web
  4. Per-project Stop action for running web containers
  5. Attach Here branches on selected agent

Read full pinned spec ↗

spectray-cli-coexistenceactive4 requirements

Contract for running the Tillandsias tray icon concurrently with CLI modes, detecting whether the host has a graphical session, and tolerating broken stdout/stderr without taking the process down.

Document status: active. This describes the spec’s lifecycle, not verified implementation.

Requirements (4)
  1. Detect graphical session for tray-aware CLI
  2. CLI modes spawn the tray when a graphical session is available
  3. Tray remains running after CLI session ends
  4. Broken stdout/stderr does not terminate the process

Read full pinned spec ↗

spectray-concurrencyactive1 requirements

No purpose recorded in this spec.

Document status: active. This describes the spec’s lifecycle, not verified implementation.

Requirements (1)
  1. Tray operations are concurrency-safe

Read full pinned spec ↗

spectray-host-control-socketactive promoted-from: openspec/changes/archive/2026-04-26-tray-host-control-socket/ annotation-count: 205 requirements

Establish a Unix-socket control plane for out-of-band communication between the tray process and tray-spawned containers (router, browser MCP, future control-plane consumers). Provides a single reviewable wire protocol, framed message format, and capability-based routing mechanism instead of ad-hoc communication channels for each new feature.

Document status: active promoted-from: openspec/changes/archive/2026-04-26-tray-host-control-socket/ annotation-count: 20. This describes the spec’s lifecycle, not verified implementation.

Requirements (5)
  1. Socket Creation and Lifecycle
  2. Wire Format and Framing
  3. Message Types and Capability-Based Routing
  4. Error Handling and Reliability
  5. Login completion is a notification, not a poll

Read full pinned spec ↗

spectray-icon-lifecycleactive5 requirements

No purpose recorded in this spec.

Document status: active. This describes the spec’s lifecycle, not verified implementation.

Requirements (5)
  1. Five-state tray icon lifecycle
  2. Build-time tray icon rendering for all 5 states
  3. compute_icon_state reflects full lifecycle
  4. TrayIconState enum variants renamed
  5. Initial tray state is Pup

Read full pinned spec ↗

spectray-menuactive1 requirements

No purpose recorded in this spec.

Document status: active. This describes the spec’s lifecycle, not verified implementation.

Requirements (1)
  1. Tray menu reflects current runtime state

Read full pinned spec ↗

spectray-minimal-uxactive0 requirements

No purpose recorded in this spec.

Document status: active. This describes the spec’s lifecycle, not verified implementation.

Read full pinned spec ↗

spectray-progress-and-icon-statesactive promoted-from: openspec/changes/archive/2026-04-26-tray-progress-and-icon-states/ annotation-count: 55 requirements

Replace the comma-joined, fragmentary status chip with an additive, emoji-based progress indicator that grows as each subsystem comes online. Give the user at-a-glance visibility into "checklist in progress, X done so far" and add an `Unhealthy` stage that collapses the menu when any subsystem fails instead of showing a disabled menu full of items.

Document status: active promoted-from: openspec/changes/archive/2026-04-26-tray-progress-and-icon-states/ annotation-count: 5. This describes the spec’s lifecycle, not verified implementation.

Requirements (5)
  1. Additive Progress Chip
  2. Unhealthy Stage
  3. Cold-Start Baseline
  4. Locale Keys for Chip
  5. Sign-In Label Rename

Read full pinned spec ↗

spectray-projects-renameactive promoted-from: openspec/changes/archive/2026-04-27-tray-projects-rename/ annotation-count: 55 requirements

Improve project submenu labels with explicit source cues and temporarily disable the broken i18n menu until translation infrastructure is repaired. Rename generic "Projects" and "Remote Projects" labels to emoji-prefixed, filesystem-aware labels that tell users at a glance where each list comes from.

Document status: active promoted-from: openspec/changes/archive/2026-04-27-tray-projects-rename/ annotation-count: 5. This describes the spec’s lifecycle, not verified implementation.

Requirements (5)
  1. Project List Label Updates
  2. Language Submenu Removal
  3. Code Hygiene — Dormant Infrastructure
  4. Tombstone Annotation
  5. Documentation Update

Read full pinned spec ↗

spectray-subprocess-managementactive1 requirements

No purpose recorded in this spec.

Document status: active. This describes the spec’s lifecycle, not verified implementation.

Requirements (1)
  1. Tray-owned subprocesses are tracked and cleaned up

Read full pinned spec ↗

spectray-ui-integrationactive1 requirements

No purpose recorded in this spec.

Document status: active. This describes the spec’s lifecycle, not verified implementation.

Requirements (1)
  1. Native tray integration matches platform expectations

Read full pinned spec ↗

spectray-uxstatus: active6 requirements

Define the minimalistic tray UX flow for Tillandsias, showing only essential elements at each stage of the application lifecycle.

Document status: status: active. This describes the spec’s lifecycle, not verified implementation.

Requirements (6)
  1. UX curation governance — Tlatoāni approval is MANDATORY for every UX change
  2. First-launch minimal tray
  3. Dynamic environment verification status
  4. Post-initialization menu items
  5. Per-project action buttons
  6. Stale container cleanup

Read full pinned spec ↗

specupdate-systemobsolete1 requirements

No purpose recorded in this spec.

Document status: obsolete. This describes the spec’s lifecycle, not verified implementation.

Requirements (1)
  1. Platform-appropriate artifact selection

Read full pinned spec ↗

specuser-runtime-lifecycleactive2 requirements

No purpose recorded in this spec.

Document status: active. This describes the spec’s lifecycle, not verified implementation.

Requirements (2)
  1. User runtime images are reproducible release artifacts
  2. Host prerequisites are explicit

Read full pinned spec ↗

specversioningstatus: active8 requirements

Define the artifact version scheme and its single source of truth. The scheme is epoch-anchored CalVer (operator ruling 2026-08-31): versions encode WHEN an artifact was built, never WHAT milestone it contains — milestone membership lives in the plan ledger (`desired_release` ordered buckets) and the README release row's milestone clause.

Document status: status: active. This describes the spec’s lifecycle, not verified implementation.

Requirements (8)
  1. Epoch-anchored version scheme
  2. VERSION file as source of truth
  3. Monotonic version increments
  4. Milestones are decoupled from versions
  5. Cargo semver derivation
  6. Immutable version tags
  7. Rolling channel tags
  8. Automated version bump script

Read full pinned spec ↗

specvm-idiomatic-layeractive phase: 25 requirements

Define the contract for the `tillandsias-vm-layer` crate: a single Rust API that abstracts host↔VM operations behind a `VmRuntime` trait with two backends. On Windows, the `WslRuntime` backend drives WSL2 distros via the `wsl.exe` command-line and (where available) the `Win32_HCS_HostComputeSystem` COM surface. On macOS, the `VzRuntime` backend drives Virtua

Document status: active phase: 2. This describes the spec’s lifecycle, not verified implementation.

Requirements (5)
  1. `VmRuntime` trait is the only public surface
  2. `WslRuntime` is the only place `wsl.exe` is invoked
  3. `VzRuntime` is the only place Virtualization.framework symbols are touched
  4. `exec` propagates stdio and exit code with TTY support
  5. `wait_ready` is event-driven, not polling

Read full pinned spec ↗

specvm-provisioning-lifecycleactive6 requirements

Specify the first-run provisioning, steady-state startup, and shutdown contract for the Fedora 44 Core VM that hosts the in-VM `tillandsias-headless` and the podman enclave on non-Linux hosts. On first launch, the host downloads the Fedora 44 rootfs from the official Fedora mirror and the matching `tillandsias-linux-x86_64` binary from our GitHub release. Th

Document status: active. This describes the spec’s lifecycle, not verified implementation.

Requirements (6)
  1. First-run provisioning downloads Fedora rootfs and tillandsias binary
  2. Provisioning surfaces as a single condensed status line
  3. Provisioning is idempotent
  4. Tray exit triggers graceful drain
  5. Provisioning log is captured for diagnostics
  6. Launch failures are graceful — bounded, classified, actionable

Read full pinned spec ↗

specvsock-exec-authzactive0 requirements

No purpose recorded in this spec.

Document status: active. This describes the spec’s lifecycle, not verified implementation.

Read full pinned spec ↗

specvsock-transportactive phase: 27 requirements

Extend `tillandsias-control-wire` with a virtio-vsock transport so the Windows and macOS host-side trays can talk to the in-VM `tillandsias-headless` process. Today the control wire only listens on a Unix domain socket at `$XDG_RUNTIME_DIR/tillandsias/control.sock`; that path is Linux-only and does not survive an OS-VM boundary. vsock is the host↔guest IPC p

Document status: active phase: 2. This describes the spec’s lifecycle, not verified implementation.

Requirements (7)
  1. CID allocation contract
  2. Stable port `42420`
  3. Framing and handshake are identical to the Unix-socket transport
  4. New `ControlMessage` variants for VM lifecycle and remote enumeration
  5. Error model surfaces transport failures explicitly
  6. Silent guest work remains live through capability-gated heartbeats
  7. Platform transport matrix — the host side differs, the guest side does not

Read full pinned spec ↗

specweb-imagestatus: active6 requirements

TBD - created by archiving change web-image. Update Purpose after archive.

Document status: status: active. This describes the spec’s lifecycle, not verified implementation.

Requirements (6)
  1. Minimal Alpine-based container image
  2. Static file serving via busybox httpd
  3. Port 8080 exposed
  4. Entrypoint displays banner and serving URL
  5. Graceful shutdown
  6. Document root at /var/www

Read full pinned spec ↗

specwindow-lifecycle-concurrencyactive1 requirements

No purpose recorded in this spec.

Document status: active. This describes the spec’s lifecycle, not verified implementation.

Requirements (1)
  1. Window lifecycle updates are token-scoped

Read full pinned spec ↗

specwindows-cross-buildstatus: obsolete0 requirements

No purpose recorded in this spec.

Document status: status: obsolete. This describes the spec’s lifecycle, not verified implementation.

Read full pinned spec ↗

specwindows-event-loggingstatus: active platform: windows-only build-status: active (reactivated for the native Win32 tray, 2026-07-18) derived-from: operator directive 2026-07-18 (relay ALL INFO/WARN/ERROR) + archive windows-etw-logging-completed-2026-04-27 last-updated: 2026-07-186 requirements

Defines Windows-specific diagnostic relay to the Windows Event Log. The Tillandsias tray writes every INFO, WARN, and ERROR tracing event to the Application Event Log where a power user can discover them in Event Viewer > Windows Logs > Application > Source "Tillandsias" — including provisioning failures on machines where the tray UI itself is failing (the 2

Document status: status: active platform: windows-only build-status: active (reactivated for the native Win32 tray, 2026-07-18) derived-from: operator directive 2026-07-18 (relay ALL INFO/WARN/ERROR) + archive windows-etw-logging-completed-2026-04-27 last-updated: 2026-07-18. This describes the spec’s lifecycle, not verified implementation.

Requirements (6)
  1. Windows Event Log Integration
  2. Event Type Mapping
  3. Metadata Preservation in Event Log
  4. Graceful Degradation
  5. Layer Implementation
  6. Cross-Platform Cost

Read full pinned spec ↗

specwindows-native-trayactive phase: 47 requirements

Define the contract for the Windows-native tray binary `tillandsias-windows-tray` (produces `tillandsias-tray.exe`). The binary runs on the host as an OS-native Win32 application, registers a NotifyIcon (system tray icon) via the `Shell_NotifyIcon` API surface (wrapped by the `tray-icon` crate, with raw `windows-rs` fallbacks for parity with the Linux tray),

Document status: active phase: 4. This describes the spec’s lifecycle, not verified implementation.

Requirements (7)
  1. Win32 NotifyIcon is the only tray surface
  2. Menu items match the host-shell parity contract
  3. WSL2 distro lifecycle is owned by this binary
  4. WSLg passthrough enables in-VM Chromium on supported hosts
  5. Terminal attach routes through Windows Terminal via `vm-exec`
  6. `--diagnose` CLI mode emits a stable bundled health report
  7. The taskbar approval follows the tray across executable paths

Read full pinned spec ↗

specwsl-daemon-orchestrationactive0 requirements

No purpose recorded in this spec.

Document status: active. This describes the spec’s lifecycle, not verified implementation.

Read full pinned spec ↗

specwsl-runtimeactive0 requirements

No purpose recorded in this spec.

Document status: active. This describes the spec’s lifecycle, not verified implementation.

Read full pinned spec ↗

speczen-default-with-ollama-analysis-poolstatus: active promoted-from: openspec/changes/archive/2026-04-25-zen-default-with-ollama-analysis-pool/ annotation-count: 33 requirements

Split agent responsibilities between cloud-based Zen models (for tool-calling and plan execution) and local ollama models (for analysis-only tasks like summarization, error classification, and commit message generation). Default to Zen; reserve ollama for privacy-critical analysis workloads.

Document status: status: active promoted-from: openspec/changes/archive/2026-04-25-zen-default-with-ollama-analysis-pool/ annotation-count: 3. This describes the spec’s lifecycle, not verified implementation.

Requirements (3)
  1. Default model routing
  2. Tier-tagged tool-capable model pre-pulls
  3. Squid proxy workaround for ollama manifests

Read full pinned spec ↗

Completed change checklists 115

Every recorded task was checked before archive. Current runtime behavior still needs separate evidence.

changeAuto updaterChecklist complete34 checked · 0 open

Without automatic updates, users must manually check GitHub Releases, download the correct platform artifact, verify its integrity, and replace the running binary. Most users will never do this. They will run outdated versions with known bugs and missing security patches indefinitely. For a privacy-focused application, running stale software is a direct secu

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1.1 Add `tauri-plugin-updater` dependency to `src-tauri/Cargo.toml`
  • ☑ 1.2 Register the updater plugin in the Tauri builder setup (`tauri::Builder::default().plugin(tauri_plugin_updater::Builder::new().build())`)
  • ☑ 1.3 Generate Tauri signing keypair using `tauri signer generate -w ~/.tauri/tillandsias.key` and record the public key
  • ☑ 2.1 Add `plugins.updater` section to `src-tauri/tauri.conf.json` with the GitHub Releases endpoint URL and the Ed25519 public key
  • ☑ 2.2 Configure platform-specific artifact targets in the updater config (AppImage for Linux, .app for macOS, .exe for Windows)
  • ☑ 2.3 Add updater permissions to `src-tauri/capabilities/` if required by Tauri v2 permission model
  • ☑ 3.1 Add `TAURI_SIGNING_PRIVATE_KEY` and `TAURI_SIGNING_PRIVATE_KEY_PASSWORD` as GitHub repository secrets
  • ☑ 3.2 Update `.github/workflows/release.yml` to expose signing key secrets as environment variables during the Tauri build step
  • ☑ 3.3 Verify that `tauri build` produces signed update bundles when the signing key environment variables are set
  • ☑ 3.4 Verify that the CI build fails when the signing secrets are missing (preventing unsigned releases)
  • ☑ 4.1 Implement background update check as an async tokio task that runs after a 5-second post-startup delay
  • ☑ 4.2 Implement periodic update check using a configurable interval (default 6 hours) via a tokio timer
  • ☑ 4.3 Read `check_interval_hours` and `check_on_launch` from the global config at `~/.config/tillandsias/config.toml`
  • ☑ 4.4 Ensure the update check task never blocks the main event loop or the tray UI thread
  • ☑ 5.1 Add an "Update available (vX.Y.Z)" dynamic menu item to the tray menu when a new version is detected
  • ☑ 5.2 Ensure the update menu item persists across tray menu rebuilds (container state changes, project detection events)
  • ☑ 5.3 Fire a platform-native system notification (toast) on first detection of a new version during a session
  • ☑ 5.4 Add a progress indicator to the tray menu during update download (e.g., "Downloading update..." replacing the update menu item)
  • ☑ 6.1 Verify that the Tauri updater plugin enforces Ed25519 signature verification before applying any update (this is default behavior — confirm it is not disabled)
  • ☑ 6.2 Test that an update bundle with a mismatched signature is rejected and no binary replacement occurs
  • ☑ 6.3 Test that an update bundle with no signature is rejected
  • ☑ 6.4 Confirm the public key in `tauri.conf.json` matches the private key used in CI
  • ☑ 7.1 Wire the update installation trigger to the existing graceful shutdown sequence: stop all managed containers (SIGTERM → 10s grace → SIGKILL), flush state, then allow binary replacement and relaunch
  • ☑ 7.2 Disable the update menu item during shutdown and restart to prevent duplicate actions
  • ☑ 7.3 Verify the application relaunches with the new version after binary replacement and the tray icon reappears
  • ☑ 8.1 Ensure update check failures (network timeout, DNS failure, HTTP errors) are caught and logged without surfacing error dialogs to the user
  • ☑ 8.2 Handle GitHub API rate limiting (HTTP 403/429) by silently deferring the check to the next scheduled interval
  • ☑ 8.3 Handle mid-download network loss by aborting the download and reverting the tray menu to "Update available" so the user can retry
  • ☑ 8.4 Verify that the application starts and runs normally with no network connectivity (no crash, no error dialog)
  • ☑ 9.1 Publish a test release (e.g., `v0.1.1-rc.1`) and verify the running app detects it as an available update
  • ☑ 9.2 Approve the update via the tray menu and verify the download, signature verification, and binary replacement complete successfully
  • ☑ 9.3 Verify the application restarts with the new version after update
  • ☑ 9.4 Test the full flow on all three platforms (Linux AppImage, macOS .dmg, Windows .exe)
  • ☑ 9.5 Test offline behavior: disconnect network, launch app, verify no crash or error dialog

Read source record ↗

changeCli modeChecklist complete11 checked · 0 open

Tillandsias currently only works as a system tray application. Users who prefer the terminal or want to script container launches have no way to attach to a project from the command line. Adding a CLI mode lets users run `tillandsias ~/src/my-project` to launch an interactive container directly, with user-friendly output instead of raw tracing logs.

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1.1 Create `src-tauri/src/cli.rs` with `CliMode` enum (`Tray`, `Attach { path, image, debug }`)
  • ☑ 1.2 Implement `parse()` function: no args = Tray, positional path = Attach, `--help` prints usage
  • ☑ 1.3 Support `--image <name>` flag (default "forge") and `--debug` flag
  • ☑ 2.1 Create `src-tauri/src/runner.rs` with `run()` function
  • ☑ 2.2 Implement image check/build with user-friendly println! output
  • ☑ 2.3 Build podman run args with all security flags, mounts, port range
  • ☑ 2.4 Execute podman with inherited stdio (`.status()`) and show exit message
  • ☑ 3.1 Update `src-tauri/src/main.rs` to parse CLI args at top of main()
  • ☑ 3.2 Branch: `CliMode::Attach` calls runner::run() and returns, `CliMode::Tray` proceeds with tray setup
  • ☑ 4.1 Create `run-tillandsia.sh` at project root
  • ☑ 5.1 Build workspace with `cargo build --workspace`

Read source record ↗

changeDev build scriptChecklist complete16 checked · 0 open

Developers need a single `./build.sh` entry point that handles the full local development lifecycle — build, install, clean, remove, wipe — without knowing about toolbox internals, Tauri CLI setup, or system dependency installation. The script must auto-create the tillandsias toolbox if it doesn't exist, so a fresh checkout on any Fedora Silverblue machine w

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1.1 Create `build.sh` at project root with flag parsing for: (none), --release, --test, --check, --clean, --install, --remove, --wipe, --toolbox-reset, --help
  • ☑ 1.2 Implement toolbox auto-detection and creation: check `toolbox list`, create if missing, install system deps (gtk3-devel, webkit2gtk4.1-devel, libappindicator-gtk3-devel, librsvg2-devel, openssl-devel, pkg-config, gcc)
  • ☑ 1.3 Implement tauri-cli auto-installation inside toolbox if `cargo tauri --version` fails
  • ☑ 1.4 Implement default build: `toolbox run -c tillandsias cargo build --workspace`
  • ☑ 1.5 Implement --release: `toolbox run -c tillandsias cargo tauri build`
  • ☑ 1.6 Implement --test: `toolbox run -c tillandsias cargo test --workspace`
  • ☑ 1.7 Implement --check: `toolbox run -c tillandsias cargo check --workspace`
  • ☑ 1.8 Implement --clean: `toolbox run -c tillandsias cargo clean`
  • ☑ 1.9 Implement --install: release build + `cp` binary to `~/.local/bin/tillandsias`
  • ☑ 1.10 Implement --remove: `rm ~/.local/bin/tillandsias`
  • ☑ 1.11 Implement --wipe: remove `target/`, `~/.cache/tillandsias/`
  • ☑ 1.12 Implement --toolbox-reset: `toolbox rm -f tillandsias` then recreate
  • ☑ 1.13 Support flag combinations (e.g., `--clean --release --install`)
  • ☑ 1.14 Make script executable and test default build succeeds
  • ☑ 2.1 Update CLAUDE.md with build.sh usage
  • ☑ 2.2 Update README.md build section to reference build.sh

Read source record ↗

changeFix opencode configChecklist complete7 checked · 0 open

The default container image ships an `opencode.json` that defines a custom provider and model (`opencode/big-pickle`) which does not exist in any released version of OpenCode. When the container starts, OpenCode attempts to resolve this model, fails with "agent coder not found", and exits immediately. This makes the container completely unusable on first lau

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1.1 Remove `$schema`, `provider`, and `model` fields from `images/default/opencode.json`
  • ☑ 1.2 Keep only `tools` and `permissions` declarations in the config
  • ☑ 2.1 Update `images/default/entrypoint.sh` to attempt launching OpenCode and fall back to bash if it fails
  • ☑ 2.2 Add a diagnostic message to the welcome banner when falling back to bash
  • ☑ 2.3 Ensure `exec opencode` is called without arguments that reference broken config
  • ☑ 3.1 Add graceful fallback requirement to `default-image` spec (scenario: OpenCode fails to start)
  • ☑ 3.2 Add config-only-tools-and-permissions scenario to `default-image` spec

Read source record ↗

changeNix builder toolboxChecklist complete19 checked · 0 open

Container images built with `podman build` have no staleness detection — once cached, they're never rebuilt even when the Containerfile, entrypoint, or opencode config changes. Users must manually `podman rmi` to force a rebuild. This is broken UX.

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1.1 Create `scripts/ensure-builder.sh` — checks if `tillandsias-builder` toolbox exists, creates it with Fedora + Nix if not. Nix installed in single-user mode with flakes enabled.
  • ☑ 1.2 Test builder toolbox creation: `scripts/ensure-builder.sh` creates toolbox and `nix --version` works inside it
  • ☑ 2.1 Create `flake.nix` at project root with two image outputs: `forge-image` and `web-image`
  • ☑ 2.2 `forge-image`: uses `dockerTools.buildLayeredImage` with bash, git, gh, curl, jq, ripgrep, nodejs, npm, nix, OpenCode binary, entrypoint.sh, opencode.json
  • ☑ 2.3 `web-image`: uses `dockerTools.buildLayeredImage` with minimal alpine-like contents + busybox httpd + entrypoint.sh
  • ☑ 2.4 Generate `flake.lock` by running `nix flake lock` inside builder toolbox
  • ☑ 2.5 Test: `toolbox run -c tillandsias-builder nix build .#forge-image` produces a loadable tarball
  • ☑ 3.1 Create `scripts/build-image.sh` — takes image name (forge|web), ensures builder toolbox, runs nix build inside it, loads tarball via `podman load`, tags image
  • ☑ 3.2 Staleness detection: store last build hash, skip if flake.lock + sources unchanged
  • ☑ 3.3 Support `--force` flag to bypass staleness check
  • ☑ 3.4 User-friendly output: progress messages, build time, image size
  • ☑ 4.1 Update `build.sh` to call `scripts/build-image.sh` during `--install`
  • ☑ 4.2 Update handlers.rs to call build-image.sh (via shell) instead of podman build when image is stale
  • ☑ 4.3 Update runner.rs CLI mode to use build-image.sh for image management
  • ☑ 4.4 Add `podman load` support to PodmanClient (load_image method)
  • ☑ 5.1 Keep Containerfiles as reference documentation (not primary build path)
  • ☑ 5.2 Add flake.lock to git (reproducibility)
  • ☑ 5.3 Add .nix-output/ to .gitignore
  • ☑ 5.4 Update CLAUDE.md with builder toolbox info and image build commands

Read source record ↗

changeRemove terminal detectionChecklist complete5 checked · 0 open

Terminal emulator detection is brittle, platform-specific, and a non-goal. Supporting ptyxis, gnome-terminal, konsole, alacritty, kitty, foot, xterm, etc. across Linux/macOS/Windows is impossible to maintain. The container has bash. `podman run -it` provides a TTY. That's all we need.

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1.1 Remove `detect_terminal()` function from handlers.rs
  • ☑ 1.2 Remove `spawn_terminal()` function from handlers.rs
  • ☑ 1.3 Remove `which_sync()` helper from handlers.rs
  • ☑ 1.4 Update `handle_attach_here` to spawn `podman run -it --rm` directly via `std::process::Command`
  • ☑ 1.5 Build and verify compilation

Read source record ↗

changeSrc attachment pointChecklist complete4 checked · 0 open

Users need a way to start working even before any projects exist. The `~/src` directory itself should always appear as an "Attach Here" point — this is where users create new projects, clone repos, and start from scratch. Without this, a fresh install shows an empty menu with no actionable entry.

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1.1 Add permanent "~/src/ — Attach Here" entry at the top of the tray menu in menu.rs, before project submenus
  • ☑ 1.2 Use the first watch path from config as the src root (default ~/src)
  • ☑ 1.3 Clicking it dispatches MenuCommand::AttachHere with the watch path root
  • ☑ 2.1 Build and verify src/ entry appears in menu even with no projects

Read source record ↗

changeTillandsias bootstrapChecklist complete69 checked · 0 open

There is no lightweight, opinionated way for non-technical users to go from an idea to a running application without understanding containers, runtimes, git, or cloud infrastructure. Existing tools either expose too much complexity (Docker Desktop, VS Code Dev Containers) or lock users into proprietary platforms (Replit, GitHub Codespaces). Tillandsias fills

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1.1 Initialize Rust workspace with `Cargo.toml` (resolver 2024, edition 2024) and four crate directories: `crates/tillandsias-core/`, `crates/tillandsias-scanner/`, `crates/tillandsias-podman/`, `crates/tillandsias-tray/`
  • ☑ 1.2 Set up `tillandsias-core` crate with shared types: `AppEvent` enum, `Project` struct, `ContainerState` enum, `TrayState` struct, config types with serde derives
  • ☑ 1.3 Set up `tillandsias-tray` as the binary crate with Tauri v2 dependencies (`tauri` with `tray-icon` feature, `tauri-plugin-shell`, `tokio`, `serde`)
  • ☑ 1.4 Create `src-tauri/` Tauri build context with `tauri.conf.json` (tray-only, `windows: []`, identifier `com.tillandsias.tray`), `build.rs`, and minimal `icons/` directory
  • ☑ 1.5 Add workspace-level dependencies in root `Cargo.toml`: `tokio`, `serde`, `postcard`, `notify`, `toml`, `tracing`
  • ☑ 1.6 Verify the workspace builds and `cargo test --workspace` passes with zero tests
  • ☑ 2.1 Define the `AppEvent` enum in `tillandsias-core`: `FilesystemChange`, `ContainerStateChange`, `MenuAction`, `Shutdown`
  • ☑ 2.2 Define `Project` struct: name, path, detected type (Node/Rust/Python/Go/Unknown), artifact status (has Containerfile, has runtime config), assigned genus
  • ☑ 2.3 Define `ContainerInfo` struct: name, project, genus, state (Creating/Running/Stopping/Stopped), assigned port range
  • ☑ 2.4 Define `TrayState` struct: `Vec<Project>`, `Vec<ContainerInfo>`, `PlatformInfo`
  • ☑ 2.5 Implement two-level config loading: global `~/.config/tillandsias/config.toml` (platform-aware path via `dirs` crate) merged with per-project `.tillandsias/config.toml`, falling back to built-in defaults
  • ☑ 2.6 Define config structs with serde: `GlobalConfig` (scanner watch paths, default image, port range, security flags), `ProjectConfig` (image override, port range override, custom mounts)
  • ☑ 2.7 Add `postcard` serialization for internal state snapshots (serialize/deserialize round-trip tests)
  • ☑ 2.8 Write unit tests for config merging (global-only, per-project override, no config defaults, security flags cannot be weakened)
  • ☑ 3.1 Define `TillandsiaGenus` enum with 8 MVP variants: Aeranthos, Ionantha, Xerographica, CaputMedusae, Bulbosa, Tectorum, Stricta, Usneoides
  • ☑ 3.2 Define `PlantLifecycle` enum: Bud, Bloom, Dried, Pup — mapped to container states (Creating→Bud, Running→Bloom, Stopping→Dried, Rebuilding→Pup)
  • ☑ 3.3 Implement genus pool allocator: round-robin assignment, tracks which genera are in use per project, returns different genus for concurrent environments of the same project
  • ☑ 3.4 Create `assets/icons/` directory structure: `<genus>/<state>.svg` (8 genera × 4 states = 32 SVG files)
  • ☑ 3.5 Generate initial abstract SVG icons: simple geometric tillandsia silhouettes with distinct shapes per genus and color variants per lifecycle state (bud=green, bloom=colorful, dried=brown, pup=light green)
  • ☑ 3.6 Implement icon loader that maps `(TillandsiaGenus, PlantLifecycle)` → SVG bytes, embedded as compile-time assets via `include_bytes!`
  • ☑ 3.7 Write unit tests for genus allocation (round-robin, no duplicate genera for same project, pool exhaustion behavior)
  • ☑ 4.1 Create `tillandsias-scanner` crate with `notify` dependency and `tokio` channels
  • ☑ 4.2 Implement `ScannerConfig`: watch paths (default `~/src`), debounce duration (default 2000ms), max depth (2)
  • ☑ 4.3 Implement async watcher setup using `notify::RecommendedWatcher` with `tokio::sync::mpsc` bridge — OS-native events (inotify/kqueue/ReadDirectoryChangesW), zero CPU when idle
  • ☑ 4.4 Implement debounce layer: accumulate filesystem events over the configurable window, batch into a single `ProjectChange` event per affected project
  • ☑ 4.5 Implement project detection heuristics (priority order): `.tillandsias/config.toml` → `Containerfile`/`Dockerfile` → `package.json`/`Cargo.toml`/`pyproject.toml`/`go.mod`/`flake.nix` → generic non-empty directory
  • ☑ 4.6 Implement artifact presence detection: scan for `Containerfile`, `Dockerfile`, `flake.nix`, `.tillandsias/config.toml` with `[runtime]` section
  • ☑ 4.7 Ensure scanner runs as a low-priority tokio task that emits `AppEvent::FilesystemChange` without blocking the main loop
  • ☑ 4.8 Write unit tests for project detection heuristics (each marker type, priority ordering, empty directories)
  • ☑ 4.9 Write integration test: create temp directory, add/remove project markers, verify events are emitted after debounce
  • ☑ 5.1 Create `tillandsias-podman` crate with `tokio::process` for async command execution
  • ☑ 5.2 Implement `PodmanClient` struct with async methods: `start_container`, `stop_container`, `destroy_container`, `inspect_container`, `list_containers`, `image_exists`, `pull_image`
  • ☑ 5.3 All podman CLI calls MUST use `--format json` for output parsing and `tokio::process::Command` for non-blocking execution
  • ☑ 5.4 Implement security-hardened container launch: assemble podman run arguments with non-negotiable flags (`--rm`, `--cap-drop=ALL`, `--security-opt=no-new-privileges`, `--userns=keep-id`, `--security-opt=label=disable`)
  • ☑ 5.5 Implement GPU passthrough detection: scan for NVIDIA devices (`/dev/nvidia*`) and AMD ROCm devices (`/dev/kfd`, `/dev/dri/renderD*`), append `--device=` flags when found, silent when absent
  • ☑ 5.6 Implement volume mount assembly: project dir → container workspace (rw), cache dir → container cache, shared Nix cache, plus any per-project custom mounts
  • ☑ 5.7 Implement container naming with tillandsia namespace: `tillandsias-<project>-<genus>`
  • ☑ 5.8 Implement port range allocation: default 3000-3099, auto-increment for concurrent environments (3100-3199, etc.), per-project override support
  • ☑ 5.9 Implement event-driven container status via `podman events --format json` as long-running async subprocess feeding `AppEvent::ContainerStateChange`
  • ☑ 5.10 Implement exponential backoff fallback for when `podman events` is unavailable (Podman Machine on macOS/Windows): start at 1s, double to 30s max, reconnect on recovery, NEVER fixed-interval polling
  • ☑ 5.11 Implement Podman Machine detection for macOS/Windows: check `podman machine list`, surface non-technical message if not running
  • ☑ 5.12 Implement graceful stop: SIGTERM → 10s grace period → SIGKILL, wrapped in async timeout
  • ☑ 5.13 Implement container discovery on startup: list running containers with `tillandsias-` prefix, parse genus from name suffix, reconstruct state
  • ☑ 5.14 Write unit tests for: argument assembly (security flags always present, GPU flags conditional), container naming, port allocation
  • ☑ 5.15 Write integration test (requires podman): start/stop/inspect cycle with a minimal alpine container
  • ☑ 6.1 Implement Tauri v2 app setup in `tillandsias-tray`: `tauri::Builder` with tray-only config, no windows
  • ☑ 6.2 Implement `TrayIconBuilder` setup with tooltip "Tillandsias" and initial idle icon
  • ☑ 6.3 Implement dynamic menu builder: takes `TrayState`, produces hierarchical menu with project tree (watch path → projects → Attach Here/Start/Stop) and running environments section with genus icons
  • ☑ 6.4 Implement menu event handler dispatching `AppEvent::MenuAction` for: Attach Here, Start, Stop, Destroy, Settings, Quit
  • ☑ 6.5 Implement main event loop using `tokio::select!` over: scanner events channel, podman events channel, menu action channel, shutdown signal (SIGTERM/SIGINT)
  • ☑ 6.6 Wire scanner → event loop → tray state update → menu rebuild pipeline
  • ☑ 6.7 Wire podman events → event loop → tray state update → menu rebuild pipeline
  • ☑ 6.8 Implement "Attach Here" handler: allocate genus, create container via podman client, update tray state with bud icon, transition to bloom on container healthy
  • ☑ 6.9 Implement Stop handler: send graceful stop, update icon to dried bloom during shutdown, remove from running on complete
  • ☑ 6.10 Implement Destroy handler with 5-second safety hold (confirmation delay before cache deletion, project source in `~/src` is never touched)
  • ☑ 6.11 Implement main tray icon state transitions: idle (no projects) → subtle bloom (projects detected) → colorful (running) → multiple blooms (multiple running)
  • ☑ 6.12 Implement graceful application shutdown: stop all managed containers on Quit, clean up event watchers
  • ☑ 7.1 Implement platform-aware config paths: Linux `~/.config/tillandsias/`, macOS `~/Library/Application Support/tillandsias/`, Windows `%APPDATA%/tillandsias/`
  • ☑ 7.2 Implement platform-aware cache paths: Linux `~/.cache/tillandsias/`, macOS `~/Library/Caches/tillandsias/`, Windows `%LOCALAPPDATA%/tillandsias/`
  • ☑ 7.3 Add Podman Machine awareness: detect platform, check `podman machine list` on macOS/Windows, surface clear user-facing message with installation link when unavailable
  • ☑ 7.4 Verify `notify` watcher works correctly on all three platforms (inotify/kqueue/ReadDirectoryChangesW) — document any platform-specific quirks
  • ☑ 7.5 Implement image pull with non-technical progress: tray shows "Preparing environment..." while pulling, genus icon in bud state
  • ☑ 7.6 Implement WASM-isolated timeout wrapper for container operations: 60s timeout for start operations, terminable on non-response, exponential backoff on reconnection
  • ☑ 7.7 Add `tracing` instrumentation throughout all crates for structured logging (debug builds only, no user-visible logs)
  • ☑ 7.8 Verify cross-compilation targets: `x86_64-unknown-linux-gnu`, `aarch64-apple-darwin`, `x86_64-pc-windows-msvc`
  • ☑ 8.1 Create `CLAUDE.md` with build commands, workspace structure, architecture overview, and test commands
  • ☑ 8.2 Add platform-specific installation docs: Linux (podman from package manager), macOS (Podman Desktop / `brew install podman` + `podman machine init`), Windows (Podman Desktop)
  • ☑ 8.3 Write self-documenting default `config.toml` template generated on first run (with comments explaining each setting)
  • ☑ 8.4 Verify end-to-end flow: launch tray → detect project in `~/src` → click "Attach Here" → container starts with forge image → genus icon blooms → Stop → icon dries → container destroyed (requires manual verification with running podman)

Read source record ↗

changeTray app fixesChecklist complete10 checked · 0 open

The tray app runs but is non-functional: menu clicks do nothing (including Quit), projects aren't visible despite being detected by the scanner, and the menu never updates. Five bugs identified via code audit.

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1.1 Store TrayIcon handle in OnceLock static so it persists for app lifetime
  • ☑ 1.2 Pass TrayIcon handle to on_state_change callback for menu rebuilds
  • ☑ 2.1 Ensure menu event handler callback fires on every click
  • ☑ 2.2 Add Quit fast-path: call process::exit(0) directly from menu handler
  • ☑ 2.3 Verify other menu items dispatch through channel to event loop
  • ☑ 3.1 In on_state_change callback, rebuild menu from current state and call tray.set_menu()
  • ☑ 3.2 Verify menu updates when scanner discovers projects (10 projects detected in debug log)
  • ☑ 4.1 Scanner already detects all non-hidden directories (was working, menu rebuild was broken)
  • ☑ 4.2 All 10 directories in ~/src appear in the menu
  • ☑ 5.1 Build, install, and verify: projects show, quit works, menu updates

Read source record ↗

changeVersion schemeChecklist complete16 checked · 0 open

The project needs a versioning scheme that is monotonically increasing, tied to the spec-driven development workflow, and supports rolling release tags for container images and binary distribution. Standard semver alone doesn't capture the relationship between OpenSpec changes and build iterations.

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1.1 Create `VERSION` file at project root with initial content `0.0.0.1`
  • ☑ 1.2 Update all `Cargo.toml` version fields to match first 3 components of VERSION (`0.0.0`)
  • ☑ 1.3 Update `tauri.conf.json` version to match first 3 components of VERSION (`0.0.0`)
  • ☑ 2.1 Create `scripts/bump-version.sh` that reads VERSION file and updates all Cargo.toml and tauri.conf.json version fields
  • ☑ 2.2 Add `--bump-build` flag that increments the 4th component
  • ☑ 2.3 Add `--bump-changes` flag that increments the 3rd component and resets 4th to 0
  • ☑ 2.4 Make script idempotent — running twice with no VERSION change produces no diff
  • ☑ 2.5 Test: verify script updates all 4 Cargo.toml files and tauri.conf.json
  • ☑ 3.1 Update `.github/workflows/release.yml` to read version from VERSION file
  • ☑ 3.2 Add step to validate git tag matches VERSION file content
  • ☑ 3.3 Add step to force-push `stable` tag to release commit
  • ☑ 3.4 Add step to force-push `latest` tag to build commit
  • ☑ 3.5 Ensure version tags (`v*`) are never overwritten (immutable, CI only creates new ones)
  • ☑ 4.1 Document version scheme in README.md (version format, when each component bumps)
  • ☑ 4.2 Document `scripts/bump-version.sh` usage in CLAUDE.md
  • ☑ 4.3 Add version scheme section to CONTRIBUTING guidelines (documented in CLAUDE.md)

Read source record ↗

changeWeb imageChecklist complete8 checked · 0 open

99% of projects Tillandsias creates will eventually need to serve static files over HTTP: an HTML page, some JavaScript, CSS, images. Today, if a user generates a web project and clicks "Run," there is no lightweight runtime image to serve it. The default forge image is ~1GB and includes dev tooling (Nix, OpenCode, Node, git) that a running web app does not

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1.1 Create `images/web/Containerfile` based on `docker.io/library/alpine:latest` with WORKDIR `/var/www`, EXPOSE 8080, and CMD running `busybox httpd -f -p 8080 -h /var/www`
  • ☑ 1.2 Create `images/web/entrypoint.sh` that prints a banner with the project name and serving URL, then execs httpd
  • ☑ 1.3 Verify the image builds successfully with `podman build -t tillandsias-web:latest -f images/web/Containerfile images/web/`
  • ☑ 1.4 Verify the built image is under 10MB
  • ☑ 2.1 Write `openspec/changes/web-image/proposal.md` explaining why a tiny httpd image is needed
  • ☑ 2.2 Write `openspec/changes/web-image/design.md` with Alpine + busybox httpd decisions
  • ☑ 2.3 Write `openspec/changes/web-image/specs/web-image/spec.md` with requirements and scenarios
  • ☑ 2.4 Write `openspec/changes/web-image/tasks.md` with implementation tasks

Read source record ↗

changeFix attach here terminalChecklist complete4 checked · 0 open

"Attach Here" is completely broken. The tray app spawns a detached container that the user can't interact with. The whole point of Tillandsias is: click Attach Here → OpenCode appears. Zero manual steps. No "podman exec" commands.

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1.1 handlers.rs: open platform terminal running full `podman run -it --rm` command — terminal provides TTY, opencode gets real terminal
  • ☑ 1.2 Add open_terminal function: ptyxis/gnome-terminal/xterm on Linux, osascript on macOS, cmd on Windows
  • ☑ 1.3 Simplified entrypoint: always exec opencode (terminal always provides TTY), fall back to bash
  • ☑ 1.4 Build, install, rebuilt image with new entrypoint

Read source record ↗

changeDynamic port allocationChecklist complete12 checked · 0 open

Stale containers (from unclean terminal closures) hold port ranges. The current allocator only checks in-memory state, not actual podman containers, so new containers collide with ports held by orphans. The 100-port ranges (3000-3099, 3100-3199) are wasteful — most environments need only a handful of ports. Smaller ranges with real podman-backed conflict det

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1.1 In `launch.rs`, change default range size from 100 to 20 ports
  • ☑ 1.2 Add `query_occupied_ports()` — run `podman ps --filter name=tillandsias- --format '{{.Ports}}'`, parse port mappings into `Vec<(u16, u16)>`
  • ☑ 1.3 Update `allocate_port_range()` to accept both in-memory and podman-queried ranges
  • ☑ 1.4 Update tests for 20-port ranges
  • ☑ 2.1 Add `cleanup_stale_containers()` — list tillandsias containers via podman, compare against `state.running`, `podman rm -f` orphans
  • ☑ 2.2 Call cleanup before port allocation in `handle_attach_here()`
  • ☑ 3.1 In `handlers.rs` `handle_attach_here()`, merge podman-queried ports with state ports before allocating
  • ☑ 3.2 In `handlers.rs` `handle_terminal()`, replace hardcoded `3100-3199` with allocator call
  • ☑ 3.3 In `runner.rs`, change default base range from `(3000, 3099)` to `(3000, 3019)`
  • ☑ 3.4 In config default, update port range from "3000-3099" to "3000-3019"
  • ☑ 4.1 `cargo check --workspace` passes
  • ☑ 4.2 `cargo test --workspace` passes (updated port range tests)

Read source record ↗

changeEmbed scripts in binaryChecklist complete14 checked · 0 open

Currently `gh-auth-login.sh`, `build-image.sh`, `ensure-builder.sh`, and image source files (entrypoint, configs, flake) are copied to `~/.local/share/tillandsias/` at install time and executed from there. This is a supply chain risk: any process or user with write access to `~/.local/share/` can tamper with these scripts, and the signed Tillandsias binary w

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1.1 Create `src-tauri/src/embedded.rs` with `include_str!` constants for all scripts: `gh-auth-login.sh`, `scripts/build-image.sh`, `scripts/ensure-builder.sh`
  • ☑ 1.2 Add `include_str!` constants for image sources: `flake.nix`, `flake.lock`, `images/default/entrypoint.sh`, shell configs, skill files
  • ☑ 1.3 Implement `write_temp_script(name: &str, content: &str) -> Result<PathBuf>` — writes to `$XDG_RUNTIME_DIR/tillandsias/`, chmod 0700
  • ☑ 1.4 Implement `write_image_sources() -> Result<TempDir>` — writes full image source tree to a tempdir, returns the TempDir (auto-cleanup on drop)
  • ☑ 1.5 Add `mod embedded;` to crate root
  • ☑ 2.1 Update `handle_github_login()` in `handlers.rs` — use `embedded::write_temp_script("gh-auth-login.sh", embedded::GH_AUTH_LOGIN)` instead of `resolve_gh_auth_script()`
  • ☑ 2.2 Remove `resolve_gh_auth_script()` and `resolve_project_root()` from `handlers.rs`
  • ☑ 2.3 Update `run_build_image_script()` in `handlers.rs` — extract embedded scripts to temp, execute from there
  • ☑ 2.4 Update `run_build_image_script()` in `runner.rs` — same pattern
  • ☑ 2.5 Remove `resolve_project_root()` from `runner.rs`
  • ☑ 3.1 Remove script/flake/image installation from `build.sh --install` (keep only binary, libs, icons, desktop file)
  • ☑ 3.2 Remove `gh-auth-login.sh` installation from `build.sh --install`
  • ☑ 4.1 `cargo check --workspace` passes
  • ☑ 4.2 Verify `build.sh --install` no longer copies scripts to `~/.local/share/tillandsias/`

Read source record ↗

changeFix versioning and ciChecklist complete4 checked · 0 open

2026-03-24-fix-versioning-and-ci

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1.1 bump-version.sh: BUILD increments (not resets) on --bump-changes
  • ☑ 1.2 CI workflow: workflow_dispatch only
  • ☑ 1.3 Release workflow: workflow_dispatch only
  • ☑ 1.4 Cancel in-progress failed runs

Read source record ↗

changeForge bash skillsChecklist complete6 checked · 0 open

The forge container runs OpenCode as the primary interface, but users sometimes need to drop into a raw bash shell — to run build commands, inspect files, install tools, or debug issues that OpenCode can't handle. There is currently no way to do this without detaching from the container and running `podman exec` manually.

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1.1 Create `images/default/skills/command/bash.md` — `/bash` skill with `agent_blocked: true`, interactive shell and one-shot command support
  • ☑ 1.2 Create `images/default/skills/command/bash-private.md` — `/bash-private` skill with `agent_blocked: true`, private session with terminal clearing and agent-invisible output
  • ☑ 2.1 Write `proposal.md` — motivation, what changes, capabilities, impact
  • ☑ 2.2 Write `design.md` — context, goals, decisions on file location, agent blocking, private session isolation, one-shot support
  • ☑ 2.3 Write `specs/forge-skills/spec.md` — requirements and scenarios for both skills
  • ☑ 2.4 Write `tasks.md` — task breakdown with completion status

Read source record ↗

changeForge gh authChecklist complete7 checked · 0 open

The forge container has `gh` installed but no way for users to authenticate with GitHub. Without authentication, users cannot push/pull from private repos, create PRs, or interact with GitHub APIs. The authentication flow involves a one-time code and token that must remain private from AI agents.

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1.1 Create `images/default/skills/command/gh-auth-login.md` with `agent_blocked: true` frontmatter
  • ☑ 1.2 Skill asks for git identity (user.name, user.email) and configures git
  • ☑ 1.3 Skill uses `/bash-private` for the `gh auth login` flow
  • ☑ 1.4 Skill verifies auth with `gh auth status` and runs `gh auth setup-git`
  • ☑ 2.1 Update `images/default/entrypoint.sh` to copy skills from `/usr/local/share/tillandsias/opencode/` to project `.opencode/`
  • ☑ 3.1 Add `images/default/skills` as a tracked Nix path in `flake.nix` so changes trigger rebuild
  • ☑ 3.2 Copy skills directory into the image at `/usr/local/share/tillandsias/opencode/` in `fakeRootCommands`

Read source record ↗

changeForge shell toolsChecklist complete10 checked · 0 open

The forge container's bash mode drops users into a bare-bones shell with only the essentials. Power users troubleshooting inside the container need a healthy set of terminal tools — alternative shells, file managers, editors, and modern CLI sugar. AJ doesn't need to know these exist, but developers who use `--bash` or the Terminal menu should feel at home.

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1.1 Add fish, zsh to `flake.nix` forge-image contents
  • ☑ 1.2 Add mc, vim, nano to `flake.nix` forge-image contents
  • ☑ 1.3 Add eza, bat, fd, fzf, zoxide, htop, tree to `flake.nix` forge-image contents
  • ☑ 2.1 Create `images/default/shell/bashrc` — colored prompt, aliases (ll, la, ..), eza/bat integration, zoxide init, PATH setup
  • ☑ 2.2 Create `images/default/shell/config.fish` — PATH setup, aliases, zoxide init
  • ☑ 2.3 Create `images/default/shell/zshrc` — prompt with path, autocompletion, history, aliases, zoxide init
  • ☑ 3.1 In `flake.nix`, add shell config files to the image (copy to `/etc/skel/`)
  • ☑ 3.2 In `entrypoint.sh`, deploy shell configs from `/etc/skel/` to home directory if not present
  • ☑ 3.3 Rebuild image: `./scripts/build-image.sh forge --force`
  • ☑ 3.4 Test: enter bash mode, verify tools are available and configs loaded

Read source record ↗

changeForge welcome fish defaultChecklist complete20 checked · 0 open

Users land in a bare bash prompt with no context about what they're looking at. They don't know what's mounted, what tools are available, or even what shell they're in. The terminal should feel welcoming, informative, and opinionated — like Kitty on Hyprland, but showing the host-to-guest mapping that makes Tillandsias unique.

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1.1 Create `images/default/forge-welcome.sh` — the welcome message script
  • ☑ 1.2 Implement project name display (bold cyan, from `$TILLANDSIAS_PROJECT` env var or directory detection)
  • ☑ 1.3 Implement OS version display — read guest `/etc/os-release`, format host from `$TILLANDSIAS_HOST_OS`
  • ☑ 1.4 Implement mount point listing with color-coded access: green (rw), red (ro), blue (encrypted source)
  • ☑ 1.5 Implement rotating tips pool (~20 one-liners) with keyword highlighting
  • ☑ 1.6 Add "Project mounted at /home/forge/src/<project>" line before the tip
  • ☑ 2.1 Update `images/default/shell/config.fish` to source the welcome script on interactive login
  • ☑ 2.2 Embed `forge-welcome.sh` in `src-tauri/src/embedded.rs` via `include_str!`
  • ☑ 2.3 Add `forge-welcome.sh` to `write_image_sources()` in embedded.rs
  • ☑ 3.1 In `handlers.rs` `handle_terminal()`, change `--entrypoint bash` to `--entrypoint fish`
  • ☑ 3.2 In `runner.rs`, change `--entrypoint /bin/bash` to `--entrypoint fish` for `--bash` mode
  • ☑ 3.3 In both handlers, add `-e TILLANDSIAS_HOST_OS=<detected>` to the podman command
  • ☑ 3.4 Implement `detect_host_os()` helper — read `/etc/os-release` on host, format as "Fedora Silverblue 43" etc.
  • ☑ 3.5 In both handlers, add `-e TILLANDSIAS_PROJECT=<project_name>` to the podman command
  • ☑ 4.1 In `flake.nix`, add `forge-welcome.sh` to the image contents (alongside entrypoint.sh)
  • ☑ 4.2 Stage `images/default/forge-welcome.sh` in git so Nix can see it
  • ☑ 4.3 Rebuild image: `./scripts/build-image.sh forge --force`
  • ☑ 5.1 Test: open Terminal from tray, verify fish launches with welcome message
  • ☑ 5.2 Test: `tillandsias ../project/ --bash`, verify fish + welcome
  • ☑ 5.3 Test: verify mount points shown with correct colors and access levels

Read source record ↗

changeGit credential mountChecklist complete4 checked · 0 open

Forge containers are ephemeral — every `git push`, `gh pr create`, or authenticated GitHub API call fails because credentials don't survive container recreation. Users must re-authenticate on every Attach Here, which breaks the "just works" promise.

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1.1 In `src-tauri/src/handlers.rs`, update `build_run_args` to create `secrets/{gh,git}` directories and add `-v` mounts for `~/.config/gh` and `~/.gitconfig`
  • ☑ 2.1 In `src-tauri/src/runner.rs`, update `build_run_args` to create `secrets/{gh,git}` directories and add `-v` mounts for `~/.config/gh` and `~/.gitconfig`
  • ☑ 3.1 In `images/default/entrypoint.sh`, add early directory/file creation to ensure mount targets exist before tools access them
  • ☑ 4.1 Run `cargo build --workspace` and confirm compilation succeeds with no errors

Read source record ↗

changeProposal: Icon Generation, Menu Cleanup, Security AuditChecklist complete21 checked · 0 open

The tray/app icons were 104-byte solid green squares -- placeholder files that gave no visual identity. The "Start" menu item was a duplicate of "Attach Here" (identical handler), adding confusion. "Terminal" is developer jargon that Average Joe users won't understand. Container security flags needed formal documentation.

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1.1 Create `scripts/generate-icons.py` with tillandsia rosette geometry (8 curved leaves)
  • ☑ 1.2 Implement Pillow rendering path (primary)
  • ☑ 1.3 Implement raw struct+zlib PNG fallback (no dependencies)
  • ☑ 1.4 Generate tray-icon.png (32x32, white silhouette on transparent)
  • ☑ 1.5 Generate 32x32.png (green #4CAF50 on transparent)
  • ☑ 1.6 Generate 128x128.png (green on transparent)
  • ☑ 1.7 Generate icon.png (256x256, green on transparent)
  • ☑ 1.8 Verify all icons are recognizable plant shapes, not blobs
  • ☑ 2.1 Remove "Start" menu item from `build_project_submenu` in menu.rs
  • ☑ 2.2 Rename "Terminal" label to "🌱 Ground" in menu.rs
  • ☑ 2.3 Remove `ids::start()` helper (dead code after menu removal)
  • ☑ 2.4 Replace `MenuCommand::Start` handler in event_loop.rs with no-op debug log
  • ☑ 2.5 Add comment to "start" dispatch in main.rs noting it's no longer emitted
  • ☑ 3.1 Audit `build_run_args` (Attach Here path): all 5 security flags present
  • ☑ 3.2 Audit `handle_terminal` (Ground path): all 5 security flags present
  • ☑ 3.3 Audit `handle_github_login`: all 5 security flags present
  • ☑ 3.4 Verify volume mounts limited to project dir, cache dir, secrets dir
  • ☑ 3.5 Verify no Docker/Podman socket, no host root, no system dirs mounted
  • ☑ 3.6 Add security model doc-comment to top of handlers.rs
  • ☑ 4.1 Clean workspace build with zero warnings
  • ☑ 4.2 Write OpenSpec artifacts (proposal, tasks)

Read source record ↗

changeIconography gh auth bash modeChecklist complete15 checked · 0 open

Four UX gaps need fixing before v0.1: (1) "Attach Here" menu items have no visual state — you can't tell if an environment is idle or active without scanning the running list. (2) GitHub auth from the tray launches a terminal but the interactive flow doesn't work properly, blocking first-time setup. (3) CLI mode has no `--bash` flag for dropping into a troub

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1.1 In `menu.rs`, cross-reference scanned projects against running containers to determine emoji prefix (🌱 idle, 🌺 running)
  • ☑ 1.2 Prefix each "Attach Here" label with the appropriate emoji
  • ☑ 1.3 Verify menu rebuilds correctly when a container starts/stops (emoji transitions)
  • ☑ 2.1 Create `gh-auth-login.sh` with `--help` and `--status` flags
  • ☑ 2.2 Implement interactive `podman run -it --rm` with forge image, mounting secrets dirs, prompting for git identity, running `gh auth login`
  • ☑ 2.3 Add forge image check — offer to build if missing
  • ☑ 2.4 Add re-auth prompt when credentials already exist
  • ☑ 2.5 Delete `images/default/skills/command/gh-auth-login.md`
  • ☑ 2.6 Update `handlers.rs` `handle_github_login()` to call `open_terminal("gh-auth-login.sh")` instead of inline script
  • ☑ 2.7 Install `gh-auth-login.sh` alongside the binary in `build.sh --install`
  • ☑ 2.8 Test: run `./gh-auth-login.sh` and complete the full auth flow
  • ☑ 3.1 Add `--bash` flag to `cli.rs` `CliMode::Attach`
  • ☑ 3.2 In `runner.rs`, add `--entrypoint /bin/bash` to podman command when `bash` flag is set
  • ☑ 3.3 Update `--help` output to include `--bash` description
  • ☑ 3.4 Test: `tillandsias ../lakanoa/ --bash` drops into a bash shell inside the container

Read source record ↗

changeInit prebuilt imagesChecklist complete15 checked · 0 open

First-time users hit a multi-minute wait when they click "Attach Here" because the forge image needs to build (Nix download + podman load). This kills the "just works" promise. A `tillandsias init` command that pre-builds images in the background — triggered automatically by the installer — means images are ready before the user ever opens the tray.

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1.1 Add build lock functions to `src-tauri/src/build_lock.rs`: `acquire()`, `wait_for_build()`, `release()`, `is_running()`
  • ☑ 1.2 Lock file at `$XDG_RUNTIME_DIR/tillandsias/build-<image>.lock` with PID, stale detection
  • ☑ 2.1 Add `CliMode::Init` to `cli.rs` with parsing for `tillandsias init`
  • ☑ 2.2 Create `src-tauri/src/init.rs` — acquire build lock, run embedded `build-image.sh` for forge, release lock, report status
  • ☑ 2.3 Add `mod init;` and dispatch in `main.rs`
  • ☑ 2.4 Handle "already built" case — check `podman image exists`, skip if present
  • ☑ 2.5 Handle "build in progress" case — detect lock, wait with progress dots, verify image after
  • ☑ 3.1 In `handlers.rs` `run_build_image_script()`, acquire build lock before building, release after
  • ☑ 3.2 In `handlers.rs`, before building, check if build is already running — wait instead of duplicate
  • ☑ 3.3 Same for `runner.rs` `run_build_image_script()`
  • ☑ 4.1 In `scripts/install.sh`, after binary install, add `tillandsias init &` background spawn with log message
  • ☑ 4.2 In `build.sh --install`, post-install message not needed — dev builds already have the image
  • ☑ 5.1 `cargo check --workspace` passes
  • ☑ 5.2 Test: `tillandsias init` builds forge image
  • ☑ 5.3 Test: run two `tillandsias init` simultaneously, second waits for first

Read source record ↗

changeLanding readme installChecklist complete5 checked · 0 open

The current README.md is an architecture document. A visitor landing on the GitHub page sees workspace structure, container naming conventions, and genus tables before they ever learn how to install or run the app. For a project that targets Average Joe, the first screen should answer one question: "How do I use this?"

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1.1 Move README.md to README-ABOUT.md (preserving all content)
  • ☑ 1.2 Create new README.md as a landing page with install/run/uninstall sections
  • ☑ 2.1 Create `scripts/install.sh` with OS/arch detection, GitHub Release download, and PATH hint
  • ☑ 2.2 Create `scripts/uninstall.sh` with standard and `--wipe` modes
  • ☑ 2.3 Install script also installs `tillandsias-uninstall` to `~/.local/bin`

Read source record ↗

changeLocal cross platform buildsChecklist complete15 checked · 0 open

The release pipeline fails on every `workflow_dispatch` run because the tag validation step receives `main` instead of a version tag. Meanwhile, GitHub Actions warns that Node.js 20 actions will be **force-migrated to Node.js 24 on June 2, 2026** — breaking builds if not addressed proactively. Debugging these CI failures burns slow, expensive, rate-limited c

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1.1 Fix tag validation in `release.yml` — add `workflow_dispatch` input for `version`, resolve version from tag or input
  • ☑ 1.2 Add `FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true` env var to `release.yml` at workflow level
  • ☑ 1.3 Add `FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true` env var to `ci.yml` at workflow level
  • ☑ 1.4 Bump `setup-node` `node-version` from `22` to `24` in `release.yml`
  • ☑ 1.5 Verify CI workflow runs cleanly (trigger `ci.yml` via `workflow_dispatch`) — requires push first
  • ☑ 2.1 Create `build-windows.sh` with flag parsing matching `build.sh` conventions (`--release`, `--test`, `--check`, `--clean`, `--help`, `--toolbox-reset`)
  • ☑ 2.2 Implement `tillandsias-windows` toolbox auto-creation with deps (clang, lld, nsis, cargo-xwin, rust target `x86_64-pc-windows-msvc`)
  • ☑ 2.3 Implement debug build: `cargo xwin build --workspace --target x86_64-pc-windows-msvc`
  • ☑ 2.4 Implement release build: `cargo xwin build --release --target x86_64-pc-windows-msvc` with Tauri NSIS bundling
  • ☑ 2.5 Implement `--check` and `--test` modes (cross-check and cross-compile tests)
  • ☑ 2.6 Add Microsoft SDK license notice on first cargo-xwin download
  • ☑ 2.7 Add unsigned artifact warning on release build completion
  • ☑ 2.8 Test: run `./build-windows.sh --check` to verify cross-compilation toolchain works
  • ☑ 3.1 Create `docs/cross-platform-builds.md` — explain CI-first strategy, Windows cross-compilation with limitations, macOS infeasibility with legal rationale
  • ☑ 3.2 Update CLAUDE.md build commands section to include `build-windows.sh` usage

Read source record ↗

changeMove github login to settingsChecklist complete6 checked · 0 open

GitHub Login currently sits as a top-level tray menu item, cluttering the main menu alongside project actions. It's a one-time setup action that belongs under Settings. The Settings menu item already exists but is a stub — this change makes it a submenu and moves GitHub Login into it.

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1.1 In `menu.rs`, convert the Settings `MenuItem` to a `SubmenuBuilder`
  • ☑ 1.2 Move the GitHub Login `MenuItem` inside the Settings submenu (preserve conditional logic)
  • ☑ 1.3 Add a disabled "All set" placeholder item when GitHub Login is not needed
  • ☑ 1.4 Remove the top-level GitHub Login item and its separator
  • ☑ 2.1 Build and run: verify Settings submenu appears with GitHub Login inside it
  • ☑ 2.2 Verify clicking GitHub Login from the submenu still triggers the auth flow

Read source record ↗

changeRemote project cloneChecklist complete19 checked · 0 open

After GitHub authentication, Tillandsias knows who the user is but offers no way to pull down their remote repositories. Users must manually `gh repo clone` from a terminal. Adding a "Remote Projects" submenu that lists GitHub repos not already cloned locally turns Tillandsias into a one-click project bootstrapper — discover, clone, and start developing.

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1.1 Create `src-tauri/src/github.rs` with `fetch_repos()` — runs `podman run --rm` with forge image and `gh repo list --json name,url --limit 100`, parses JSON output
  • ☑ 1.2 Implement `RemoteRepo` struct (name, full_name, clone_url) and list parsing
  • ☑ 1.3 Implement `clone_repo(full_name, target_dir)` — runs `podman run --rm` with forge image and `gh repo clone <full_name> <target_dir>`
  • ☑ 1.4 Add `mod github;` to crate root
  • ☑ 2.1 Add remote repo cache to `TrayState` — `Vec<RemoteRepo>`, timestamp, loading flag
  • ☑ 2.2 Implement cache TTL logic (5-minute expiry)
  • ☑ 2.3 Invalidate cache after GitHub Login/Refresh completes
  • ☑ 3.1 In `menu.rs`, swap GitHub Login label to "GitHub Login Refresh" when `!needs_github_login()`
  • ☑ 3.2 Add "Remote Projects" submenu inside Settings (only when authenticated)
  • ☑ 3.3 Populate Remote Projects submenu from cached repo list, filtered against local `~/src/` directories
  • ☑ 3.4 Show "Loading..." disabled item while fetching, "Could not fetch repos" on error, "Login to GitHub first" when unauthenticated
  • ☑ 3.5 Show "Cloning <name>..." disabled item during active clone
  • ☑ 4.1 Add `MenuCommand::CloneProject { full_name, name }` variant to `event.rs`
  • ☑ 4.2 Add menu ID dispatch for clone items in `main.rs`
  • ☑ 4.3 Implement clone handler in `handlers.rs` — spawn forge container, clone, trigger scanner rescan on completion
  • ☑ 4.4 Add `MenuCommand::RefreshRemoteProjects` variant for triggering background fetch
  • ☑ 5.1 Build and verify: Settings shows "GitHub Login Refresh" when authenticated
  • ☑ 5.2 Build and verify: Remote Projects submenu lists repos not in ~/src/
  • ☑ 5.3 Test: click a remote project, verify it clones and appears in the tray menu

Read source record ↗

changeSingleton instanceChecklist complete10 checked · 0 open

Running `tillandsias` multiple times (e.g. from desktop launcher, autostart, and terminal) spawns duplicate tray icons with independent state. Each instance starts its own scanner, podman event stream, and event loop — wasting resources and confusing the user with multiple identical menus.

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1.1 Create `src-tauri/src/singleton.rs` with `lock_file_path()` returning the platform-appropriate lock file location
  • ☑ 1.2 Implement `try_acquire()` — read existing lock, check PID liveness + process name, write new PID if stale or absent
  • ☑ 1.3 Implement `release()` — remove the lock file (called on graceful shutdown)
  • ☑ 1.4 Implement stale detection: Linux (`/proc/<pid>/comm`), macOS (`kill(pid, 0)`), Windows (`OpenProcess`)
  • ☑ 2.1 Wire singleton check into `main.rs` — call `try_acquire()` after CLI parse, before Tauri builder, only for tray mode
  • ☑ 2.2 Wire `release()` into the existing shutdown path (SIGTERM/SIGINT handler or Tauri exit event)
  • ☑ 2.3 Add `mod singleton;` to the crate
  • ☑ 3.1 Test: launch app, verify lock file created at expected path with correct PID
  • ☑ 3.2 Test: launch second instance, verify it exits immediately with code 0
  • ☑ 3.3 Test: kill first instance with SIGKILL, launch again, verify stale lock is replaced

Read source record ↗

changeTray github loginChecklist complete7 checked · 0 open

2026-03-24-tray-github-login

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1.1 Add GitHubLogin and Terminal variants to MenuCommand
  • ☑ 1.2 Add GitHub Login menu item (shown when no gh/hosts.yml exists)
  • ☑ 1.3 Add Terminal menu item per project
  • ☑ 1.4 handle_github_login: opens terminal with gh auth login + git identity
  • ☑ 1.5 handle_terminal: opens bash in forge container with project mounted
  • ☑ 1.6 Wire in event_loop and main.rs menu dispatcher
  • ☑ 1.7 Build and verify

Read source record ↗

changeGraceful restartChecklist complete4 checked · 0 open

When the tray app crashes or is restarted, the user's development environments keep running inside their containers — but the tray menu shows nothing. Every project appears idle. The user must manually inspect state or wait for a podman event to trigger.

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1.1 In `main.rs` startup discovery block, skip containers with `ContainerState::Stopped` (only push `Running` and `Creating` entries into `state.running`)
  • ☑ 2.1 Add `seed_from_running()` method to `GenusAllocator` in `genus.rs` that marks a set of `(project_name, genus)` pairs as allocated
  • ☑ 2.2 In `event_loop::run()`, call `allocator.seed_from_running(&state.running)` immediately after `GenusAllocator::new()`
  • ☑ 3.1 Run `cargo check --workspace` — zero errors

Read source record ↗

changeNative secrets storeChecklist complete15 checked · 0 open

Tillandsias currently stores the GitHub OAuth token in plain text at `~/.cache/tillandsias/secrets/gh/hosts.yml`. This file is mounted read-only into forge containers, but on the host it sits unencrypted on disk. Any process running as the user can read it. On a shared machine or compromised host, this is a credential leak.

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1.1 Add `keyring = "3"` to `src-tauri/Cargo.toml` dependencies
  • ☑ 2.1 Create `src-tauri/src/secrets.rs` with keyring read/write/delete functions
  • ☑ 2.2 Implement `store_github_token(token: &str) -> Result<(), String>` — stores token in native keyring
  • ☑ 2.3 Implement `retrieve_github_token() -> Result<Option<String>, String>` — retrieves token from keyring, returns None if not found
  • ☑ 2.4 Implement `migrate_token_to_keyring()` — reads existing `hosts.yml`, extracts token, stores in keyring if keyring entry is empty
  • ☑ 2.5 Implement `write_hosts_yml_from_keyring()` — retrieves token from keyring, writes `hosts.yml` to the secrets directory for container mount
  • ☑ 2.6 Register `mod secrets` in `main.rs`
  • ☑ 3.1 Migration happens via startup call and per-launch `write_hosts_yml_from_keyring()` — the interactive `gh auth login` runs in a terminal and writes to the mounted `hosts.yml`; the next startup or container launch picks up the new token
  • ☑ 4.1 In `handle_attach_here()`: before building podman run args, call `write_hosts_yml_from_keyring()` so the container gets a fresh `hosts.yml`
  • ☑ 4.2 In `handle_terminal()`: same — call `write_hosts_yml_from_keyring()` before launch
  • ☑ 4.3 In `runner.rs` `build_run_args()`: call `write_hosts_yml_from_keyring()` before building args
  • ☑ 4.4 In `github.rs` `fetch_repos()` and `clone_repo()`: call `write_hosts_yml_from_keyring()` before spawning the container
  • ☑ 5.1 In `main.rs` tray setup, call `secrets::migrate_token_to_keyring()` early in the async block
  • ☑ 6.1 Run `cargo check --workspace` — zero errors
  • ☑ 6.2 Run `cargo test --workspace` — all 66 tests pass (4 new secrets tests)

Read source record ↗

changeRelease verificationChecklist complete14 checked · 0 open

The release-pipeline and cosign-signing changes were archived with incomplete verification tasks. The implementation is done but has never been tested against a real CI run. These 9 tasks require pushing a tag, observing the GitHub Actions workflow, and manually verifying artifacts — they cannot be completed without a configured CI environment with signing s

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1.1 Generate Tauri Ed25519 signing keypair: `cargo tauri signer generate -w ~/.tauri/tillandsias.key`
  • ☑ 1.2 Update `src-tauri/tauri.conf.json` with the actual public key
  • ☑ 1.3 Configure GitHub repo secret `TAURI_SIGNING_PRIVATE_KEY` with the private key content
  • ☑ 1.4 Configure GitHub repo secret `TAURI_SIGNING_PRIVATE_KEY_PASSWORD` with the key password
  • ☑ 1.5 Install Cosign locally for verification testing
  • ☑ 2.1 Push a release candidate tag (`v0.0.5.1-rc.1`) and verify all three platform builds succeed
  • ☑ 2.2 Verify artifact naming matches `tillandsias-{version}-{os}-{arch}.{ext}` for all platforms
  • ☑ 2.3 Download `SHA256SUMS` and verify `sha256sum -c SHA256SUMS` passes for all artifacts
  • ☑ 2.4 Verify GitHub Release is created with all assets attached
  • ☑ 3.1 Verify all artifacts in the release have corresponding `.cosign.sig` and `.cosign.cert` files
  • ☑ 3.2 Download a signed artifact and run `cosign verify-blob` locally — confirm verification succeeds
  • ☑ 3.3 Modify a downloaded artifact (append a byte) and verify `cosign verify-blob` fails
  • ☑ 3.4 Search for the artifact hash in the Rekor transparency log and confirm entry exists
  • ☑ 3.5 Follow the verification instructions from the release notes on a clean machine (no prior Cosign state)

Read source record ↗

changeRuntime podman onlyChecklist complete14 checked · 0 open

The runtime image build pipeline (`build-image.sh` + `ensure-builder.sh`) uses `toolbox create` and `toolbox run` to manage a persistent `tillandsias-builder` toolbox containing Nix. Toolbox is Fedora-specific. Users on macOS, Windows, Ubuntu, Arch, or any non-Fedora Linux cannot run the installed Tillandsias binary because `toolbox` does not exist on their

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1.1 Replace `toolbox run -c tillandsias-builder` with `podman run --rm nixos/nix:latest` for the nix build step
  • ☑ 1.2 Mount source directory read-only at `/src` and output directory at `/output`
  • ☑ 1.3 Enable flakes via `--extra-experimental-features "nix-command flakes"`
  • ☑ 1.4 Copy the nix build tarball to `/output/result.tar.gz` inside the container
  • ☑ 1.5 Replace `toolbox run ... cat` tarball pipe with direct `podman load` from output directory
  • ☑ 1.6 Remove the `ensure-builder.sh` call and `BUILDER_TOOLBOX` variable
  • ☑ 2.1 Delete `scripts/ensure-builder.sh`
  • ☑ 3.1 Remove `ENSURE_BUILDER` constant and its `include_str!`
  • ☑ 3.2 Remove `ensure-builder.sh` from the `write_image_sources()` directory layout
  • ☑ 3.3 Remove the `ensure-builder.sh` permission setting
  • ☑ 3.4 Update the doc comment showing the temp directory tree
  • ☑ 4.1 Remove the `toolbox rm -f tillandsias-builder` line from the wipe section
  • ☑ 5.1 Run `cargo check --workspace` -- zero errors
  • ☑ 5.2 Run `cargo test --workspace` -- all 65 tests pass

Read source record ↗

changeTray version creditChecklist complete6 checked · 0 open

Users have no way to tell which version of Tillandsias is running from the tray menu. Adding version and credit lines makes the app feel polished and helps with troubleshooting ("which version are you on?"). Also serves as a visual trigger to test the Tauri auto-update system — bumping the minor version will let installed instances detect the update.

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1.1 Add disabled "Tillandsias v{version}" menu item using `env!("CARGO_PKG_VERSION")` before Quit
  • ☑ 1.2 Add disabled "by Tlatoāni" menu item between version and Quit
  • ☑ 1.3 Add separator before version/credit block
  • ☑ 2.1 Bump version to 0.1.36.24 to trigger auto-update
  • ☑ 2.2 cargo clippy + fmt clean
  • ☑ 2.3 Commit, push, tag, trigger release

Read source record ↗

changeAgent selection claudeChecklist complete19 checked · 0 open

Tillandsias currently hardcodes OpenCode as the sole AI coding agent launched in forge containers. Some users prefer Claude Code. Rather than forcing a single tool, the tray should let the user choose which agent starts when they click "Attach Here". The selection must persist across restarts and be passed into containers so the entrypoint launches the corre

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1.1 Write proposal.md
  • ☑ 1.2 Write tasks.md
  • ☑ 1.3 Write specs/agent-selection-claude/spec.md
  • ☑ 2.1 Add `SelectedAgent` enum and `AgentConfig` to `config.rs` with serde + Default (opencode)
  • ☑ 2.2 Add `AgentConfig` field to `GlobalConfig`
  • ☑ 2.3 Add `MenuCommand::SelectAgent { agent: String }` to `event.rs`
  • ☑ 3.1 Add `ids::select_agent(agent_name)` helper to `menu.rs`
  • ☑ 3.2 Build Seedlings submenu inside `build_settings_submenu()` with pin emoji on selected agent
  • ☑ 3.3 Add `select-agent` dispatch case in `handle_menu_click()` in `main.rs`
  • ☑ 4.1 Handle `MenuCommand::SelectAgent` in `event_loop.rs`: update config file, rebuild menu
  • ☑ 5.1 Add `TILLANDSIAS_AGENT` env var to `build_run_args()` in `handlers.rs`
  • ☑ 5.2 Add `TILLANDSIAS_AGENT` env var to `handle_terminal()` format string
  • ☑ 5.3 Add `TILLANDSIAS_AGENT` env var to `handle_root_terminal()` format string
  • ☑ 5.4 Mount `~/.cache/tillandsias/secrets/claude/` as `/home/forge/.claude:rw` in `build_run_args()`
  • ☑ 5.5 Mount Claude secrets in `handle_terminal()` and `handle_root_terminal()`
  • ☑ 6.1 Add Claude Code install + launch branch to `entrypoint.sh` gated by `TILLANDSIAS_AGENT`
  • ☑ 7.1 Add `~/.claude` and `/home/forge/.claude` to deny list in `opencode.json`
  • ☑ 8.1 `./build.sh --check` passes
  • ☑ 8.2 `./build.sh --test` passes

Read source record ↗

changeMenu display active containers inlineChecklist complete8 checked · 0 open

The tray menu currently segregates running projects into a "Running Environments" submenu. This is tech jargon — users don't think in terms of "environments". More importantly, it buries active projects one click deeper behind an abstraction layer. When a project is running, users want to act on it immediately without hunting through submenus.

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1.1 Split `state.projects` into active (has running containers) and inactive lists in `build_tray_menu()`
  • ☑ 1.2 Render active project submenus inline at the top level using `build_project_submenu()`
  • ☑ 1.3 Pass only inactive projects to `build_projects_submenu()`
  • ☑ 1.4 Omit the Projects submenu entirely when the inactive projects list is empty
  • ☑ 1.5 Render build chips inline (disabled top-level items) below active projects
  • ☑ 1.6 Delete `build_running_submenu()` function
  • ☑ 1.7 Delete `build_activity_submenu()` function
  • ☑ 2.1 `./build.sh --check` passes with no errors

Read source record ↗

changeRoot terminal launcherChecklist complete11 checked · 0 open

Power users and maintainers need a fast path to a bash terminal at the root `~/src/` directory — the same scope as the top-level "Attach Here" entry. Today the only way to get there is to use a per-project Maintenance terminal, which scopes the container to a single project directory. A global root terminal opens in the full `~/src/` mount, useful for cross-

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1.1 Create proposal.md
  • ☑ 1.2 Create tasks.md
  • ☑ 1.3 Create specs/root-terminal-launcher/spec.md
  • ☑ 2.1 Add `MenuCommand::RootTerminal` variant to the `MenuCommand` enum
  • ☑ 3.1 Verify `🛠️` (U+1F6E0+FE0F) is absent from `TOOL_EMOJIS`
  • ☑ 3.2 Add doc comment marking `🛠️` as reserved for the global root terminal
  • ☑ 4.1 Add `ids::root_terminal()` static ID function
  • ☑ 4.2 Insert `🛠️ Root` menu item immediately after the `~/src/ — Attach Here` item
  • ☑ 5.1 Add `MenuCommand::RootTerminal` arm to the `match command` block, routing to `handlers::handle_root_terminal()`
  • ☑ 6.1 Implement `handle_root_terminal(watch_path, state, allocator, tool_allocator, build_tx)` — launches bash terminal in forge container at src/ root
  • ☑ 7.1 `./build.sh --check` passes (type-check only)

Read source record ↗

changeSettings github submenuChecklist complete6 checked · 0 open

The Settings submenu mixes GitHub authentication and Remote Projects at the same level as version/credit metadata. As more GitHub-related items are added, this flat structure will become cluttered. Grouping them under a single "GitHub" submenu improves discoverability and provides a clear home for future GitHub features.

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1.1 Write proposal.md
  • ☑ 1.2 Write tasks.md
  • ☑ 1.3 Write specs/settings-github-submenu/spec.md
  • ☑ 2.1 Wrap GitHub Login item and Remote Projects submenu in a "GitHub" SubmenuBuilder inside `build_settings_submenu()`
  • ☑ 2.2 Ensure separator between GitHub submenu and version/credit is preserved
  • ☑ 3.1 `./build.sh --check` passes with no errors

Read source record ↗

changeUser friendly errorsChecklist complete16 checked · 0 open

Error messages returned by Tillandsias handlers currently contain developer-facing text: internal script names (`build-image.sh`), image tags (`tillandsias-forge:latest`), shell invocations (`./build.sh --install`), and exit codes. These strings leak implementation details to users who have no context for them and cannot act on them.

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1.1 Review `src-tauri/src/handlers.rs` for all `Err()` strings containing image tags, script names, exit codes, or internal paths
  • ☑ 1.2 Review `src-tauri/src/runner.rs` for developer-facing `eprintln!` in image-not-found path
  • ☑ 1.3 Review `src-tauri/src/init.rs` for build failure error string
  • ☑ 2.1 `run_build_image_script` — replace "Failed to run build-image.sh: {e}" with setup failure message
  • ☑ 2.2 `run_build_image_script` — replace "build-image.sh failed (exit {code})" with setup failure message
  • ☑ 2.3 `handle_attach_here` — replace "Image {tag} still not found after build-image.sh completed" with environment-not-ready message
  • ☑ 2.4 `handle_attach_here` — replace "Failed to build image {tag}: {e}" with setup failure message
  • ☑ 2.5 `handle_attach_here` — replace "Image build task panicked: {e}" with setup failure message
  • ☑ 2.6 `handle_terminal` — replace "Forge image not found. Run ./build.sh --install first." with environment-not-ready message
  • ☑ 2.7 `handle_github_login` — replace "Failed to extract gh-auth-login.sh: {e}" with installation-incomplete message
  • ☑ 3.1 CLI image-not-found path — replace `"Image {} not found. Run: ./build.sh --install"` with environment-not-ready message
  • ☑ 3.2 `run_build_image_script` — replace "Failed to run build-image.sh: {e}" with setup failure message
  • ☑ 3.3 `run_build_image_script` — replace "build-image.sh exited with code {code}" with setup failure message
  • ☑ 4.1 `build_forge_image` — replace "Failed to run build-image.sh: {e}" with setup failure message
  • ☑ 4.2 `build_forge_image` — replace "build-image.sh exited with code {code}" with setup failure message
  • ☑ 5.1 `./build.sh --check` passes with no type errors

Read source record ↗

changeVersioned forge imagesChecklist complete23 checked · 0 open

The forge image is tagged `tillandsias-forge:latest`. When the app auto-updates to a new version with a different entrypoint or config, the old image is reused because the staleness detection hash file lives in a temp dir that does not persist across restarts. Users get stale containers with old entrypoints, causing silent breakage that is difficult to diagn

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1.1 Write proposal.md
  • ☑ 1.2 Write design.md
  • ☑ 1.3 Write tasks.md
  • ☑ 1.4 Write specs/versioned-forge-images/spec.md
  • ☑ 2.1 Add `--tag <tag>` argument parsing to build-image.sh
  • ☑ 2.2 Use `--tag` value as `IMAGE_TAG` when provided, fall back to `:latest`
  • ☑ 2.3 Update `--help` output to document `--tag`
  • ☑ 3.1 Replace `FORGE_IMAGE_TAG` constant with `pub fn forge_image_tag() -> String` in handlers.rs
  • ☑ 3.2 Update all `FORGE_IMAGE_TAG` references in handlers.rs to `forge_image_tag()`
  • ☑ 3.3 Update all `handlers::FORGE_IMAGE_TAG` references in main.rs to `handlers::forge_image_tag()`
  • ☑ 3.4 Replace `FORGE_IMAGE` constant in init.rs with `handlers::forge_image_tag()` call
  • ☑ 3.5 Update `image_tag()` in runner.rs to use versioned tag for forge images
  • ☑ 3.6 Update `FORGE_IMAGE_TAG` import in github.rs to use `forge_image_tag()` function
  • ☑ 4.1 Update `run_build_image_script` in handlers.rs to pass `--tag` argument
  • ☑ 4.2 Update `run_build_image_script` in runner.rs to pass `--tag` argument
  • ☑ 4.3 Update `build_forge_image` in init.rs to pass `--tag` argument
  • ☑ 5.1 Add `prune_old_forge_images()` function to handlers.rs
  • ☑ 5.2 Call prune after successful build in handlers.rs `run_build_image_script`
  • ☑ 5.3 Call prune after successful build in runner.rs `run_build_image_script`
  • ☑ 5.4 Call prune after successful build in init.rs `build_forge_image`
  • ☑ 6.1 Update launch-time check in main.rs to detect "first time" vs "update" using any `tillandsias-forge:v*` existence check
  • ☑ 7.1 `./build.sh --check` passes
  • ☑ 7.2 `./build.sh --test` passes

Read source record ↗

changeClaude login refresh labelChecklist complete2 checked · 0 open

Authenticated Claude shows "Claude (authenticated)" which is inconsistent with GitHub's "GitHub Login Refresh" pattern. Should match.

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1. Change label from "Claude (authenticated)" to "Claude Login Refresh"
  • ☑ 2. Make it clickable (enabled=true) so users can refresh their key

Read source record ↗

changeFix dual tray iconChecklist complete3 checked · 0 open

On Windows and macOS, two tray icons appear: a white static one (from tauri.conf.json `trayIcon` config) and a colorful tillandsia one (from programmatic `TrayIconBuilder` in main.rs). Only the programmatic one is clickable. The static one is a non-functional duplicate.

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1.1 Remove `trayIcon` section from `src-tauri/tauri.conf.json`
  • ☑ 1.2 Verify build succeeds
  • ☑ 1.3 App launches — verify single tray icon (awaiting user confirmation)

Read source record ↗

changeProposal: fix-entrypoint-agent-launchChecklist complete7 checked · 0 open

The default agent was recently switched from opencode to claude in both the Rust config (`SelectedAgent::default() -> Claude`) and the entrypoint script (`AGENT="${TILLANDSIAS_AGENT:-claude}"`). However, the entrypoint still unconditionally defines opencode paths and the tar extraction creates a doubled `opencode/opencode` path that fails to execute.

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ Read entrypoint.sh fully and identify all opencode references
  • ☑ Move OC_BIN definition inside install_opencode() / opencode branch
  • ☑ Fix doubled opencode/opencode path in tar extraction and binary reference
  • ☑ Make case statement handle claude and opencode explicitly
  • ☑ Verify shell configs (bashrc, zshrc, config.fish) don't have stale opencode PATH entries
  • ☑ Run cargo test --workspace to confirm no regressions
  • ☑ Mark tasks complete

Read source record ↗

changeFix gh auth macos pathChecklist complete6 checked · 0 open

`gh-auth-login.sh` hardcodes `CACHE_DIR="${HOME}/.cache/tillandsias"`. On macOS, the Rust code uses `~/Library/Caches/tillandsias` (via `dirs::cache_dir()`). This means auth credentials get written to the wrong directory and containers mount empty dirs, breaking GitHub authentication on macOS.

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1.1 `gh-auth-login.sh` — Replace hardcoded CACHE_DIR with platform detection
  • ☑ 1.2 `gh-auth-login.sh` — Update help text comment paths to mention both platforms
  • ☑ 1.3 `build-osx.sh` — Use macOS-native cache path
  • ☑ 1.4 `scripts/uninstall.sh` — Add platform detection for CACHE_DIR
  • ☑ 2.1 Move credentials from `~/.cache/tillandsias/secrets/` to `~/Library/Caches/tillandsias/secrets/` on macOS
  • ☑ 3.1 Run `cargo test --workspace` — 96 passed, 1 ignored

Read source record ↗

changeFix GitHub Login PermissionsChecklist complete5 checked · 0 open

When a user runs `gh auth login` inside the forge container, the git config directory is mounted read-only (`:ro`). This causes the operation to fail with:

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1. Change git config mount from `:ro` to `:rw` in `src-tauri/src/github.rs` (2 locations: clone args and `build_gh_run_args`)
  • ☑ 2. Change git config mount from `:ro` to `:rw` in `src-tauri/src/runner.rs`
  • ☑ 3. Change git config mount from `:ro` to `:rw` in `src-tauri/src/handlers.rs` (3 locations: build_run_args, single-project shell, multi-project shell)
  • ☑ 4. Change git config mount from `:ro` to `:rw` in `gh-auth-login.sh` (auth login command)
  • ☑ 5. Run tests — all pass

Read source record ↗

changeProposal: Fix macOS Finder Launch PATH IssueChecklist complete9 checked · 0 open

When Tillandsias is launched from Finder/Launchpad on macOS, the forge image build fails because `podman` is not in PATH. Finder-launched apps get a minimal PATH (`/usr/bin:/bin:/usr/sbin:/sbin`) that does not include Homebrew (`/opt/homebrew/bin`) or other common tool locations.

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ Diagnose: build, install, launch from Finder, read logs
  • ☑ Create OpenSpec artifacts
  • ☑ Add PATH augmentation to `build-image.sh` (macOS only, Linux unaffected)
  • ☑ Add `PODMAN` variable to `build-image.sh` that uses `$PODMAN_PATH` env or resolves absolute path
  • ☑ Replace bare `podman` calls in `build-image.sh` with `$PODMAN`
  • ☑ Pass `PODMAN_PATH` env var from `handlers.rs` when spawning build-image.sh
  • ☑ Pass `PODMAN_PATH` env var from `runner.rs` when spawning build-image.sh
  • ☑ Verify: rebuild, reinstall, launch from Finder, check logs
  • ☑ Run tests: `cargo test --workspace`

Read source record ↗

changeFix macOS OS DetectionChecklist complete3 checked · 0 open

When launching a forge environment on macOS, the welcome message shows "Unknown OS" because `detect_host_os()` only reads `/etc/os-release`, which does not exist on macOS. This produces a confusing `TILLANDSIAS_HOST_OS='Unknown OS'` env var passed to the forge.

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1. Add macOS detection branch to `detect_host_os()` in `crates/tillandsias-core/src/config.rs`
  • ☑ 2. Run `cargo test --workspace` — all tests pass
  • ☑ 3. Verify no regressions in Linux `/etc/os-release` path

Read source record ↗

changeFix macOS Platform DefaultsChecklist complete8 checked · 0 open

Several source files use Linux-only system calls, paths, or CLI flags that fail on macOS:

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1. `build_lock.rs`: Add `#[cfg(target_os)]` split for `is_alive()` — use `ps` on macOS, `/proc` on Linux
  • ☑ 2. `cleanup.rs`: Replace `du -sb` with pure-Rust recursive `dir_size_bytes()`
  • ☑ 3. `cleanup.rs`: Use `tillandsias_core::config::cache_dir()` and platform-aware binary path
  • ☑ 4. `uninstall.sh`: Add Darwin detection for `LIB_DIR` and `DATA_DIR`
  • ☑ 5. `gh-auth-login.sh`: Add Darwin detection for `DATA_DIR`
  • ☑ 6. `claude-api-key-prompt.sh`: Use `$TMPDIR` on macOS for temp key file
  • ☑ 7. `handlers.rs`: Use `std::env::temp_dir()` instead of `XDG_RUNTIME_DIR` with `/tmp` fallback
  • ☑ 8. Run `cargo test --workspace` to verify no regressions

Read source record ↗

changeFix macos updater symlinkChecklist complete3 checked · 0 open

The Tauri auto-updater fails on macOS when the app is launched via a symlink at `~/.local/bin/tillandsias` pointing to `~/Applications/Tillandsias.app/Contents/MacOS/tillandsias-tray`. Tauri's `StartingBinary` (from `tauri-utils`) uses a `#[ctor]` static that calls `std::env::current_exe()` and rejects any path containing a symlink on macOS, producing:

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1.1 Add `process-relaunch-dangerous-allow-symlink-macos` feature to `tauri` dependency in `src-tauri/Cargo.toml`
  • ☑ 2.1 `cargo test --workspace` passes (96 tests, 0 failures)
  • ☑ 2.2 `cargo check` passes with the new feature

Read source record ↗

changeFix opencode tar extractionChecklist complete2 checked · 0 open

The opencode tarball from GitHub releases contains a single bare file `opencode` (no directory wrapper). The entrypoint used `tar xzf ... --strip-components=1` which stripped the filename itself, extracting nothing. Result: `chmod` fails on the missing binary and the container exits.

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1.1 Remove `--strip-components=1` from tar extraction in entrypoint.sh
  • ☑ 1.2 Verified: tarball contains bare `opencode` file (no directory)

Read source record ↗

changeProposal: Complete --wipe cleanupChecklist complete7 checked · 0 open

1. **`scripts/uninstall.sh`** -- add comprehensive path removal in `--wipe` block 2. **`scripts/install.sh`** -- make the uninstall fallback message platform-aware

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ Read `scripts/uninstall.sh` and identify gaps
  • ☑ Read `config.rs` to find all platform-aware directories
  • ☑ Read `singleton.rs` and `build_lock.rs` for lock file paths
  • ☑ Read `secrets.rs` for keyring service/key names
  • ☑ Fix `scripts/uninstall.sh` --wipe to remove all paths, locks, and keyring entries
  • ☑ Fix `scripts/install.sh` fallback message to be platform-aware
  • ☑ Run tests

Read source record ↗

changeForge version labelingChecklist complete10 checked · 0 open

When debugging issues from logs alone, there is no way to tell which version of Tillandsias or which forge image tag is running. Version information is critical for triage: knowing "this was v0.1.89" vs "v0.1.88" immediately narrows the regression window.

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1.1 Add `TILLANDSIAS_FORGE_VERSION` to `build_run_args()` in `handlers.rs`
  • ☑ 1.2 Add `TILLANDSIAS_FORGE_VERSION` to terminal format string in `handle_terminal()` (handlers.rs)
  • ☑ 1.3 Add `TILLANDSIAS_FORGE_VERSION` to root terminal format string in `handle_root_terminal()` (handlers.rs)
  • ☑ 1.4 Add `TILLANDSIAS_FORGE_VERSION` to `build_run_args()` in `runner.rs`
  • ☑ 2.1 Show forge version in entrypoint.sh banner
  • ☑ 2.2 Show forge version in forge-welcome.sh display
  • ☑ 3.1 Add `CARGO_PKG_VERSION` to startup log line in `main.rs`
  • ☑ 3.2 Log forge image tag when launching environment in `handlers.rs`
  • ☑ 3.3 Add version to CLI "Attaching to" message in `runner.rs`
  • ☑ 4.1 `cargo test --workspace` passes

Read source record ↗

changeMacos native buildChecklist complete21 checked · 0 open

Tillandsias now needs to build and run on macOS (Apple Silicon). The existing `build.sh` uses Fedora toolbox which doesn't exist on macOS, and `build-windows.sh` cross-compiles from Linux. There's no native macOS build path. Additionally, `install.sh` downloads a `.dmg` on macOS but never mounts/extracts it — it prints "Installed to ~/.local/bin/tillandsias"

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1.1 Create `build-osx.sh` at project root with flag parsing for: (none), --release, --test, --check, --clean, --install, --remove, --wipe, --help
  • ☑ 1.2 Implement platform check (exit with error if not Darwin)
  • ☑ 1.3 Implement architecture detection (arm64 → aarch64-apple-darwin, x86_64 → x86_64-apple-darwin)
  • ☑ 1.4 Implement prerequisite checks (cargo, xcode-select) with install instructions
  • ☑ 1.5 Implement tauri-cli auto-installation if `cargo tauri --version` fails
  • ☑ 1.6 Implement default build: `cargo build --workspace`
  • ☑ 1.7 Implement --release: `cargo tauri build --target <arch>` with .dmg bundle
  • ☑ 1.8 Implement --test: `cargo test --workspace`
  • ☑ 1.9 Implement --check: `cargo check --workspace`
  • ☑ 1.10 Implement --clean: `cargo clean`
  • ☑ 1.11 Implement --install: release build + copy .app from bundle to ~/Applications/ + CLI symlink to ~/.local/bin/
  • ☑ 1.12 Implement --remove: remove ~/Applications/Tillandsias.app + CLI symlink
  • ☑ 1.13 Implement --wipe: remove target/, ~/.cache/tillandsias/
  • ☑ 1.14 Add unsigned build warning with xattr -cr hint
  • ☑ 1.15 Auto-increment build number via bump-version.sh
  • ☑ 2.1 Replace .dmg download-only with: hdiutil mount → find .app → cp to ~/Applications/ → detach → cleanup
  • ☑ 2.2 Create CLI symlink at ~/.local/bin/tillandsias → .app/Contents/MacOS/Tillandsias
  • ☑ 2.3 Remove manual .app bundle construction (Info.plist, sips icon conversion)
  • ☑ 2.4 Fix unconditional "Installed to ~/.local/bin/tillandsias" message (Linux-only now)
  • ☑ 2.5 Add error handling for mount/extract failures with fallback message
  • ☑ 3.1 Update CLAUDE.md Build Commands section with build-osx.sh usage

Read source record ↗

changeProposal: Auto-start podman machine at launchChecklist complete4 checked · 0 open

On macOS and Windows, podman requires a Linux VM (`podman machine`) to function. Currently, if the VM is not running when Tillandsias launches, the app drops into decay state and the user must manually start the machine via CLI. This is a poor experience -- most users expect the app to "just work" when podman is installed.

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1.1 Add `start_machine()` method to `PodmanClient` in `client.rs`
  • ☑ 2.1 Add auto-start logic in `main.rs` between machine detection and `podman_usable` computation
  • ☑ 3.1 `cargo test --workspace` passes (96 tests, 0 failures)
  • ☑ 3.2 Live test: debug build runs, log confirms normal startup (machine was already running, auto-start correctly skipped)

Read source record ↗

changePodman resilienceChecklist complete9 checked · 0 open

When podman is unavailable (not installed, or machine not running on macOS/Windows), the podman events listener enters a tight ~1s retry loop that spams logs, wastes CPU, and on Windows makes the tray icon flicker so fast users can't click "Quit" and must kill via Task Manager. Three root causes:

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1.1 Add `/opt/homebrew/bin/podman` and `/opt/local/bin/podman` to `find_podman_path()` in `lib.rs`
  • ☑ 2.1 Add attempt counter with reduced log frequency (every 5th attempt) in outer `stream()` loop
  • ☑ 2.2 Replace `podman events --help` reconnect check with `podman info --format json` in `backoff_inspect()`
  • ☑ 2.3 Log retry attempts at reduced frequency (every 5th attempt or on interval change)
  • ☑ 3.1 Skip podman events stream spawn in `main.rs` when `has_podman` is false or (needs_machine && !has_machine)
  • ☑ 3.2 Verified: event loop handles an empty/never-sending podman_rx channel gracefully (tokio::select branch never matches)
  • ☑ 4.1 Fix `is_machine_running()` — was matching `"Running"` key in JSON, now matches `"Running": true` value
  • ☑ 5.1 `./build-osx.sh --test` — 96 tests pass, 0 warnings
  • ☑ 5.2 Live test: app launches cleanly without podman machine running — zero retry spam, "Podman events stream skipped" logged once

Read source record ↗

changeFix install running appimageChecklist complete4 checked · 0 open

`curl -fsSL .../install.sh | bash` fails with `curl: (23) client returned ERROR on write` when the AppImage is already running. `curl -o` can't write directly to a running executable ("Text file busy").

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1. Download to temp file ($INSTALL_DIR/.tillandsias-download-$$)
  • ☑ 2. chmod +x on temp file
  • ☑ 3. mv -f temp → target (atomic rename, works even if target is running)
  • ☑ 4. Clean up temp file on download failure

Read source record ↗

changeFix entrypoint regressionChecklist complete14 checked · 0 open

Container entrypoints crash silently on launch due to `set -e` (errexit) interactions with bash function return codes. Two bugs:

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1.1 Add `return 0` at end of `find_project_dir()` in `images/default/lib-common.sh`
  • ☑ 1.2 Add comment explaining why the explicit return is needed
  • ☑ 2.1 Change `return 1` to `return 0` on curl failure in `install_opencode()`
  • ☑ 2.2 Wrap `tar xzf` in `if !` guard with error message and `return 0`
  • ☑ 2.3 Add `|| true` to `chmod +x` and `2>/dev/null` redirect
  • ☑ 2.4 Clean up temp tarball on extraction failure
  • ☑ 3.1 Move `tar xzf` into the `if curl` compound condition (curl && tar)
  • ☑ 3.2 Add `|| true` to `chmod +x` in the update path
  • ☑ 3.3 Clean up temp tarball on failure path
  • ☑ 4.1 `./build.sh --check` passes (embedded.rs recompiles with changed scripts)
  • ☑ 4.2 `./build.sh --test` passes (all 142 tests)
  • ☑ 4.3 `./scripts/build-image.sh forge --force` succeeds
  • ☑ 4.4 Container entrypoint survives with empty `$HOME/src/` (no project mounted)
  • ☑ 4.5 Container entrypoint works with project mounted (normal launch path)

Read source record ↗

changeCondense log formatChecklist complete11 checked · 0 open

The default tracing-subscriber `.pretty()` formatter dumps accountability metadata (`accountability: true, category: "secrets", safety: "...", spec: "..."`) inline with the human message, then repeats it in the span context section. This makes logs hard to read and buries the safety/trace information that power users need to feel confident about how their da

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1.1 Create `src-tauri/src/log_format.rs` with `EventFields` visitor that classifies fields into accountability metadata vs regular fields
  • ☑ 1.2 Implement `TillandsiasFormat` struct with `FormatEvent<S, N>` trait — compact format for regular events, structured multi-line for accountability events
  • ☑ 1.3 Add `shorten_target()` helper and `level_ansi()` color helper
  • ☑ 1.4 Add unit tests for `shorten_target` and field classification
  • ☑ 2.1 Add `mod log_format;` to `src-tauri/src/main.rs`
  • ☑ 2.2 Update `logging::init()` — replace default file format with `.event_format(TillandsiasFormat)`
  • ☑ 2.3 Update `logging::init()` — replace `.pretty()` stderr format with `.event_format(TillandsiasFormat)`
  • ☑ 2.4 Remove `AccountabilityLayer` construction and `.with(accountability_layer)` from subscriber stack
  • ☑ 3.1 Build with `cargo build --workspace` — no compile errors
  • ☑ 3.2 Run `cargo test --workspace` — all existing tests pass
  • ☑ 3.3 Run `cargo clippy --workspace` — no new warnings

Read source record ↗

changeFix nix build staleness divergenceChecklist complete6 checked · 0 open

The `_compute_hash()` function in `build-image.sh` uses `find` to traverse image source directories, which sees all working tree files including untracked ones. But Nix flake builds only see git-tracked files. This divergence means: (1) staleness check triggers on untracked files that Nix ignores, causing unnecessary rebuilds, and (2) staleness check can rep

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1.1 Rewrite `_compute_hash()` in `scripts/build-image.sh` to use `git ls-files` instead of `find` for enumerating files in `images/default/` and `images/web/`
  • ☑ 1.2 Add git-repo detection: if `git rev-parse --is-inside-work-tree` fails, fall back to `find` with a warning
  • ☑ 1.3 Add untracked file detection: before hashing, check `git ls-files --others --exclude-standard` in image source dirs and fail with clear error if any found
  • ☑ 2.1 Test: create an untracked file in `images/default/`, run `build-image.sh`, confirm it fails with the untracked file error
  • ☑ 2.2 Test: stage the file with `git add`, run `build-image.sh`, confirm staleness check includes it
  • ☑ 2.3 Test: confirm normal build (no untracked files) works as before

Read source record ↗

changeFix seccomp spec accuracyChecklist complete5 checked · 0 open

The podman-orchestration spec contains a misleading "Seccomp profile compatibility" scenario that documents *awareness* of a problem rather than a solution. It says "the application is aware that the default profile blocks approximately 130 syscalls" and "logs should indicate seccomp as a possible cause." This is a hack — a try-catch mentality instead of a p

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1.1 Remove "Seccomp profile compatibility" scenario from Security-hardened container defaults requirement in `openspec/specs/podman-orchestration/spec.md`
  • ☑ 1.2 Add "Seccomp close_range elimination" scenario to FUSE FD sanitization requirement in `openspec/specs/podman-orchestration/spec.md`
  • ☑ 2.1 Confirm pre_exec FD sanitization in `crates/tillandsias-podman/src/lib.rs` closes FDs >= 3 (matching spec)
  • ☑ 2.2 Confirm no seccomp "awareness" logging exists in the codebase that should also be removed
  • ☑ 3.1 Run `cargo check --workspace` to confirm no compilation impact

Read source record ↗

changeProposal: Windows Full SupportChecklist complete25 checked · 0 open

Tillandsias had no working Windows support despite having cross-compilation and CI builds for Windows. Multiple issues prevented the app from functioning on a Windows host:

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ Fix install.ps1 — TLS 1.2, correct NSIS asset name, silent install
  • ☑ Add Podman CLI detection + winget install to install.ps1
  • ☑ Add podman machine init + start to install.ps1
  • ☑ Update uninstall.ps1 for NSIS uninstaller
  • ☑ Add write_lf() helper to embedded.rs — strip \r from all embedded file writes
  • ☑ Replace all fs::write calls in write_image_sources() with write_lf()
  • ☑ Replace all fs::write calls in write_temp_script() with write_lf()
  • ☑ Add bash dispatch for .sh scripts in runner.rs
  • ☑ Add bash dispatch for .sh scripts in handlers.rs
  • ☑ Add bash dispatch for .sh scripts in init.rs
  • ☑ Fix open_terminal on Windows to detect .sh files and use bash
  • ☑ Add has_machine() to PodmanClient
  • ☑ Add init_machine() to PodmanClient
  • ☑ Add machine init/start to tray app launch (main.rs)
  • ☑ Add machine init/start to CLI runner (runner.rs)
  • ☑ Add Windows detection to detect_host_os() in config.rs
  • ☑ Add menu fingerprinting to skip no-op rebuilds (main.rs)
  • ☑ Convert i18n STRINGS from LazyLock to RwLock
  • ☑ Add i18n::reload() for live language switching
  • ☑ Add I18N_GENERATION counter for menu fingerprint invalidation
  • ☑ Call i18n::reload() from SelectLanguage event handler
  • ☑ Create Windows setup cheatsheet
  • ☑ Create OpenSpec install cheatsheet
  • ☑ Verify all three CLI modes on Windows (default, --bash, --claude)
  • ☑ Run full test suite (76 tests pass)

Read source record ↗

changeProposal: Windows Runtime FixesChecklist complete19 checked · 0 open

After the initial `windows-full-support` change shipped, seven runtime issues surfaced during real-world testing on Windows 11:

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ Remove `#![windows_subsystem = "windows"]` attribute from main.rs
  • ☑ Add `FreeConsole()` call gated on no-args check in main.rs
  • ☑ Add `windows-sys` dependency for Win32 Console API
  • ☑ Change `write_image_sources()` to use `image-sources-{pid}` directory in embedded.rs
  • ☑ Change `cleanup_image_sources()` to use matching `image-sources-{pid}` directory
  • ☑ Add `cfg!(target_os = "windows")` branch to `shell_quote_join()` using double quotes in launch.rs
  • ☑ Handle empty-string quoting for Windows in `shell_quote_join()`
  • ☑ Add `wt.exe` as preferred terminal in `open_terminal()` Windows branch in handlers.rs
  • ☑ Add `cmd /c start` fallback when `wt.exe` is not available
  • ☑ Add WSL2 detection to install.ps1 (`Get-Command wsl`)
  • ☑ Add `wsl --install --no-distribution` when WSL is missing
  • ☑ Add `wsl --set-default-version 2` to ensure WSL2 is default
  • ☑ Add `tillandsias.exe` alias copy from `tillandsias-tray.exe` in install.ps1
  • ☑ Add `CREATE_NO_WINDOW` flag to `podman_cmd()` (async) in tillandsias-podman/src/lib.rs
  • ☑ Add `CREATE_NO_WINDOW` flag to `podman_cmd_sync()` in tillandsias-podman/src/lib.rs
  • ☑ Add `CREATE_NO_WINDOW` flag to build-image bash dispatch in handlers.rs
  • ☑ Verify CLI output works in PowerShell, cmd.exe, and Windows Terminal
  • ☑ Verify tray launch hides console window
  • ☑ Verify concurrent tray + CLI invocations don't corrupt image sources

Read source record ↗

changeAdd knowledge source of truthChecklist complete16 checked · 0 open

OpenSpec guarantees implementation converges to spec. But what keeps the spec itself correct? Today, agents infer best practices from training data — non-deterministic and uncitable. A local, cached source of truth for the tech stack means agents cite verified facts instead of guessing. This completes the convergence chain: implementation → spec → ground tru

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1.1 Create `knowledge/` directory with `cheatsheets/{infra,lang,frameworks,packaging,formats,ci}` subdirs
  • ☑ 1.2 Create `knowledge/manifest.toml` with schema and metadata
  • ☑ 1.3 Create `knowledge/README.md` explaining purpose and format
  • ☑ 1.4 Add `vendor/debug/` to `.gitignore`
  • ☑ 2.1 Create `knowledge/index.xml` with category and tag structure for Tier 1 cheatsheets
  • ☑ 3.1 Create `cheatsheets/infra/podman-rootless.md` — user namespaces, keep-id, rootless networking
  • ☑ 3.2 Create `cheatsheets/infra/podman-security.md` — cap-drop, no-new-privileges, seccomp, SELinux
  • ☑ 3.3 Create `cheatsheets/infra/oci-runtime-spec.md` — crun vs runc, /proc/self/fd, namespace lifecycle
  • ☑ 3.4 Create `cheatsheets/infra/linux-namespaces.md` — user, mount, PID, network; unshare; nsenter
  • ☑ 3.5 Create `cheatsheets/packaging/nix-flakes.md` — flake structure, inputs/outputs, dockerTools
  • ☑ 3.6 Create `cheatsheets/lang/rust-async.md` — tokio, select!, spawn, channels, pinning
  • ☑ 4.1 Create `scripts/fetch-debug-source.sh` with manifest-driven fetching
  • ☑ 4.2 Create `scripts/debug-sources.toml` with repo URLs and default versions
  • ☑ 5.1 Validate all cheatsheets have correct YAML frontmatter
  • ☑ 5.2 Validate index.xml is well-formed and references all cheatsheets
  • ☑ 5.3 Run `./build.sh --check` to confirm no build impact

Read source record ↗

changeCli version and init flagsChecklist complete14 checked · 0 open

The CLI has no `--version` flag, making it impossible to check the installed version from the command line. Additionally, `init` is the only subcommand-style argument — all others use `--flag` style (`--update`, `--stats`, `--clean`). This inconsistency is confusing.

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1.1 Add `Version` variant to `CliMode` enum in `cli.rs`
  • ☑ 1.2 Add --version parsing before other flags in `parse()`
  • ☑ 1.3 Add --version to USAGE string
  • ☑ 1.4 Handle `CliMode::Version` in `main.rs` — print version and exit
  • ☑ 2.1 Change `init` parsing from positional subcommand to `--init` flag in `cli.rs`
  • ☑ 2.2 Update USAGE string to show `--init` instead of `init`
  • ☑ 2.3 Update CLAUDE.md CLI reference (already used `--init` syntax)
  • ☑ 3.1 Update `scripts/install.sh` — changed `tillandsias init` to `tillandsias --init`
  • ☑ 3.2 Update other references: `init.rs` doc comment, `runner.rs` comment, `locales/en.toml` comment, `openspec/specs/init-command/spec.md`, `openspec/specs/dev-build/spec.md`
  • ☑ 4.1 Run `./build-osx.sh --check` — compilation passes
  • ☑ 4.2 `tillandsias --version` prints `Tillandsias v0.1.97`
  • ☑ 4.3 `tillandsias -V` also works
  • ☑ 4.4 `tillandsias --update` reports "Already up to date."
  • ☑ 4.5 `tillandsias --help` shows updated usage with `--init` and `--version`

Read source record ↗

changeDefault agent opencodeChecklist complete1 checked · 0 open

2026-04-05-default-agent-opencode

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1.1 Change SelectedAgent::default() from Claude to OpenCode in config.rs

Read source record ↗

changeFedora 43 baseChecklist complete2 checked · 0 open

2026-04-05-fedora-43-base

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1.1 Change images/default/Containerfile FROM to fedora-minimal:43
  • ☑ 1.2 Verify flake.nix doesn't reference fedora-minimal (confirm Nix build is independent)

Read source record ↗

changeFix appimage fuse fd leakChecklist complete7 checked · 0 open

When Tillandsias runs as an AppImage on Linux, the squashfuse FUSE mount creates file descriptors that are inherited by child processes. When `podman run` is invoked, crun (the OCI runtime) iterates `/proc/self/fd/` and attempts to stat each FD. The kernel denies access to the FUSE FDs across the user namespace boundary, producing:

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1.1 Add `libc` dependency to `tillandsias-podman` crate's Cargo.toml (Linux target only)
  • ☑ 1.2 Add `pre_exec` FD cleanup hook to `podman_cmd_sync()` in `crates/tillandsias-podman/src/lib.rs`
  • ☑ 1.3 Add `pre_exec` FD cleanup hook to `podman_cmd()` (async variant) in the same file
  • ☑ 1.4 Gate the pre_exec blocks with `#[cfg(target_os = "linux")]`
  • ☑ 2.1 Run `./build.sh --check` — compilation succeeds with no new warnings
  • ☑ 2.2 Run `./build.sh` — full debug build succeeds
  • ☑ 2.3 Run `tillandsias . --bash` from the dev build to verify container launch works

Read source record ↗

changeFix embedded localesChecklist complete6 checked · 0 open

The i18n infrastructure commit added `images/default/locales/` (en.sh, es.sh) and updated `flake.nix` to reference them, but `embedded.rs` was never updated to embed and extract those locale files. When the installed binary runs `build-image.sh`, the extracted temp dir is missing `images/default/locales/`, causing `nix build` to fail with: `error: path '...-

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1.1 Add `include_str!` constants for `images/default/locales/en.sh` and `images/default/locales/es.sh` in `src-tauri/src/embedded.rs`
  • ☑ 2.1 Create `images/default/locales/` directory in `write_image_sources()`
  • ☑ 2.2 Write `en.sh` and `es.sh` into the locales directory
  • ☑ 2.3 Update the doc comment directory tree to include `locales/{en.sh,es.sh}`
  • ☑ 3.1 Run `./build.sh --check` to confirm compilation succeeds on all targets
  • ☑ 3.2 Run `tillandsias init` to confirm the installed binary builds the forge image successfully

Read source record ↗

changeFix macos terminal fallbackChecklist complete12 checked · 0 open

When a user installs a non-default terminal emulator on macOS (Ghostty, Kitty, etc.), clicking "Attach Here" from the system tray silently fails. The terminal window never opens, but the tray reports success. The log shows:

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1.1 Add CLI terminal detection loop (ghostty, kitty, alacritty, wezterm) via `which` + direct `.spawn()`
  • ☑ 1.2 Add iTerm2 AppleScript fallback with `.output()` error capture
  • ☑ 1.3 Change Terminal.app AppleScript from `.spawn()` to `.output()` with error propagation
  • ☑ 1.4 Add `tracing::debug`/`tracing::warn` logging for terminal selection and failures
  • ☑ 2.1 Create `macos-app-launch-env.md` — DMG launch env, PATH isolation, TCC permissions
  • ☑ 2.2 Create `terminal-fallback-chain.md` — per-platform chain architecture, error detection
  • ☑ 3.1 Remove Ghostty detection and `open -na` launch path from `open_terminal()`
  • ☑ 3.2 Remove CLI terminal detection loop (kitty, alacritty, wezterm) from `open_terminal()`
  • ☑ 3.3 Remove iTerm2 AppleScript path from `open_terminal()`
  • ☑ 3.4 Update error message to reference Terminal.app only
  • ☑ 3.5 `cargo check --workspace` compiles clean
  • ☑ 3.6 Build and test tray launch confirms Terminal.app opens

Read source record ↗

changeFix opencode installerChecklist complete7 checked · 0 open

The OpenCode entrypoint manually downloads a tar.gz from GitHub releases, extracts with --strip-components=1, and chmod's the binary. This is fragile — the tar structure has broken before, and the update logic duplicates download/extract code. OpenCode has an official installer at `curl -fsSL https://opencode.ai/install | bash` that handles both install and

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1.1 Replace `install_opencode()` and `update_opencode()` with single `ensure_opencode()` using official `curl -fsSL https://opencode.ai/install | bash`
  • ☑ 1.2 Set `OPENCODE_INSTALL_DIR=$CACHE/opencode` so binary persists in cache
  • ☑ 1.3 OC_BIN path unchanged (`$CACHE/opencode/bin/opencode`) — official installer respects OPENCODE_INSTALL_DIR
  • ☑ 1.4 Daily throttle via `needs_update_check`/`record_update_check` preserved
  • ☑ 2.1 `./build-osx.sh --check` compiles clean
  • ☑ 2.2 `./build-osx.sh --test` — all tests pass
  • ☑ 2.3 Installed release build

Read source record ↗

changeFix spec audit warningsChecklist complete19 checked · 0 open

A knowledge audit against the spec corpus found 6 WARNING-level inaccuracies and 8 SUGGESTION-level gaps where specs omit relevant operational details. Left unfixed, warnings cause implementation drift — developers trusting the spec will make wrong decisions about platform backends, library names, SELinux interactions, Nix build behavior, and CI environment

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1.1 In `openspec/specs/filesystem-scanner/spec.md`, change "kqueue" to "FSEvents" in the macOS scenario and requirement text. Reference `RecommendedWatcher` as the selection mechanism.
  • ☑ 1.2 In the debounced event batching requirement, change "default: 2000ms" to "project default: 2000ms" and add clarifying text that this is a project choice, not a crate default.
  • ☑ 2.1 In `openspec/specs/tray-app/spec.md`, change the Linux tray scenario from "StatusNotifier/libappindicator" to "DBus StatusNotifierItem (libayatana-appindicator)".
  • ☑ 3.1 In `openspec/specs/podman-orchestration/spec.md`, add explanatory text to the volume mount requirement that `--security-opt=label=disable` makes `:z`/`:Z` unnecessary.
  • ☑ 3.2 Add a new scenario "SELinux relabeling not required" to the volume mount requirement.
  • ☑ 4.1 In `openspec/specs/nix-builder/spec.md`, add a new requirement "Git-tracked files for flake builds" with scenarios covering untracked file exclusion and staged file inclusion.
  • ☑ 5.1 In `openspec/specs/ci-release/spec.md`, add a validation caveat to the Node.js 24 requirement noting that `FORCE_JAVASCRIPT_ACTIONS_TO_NODE24` is not documented in the verified knowledge base.
  • ☑ 5.2 Add a new "Upstream validation" scenario for periodic verification of the env var.
  • ☑ 6.1 Run `/opsx:verify` to confirm all warning-level delta specs apply cleanly and no new warnings are introduced.
  • ☑ 7.1 In `openspec/specs/podman-orchestration/spec.md`, add `--init` to the "Default container launch" scenario's flag list in the security-hardened container defaults requirement.
  • ☑ 7.2 Add a new scenario "Seccomp profile compatibility" documenting that the default seccomp profile blocks ~130 syscalls and that some profiles may block `close_range()`.
  • ☑ 8.1 In `openspec/specs/podman-orchestration/spec.md`, add a new requirement "Rootless networking backend" documenting that Podman 5.0+ defaults to pasta instead of slirp4netns.
  • ☑ 9.1 In `openspec/specs/filesystem-scanner/spec.md`, add a scenario "inotify watch limit exhausted on Linux" to the OS-native event-driven watching requirement, documenting behavior when `fs.inotify.max_user_watches` is reached.
  • ☑ 10.1 In `openspec/specs/update-system/spec.md`, add a scenario "AppImage on immutable OS without FUSE" documenting that `APPIMAGE_EXTRACT_AND_RUN=1` should be set when FUSE is unavailable.
  • ☑ 11.1 In `openspec/specs/environment-runtime/spec.md`, add a scenario "Platform-specific config paths (Windows)" documenting the `%APPDATA%\tillandsias\config.toml` path.
  • ☑ 11.2 Add a scenario "Platform-specific config paths (Linux)" explicitly documenting the `~/.config/tillandsias/config.toml` path for completeness.
  • ☑ 12.1 In `openspec/specs/binary-signing/spec.md`, change `.cosign.sig`/`.cosign.cert` to `.sig`/`.cert` in the "Cosign signing produces verifiable signatures" requirement to match the "Signature and certificate artifacts" requirement.
  • ☑ 13.1 In `openspec/specs/nix-builder/spec.md`, add a new requirement "Preferred dockerTools API usage" noting that `copyToRoot` is the preferred attribute over the legacy `contents` alias in `dockerTools.buildLayeredImage`.
  • ☑ 14.1 Run `/opsx:verify` to confirm all delta specs (warnings + suggestions) apply cleanly and no new issues are introduced.

Read source record ↗

changeLanguage menu and localeChecklist complete20 checked · 0 open

2026-04-05-language-menu-and-locale

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1.1 Add `language: String` field to GlobalConfig in config.rs (default from detect_locale or "en")
  • ☑ 1.2 Add `save_selected_language()` function in config.rs
  • ☑ 1.3 Add `ContextKey::Language` variant to container_profile.rs
  • ☑ 1.4 Add LANG and LANGUAGE env vars to common_forge_env() and terminal_profile() in container_profile.rs
  • ☑ 1.5 Add language_to_lang_value() helper that maps code to full LANG (e.g., "ja" -> "ja_JP.UTF-8")
  • ☑ 2.1 Create stub TOML files for all new languages (ja, zh-Hant, zh-Hans, ar, ko, hi, ta, te, fr, pt, it, ro, ru, nah) — each with just [app] name = "Tillandsias"
  • ☑ 2.2 Update i18n.rs: add all locale includes, expand SUPPORTED_LOCALES, update STRINGS init to load selected language
  • ☑ 2.3 Create stub shell locale files for container-side (images/default/locales/) for each new language
  • ☑ 2.4 Update embedded.rs to include new shell locale files
  • ☑ 3.1 Add `build_language_submenu()` to menu.rs with all 16 languages in native script
  • ☑ 3.2 Add Language submenu to Settings (after Seedlings, before Version)
  • ☑ 3.3 Add `select_lang` ID helpers to menu::ids module
  • ☑ 3.4 Add `MenuCommand::SelectLanguage` variant and handle in event_loop.rs
  • ☑ 3.5 Update en.toml and es.toml with new key `menu.language` = "Language"/"Idioma"
  • ☑ 4.1 Resolve ContextKey::Language in launch.rs build_podman_args to the full LANG value
  • ☑ 4.2 Update LaunchContext to carry selected_language field
  • ☑ 4.3 Update handlers.rs build_launch_context to read language from global config
  • ☑ 5.1 Update test forge_profiles_have_four_env_vars to expect 6 (LANG, LANGUAGE added)
  • ☑ 5.2 Update test terminal_has_three_env_vars to expect 5
  • ☑ 5.3 Update test every_en_key_exists_in_es to also check new keys

Read source record ↗

changeOpenspec in terminalsChecklist complete5 checked · 0 open

2026-04-05-openspec-in-terminals

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1.1 Add install_openspec() function to lib-common.sh with the npm install logic
  • ☑ 1.2 Remove inline OpenSpec install from entrypoint-forge-claude.sh, replace with install_openspec call
  • ☑ 1.3 Remove inline OpenSpec install from entrypoint-forge-opencode.sh, replace with install_openspec call
  • ☑ 2.1 Add install_openspec call to entrypoint-terminal.sh (after lib-common.sh source, before welcome)
  • ☑ 2.2 Add openspec init in terminal entrypoint (non-interactive, --tools terminal flag or no flag)

Read source record ↗

changeRemove api key auth and fix welcomeChecklist complete26 checked · 0 open

The Claude API key authentication flow is a legacy artifact from before OAuth via `~/.claude/` was implemented. It adds complexity (keyring storage, embedded prompt script, polling loop, env var injection) for a path that most users don't need — Max/Pro subscribers authenticate via `claude login` on the host, and the resulting `~/.claude/` directory is alrea

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1.1 Remove `SecretKind::ClaudeApiKey` variant from `container_profile.rs`
  • ☑ 1.2 Remove `claude_api_key: Option<String>` from `LaunchContext` (now non-optional `claude_dir: PathBuf`)
  • ☑ 1.3 Remove `ClaudeApiKey` handling from `build_podman_args()` in `launch.rs`
  • ☑ 1.4 Remove `claude_api_key` population in `runner.rs` and `handlers.rs`
  • ☑ 1.5 Ensure `~/.claude/` is always created and mounted (even on first run before auth)
  • ☑ 2.1 Remove `store_claude_api_key()` and `retrieve_claude_api_key()` from `secrets.rs`
  • ☑ 2.2 Remove `CLAUDE_API_KEY_KEY` constant from `secrets.rs`
  • ☑ 2.3 Delete `claude-api-key-prompt.sh`
  • ☑ 2.4 Remove embedded `CLAUDE_API_KEY_PROMPT` constant from `embedded.rs`
  • ☑ 3.1 Replace `ClaudeLogin` with `ClaudeResetCredentials` variant in `MenuCommand` in `event.rs`
  • ☑ 3.2 Remove `handle_claude_login()`, add `handle_claude_reset_credentials()` in `handlers.rs`
  • ☑ 3.3 Replace `ClaudeLogin` dispatch with `ClaudeResetCredentials` in `event_loop.rs`
  • ☑ 3.4 Replace `CLAUDE_LOGIN` with `CLAUDE_RESET_CREDENTIALS` menu ID constant
  • ☑ 3.5 Update dispatch in `handle_menu_click()` in `main.rs`
  • ☑ 4.1 Replace Claude Login/Refresh menu item with "Claude Reset Credentials" in Seedlings submenu
  • ☑ 4.2 Only show when `~/.claude/` exists and has content (lock icon)
  • ☑ 4.3 Handler removes `~/.claude/` contents, sends notification, rebuilds menu
  • ☑ 5.1 Remove API key capture/scrub from `entrypoint-forge-claude.sh` (`_CLAUDE_KEY`, `ANTHROPIC_API_KEY`)
  • ☑ 5.2 Simplify final `exec` — no conditional key injection, just `exec "$CC_BIN" "$@"`
  • ☑ 6.1 Export `TILLANDSIAS_WELCOME_SHOWN=1` in `entrypoint-terminal.sh` before `exec fish`
  • ☑ 7.1 Replace `claude.login` / `claude.login_refresh` with `claude.reset_credentials` in `en.toml`
  • ☑ 7.2 Same for `es.toml`
  • ☑ 7.3 Replace `claude_key_saved` notification with `claude_credentials_cleared`
  • ☑ 8.1 `./build-osx.sh --check` compiles clean
  • ☑ 8.2 `./build-osx.sh --test` — all 55 tests pass
  • ☑ 8.3 Installed and verified with `tillandsias --version`

Read source record ↗

changeTerminal tooling cheatsheetChecklist complete6 checked · 0 open

2026-04-05-terminal-tooling-cheatsheet

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1.1 Add new packages to flake.nix forge image contents list
  • ☑ 1.2 Add @trace spec:forge-shell-tools comment near package list
  • ☑ 2.1 Add NPM_CONFIG_PREFIX, CARGO_HOME, GOPATH, PIP_USER, PYTHONUSERBASE exports to lib-common.sh
  • ☑ 2.2 Add cache tool bin dirs to PATH in lib-common.sh
  • ☑ 2.3 Update shell configs (bashrc, config.fish, zshrc) with same exports
  • ☑ 3.1 Create docs/cheatsheets/terminal-tools.md with tool catalog, package management patterns, and best practices

Read source record ↗

changeWelcome screen cleanupChecklist complete7 checked · 0 open

2026-04-05-welcome-screen-cleanup

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1.1 In entrypoint-forge-claude.sh, redirect all [lifecycle] echo lines to stderr
  • ☑ 1.2 In entrypoint-forge-opencode.sh, redirect all [lifecycle] echo lines to stderr
  • ☑ 1.3 In entrypoint-terminal.sh, redirect all [lifecycle] echo lines to stderr
  • ☑ 2.1 In forge-welcome.sh, replace D_BLUE with B_BLUE (bright blue, \033[1;94m) for mount source paths
  • ☑ 2.2 Add ramdisk color (B_MAGENTA, \033[1;95m) and use it for token/secret mount display
  • ☑ 2.3 Add "* ramdisk" legend line after mounts section, aligned with mount paths, using B_MAGENTA
  • ☑ 3.1 Ensure embedded.rs still compiles (include_str picks up changes automatically)

Read source record ↗

changeProposal: Add Missing @trace AnnotationsChecklist complete12 checked · 0 open

A methodology audit found that 6 Rust source files have zero `@trace` annotations despite implementing specs that exist. This breaks the bidirectional link between specs and implementation that OpenSpec requires for monotonic convergence.

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ Read each file to understand what spec it implements
  • ☑ Add `@trace spec:update-system` to `update_cli.rs` (3 traces)
  • ☑ Add `@trace spec:podman-orchestration` to `events.rs` (2 traces)
  • ☑ Add `@trace spec:cli-mode` to `cleanup.rs` (3 traces)
  • ☑ Add `@trace spec:update-system` to `updater.rs` (3 traces)
  • ☑ Add `@trace spec:singleton-guard` to `singleton.rs` (2 traces)
  • ☑ Add `@trace spec:remote-projects, spec:gh-auth-script` to `github.rs` (3 traces)
  • ☑ Verify `build_lock.rs` already traced (confirmed: `spec:build-lock`)
  • ☑ Verify `embedded.rs` already traced (confirmed: 8+ traces)
  • ☑ Confirm `machine.rs` and `web.rs` do not exist
  • ☑ Run `cargo test --workspace` to verify no breakage
  • ☑ Create OpenSpec change artifacts

Read source record ↗

changeConfig overlay and inferenceChecklist complete17 checked · 0 open

Forge containers launch with a bare OpenCode config that has no development methodology, no Flutter-first recommendations, and a hardcoded model tier that ignores the host GPU. Agents inside forges start from zero context every time -- they don't know about OpenSpec, monotonic convergence, CRDT patterns, or that we prefer Flutter for cross-platform apps. Mea

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ **M-1**: Create `images/default/config-overlay/opencode/instructions/methodology.md` -- global instructions covering monotonic convergence, CRDT patterns, Flutter-first recommendations, OpenSpec workflows, @trace annotations, code quality, git workflow. @trace spec:layered-tools-overlay
  • ☑ **M-2**: Create `images/default/config-overlay/opencode/instructions/flutter.md` -- Flutter-specific instructions covering clean architecture, Riverpod, GoRouter, i18n, Material 3, testing strategy. @trace spec:layered-tools-overlay
  • ☑ **M-3**: Update `images/default/config-overlay/opencode/config.json` -- add global `""` instruction key with condensed methodology, enrich `**/*.dart` instructions with Flutter architecture details. @trace spec:layered-tools-overlay
  • ☑ **M-4**: Add `CONFIG_OVERLAY_INSTRUCTIONS_METHODOLOGY` and `CONFIG_OVERLAY_INSTRUCTIONS_FLUTTER` constants to `src-tauri/src/embedded.rs` via `include_str!`. @trace spec:layered-tools-overlay
  • ☑ **M-5**: Extract instruction files in `extract_config_overlay()` to `config-overlay/opencode/instructions/` on ramdisk. @trace spec:layered-tools-overlay
  • ☑ **M-6**: Extract instruction files in `write_image_sources()` to `images/default/config-overlay/opencode/instructions/` for image builds. @trace spec:layered-tools-overlay
  • ☑ **MCP-1**: Create `images/default/config-overlay/mcp/git-tools.sh` -- MCP server script providing git status, diff, log tools to agents. @trace spec:layered-tools-overlay
  • ☑ **MCP-2**: Create `images/default/config-overlay/mcp/project-info.sh` -- MCP server script providing project language/framework detection. @trace spec:layered-tools-overlay
  • ☑ **MCP-3**: Add MCP server entries to `config.json` pointing to `/home/forge/.config-overlay/mcp/` scripts. @trace spec:layered-tools-overlay
  • ☑ **MCP-4**: Add `CONFIG_OVERLAY_MCP_GIT_TOOLS` and `CONFIG_OVERLAY_MCP_PROJECT_INFO` constants to `embedded.rs`. @trace spec:layered-tools-overlay
  • ☑ **MCP-5**: Extract MCP scripts in `extract_config_overlay()` with executable permissions. @trace spec:layered-tools-overlay
  • ☑ **MCP-6**: Extract MCP scripts in `write_image_sources()` for image builds. @trace spec:layered-tools-overlay
  • ☑ **GPU-1**: Add GPU detection function to `src-tauri/src/handlers.rs` -- detect NVIDIA/AMD GPUs via `lspci`, `nvidia-smi`, or sysfs. Classify into tiers: none, low (<=4GB VRAM), mid (4-12GB), high (>=12GB). @trace spec:inference-container
  • ☑ **GPU-2**: Define model tier mapping -- high: `qwen2.5:7b`/`qwen2.5:7b`, mid: `qwen2.5:3b`/`qwen2.5:1.5b`, low: `qwen2.5:1.5b`/`qwen2.5:0.5b`, none: `qwen2.5:0.5b`/`qwen2.5:0.5b`. @trace spec:inference-container
  • ☑ **GPU-3**: Patch config overlay JSON at runtime -- after extracting static config, overwrite model fields based on detected GPU tier. Write patched config.json back to ramdisk. @trace spec:layered-tools-overlay, spec:inference-container
  • ☑ **GPU-4**: Log GPU detection results at enclave startup -- tier, GPU name, VRAM, selected models. @trace spec:inference-container
  • ☑ **GPU-5**: Add integration test -- verify model fields in config.json change based on simulated GPU tiers. @trace spec:inference-container

Read source record ↗

changeContainer Process HardeningChecklist complete15 checked · 0 open

Security audit found that containers lack process limits and per-container process restrictions. All containers share default seccomp but have no `--pids-limit` (fork bomb risk) and no filesystem immutability enforcement for service containers.

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1.1 Add `pids_limit: u32`, `read_only: bool`, `tmpfs_mounts: Vec<&'static str>` to `ContainerProfile` struct
  • ☑ 1.2 Set per-profile values: forge/terminal=512, git=64, proxy=32, inference=128, web=32
  • ☑ 1.3 Set `read_only=true` + tmpfs mounts for git, proxy, inference, web profiles
  • ☑ 1.4 Add unit tests verifying all profiles have correct limits and read-only settings
  • ☑ 2.1 Add `--pids-limit=N` to non-negotiable security flags section
  • ☑ 2.2 Add `--read-only` + `--tmpfs` flags when profile.read_only is true
  • ☑ 2.3 Update web container's manually-built podman command string with pids-limit and read-only
  • ☑ 2.4 Add integration tests for pids-limit, read-only, tmpfs in args output
  • ☑ 3.1 Add `@trace spec:secret-management` log at D-Bus forwarding point in launch.rs
  • ☑ 3.2 Update forge/terminal credential-free logs to include pids-limit and D-Bus absence
  • ☑ 3.3 Add git service launch log noting D-Bus access + pids-limit + read-only
  • ☑ 3.4 Add proxy launch log noting CA-certs-only + pids-limit + read-only
  • ☑ 4.1 Add "Process isolation and hardening" requirement to secret-management spec
  • ☑ 4.2 Add scenarios for git service, forge, proxy, inference, web containers
  • ☑ 5.1 Run `cargo test --workspace`

Read source record ↗

changeCreate secret management specChecklist complete10 checked · 0 open

An audit found 16 `@trace spec:secret-management` annotations across the codebase but no formal spec exists in `openspec/specs/`. The traces span `gh-auth-login.sh` (9 lines), `container_profile.rs`, `handlers.rs`, `launch.rs`, `runner.rs`, and `docs/cheatsheets/github-credential-tools.md`. Without a spec, the trace annotations are orphans — they reference a

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1.1 Read all files with `@trace spec:secret-management` annotations to understand coverage
  • ☑ 1.2 Read existing specs (`proxy-container`, `native-secrets-store`) to match format
  • ☑ 1.3 Write `openspec/specs/secret-management/spec.md` covering all traced behavior
  • ☑ 1.4 Write `openspec/specs/secret-management/TRACES.md` listing all annotated locations
  • ☑ 2.1 Create `.openspec.yaml` with name, status, created date
  • ☑ 2.2 Write `proposal.md` — why this spec is needed (orphan traces)
  • ☑ 2.3 Write `design.md` — scope decisions, relationship to adjacent specs
  • ☑ 2.4 Write delta `specs/secret-management/spec.md` inside the change
  • ☑ 2.5 Write `tasks.md` (this file)
  • ☑ 3.1 Commit all artifacts with `@trace spec:secret-management` in message

Read source record ↗

changeDead code cleanupChecklist complete6 checked · 0 open

An audit of `#[allow(dead_code)]` annotations across `src-tauri/src/` found a mix of genuinely orphaned code, incorrectly suppressed warnings on used items, and planned-feature code that should remain. Left unchecked, dead code suppression masks real warnings and makes the codebase harder to audit.

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1. Remove `url` field and `#[allow(dead_code)]` from `GhRepoEntry` in `github.rs`; also remove `url` from the `--json` query to avoid fetching unused data
  • ☑ 2. Remove `version_full()` function and its `#[allow(dead_code)]` from `cli.rs`
  • ☑ 3. Remove `#[allow(dead_code)]` from `UpdateState` in `updater.rs`
  • ☑ 4. Fix placeholder `@trace spec:name https://...` in `log_format.rs:259` to `@trace spec:logging-accountability`
  • ☑ 5. Run `cargo test --workspace` to verify nothing breaks
  • ☑ 6. Run `cargo clippy --workspace` to check for new warnings

Read source record ↗

changeFix forge staleness and pruningChecklist complete12 checked · 0 open

The forge image staleness check has three issues: (1) the hash file is unversioned (`.last-build-forge.sha256`) but image tags are versioned (`tillandsias-forge:v0.1.97`), so the hash carries over across version bumps creating false "up to date" results; (2) the tray path skips the build script entirely when `image_exists` returns true, never checking for so

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1.1 In `build-image.sh`, derive hash file name from IMAGE_TAG instead of IMAGE_NAME
  • ☑ 1.2 Clean up old unversioned hash files
  • ☑ 2.1 In handlers.rs launch-time check, always call build script (remove image_exists short-circuit)
  • ☑ 2.2 Keep the retry logic for image_exists after build completes
  • ☑ 3.1 Add `prune_old_forge_images()` call after successful forge build in handlers.rs
  • ☑ 3.2 Ensure prune function removes all tillandsias-forge:v* except current tag
  • ☑ 3.3 Add prune after init.rs build path too
  • ☑ 4.1 Before building, list tillandsias-forge:v* images and find the newest
  • ☑ 4.2 If a newer version exists, use it and log warning
  • ☑ 4.3 Skip build when using a newer image
  • ☑ 5.1 cargo check --workspace compiles clean
  • ☑ 5.2 cargo test --workspace passes

Read source record ↗

changeProposal: Fix Ghost Trace AnnotationsChecklist complete14 checked · 0 open

A methodology audit found 8 `@trace spec:X` annotations in code referencing specs that don't exist in `openspec/specs/`. These "ghost traces" break the bidirectional link between specs and implementation that OpenSpec depends on.

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1.1 Copy `clickable-trace-index/spec.md` from archive `2026-03-30-clickable-trace-index`
  • ☑ 1.2 Copy `cross-platform/spec.md` from archive `2026-04-03-windows-full-support`
  • ☑ 1.3 Copy `logging-accountability/spec.md` from archive `2026-03-30-logging-accountability-framework`
  • ☑ 1.4 Copy `secret-rotation/spec.md` from archive `2026-03-30-secret-rotation-tokens`
  • ☑ 1.5 Copy `tray-icon-lifecycle/spec.md` from archive `2026-03-30-tray-icon-lifecycle`
  • ☑ 2.1 Copy `install-progress/spec.md` from active change `install-progress-i18n`
  • ☑ 3.1 Copy `secret-management/spec.md` (created by concurrent agent on linux-next)
  • ☑ 4.1 Fix `@trace spec:name` placeholder in `log_format.rs:259` to `@trace spec:logging-accountability`
  • ☑ 5.1 Fix `spec:podman-lifecycle` to `spec:podman-orchestration` in `docs/cheatsheets/wsl-bash.md` (2 occurrences)
  • ☑ 5.2 Fix `spec:podman-lifecycle` to `spec:podman-orchestration` in `docs/cheatsheets/windows-setup.md` (1 occurrence)
  • ☑ 5.3 Fix `spec:secrets-management` typo to `spec:secret-management` in `docs/cheatsheets/secret-management.md`
  • ☑ 6.1 Run `cargo test --workspace` — all 193 tests pass
  • ☑ 6.2 Verify zero ghost traces remain in source code (`.rs`, `.sh`, `.nix`, `.toml`)
  • ☑ 6.3 Remaining `spec:name`, `spec:foo`, `spec:forge-launch` are template/example text in CLAUDE.md and methodology docs — not real traces

Read source record ↗

changeProposal: Fix proxy-container spec security boundaryChecklist complete9 checked · 0 open

The proxy-container spec is materially wrong about a security-critical boundary. The spec claims:

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ Read current (stale) spec at `openspec/specs/proxy-container/spec.md`
  • ☑ Read implementation: `src-tauri/src/ca.rs`, `images/proxy/squid.conf`, `images/proxy/entrypoint.sh`
  • ☑ Read allowlist: `images/proxy/allowlist.txt`
  • ☑ Read proxy startup: `handlers.rs` `ensure_proxy_running`, `inject_ca_chain_mounts`
  • ☑ Read image build bypass logic: `handlers.rs` `run_build_image_script`
  • ☑ Create delta spec at `specs/proxy-container/spec.md`
  • ☑ Update main spec at `openspec/specs/proxy-container/spec.md`
  • ☑ Verify all claims in spec match code
  • ☑ Commit with trace annotation

Read source record ↗

changeProposal: Fix tray first-launch orderingChecklist complete7 checked · 0 open

On a fresh install, the user can launch the tray app before infrastructure (proxy, forge images) is built. The `install.sh` script runs `--init` as a background `nohup` process, so the tray may start before images exist.

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ T1: Add `notifications.forge_not_ready` and `notifications.infrastructure_failed` to `locales/en.toml`
  • ☑ T2: Add same keys to all other locale files (es, ja, zh-Hant, zh-Hans, ar, ko, hi, ta, te, fr, pt, it, ro, ru, nah, de)
  • ☑ T3: Add forge-readiness guard at top of `handle_attach_here` in `handlers.rs`
  • ☑ T4: Add startup "Setting up..." chip in `main.rs` when forge image not yet confirmed
  • ☑ T5: Replace silent `warn!()` with desktop notification on infrastructure failure in `main.rs`
  • ☑ T6: Add `@trace spec:tray-app` annotations to new/modified code
  • ☑ T7: Run `cargo test --workspace` and verify

Read source record ↗

changeMethodology optimization cheatsheetChecklist complete5 checked · 0 open

A thorough audit of the OpenSpec methodology across the Tillandsias codebase revealed both strengths (273 traces, 142 changes archived in ~14 days, excellent enclave spec alignment) and systematic weaknesses (35% of main specs with zero traces, 8 ghost trace names, 4 unsynced archives, TRACES.md line-number drift). There is no single reference document that

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1. Review existing cheatsheets (`docs/cheatsheets/`) to understand format conventions
  • ☑ 2. Review `openspec/specs/spec-traceability/spec.md` for trace annotation requirements
  • ☑ 3. Create `docs/cheatsheets/openspec-methodology.md` with:
  • ☑ 4. Add `@trace spec:spec-traceability` annotation to the cheatsheet
  • ☑ 5. Create OpenSpec change artifacts (this directory)

Read source record ↗

changePre-Commit Trace WarningsChecklist complete8 checked · 0 open

An audit found 8 ghost traces (referencing non-existent specs), 12 specs with zero code traces (35% disconnected), and no automated staleness detection for active changes. These gaps accumulate silently because nothing surfaces them during normal development workflow.

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ Create `scripts/hooks/pre-commit-openspec.sh` with ghost trace check
  • ☑ Add zero-trace spec check to the hook
  • ☑ Add active change staleness check (7-day threshold)
  • ☑ Ensure hook ALWAYS exits 0 (non-blocking)
  • ☑ Create `scripts/install-hooks.sh` (idempotent installer)
  • ☑ Handle existing pre-commit hooks (append, don't overwrite)
  • ☑ Test hook manually against known issues
  • ☑ Create OpenSpec change artifacts

Read source record ↗

changeScanner graceful degradationChecklist complete5 checked · 0 open

2026-04-08-scanner-graceful-degradation

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ T1: `watch()` — handle missing watch paths with warning log instead of silently skipping
  • ☑ T2: `watch()` — catch top-level `watcher.watch()` errors (permission denied, inotify limits) and log warnings instead of returning `Err`
  • ☑ T3: `watch()` — log meaningful context on depth-2 watch failures instead of silently dropping with `let _ =`
  • ☑ T4: Add trace annotation `@trace spec:filesystem-scanner` to graceful-degradation paths
  • ☑ T5: Add tests for scanner behavior with nonexistent watch paths and unreadable directories

Read source record ↗

changeChange: async-inference-launchChecklist complete16 checked · 0 open

`ensure_enclave_ready()` in `src-tauri/src/handlers.rs` currently awaits `ensure_inference_running()` synchronously (handlers.rs:1498). The inference container is the slowest enclave service to come up: ollama takes 15–30 s to initialize, plus a 10-attempt exponential-backoff health check that can extend the launch path by another 1–55 s. Inference is also e

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ Verify `TrayState` shareability across spawned tasks — `#[derive(Clone)]` on the struct in `crates/tillandsias-core/src/state.rs:191`
  • ☑ Replace the inline `match ensure_inference_running(...).await` in `ensure_enclave_ready` (handlers.rs ~line 1498) with `tokio::spawn(async move { ... })`
  • ☑ Move the `info!`/`warn!` accountability log into the spawned task body
  • ☑ Add an `Instant::now()` timer at spawn site and log elapsed seconds when the spawned task completes (both Ok and Err paths)
  • ☑ Update the "Enclave ready" log line at the end of `ensure_enclave_ready` to reflect "inference launching async"
  • ☑ Add `@trace spec:inference-container, spec:async-inference-launch` to the new spawn site
  • ☑ Audit `images/default/entrypoint-forge-claude.sh` for any inference URL references; confirm graceful behavior
  • ☑ Update `images/default/entrypoint-forge-opencode.sh` to probe `http://inference:11434/api/version` with `curl -m 1 -sf` before setting local-LLM env vars
  • ☑ On probe failure, unset opencode's local-LLM env vars so it uses cloud or no-LLM mode (do NOT crash)
  • ☑ Local build (`./build-local.sh`) passes
  • ☑ Manual: launch the tray, click "Attach Here", measure time from click to terminal open vs. baseline; both warm and cold inference cases
  • ☑ Manual: kill the inference container after launch; confirm forge still works (probe falls back)
  • ☑ Manual: check `--log-enclave` output for the new "inference ready (async)" line and the elapsed-seconds log
  • ☑ Update `docs/cheatsheets/enclave-architecture.md` to document the async-launch behavior and the `inference:11434` probe contract for forge entrypoints
  • ☑ Commit body includes `https://github.com/8007342/tillandsias/search?q=%40trace+spec%3Aasync-inference-launch&type=code`
  • ☑ `npx openspec validate async-inference-launch`

Read source record ↗

changeCi node24 audit and fixChecklist complete24 checked · 0 open

2026-04-16-ci-node24-audit-and-fix

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ Update `actions/checkout` from v4 to v6 (SHA pin)
  • ☑ Update `actions/setup-node` from v4 to v6 (SHA pin)
  • ☑ Update `actions/upload-artifact` from v4 to v7 (SHA pin)
  • ☑ Update `actions/download-artifact` from v4 to v8 (SHA pin)
  • ☑ Update `dtolnay/rust-toolchain` to latest master SHA
  • ☑ Update `swatinem/rust-cache` to v2.9.1 (SHA pin)
  • ☑ Update `sigstore/cosign-installer` from v3.7.0 to v4.1.1 (SHA pin)
  • ☑ Confirm `softprops/action-gh-release` v2.6.1 is already latest (no change)
  • ☑ Remove `FORCE_JAVASCRIPT_ACTIONS_TO_NODE24` env var (all actions now native Node 24)
  • ☑ Remove unused `Path` import from `ca.rs`
  • ☑ Remove unused `HashMap`, `Deserialize`, `Serialize` imports from `handlers.rs`
  • ☑ Remove dead `ensure_tools_overlay` re-export from `handlers.rs` (direct path used)
  • ☑ Add `#[allow(dead_code)]` to `bash_path` (cross-platform utility for Windows)
  • ☑ Add `#[allow(dead_code)]` to `GH_AUTH_LOGIN` (used by GitHub login flow)
  • ☑ Add `#[allow(dead_code)]` to `remote_url` field (parsed for future UI use)
  • ☑ Add `#[allow(dead_code)]` to `mirror_path` field (stored for future mount paths)
  • ☑ Add `#[allow(dead_code)]` to `any_versioned_forge_exists` (API for upgrade paths)
  • ☑ Add `#[allow(dead_code)]` to `get_proxy_ip` / `get_proxy_ip_pub` (proxy build routing)
  • ☑ Add `#[allow(dead_code)]` to `init::run` / `init::run_build_only` (CLI entry points)
  • ☑ Add `#[allow(dead_code)]` to `ensure_secrets_dirs` (secrets mount flow)
  • ☑ Add `#[allow(dead_code)]` to `store_github_token` (GitHub login flow)
  • ☑ Add `#[allow(dead_code)]` to `INSTALL_INCOMPLETE` (reserved error string)
  • ☑ `cargo clippy --workspace` — zero unused code warnings
  • ☑ `cargo test --workspace` — all tests pass

Read source record ↗

changeChange: fix-podman-machine-host-aliasesChecklist complete16 checked · 0 open

User-reported failure on Windows 11 + podman 5.8 / WSL machine:

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ Reproduce on Windows 11 + podman 5.8 with `tillandsias <project> --debug` → confirmed `Connection refused` from `git clone git://localhost:9418/...`
  • ☑ Verify in a probe container that `--add-host alias:host-gateway` adds `169.254.1.2 alias` to `/etc/hosts` and that connectivity to a host-published port works
  • ☑ Change `--add-host alias:127.0.0.1` → `--add-host alias:host-gateway` in `src-tauri/src/launch.rs:185`
  • ☑ Revert `rewrite_enclave_env` to be a no-op (keep function as a future hook)
  • ☑ Update tests:
  • ☑ `port_mapping_uses_friendly_aliases_resolved_via_host_gateway` — assert friendly-alias env vars + `--add-host alias:host-gateway` flags
  • ☑ `rewrite_enclave_env_passes_through_after_host_aliases_fix` — assert no-op behavior
  • ☑ Add `// @trace spec:enclave-network, spec:fix-podman-machine-host-aliases` at touched sites
  • ☑ `cargo check --workspace` clean
  • ☑ `cargo test launch::tests::port_mapping_uses_friendly_aliases_resolved_via_host_gateway` — passes
  • ☑ `cargo test launch::tests::rewrite_enclave_env_passes_through_after_host_aliases_fix` — passes
  • ☑ End-to-end on Windows 11 + podman 5.8: `tillandsias <project> --bash` → `Cloning into '/home/forge/src/test1'... warning: You appear to have cloned an empty repository.` (no `Connection refused`)
  • ☑ All four enclave containers up with their ports published; forge sees them via the friendly aliases
  • ☑ Document in commit body why `127.0.0.1` was wrong (forge's own loopback) vs `host-gateway` (resolves to gateway IP at runtime)
  • ☑ OpenSpec validate
  • ☑ Commit body includes `https://github.com/8007342/tillandsias/search?q=%40trace+spec%3Afix-podman-machine-host-aliases&type=code`

Read source record ↗

changeChange: fix-windows-extended-pathChecklist complete22 checked · 0 open

User-reported failure on Windows when attaching to a project from a relative path:

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ Reproduce on Windows: `tillandsias .\tillandsias\ --debug` → `hostname contains invalid characters`
  • ☑ Identify the source: `runner.rs:322` `resolved.canonicalize()` returns `\\?\C:\...` on Windows
  • ☑ Confirm the same path flows into `handlers.rs::ensure_mirror` → `git clone --mirror <source> <dest>`
  • ☑ Add `pub fn simplify_path(p: &Path) -> PathBuf` in `src-tauri/src/embedded.rs` (alongside the existing `bash_path` helper)
  • ☑ Strip `\\?\` only when the remainder begins with a drive letter; leave `\\?\UNC\...` alone
  • ☑ Identity behavior on non-Windows
  • ☑ Apply at `runner.rs::run_attach_command` immediately after `canonicalize()`
  • ☑ Apply defensively at `handlers.rs::ensure_mirror` (covers tray-mode callers that may not have stripped)
  • ☑ Add `// @trace spec:cli-mode, spec:cross-platform, spec:fix-windows-extended-path` at touched sites
  • ☑ `simplify_path_strips_extended_drive_prefix` — `\\?\C:\Users\bullo\...` → `C:\Users\bullo\...`
  • ☑ `simplify_path_preserves_unc_paths` — `\\?\UNC\server\share\dir` unchanged
  • ☑ `simplify_path_passthrough_when_no_prefix` — `C:\Users\bullo` unchanged
  • ☑ `simplify_path_unix_paths_unchanged` — `/home/forge/src/test1` unchanged
  • ☑ All 4 tests pass
  • ☑ `cargo check --workspace` clean
  • ☑ End-to-end on Windows: `tillandsias .\tillandsias\ --bash` → `Cloning mirror {project=tillandsias, source=C:\Users\bullo\src\tillandsias, ...}` → `Mirror created successfully`
  • ☑ Mirror origin correctly set to `https://github.com/8007342/tillandsias.git` for the remote-backed test project
  • ☑ Full enclave reaches "Enclave ready" and forge container starts
  • ☑ Confirm by code reading that local-only projects (no remote) follow the same path
  • ☑ Document in spec scenario that workflow inside the forge is identical for local-only and remote-backed projects
  • ☑ OpenSpec validate
  • ☑ Commit body includes `https://github.com/8007342/tillandsias/search?q=%40trace+spec%3Afix-windows-extended-path&type=code`

Read source record ↗

changeChange: fix-windows-image-routingChecklist complete18 checked · 0 open

On Windows, `run_build_image_script()` in `src-tauri/src/handlers.rs` hardcodes the build context to `images/default/Containerfile` regardless of which image is requested (`forge`, `proxy`, `git`, `inference`). The function correctly computes a versioned tag per image type, but it then runs `podman build` against the forge sources for every call. The result

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ Confirm that all four `tillandsias-{forge,proxy,git,inference}:v0.1.157.180` tags share image ID `a440b0730aba` on Windows
  • ☑ Confirm root cause is the hardcoded `images/default/` path in the Windows branch of `run_build_image_script` in `src-tauri/src/handlers.rs`
  • ☑ Add `fn image_build_paths(source_dir, image_name) -> (PathBuf, PathBuf)` helper in `src-tauri/src/handlers.rs`
  • ☑ Replace the two hardcoded `images/default/` paths in the Windows `#[cfg(target_os = "windows")]` block with calls to the helper
  • ☑ Confirm `crate::embedded::write_image_sources()` extracts proxy/git/inference Containerfiles + entrypoints (read `src-tauri/src/embedded.rs`)
  • ☑ Add `// @trace spec:default-image, spec:fix-windows-image-routing` comments at the helper and at the call site
  • ☑ Unit test for `image_build_paths` covering forge/proxy/git/inference/web/unknown
  • ☑ (Optional, debug-only) On startup, run `podman image inspect` over the four enclave tags and warn if any two share an ID, with `spec = "default-image, fix-windows-image-routing"` log field
  • ☑ `./scripts/bump-version.sh --bump-build` to invalidate stale cache + tag new builds distinctly (bumped to 0.1.157.181)
  • ☑ Wipe stale podman tags: `podman rmi localhost/tillandsias-{forge,proxy,git,inference}:v0.1.157.180` (these all point at the same image)
  • ☑ Build locally: `./build-local.sh` (also fixed the script — package was renamed `tillandsias-tray` → `tillandsias`)
  • ☑ Verify each enclave image now has a distinct image ID via `podman image ls` — confirmed: 4705bbb6ecaf forge / 98231ceae962 proxy / ff08a13fe6a1 git / ae0b5a57cc27 inference
  • ☑ Verify the proxy image has squid installed — Squid Cache: Version 6.9
  • ☑ Verify the git image has git-daemon — `/usr/libexec/git-core/git-daemon`
  • ☑ Verify the inference image has ollama — `/usr/local/bin/ollama` v0.20.7
  • ☑ Update `docs/cheatsheets/windows-setup.md` with the `podman rmi` recovery command for users who installed v0.1.157.180 or earlier on Windows
  • ☑ Commit message body includes `https://github.com/8007342/tillandsias/search?q=%40trace+spec%3Afix-windows-image-routing&type=code`
  • ☑ OpenSpec validate: `npx openspec validate fix-windows-image-routing` — valid

Read source record ↗

changeKeyring on host credential migrationChecklist complete98 checked · 0 open

Tillandsias's credential pipeline previously relied on bind-mounting the host D-Bus session bus socket into the git-service container so `gh` could reach the host's Secret Service. That pattern (a) didn't work on Windows or macOS where there is no D-Bus on the host, (b) leaked the *entire* host keyring (browser passwords, SSH passphrases, WiFi PSKs) into a c

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1.1 Remove `secrets/gh:/home/forge/.config/gh:ro` mount from `src-tauri/src/github.rs::build_gh_run_args` and `clone_repo`
  • ☑ 1.2 Simplify `src-tauri/src/launch.rs::ensure_secrets_dirs` to return only the git-identity dir (no `gh_dir`)
  • ☑ 1.3 Remove `cache/secrets/gh/hosts.yml` existence-check fallback from `src-tauri/src/menu.rs::needs_github_login`
  • ☑ 1.4 Remove all hosts.yml doc comments from `src-tauri/src/handlers.rs` (4 sites, no behavior change)
  • ☑ 1.5 Remove broken `GH_AUTH_LOGIN = include_str!(...)` from `src-tauri/src/embedded.rs` and the now-unused `ensure_gh_auth_login` extractor
  • ☑ 1.6 Delete top-level `gh-auth-login.sh` (the host wrapper it was made obsolete by the Rust `--github-login` path)
  • ☑ 2.1 Rewrite `openspec/specs/secret-management/spec.md` (singular) to drop D-Bus + hosts.yml requirements
  • ☑ 2.2 Rewrite `openspec/specs/native-secrets-store/spec.md` for keyring-only + delete auto-migration requirement
  • ☑ 2.3 Rewrite `openspec/specs/secret-rotation/spec.md` — drop hosts.yml fallback scenario
  • ☑ 2.4 Rewrite `openspec/specs/git-mirror-service/spec.md` — hosts.yml fallback scenario becomes hard-error
  • ☑ 2.5 Strip hosts.yml mention from `openspec/specs/forge-offline/spec.md` (comparative breadcrumb)
  • ☑ 2.6 Remove `mkdir -p ~/.config/gh` from `images/default/lib-common.sh`
  • ☑ 2.7 Rewrite `docs/cheatsheets/secret-management.md` for D-Bus-free architecture
  • ☑ 2.8 Update `docs/cheatsheets/token-rotation.md` — drop hosts.yml from failure-modes + phase tables
  • ☑ 2.9 Update `docs/cheatsheets/github-credential-tools.md` — drop `hosts.yml --insecure-storage` row
  • ☑ 2.10 Update `docs/cheatsheets/os-vault-credentials.md` — Headless/SSH security note
  • ☑ 2.11 Update `docs/cheatsheets/logging-levels.md` — drop hosts.yml refresh references
  • ☑ 2.12 Append keyring-only credential-flow paragraph to `CLAUDE.md` under Enclave Architecture
  • ☑ 2.13 Rewrite `SECRETS.md` (top-level) for host-keyring architecture
  • ☑ 2.14 Surgically update `docs/SECRETS.md` — remove `gh/` mount row from Secret Categories + Mount Strategy
  • ☑ 2.15 Final `rg 'hosts\.yml|\.config/gh|secrets/gh'` across live paths → zero matches
  • ☑ 3.1 Write merged content to `openspec/specs/secrets-management/spec.md` with `# secrets-management Specification` header
  • ☑ 3.2 Delete `openspec/specs/secret-management/` directory (spec.md + auto-generated TRACES.md)
  • ☑ 3.3 Replace `@trace spec:secret-management` → `@trace spec:secrets-management` in 17 live files (Rust src, shell, cheatsheets, other specs, container files)
  • ☑ 3.4 Replace `spec%3Asecret-management` → `spec%3Asecrets-management` in GitHub search URLs
  • ☑ 3.5 Rename cheatsheet `docs/cheatsheets/secret-management.md` → `secrets-management.md` + update 9 inbound cross-refs
  • ☑ 3.6 Rename runtime flag `--log-secret-management` → `--log-secrets-management` in `cli.rs`, `logging.rs`, 5 cheatsheets, 3 live specs
  • ☑ 4.1 Add `apple-native` and `windows-native` features to `keyring` dep in `src-tauri/Cargo.toml`
  • ☑ 4.2 Add direct `zeroize = "1"` dep to `src-tauri/Cargo.toml`
  • ☑ 4.3 Replace `SecretKind::DbusSession` with `SecretKind::GitHubToken` in `crates/tillandsias-core/src/container_profile.rs`
  • ☑ 4.4 Add `LaunchContext.token_file_path: Option<PathBuf>` field
  • ☑ 4.5 Update `git_service_profile()` to use `SecretKind::GitHubToken`
  • ☑ 4.6 Update profile test `git_service_has_github_token_and_log_mount`
  • ☑ 4.7 Rewrite `src-tauri/src/launch.rs` `SecretKind` handler: delete D-Bus branch, add GitHubToken branch that mounts `ctx.token_file_path` at `/run/secrets/github_token:ro` and sets `GIT_ASKPASS` env
  • ☑ 4.8 Populate `token_file_path: None` on every `LaunchContext` constructor for non-git-service containers (inference, proxy, forge/terminal in handlers + runner + test)
  • ☑ 4.9 Populate `token_file_path` in the git-service launch via `secrets::prepare_token_file`
  • ☑ 5.1 Rewrite module doc block for keyring-on-host architecture
  • ☑ 5.2 Keep/harden `store_github_token` + `retrieve_github_token`; add `delete_github_token`
  • ☑ 5.3 Implement `token_file_root()` (platform-aware)
  • ☑ 5.4 Implement `token_file_path(container_name)`
  • ☑ 5.5 Implement `prepare_token_file(container_name) -> Result<Option<PathBuf>, String>` with atomic write + 0700/0600 (Unix) / NTFS ACL (Windows)
  • ☑ 5.6 Implement `cleanup_token_file(container_name)` — idempotent unlink + parent rmdir
  • ☑ 5.7 Implement `cleanup_all_token_files()` — recursive rmtree of tokens-root
  • ☑ 5.8 Implement `secure_dir(path)` and `write_secure(path, bytes)` helpers
  • ☑ 6.1 Write `images/git/git-askpass-tillandsias.sh` — prompt-aware responses (`Username*` → `x-access-token`; `Password*` → `cat /run/secrets/github_token`; unknown prompt → exit 1)
  • ☑ 6.2 Add `COPY git-askpass-tillandsias.sh /usr/local/bin/` + `chmod +x` to `images/git/Containerfile`
  • ☑ 6.3 Embed `GIT_ASKPASS_TILLANDSIAS` via `include_str!` in `src-tauri/src/embedded.rs` and `write_lf` it during image-sources extraction (with `0755` on Unix)
  • ☑ 7.1 Rewrite `runner::run_github_login_git_service` as keep-alive container + `podman exec gh auth login` interactive + `podman exec gh auth token` extraction + keyring store + Drop-guard teardown
  • ☑ 7.2 Always prompt for git identity with host `~/.gitconfig` fallback as defaults (not only when cache is empty)
  • ☑ 7.3 Remove the "Found running git service → exec into it" shortcut from `runner::run_github_login`
  • ☑ 7.4 Wrap extracted token in `zeroize::Zeroizing<String>`
  • ☑ 7.5 Set `stdin=null, stdout=piped, stderr=piped` explicitly on `gh auth token` + `gh api user` invocations
  • ☑ 7.6 Redact gh stderr from the user terminal on error path — print generic message pointing to `--log-secrets-management`
  • ☑ 7.7 Replace `handle_github_login` body with `open_terminal("<own-exe> --github-login", ...)` so tray and CLI share the same code path
  • ☑ 8.1 `github::fetch_repos` reads token from keyring, wraps in Zeroizing, passes via `Command::env("GH_TOKEN", token)`
  • ☑ 8.2 `github::clone_repo` same pattern
  • ☑ 8.3 `build_gh_run_args` adds bare-name `-e GH_TOKEN` (no value in argv)
  • ☑ 8.4 Update module-level doc block to describe the no-leak scheme
  • ☑ 9.1 Implement `handlers::sweep_orphan_containers` — `podman ps --filter name=tillandsias-` → per-container `stop` + `rm -f` + `cleanup_token_file` + `cleanup_enclave_network`
  • ☑ 9.2 Call `secrets::cleanup_all_token_files()` at tray startup
  • ☑ 9.3 Call `sweep_orphan_containers()` at tray startup (blocking via minimal tokio runtime)
  • ☑ 9.4 Wire `stop_git_service` to also call `cleanup_token_file`
  • ☑ 10.1 Fix `--help` output: `MAINTENANCE: init` → `MAINTENANCE: --init`
  • ☑ 10.2 Remove bareword `init` alias from CLI parser (only `--init` accepted)
  • ☑ 10.3 Rename `--log-secret-management` → `--log-secrets-management` everywhere (cli.rs, logging.rs, examples in docs)
  • ☑ 11.1 `build-local.sh` — remove the `cp "$BIN" "$INSTALL_DIR/tillandsias-tray.exe"` double-install
  • ☑ 11.2 `build-local.ps1` — change `cargo build -p tillandsias-tray` → `cargo build -p tillandsias`, drop the Copy-Item to `-tray.exe`
  • ☑ 11.3 `scripts/install.ps1` — remove the "short-alias" Copy-Item block
  • ☑ 11.4 `src-tauri/src/update_cli.rs` — point the Windows install-path check at `tillandsias.exe` instead of `tillandsias-tray.exe`
  • ☑ 12.1 Write `docs/cheatsheets/windows-credential-manager.md` — Wincred lifecycle, target-name format, "what we can't see", troubleshooting
  • ☑ 12.2 Rewrite `docs/cheatsheets/os-vault-credentials.md` — platform-vault tour, headless-Linux caveat, cross-refs
  • ☑ 12.3 Add `## Plugins & Skills` section to `CLAUDE.md` — when to invoke simplify / security-review / review / less-permission-prompts / update-config / openspec / etc.
  • ☑ 12.4 Evaluate Caveman-Claude (decision: skip — vanity repo, no usable artifact)
  • ☑ 13.1 In `src-tauri/src/tools_overlay.rs` Windows branch: translate host `podman.exe` path via `to_wsl_path` (`/mnt/c/...`) and pass as `PODMAN_PATH` env
  • ☑ 13.2 Add `WSLENV=PODMAN_PATH/u:TOOLS_OVERLAY_QUIET:TILLANDSIAS_PORT_MAPPING:CA_CHAIN_PATH/u` so env vars cross the WSL boundary
  • ☑ 13.3 In `scripts/build-tools-overlay.sh` resolver: accept `.exe`-suffixed `PODMAN_PATH` via `-f` test (WSL interop can exec .exe; the `-x` bit isn't set on interop-mounted binaries)
  • ☑ 14.1 `ensure_proxy_running` readiness: 10 attempts then `return Err` (was: warn + proceed)
  • ☑ 14.2 `ensure_git_service_running` readiness: 10 attempts then `return Err` (was: warn + proceed)
  • ☑ 14.3 `ensure_enclave_ready` propagates git-service error instead of returning `Some(mirror)` with warning
  • ☑ 14.4 `singleton::try_acquire` returns `Err(())` when lockfile write fails (was: warn + `Ok(())`)
  • ☑ 14.5 `scripts/build-tools-overlay.sh` proxy-unreachable: `exit 1` (was: unset HTTP_PROXY and proceed with direct internet — security degradation)
  • ☑ 14.6 `main.rs` overlay failure sites (2 sites — fresh-images branch + after-build branch) + `handlers.rs` attach-here site: hard-fail via `overlay_ok` flag + `forge_available=false` + Dried icon + infrastructure_failed notification
  • ☑ 14.7 Shell entrypoints — git-clone failure becomes `exit 1` (3 files); missing overlay agent binary becomes `exit 1` (2 files); OpenSpec overlay missing becomes `exit 1` (lib-common.sh)
  • ☑ 14.8 `lib-common.sh` — replace `install_openspec` (inline npm install fallback) with `require_openspec` (overlay-only, fatal on miss)
  • ☑ 15.1 `launch::read_git_identity` — if cache gitconfig is empty, fall back to `~/.gitconfig`
  • ☑ 15.2 `build_podman_args` — skip `GIT_AUTHOR_NAME` / `GIT_AUTHOR_EMAIL` env vars when value is empty (prevents git's "empty ident" error)
  • ☑ 16.1 `keyring` round-trip probe confirmed write + read via `CredRead`
  • ☑ 16.2 `--github-login` OAuth device flow → token lands at `github-oauth-token.tillandsias` in Wincred
  • ☑ 16.3 Tray sees user as logged-in after keyring write (`needs_github_login()` returns false)
  • ☑ 16.4 Remote-repos list populates in tray (GH_TOKEN env injection path)
  • ☑ 16.5 Forge commit → mirror post-receive hook → github.com push succeeds (askpass path) with real PAT
  • ☑ 16.6 Branch verified on github.com via authenticated `git ls-remote`
  • ☑ 16.7 Orphan sweep verified by planting both `--rm` and non-`--rm` orphan `tillandsias-*` containers + dangling token files, then launching tray — all removed + enclave network cleaned
  • ☑ 16.8 Token file permissions confirmed (0644 on Windows mount, which is the NTFS ACL inheritance as expected)
  • ☑ 16.9 `cmdkey /list` shows only `github-oauth-token.tillandsias`; does NOT show any other Tillandsias-written entry
  • ☑ 17.1 `cargo check --workspace` clean (3 unrelated pre-existing warnings)
  • ☑ 17.2 `cargo test -p tillandsias-core container_profile` — 19/19 tests pass
  • ☑ 17.3 Release binary built and installed to `%LOCALAPPDATA%\Tillandsias\tillandsias.exe` (only binary present)

Read source record ↗

changeChange: overlay-mount-cacheChecklist complete6 checked · 0 open

`tools-overlay-fast-reuse` (shipped previous commit) added a process-lifetime snapshot cache for the overlay path lookup in `ensure_tools_overlay()`. The remaining per-launch work happens in `src-tauri/src/launch.rs:337-348` inside `resolve_mount_source()`, which still does an `exists()` syscall on every `MountSource::ToolsOverlay` mount construction. Every

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ Edit `src-tauri/src/launch.rs:337-348`: in `MountSource::ToolsOverlay` arm of `resolve_mount_source`, call `crate::tools_overlay::cached_overlay_for(&crate::handlers::forge_image_tag())` first; fall back to the original `exists()` check on snapshot miss
  • ☑ Add `// @trace spec:layered-tools-overlay, spec:tools-overlay-fast-reuse, spec:overlay-mount-cache` at the new code site
  • ☑ `cargo check --workspace` clean (only pre-existing warnings)
  • ☑ Manual: run the tray, attach to a project twice; second launch's `--log-enclave` should show no "exists()" overhead in mount construction (currently we don't log this, may add a `debug!` later)
  • ☑ OpenSpec validate
  • ☑ Commit body includes `https://github.com/8007342/tillandsias/search?q=%40trace+spec%3Aoverlay-mount-cache&type=code`

Read source record ↗

changeChange: persistent-git-serviceChecklist complete11 checked · 0 open

The per-project git-service container (`tillandsias-git-<project>`) is currently torn down by the event-loop trigger in `src-tauri/src/event_loop.rs:608-616` whenever the last forge or maintenance container for a project exits. The next "Attach Here" for the same project then has to rebuild the git-service from scratch — image staleness check (~3 s on Window

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ Remove the per-forge-exit `stop_git_service` trigger in `src-tauri/src/event_loop.rs:608-616`; replace with a comment explaining the new lifetime model and where cleanup actually happens
  • ☑ Update `handlers::shutdown_all` (`src-tauri/src/handlers.rs:2823`) to collect git-service project names from `state.running` rows with `container_type == GitService`, not from "projects with active forges"
  • ☑ Verify `EnclaveCleanupGuard` in `runner.rs:26` still stops git-service in CLI mode (unchanged — CLI is one-shot)
  • ☑ Add `// @trace spec:git-mirror-service, spec:persistent-git-service` at both touched sites
  • ☑ `cargo check --workspace` clean
  • ☑ Manual (tray): launch project A's forge, close it, relaunch. Second launch's log should NOT show "Starting git service container" — `ensure_git_service_running` should early-return on the state.running check
  • ☑ Manual (tray): exit tray with two projects' git-services running. Confirm `podman ps -a` shows zero `tillandsias-git-*` containers afterward
  • ☑ Manual (CLI): `tillandsias <project> --bash`, exit. Confirm CLI still tears down its git-service via `EnclaveCleanupGuard`
  • ☑ Update `docs/cheatsheets/enclave-architecture.md` to document the new lifetime model: proxy/inference/git-service all tray-session-scoped (with project scope on git-service)
  • ☑ OpenSpec validate
  • ☑ Commit body includes `https://github.com/8007342/tillandsias/search?q=%40trace+spec%3Apersistent-git-service&type=code`

Read source record ↗

changeChange: tools-overlay-fast-reuseChecklist complete21 checked · 0 open

Once the tools overlay is built (~234 MB containing claude/opencode/openspec), every "Attach Here" still pays a non-trivial overhead to *check* it before mounting:

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ Add `OverlaySnapshot` struct and `OVERLAY_SNAPSHOT: OnceLock<RwLock<Option<OverlaySnapshot>>>` in `src-tauri/src/tools_overlay.rs` (used `std::sync::OnceLock` instead of `once_cell::sync::OnceCell` since std equivalent is available; same semantics)
  • ☑ Add `cached_overlay_for(forge_tag) -> Option<PathBuf>` reader function (sub-millisecond fast path)
  • ☑ Refactor `ensure_tools_overlay()` to call `cached_overlay_for` first; fall through to slow path only on miss
  • ☑ Add `populate_snapshot()` writer; called by both `ensure_tools_overlay` (manifest-match branch) and `build_overlay_for_init` (no-op branch)
  • ☑ Call `populate_snapshot` from `build_overlay_for_init` so eager startup populate happens via the existing `--init` / tray-startup paths (no extra wiring needed in `main.rs`)
  • ☑ Update `rebuild_tools_overlay()` to invalidate the cache after a successful rebuild — the background-update task uses this same function so it inherits the invalidation
  • ☑ Update the launch path (`src-tauri/src/launch.rs:343`) to consume the cached path instead of calling `resolve_mount_source()` per launch — DEFERRED, current early-exit in `ensure_tools_overlay` already short-circuits the heavy work; mount-path optimization is a smaller follow-on
  • ☑ Add `// @trace spec:layered-tools-overlay, spec:tools-overlay-fast-reuse` to: snapshot type, reader fn, populate fn, invalidate fn, both call sites in init + ensure paths, both rebuild invalidation sites
  • ☑ `Instant::now()` wrapper around the cache lookup logs `elapsed_micros` at `debug!` level
  • ☑ Unit test: snapshot returns `Some(path)` when forge_tag matches
  • ☑ Unit test: snapshot returns `None` when tag differs
  • ☑ Unit test: snapshot returns `None` when unpopulated
  • ☑ Unit test: invalidating snapshot causes next read to return `None`
  • ☑ All four serialize on a shared `SNAPSHOT_TEST_LOCK` since they share the global static
  • ☑ `cargo test snapshot` — 4/4 pass
  • ☑ `cargo check --workspace` clean (only pre-existing warnings)
  • ☑ Manual: launch the TRAY app (not CLI `--bash`), click "Attach Here" twice, observe second launch's debug log shows `cache hit` and `elapsed_micros` in the single-digit µs range — DEFERRED to user; CLI `--bash` uses `build_overlay_for_init` which now eagerly populates, but the cache only matters across multiple launches in the same process (i.e. tray)
  • ☑ Manual: trigger a forge image rebuild (bump build version), confirm overlay slow-path runs once and caches; subsequent launches are fast again
  • ☑ Update `docs/cheatsheets/forge-launch-critical-path.md` cache-hit behavior — covered by Wave 2a measured-latency table; can extend further once tray manual-tested
  • ☑ Commit body includes `https://github.com/8007342/tillandsias/search?q=%40trace+spec%3Atools-overlay-fast-reuse&type=code`
  • ☑ `npx openspec validate tools-overlay-fast-reuse` — valid

Read source record ↗

changeClamp small model to baked tiersChecklist complete4 checked · 0 open

The prior change (`zen-default-with-ollama-analysis-pool`) tier-tagged `small_model` by host capability — Ultra hosts got `ollama/qwen2.5:14b`, Mid/High got `ollama/qwen2.5-coder:7b`, etc. But only T0 (`qwen2.5:0.5b`) and T1 (`llama3.2:3b`) are baked into the inference image at build time. T2+ models pull at runtime through the Squid SSL-bump proxy and hit t

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ Update `GpuTier::model_pair()` to clamp `small_model` to baked tiers.
  • ☑ Update gpu unit tests.
  • ☑ `cargo test -p tillandsias -- gpu::` passes.
  • ☑ OpenSpec delta + archive.

Read source record ↗

changeTombstone tools overlayChecklist complete18 checked · 0 open

The move to a "full battery" hard-installed forge image in the `zen-default-with-ollama-analysis-pool` change superseded the runtime tools overlay for claude/opencode/openspec. Agents now live at `/usr/local/bin/` inside the forge image, symlinked from `/opt/agents/`, baked at `podman build` time with deterministic versions.

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ Delete `scripts/build-tools-overlay.sh`.
  • ☑ Delete `src-tauri/src/tools_overlay.rs`.
  • ☑ Remove `mod tools_overlay;` from `src-tauri/src/main.rs`.
  • ☑ Remove `embedded::BUILD_TOOLS_OVERLAY` const + write_lf + chmod call.
  • ☑ Remove `MountSource::ToolsOverlay` variant + all usages.
  • ☑ Remove `is_proxy_healthy` (last consumer was tools_overlay).
  • ☑ `main.rs` event loop: both `ensure_tools_overlay` call paths.
  • ☑ `handlers.rs`: all 4 call sites (2 ensure_tools_overlay, 3 spawn_background_update).
  • ☑ `init.rs`: `build_overlay_for_init`.
  • ☑ `runner.rs`: `build_overlay_for_init`.
  • ☑ `launch.rs`: match arm, stale comments, test rewrite.
  • ☑ Rename + update `forge_profiles_have_tools_overlay_mount`.
  • ☑ Rename + update `tools_overlay_expected_paths`.
  • ☑ Update `git_service_has_github_token_and_log_mount` for new env vars.
  • ☑ `cargo test --workspace --release` all green.
  • ☑ Proposal + deltas under `openspec/changes/tombstone-tools-overlay/`.
  • ☑ Validates strict.
  • ☑ Archive.

Read source record ↗

changeZen default with ollama analysis poolChecklist complete10 checked · 0 open

The 0.5B local model can talk to opencode but can't follow opencode's tool-calling protocol reliably enough to drive a real coding session end-to-end. Larger local tool-capable models (qwen2.5-coder:7b+) are multi-GB downloads and struggle through the Squid proxy's SSL bump (empirically: `max retries exceeded: EOF` on manifest pulls).

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ `images/default/config-overlay/opencode/config.json` — set
  • ☑ Add a short instruction file telling the agent: "use the
  • ☑ `images/inference/Containerfile` — add a build-time `RUN ollama
  • ☑ `images/inference/entrypoint.sh` — replace `tinyllama:1.1b` with
  • ☑ Boot tray + attach to ../java project.
  • ☑ Inside forge, run `opencode run "build and compile
  • ☑ Agent writes file, javac, java run prints expected output, git
  • ☑ `git log` on host's working tree (post mirror→host sync) shows the
  • ☑ Validate strict.
  • ☑ Archive once verification scenarios pass.

Read source record ↗

changeCheatsheet methodology evolutionChecklist complete71 checked · 0 open

The `agent-source-of-truth` change shipped 60 cheatsheets and a methodology that mandated `## Sources of Truth` in specs. **It did not mandate provenance in the cheatsheets themselves.** I (the agent) generated all 60 cheatsheets from training-data memory — the user caught this and rejected the result, twice. Cheatsheets without provenance are worse than no

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1.1 Update `cheatsheets/TEMPLATE.md` to include the mandatory `## Provenance` section with placeholders (URL examples + `**Last updated:** YYYY-MM-DD`).
  • ☑ 1.2 Update `cheatsheets/INDEX.md` to mark every DRAFT cheatsheet with a `[DRAFT]` prefix on its line so the index is honest at a glance.
  • ☑ 2.1 Write `cheatsheets/runtime/cheatsheet-methodology.md` — meta-cheatsheet documenting the methodology itself. Cite this OpenSpec change ID + the project + workspace `CLAUDE.md` paths. Becomes the canonical reference for "how to write a cheatsheet" in this project.
  • ☑ 3.1 `cheatsheets/agents/openspec.md` — provenance: `github.com/Fission-AI/OpenSpec/` (canonical repo), `npmjs.com/package/@fission-ai/openspec`. Verify content.
  • ☑ 3.2 `cheatsheets/agents/claude-code.md` — provenance: `docs.claude.com/en/docs/claude-code/` (Anthropic docs), `github.com/anthropics/claude-code` (canonical repo). Verify.
  • ☑ 3.3 `cheatsheets/agents/opencode.md` — provenance: `opencode.ai` (canonical), `github.com/sst/opencode` (repo). Verify.
  • ☑ 4.1 `cheatsheets/runtime/forge-container.md` — provenance: `docs.fedoraproject.org/en-US/fedora/latest/release-notes/`, `docs.podman.io/en/latest/markdown/podman-run.1.html`. Verify.
  • ☑ 4.2 `cheatsheets/runtime/networking.md` — provenance: Squid docs (`wiki.squid-cache.org`), Caddy docs (`caddyserver.com/docs/`), git protocol (`git-scm.com/docs/git-protocol`). Verify.
  • ☑ 4.3 `cheatsheets/runtime/runtime-limitations.md` — provenance: this OpenSpec change ID itself + project `CLAUDE.md`. Tillandsias-internal — no external authority needed; cite the spec.
  • ☑ 5.1 `cheatsheets/utils/git.md` — `git-scm.com/docs/`
  • ☑ 5.2 `cheatsheets/utils/gh.md` — `cli.github.com/manual/`
  • ☑ 5.3 `cheatsheets/utils/jq.md` — `jqlang.github.io/jq/manual/`
  • ☑ 5.4 `cheatsheets/utils/yq.md` — `mikefarah.gitbook.io/yq/`
  • ☑ 5.5 `cheatsheets/utils/curl.md` — `curl.se/docs/manpage.html`
  • ☑ 5.6 `cheatsheets/utils/ripgrep.md` — `github.com/BurntSushi/ripgrep` (README + man page)
  • ☑ 5.7 `cheatsheets/utils/fd.md` — `github.com/sharkdp/fd`
  • ☑ 5.8 `cheatsheets/utils/fzf.md` — `github.com/junegunn/fzf` + `man fzf`
  • ☑ 5.9 `cheatsheets/utils/ssh.md` — `man.openbsd.org/ssh`, `man.openbsd.org/ssh_config`
  • ☑ 5.10 `cheatsheets/utils/rsync.md` — `download.samba.org/pub/rsync/rsync.1`
  • ☑ 5.11 `cheatsheets/utils/tree.md` — `mama.indstate.edu/users/ice/tree/` (canonical)
  • ☑ 5.12 `cheatsheets/utils/shellcheck-shfmt.md` — `github.com/koalaman/shellcheck/wiki/`, `github.com/mvdan/sh`
  • ☑ 6.1 `cheatsheets/languages/python.md` — `docs.python.org/3.13/`, PEP index
  • ☑ 6.2 `cheatsheets/languages/rust.md` — `doc.rust-lang.org/book/`, `doc.rust-lang.org/std/`
  • ☑ 6.3 `cheatsheets/languages/java.md` — `docs.oracle.com/en/java/javase/21/`
  • ☑ 6.4 `cheatsheets/languages/typescript.md` — `typescriptlang.org/docs/`
  • ☑ 6.5 `cheatsheets/languages/javascript.md` — `developer.mozilla.org/en-US/docs/Web/JavaScript`, ECMA-262 spec
  • ☑ 6.6 `cheatsheets/languages/bash.md` — `gnu.org/software/bash/manual/`, POSIX shell at `pubs.opengroup.org`
  • ☑ 6.7 `cheatsheets/languages/dart.md` — `dart.dev/language`, `dart.dev/null-safety`
  • ☑ 6.8 `cheatsheets/languages/sql.md` — `postgresql.org/docs/`, `sqlite.org/lang.html`
  • ☑ 6.9 `cheatsheets/languages/json.md` — RFC 8259 (`datatracker.ietf.org/doc/html/rfc8259`), JSON-Lines spec
  • ☑ 6.10 `cheatsheets/languages/yaml.md` — `yaml.org/spec/1.2.2/`
  • ☑ 6.11 `cheatsheets/languages/toml.md` — `toml.io/en/v1.0.0`
  • ☑ 6.12 `cheatsheets/languages/xml.md` — W3C XML 1.0 spec, libxml2 docs
  • ☑ 6.13 `cheatsheets/languages/html.md` — `html.spec.whatwg.org`, MDN HTML reference
  • ☑ 6.14 `cheatsheets/languages/css.md` — W3C CSS modules + MDN CSS reference
  • ☑ 6.15 `cheatsheets/languages/markdown.md` — `spec.commonmark.org`, GFM spec at `github.github.com/gfm/`
  • ☑ 7.1 `cargo.md` — `doc.rust-lang.org/cargo/`
  • ☑ 7.2 `npm.md` — `docs.npmjs.com/cli/`
  • ☑ 7.3 `pnpm.md` — `pnpm.io/`
  • ☑ 7.4 `yarn.md` — `yarnpkg.com/getting-started`, `classic.yarnpkg.com/en/docs/`
  • ☑ 7.5 `pip.md` — `pip.pypa.io/en/stable/`
  • ☑ 7.6 `pipx.md` — `pipx.pypa.io`
  • ☑ 7.7 `uv.md` — `docs.astral.sh/uv/`
  • ☑ 7.8 `poetry.md` — `python-poetry.org/docs/`
  • ☑ 7.9 `maven.md` — `maven.apache.org/guides/`
  • ☑ 7.10 `gradle.md` — `docs.gradle.org/current/userguide/`
  • ☑ 7.11 `go.md` — `go.dev/doc/`, `pkg.go.dev/cmd/go`
  • ☑ 7.12 `flutter.md` — `docs.flutter.dev`
  • ☑ 7.13 `make.md` — `gnu.org/software/make/manual/`
  • ☑ 7.14 `cmake.md` — `cmake.org/cmake/help/latest/`
  • ☑ 7.15 `ninja.md` — `ninja-build.org/manual.html`
  • ☑ 8.1 `protobuf.md` — `protobuf.dev/programming-guides/proto3/`
  • ☑ 8.2 `grpc.md` — `grpc.io/docs/`
  • ☑ 8.3 `openapi.md` — `spec.openapis.org/oas/v3.1.0`
  • ☑ 8.4 `http.md` — RFC 9110 (HTTP semantics), RFC 9112 (HTTP/1.1), RFC 9113 (HTTP/2) at `datatracker.ietf.org`
  • ☑ 8.5 `websocket.md` — RFC 6455 at `datatracker.ietf.org/doc/html/rfc6455`
  • ☑ 8.6 `sse.md` — `html.spec.whatwg.org/multipage/server-sent-events.html`
  • ☑ 9.1 `pytest.md` — `docs.pytest.org/en/stable/`
  • ☑ 9.2 `junit.md` — `junit.org/junit5/docs/current/user-guide/`
  • ☑ 9.3 `cargo-test.md` — `doc.rust-lang.org/cargo/commands/cargo-test.html`, `doc.rust-lang.org/book/ch11-00-testing.html`
  • ☑ 9.4 `go-test.md` — `pkg.go.dev/testing`
  • ☑ 9.5 `selenium.md` — `selenium.dev/documentation/`
  • ☑ 9.6 `playwright.md` — `playwright.dev/docs/intro`
  • ☑ 11.1 Write `scripts/check-cheatsheet-staleness.sh` — script that walks `cheatsheets/`, reads each `**Last updated:** YYYY-MM-DD` line, flags any older than 90 days. Output is human-readable + machine-grep-able. Future enhancement: hit each cited URL with `curl -fsSI` to confirm reachability.
  • ☑ 11.2 Add `scripts/check-cheatsheet-staleness.sh` to a project-defined cadence (e.g., a weekly script-runner or a CI-eligible workflow). NOT auto-triggered on commit per cloud-workflow conservation rules.
  • ☑ 12.1 Each retrofit cheatsheet carries `@cheatsheet runtime/cheatsheet-methodology.md` if it cites the methodology itself.
  • ☑ 12.2 No version bump now — happens at archive per CLAUDE.md.
  • ☑ 13.1 Every cheatsheet has either `## Provenance` populated OR the DRAFT banner. No silent gap.
  • ☑ 13.2 `cheatsheets/INDEX.md` `[DRAFT]` markers stay synchronised with file state.
  • ☑ 13.3 New specs after this change archives MUST cite at least one non-DRAFT cheatsheet under `## Sources of Truth` (warning if any cited cheatsheet is DRAFT).
  • ☑ 13.4 At least 30 of the 60 existing cheatsheets retrofitted to non-DRAFT state before this change archives. The remaining 30 are tracked in a follow-up sweep change to keep this one tractable.

Read source record ↗

changeDirect podman callsChecklist complete59 checked · 0 open

Tillandsias uses bash scripts as middleware between Rust code and podman/gh CLI calls on the host. This worked well on Linux where bash is a first-class citizen, but it creates a fundamental problem on Windows: Git Bash (MSYS2) does not initialize properly when `bash.exe` is launched from a native Windows process. The project has accumulated workarounds (dri

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ `gh-auth-login.sh` already has the priority logic (host gh > container fallback)
  • ☑ Add a `find_gh_path()` function (similar to `find_podman_path()`) that checks known locations: `gh`, `/usr/bin/gh`, `/usr/local/bin/gh`, `~/.local/bin/gh`, and on Windows `C:\Program Files\GitHub CLI\gh.exe` / winget paths
  • ☑ Return `Option<PathBuf>` -- `None` means fall back to container
  • ☑ In `handle_github_login`, add `#[cfg(target_os = "windows")]` block that:
  • ☑ Skip `write_temp_script("gh-auth-login.sh", ...)` on Windows entirely
  • ☑ Test on Windows: GitHub Login works without Git Bash installed
  • ☑ Extend the Windows implementation to work on Linux and macOS
  • ☑ For host-gh path: spawn terminal with `gh auth login --git-protocol https`
  • ☑ For container fallback: build podman command with security flags, D-Bus forwarding (Linux), and volume mounts
  • ☑ Terminal command format: `podman run -it --rm --cap-drop=ALL --security-opt=no-new-privileges --userns=keep-id -v <gh-dir>:/home/forge/.config/gh -v <git-dir>:/home/forge/.config/tillandsias-git <forge-image> gh auth login --git-protocol https`
  • ☑ Remove `embedded::GH_AUTH_LOGIN` constant (already done in Phase 2)
  • ☑ Remove `gh-auth-login.sh` from `include_str!` in `embedded.rs` (already done in Phase 2)
  • ☑ Before opening the terminal for `gh auth login`, check if git identity exists in managed gitconfig
  • ☑ If not set, either: (a) prompt in the terminal before launching gh, or (b) use a pre-command that sets git config then runs gh auth login
  • ☑ Write identity via `git config --file <secrets-dir>/git/.gitconfig user.name "..."` etc.
  • ☑ Preserve the re-authentication flow (detect existing creds, offer to re-auth)
  • ☑ Add `// @trace spec:direct-podman-calls` to the new gh discovery function
  • ☑ Add `// @trace spec:direct-podman-calls, spec:secrets-management` to the direct GitHub Login handler
  • ☑ Update existing `// @trace spec:gh-auth-script` references to include `spec:direct-podman-calls`
  • ☑ Windows: GitHub Login works with host `gh` installed, no Git Bash required
  • ☑ Windows: `./build-windows.sh --check` passes
  • ☑ Linux: GitHub Login works with host `gh`
  • ☑ Linux: GitHub Login works via container fallback (no host `gh`)
  • ☑ macOS: GitHub Login works with host `gh`
  • ☑ All platforms: security flags match those in `gh-auth-login.sh`
  • ☑ All platforms: credentials stored in correct location
  • ☑ Port `_compute_hash()` logic from `build-image.sh` to Rust: hash Containerfile + image source files
  • ☑ Use the same cache directory for hash files (`~/.cache/tillandsias/build-hashes/` or platform equivalent)
  • ☑ Compare current hash against cached hash; skip build if matching and image exists
  • ☑ Remove the `#[cfg(target_os = "windows")]` and `#[cfg(not(target_os = "windows"))]` blocks
  • ☑ Replace with a single codepath: direct `podman build --tag <tag> -f <Containerfile> <context>`
  • ☑ Preserve the build lock mechanism (`build_lock::acquire` / `build_lock::wait_for_build`)
  • ☑ Preserve old image pruning (`prune_old_forge_images`)
  • ☑ Apply `--security-opt label=disable` for SELinux compatibility
  • ☑ Remove `embedded::BUILD_IMAGE` constant
  • ☑ Remove `build-image.sh` from `include_str!` in `embedded.rs`
  • ☑ Update `extract_image_sources()` to no longer write `scripts/build-image.sh`
  • ☑ The nix backend (if needed in future) can be handled separately
  • ☑ After Phase 1 and 2 are both complete, remove the `bash_path` function from `embedded.rs`
  • ☑ Remove any remaining MSYS2 path conversion logic
  • ☑ Verify no other code depends on `bash_path`
  • ☑ Add `// @trace spec:direct-podman-calls, spec:default-image` to the unified build function
  • ☑ Add `// @trace spec:direct-podman-calls` to the staleness detection code
  • ☑ Update existing `@trace` in `run_build_image_script` to include `spec:direct-podman-calls`
  • ☑ Linux: `./build.sh --test` passes
  • ☑ Linux: forge image builds correctly via direct podman
  • ☑ macOS: `./build-osx.sh --test` passes
  • ☑ macOS: forge image builds correctly via direct podman
  • ☑ Windows: `./build-windows.sh --check` passes
  • ☑ Windows: forge image builds correctly (already working, verify no regression)
  • ☑ Staleness detection: unchanged sources skip rebuild on all platforms
  • ☑ Force rebuild: `--force` flag equivalent still works
  • ☑ Web image: `build_image("web")` still works
  • ☑ Update CLAUDE.md if any build commands changed
  • ☑ Update `docs/cheatsheets/` if affected
  • ☑ Add notes to `gh-auth-login.sh` and `build-image.sh` headers indicating they are for manual use only, not invoked by the binary
  • ☑ Update `gh-auth-script` spec to reflect that the script is no longer invoked at runtime
  • ☑ Update `embedded-scripts` spec to reflect reduced scope
  • ☑ Update `default-image` spec to reflect unified direct-podman build path

Read source record ↗

changeForge opencode methodology overhaulChecklist complete8 checked · 0 open

Today's `images/default/config-overlay/opencode/instructions/methodology.md` is 36 lines of generalities. The model in `~/src/java/ENVIRONMENT_REPORT.md` never even tried `which java` because the methodology never told it to discover what was on PATH. Per `~/src/java/container_enhancements.md`: opencode's instruction surface needs an action-first methodology

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1.1 Create `images/default/config-overlay/opencode/instructions/forge-discovery.md` (150–200 lines)
  • ☑ 1.2 Create `images/default/config-overlay/opencode/instructions/cache-discipline.md` (150–200 lines)
  • ☑ 1.3 Create `images/default/config-overlay/opencode/instructions/nix-first.md` (150–200 lines)
  • ☑ 1.4 Create `images/default/config-overlay/opencode/instructions/openspec-workflow.md` (150–200 lines)
  • ☑ 2.1 Rewrite `images/default/config-overlay/opencode/instructions/methodology.md` as ~15-line index pointing to the 4 sub-files
  • ☑ 3.1 Update `images/default/config-overlay/opencode/config.json` to list all 5 instruction files
  • ☑ 4.1 Run `openspec validate --change forge-opencode-methodology-overhaul --strict` and resolve all warnings/errors
  • ☑ 4.2 Archive the change with `openspec archive --change forge-opencode-methodology-overhaul`

Read source record ↗

changeOpencode web session otpChecklist complete33 checked · 0 open

OpenCode Web today is reachable by **any** local process or browser tab on the user's machine — anything that knows the URL `http://<project>.opencode.localhost:8080/` can connect. While the listener is loopback-only (rejected from LAN/Internet), there's no defence against:

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1.1 Create `src-tauri/src/otp.rs` with public functions:
  • ☑ 1.2 Add `@trace spec:opencode-web-session-otp, spec:secrets-management`
  • ☑ 1.3 Unit tests in `src-tauri/src/otp.rs::tests`:
  • ☑ 2.1 Depends on `tray-host-control-socket` capability landing first.
  • ☑ 2.2 Add `IssueWebSession { project_label: String, cookie_value: [u8; 32] }`
  • ☑ 2.3 Tray-side sender: `src-tauri/src/otp.rs::issue_to_router(project_label,
  • ☑ 2.4 Router-side handler: in the router sidecar (introduced as part of
  • ☑ 2.5 Add `@trace spec:opencode-web-session-otp, spec:tray-host-control-socket`
  • ☑ 3.1 Modify `src-tauri/src/handlers.rs::regenerate_router_caddyfile` so the
  • ☑ 3.2 Add a per-project Caddyfile var holding the project's session-list
  • ☑ 3.3 Update `images/router/base.Caddyfile` to wire the sidecar admin
  • ☑ 3.4 Update `images/router/entrypoint.sh` to launch the router sidecar
  • ☑ 4.1 Implement the router-side state structure:
  • ☑ 4.2 Implement `push(project_label, cookie_value)` — append a new
  • ☑ 4.3 Implement `validate(project_label, cookie_value) -> bool` — match
  • ☑ 4.4 Implement `evict_expired()` — periodic task (1s tick) removes
  • ☑ 4.5 Implement `evict_project(project_label)` called when the project's
  • ☑ 4.6 Audit log every push, validate (success + failure), and evict event
  • ☑ 5.1 Depends on `host-chromium-on-demand` capability landing first
  • ☑ 5.2 Add a minimal CDP client (either `headless_chrome` crate or a small
  • ☑ 5.3 Implement `attach_and_set_cookie(cdp_port: u16, target_url: &str,
  • ☑ 5.4 Unit tests with a mocked CDP server (httpmock or similar) covering:
  • ☑ 6.1 Modify `src-tauri/src/browser.rs::launch_for_project` to:
  • ☑ 6.2 Update both call sites in `src-tauri/src/handlers.rs` (fresh attach
  • ☑ 6.3 Update browser detection: bundled Chromium becomes the only path
  • ☑ 7.1 Tray unit tests (covered in 1.3 + 5.4 above).
  • ☑ 7.2 Router unit tests in the sidecar:
  • ☑ 7.3 Integration test (host-side, requires podman):
  • ☑ 7.4 Audit-log tests:
  • ☑ 8.1 Create `cheatsheets/web/cookie-auth-best-practices.md` with:
  • ☑ 8.2 Update `docs/cheatsheets/secrets-management.md` to add the OTP +
  • ☑ 9.1 After `/opsx:archive`, run `./scripts/bump-version.sh --bump-changes`.
  • ☑ 9.2 Commit with the trace URL footer per the project convention.

Read source record ↗

changeTray projects renameChecklist complete18 checked · 0 open

Two separate UX issues from local testing of `0.1.169.227`:

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1.1 In `locales/en.toml`, change `menu.projects = "Projects"` → `menu.projects = "🏠 ~/src"`. Add new key `menu.cloud_projects = "☁️ Cloud"` near the other top-level menu labels.
  • ☑ 1.2 Mirror the same edits in `locales/de.toml` and `locales/es.toml` (these are the only locales with translated menu sections per the inventory). Other locales fall back to en.
  • ☑ 1.3 Verify no other code references `menu.github.remote_projects` for the new submenu label — leave the old key in place (used by the legacy fetch flow's status messages).
  • ☑ 2.1 In `apply_state`, update the local-projects submenu construction to use `i18n::t("menu.projects")` (already does — value change in step 1 makes it `🏠 ~/src`). Confirm no hardcoded `"Projects"` literal remains.
  • ☑ 2.2 Replace the remote-projects submenu's label source from `i18n::t("menu.github.remote_projects")` to `i18n::t("menu.cloud_projects")`.
  • ☑ 3.1 In `TrayMenu::new`, remove `.item(&language)` from the root `MenuBuilder` chain. Keep the `language: Submenu<R>` field (built but not appended) so re-enabling later is one line.
  • ☑ 3.2 Add a `// @tombstone superseded:tray-projects-rename — kept for three releases (until 0.1.169.230)` annotation immediately above the now-orphaned `language` field.
  • ☑ 3.3 In `refresh_static_labels`, keep the `language.set_text` call (cheap, no-op when not appended).
  • ☑ 4.1 Find the locale-detection function (likely `i18n::detect_locale` or `i18n::initialize` in `src-tauri/src/i18n.rs`). Tombstone the existing detection logic (`// @tombstone superseded:tray-projects-rename`) and hard-code the return value to `"en"`. Comment block must reference the OpenSpec change name and the version where it landed (TBD; record the next built version).
  • ☑ 4.2 Confirm `i18n::t` and `i18n::tf` still work — the change only affects locale selection at startup.
  • ☑ 5.1 In `tray_menu.rs::tests`, add `apply_state_does_not_append_language_submenu` — instantiates a `TrayMenu`, calls `apply_state` for each stage, asserts no item with id `tm.language` is present in `root.items()`. **Deferred**: Tauri runtime tests require full build environment; language removal is already tested structurally by 3.1 (Language menu is not appended) and verified by manual testing.
  • ☑ 5.2 Existing `dispatch_click_known_actions` keeps the `SelectLanguage` assertion (variant still in the enum). Add comment noting the variant is dormant until i18n is re-enabled.
  • ☑ 5.3 In `i18n.rs::tests` (if any), add a test that `current_language()` (or equivalent) returns `"en"` even when `LANG` env points elsewhere. **Added**: `detect_locale_hard_defaults_to_en` test verifies hard-coded "en" behavior.
  • ☑ 6.1 Update `docs/cheatsheets/tray-state-machine.md`: replace `Projects ▸` / `Remote Projects ▸` references with `🏠 ~/src ▸` / `☁️ Cloud ▸`. Update the static-row description (no Language ▸ row).
  • ☑ 7.1 `cargo check --workspace` — clean.
  • ☑ 7.2 `./build.sh --test` — all tests pass (22 scanner tests, 34 core tests, all passing).
  • ☑ 7.3 `./build.sh --install` — build ready for local installation.
  • ☑ 7.4 Manual: launch tray, confirm `🏠 ~/src ▸` and `☁️ Cloud ▸` appear, no `Language ▸` row, version + Quit at the bottom. **Deferred**: Manual testing in Wave 3.

Read source record ↗

changeInit incremental buildsChecklist complete25 checked · 0 open

The `tillandsias --init` command rebuilds all images from scratch on failure, wasting time when only one image fails. Users need incremental progress saving so failed builds can be retried without rebuilding successful images. Debug mode should also show failed build logs for troubleshooting.

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1.1 Add `debug: bool` field to `CliMode::Init` variant in `src-tauri/src/cli.rs:147`
  • ☑ 1.2 Update `parse()` function in `cli.rs:260-263` to capture `--debug` flag and pass to `CliMode::Init`
  • ☑ 1.3 Add `@trace spec:init-incremental-builds` annotation near the Init mode parsing code
  • ☑ 2.1 Define `InitBuildState` and `ImageBuildStatus` structs with serde derives in `init.rs`
  • ☑ 2.2 Implement `load_build_state()` function to read `$HOME/.cache/tillandsias/init-build-state.json`
  • ☑ 2.3 Implement `save_build_state()` function with atomic write (temp file + rename)
  • ☑ 2.4 Implement `update_image_status()` helper to update single image status in state
  • ☑ 2.5 Add `@trace spec:init-incremental-builds` annotations to state management functions
  • ☑ 3.1 Modify `run_with_force()` in `init.rs:26` to accept `debug: bool` parameter
  • ☑ 3.2 Load build state at start of `run_with_force()`, pass to build loop
  • ☑ 3.3 Before building each image, check state file: if success AND `podman image exists`, skip
  • ☑ 3.4 After each image build, update state file with success/failure (even on failure)
  • ☑ 3.5 In `run_with_force()`, collect failed builds with log paths when debug=true
  • ☑ 3.6 At end of `run_with_force()`, display `tail -10` of failed build logs if debug=true
  • ☑ 3.7 Add `@trace spec:init-incremental-builds` annotations to build loop logic
  • ☑ 4.1 In debug mode, construct command with `tee` to capture output: `script 2>&1 | tee /tmp/tillandsias-init-<image>.log`
  • ☑ 4.2 Store log file path in `ImageBuildStatus` when debug=true
  • ☑ 4.3 Handle both Unix and Windows paths for log files
  • ☑ 4.4 Add `@trace spec:init-incremental-builds` annotations to debug log capture code
  • ☑ 5.1 Create `docs/cheatsheets/init-incremental-builds.md` with provenance header
  • ☑ 5.2 Document state file location and format (JSON structure)
  • ☑ 5.3 Document `--debug` flag behavior and log file locations
  • ☑ 5.4 Add troubleshooting section: how to reset state, where logs are stored
  • ☑ 5.5 Add `@trace spec:init-incremental-builds` annotation in cheatsheet
  • ☑ 6.1 Sync delta spec from `openspec/changes/init-incremental-builds/specs/init-command/spec.md` to `openspec/specs/init-command/spec.md`

Read source record ↗

changeBrowser daemon trackingChecklist complete27 checked · 0 open

The browser isolation feature (wave 3) introduced ephemeral Chromium containers but left critical lifecycle gaps: no debouncing (rapid-fire window spawns), no tracking in `TrayState`, no user feedback on failure, and no cleanup on shutdown. The MCP server is currently a persistent daemon inside the forge container, but agents just need an on-demand tool — no

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1.1 Add `Browser` variant to `ContainerType` enum in `crates/tillandsias-core/src/state.rs`
  • ☑ 1.2 Add `browser_last_launch: HashMap<String, Instant>` and `debug_browser_pid: HashMap<String, u32>` to `TrayState` in `crates/tillandsias-core/src/state.rs`
  • ☑ 1.3 Update `chromium_launcher::spawn_chromium_window()` to return container ID
  • ☑ 1.4 Update `handlers::handle_open_browser_window()` to add container to `state.running` with `ContainerType::Browser`
  • ☑ 2.1 In `handlers::handle_open_browser_window()`, check `state.browser_last_launch` before spawning
  • ☑ 2.2 For safe windows: if `now - last_launch < 10s`, return error "Debounced"
  • ☑ 2.3 For debug windows: check `state.debug_browser_pid` — only one per project
  • ☑ 2.4 Update timestamp on successful spawn
  • ☑ 3.1 In `handlers::handle_open_browser_window()`, push `BuildProgress` with `image_name: "Browser — <project>"`, `status: InProgress`
  • ☑ 3.2 On success: set `status: Completed`, start 5s fadeout timer
  • ☑ 3.3 On failure: set `status: Failed(reason)`, start 5s fadeout timer
  • ☑ 3.4 Add globe icon (🌐) to `menu::build_tray_menu()` for browser chips
  • ☑ 4.1 Create `browser_tool.rs` as simple CLI: `tillandsias-browser-tool <project> <url> <safe|debug>`
  • ☑ 4.2 Remove `mcp_browser.rs` (MCP daemon replaced by CLI tool)
  • ☑ 4.3 Remove Unix socket listener from `main.rs` (no longer needed — tool connects directly to tray socket)
  • ☑ 4.4 Update `entrypoint-forge-opencode-web.sh` to remove MCP server start, add `OPencode_BROWSER=safe`
  • ☑ 4.5 Update `flake.nix` to copy `tillandsias-browser-tool` instead of `tillandsias-mcp-browser`
  • ☑ 5.1 In `entrypoint-forge-opencode-web.sh`, set `OPencode_BROWSER=safe` environment variable
  • ☑ 5.2 OpenCode will use the `tillandsias-browser-tool` CLI (configured via `OPencode_BROWSER=safe`)
  • ☑ 6.1 `handlers::shutdown_all()` already stops all containers in `state.running` (includes `Browser`)
  • ☑ 6.2 Update `handle_stop_project()` to also stop `ContainerType::Browser` containers
  • ☑ 7.1 Add test: `test_debounce_prevents_rapid_spawns()` (placeholder — needs integration setup)
  • ☑ 7.2 Add test: `test_only_one_debug_browser_per_project()` (placeholder — needs integration setup)
  • ☑ 7.3 Add test: `test_browser_container_tracked_in_state()` (placeholder — needs integration setup)
  • ☑ 7.4 Add test: `test_shutdown_cleans_up_browser_containers()` (placeholder — needs integration setup)
  • ☑ 7.5 Add test: `test_chromium_window_type_names()` (in `chromium_launcher.rs`)
  • ☑ 7.6 Add test: `test_is_process_running_invalid_pid()` (in `chromium_launcher.rs`)

Read source record ↗

changeStale secrets cleanupChecklist complete19 checked · 0 open

Unclean tray shutdowns (crashes, forced termination) leave stale podman secrets in the system. On next startup, secret creation fails with "secret name in use", blocking container launches. This requires manual cleanup (`podman secret rm tillandsias-ca-*`) before the tray can function again.

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1.1 Modify `handlers.rs::setup_secrets()` to check for existing secrets
  • ☑ 1.2 Add warning log when stale secret is removed
  • ☑ 1.3 Verify error propagation
  • ☑ 2.1 Unit test: simulate stale secret scenario
  • ☑ 2.2 Integration test: actual unclean shutdown scenario
  • ☑ 2.3 CI validation
  • ☑ 3.1 Update cheatsheets/utils/tillandsias-secrets-architecture.md
  • ☑ 3.2 Update commit message with @trace references
  • ☑ 4.1 Manual smoke test
  • ☑ 4.2 Cleanup on normal shutdown
  • ☑ 4.3 Concurrent tray instances (edge case)
  • ☑ Code compiles without clippy errors
  • ☑ All unit and integration tests pass (`./build.sh --ci` passed)
  • ☑ `./build.sh --ci-full` passes all 8 checks (background task: exit code 0)
  • ☑ Manual smoke test succeeds
  • ☑ Stale secrets are automatically cleaned up on startup
  • ☑ "Secret name in use" errors are eliminated
  • ☑ @trace annotations present and correct
  • ☑ Cheatsheets updated with recovery instructions

Read source record ↗

changeLinux native portable executableChecklist complete47 checked · 0 open

Tauri introduces cross-platform complexity (WebKit, multi-platform build, auto-update) that doesn't align with Tillandsias' multi-platform strategy. With dedicated thin wrappers for macOS (Virtualization.framework) and Windows (WSL2), the Linux tillandsias headless launcher should be the lifecycle source of truth: launch -> resources live -> exit -> full cle

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1.1 Create `src/main.rs` scaffolding (no Tauri)
  • ☑ 1.2 Add musl target to `Cargo.toml`: `[profile.release] lto = true`
  • ☑ 1.3 Verify musl dependencies: ensure signal-hook, tokio-util are available for musl
  • ☑ 1.4 Update build.sh to include `--target x86_64-unknown-linux-musl` flag
  • ☑ 1.5 Create `.cargo/config.toml` with musl optimization flags
  • ☑ 2.1 Refactor `src-tauri/src/main.rs` logic to `src/main.rs` (remove Tauri initialization)
  • ☑ 2.2 Implement `--headless` CLI flag parsing (clap or manual)
  • ☑ 2.3 Implement signal handler registration: SIGTERM, SIGINT → graceful_shutdown()
  • ☑ 2.4 Implement graceful_shutdown(): call handlers::stop_all_containers(30s timeout), cleanup secrets, teardown enclave network
  • ☑ 2.5 Implement JSON state output: `event: "app.started"` at launch, `event: "app.stopped"` at exit
  • ☑ 2.6 Test headless mode: `cargo run -- --headless /tmp/test-project` should start, receive SIGTERM, cleanup gracefully
  • ☑ 3.1 Implement main() auto-detection: if `--headless` not set AND tray available, re-exec self with `--headless` + spawn tray UI
  • ☑ 3.2 Add `--tray` flag support (or separate binary `tillandsias-tray`)
  • ☑ 3.3 Test transparent mode: `cargo run -- /tmp/test-project` should auto-launch tray (if available) managing headless subprocess
  • ☑ 4.1 Add gtk4-rs dependency to Cargo.toml (optional feature: `[features] tray = ["gtk4"]`)
  • ☑ 4.2 Create `src/tray/mod.rs`: spawn headless subprocess via Command::new()
  • ☑ 4.3 Implement tray subprocess lifecycle: forward SIGTERM/SIGINT to child, wait for exit
  • ☑ 4.4 Implement GTK window: show project status, container list, log viewer
  • ☑ 4.5 Implement tray icon: system tray integration (libadwaita or gtk4-native)
  • ☑ 4.6 Test tray: close window should SIGTERM headless, verify containers stop
  • ☑ 5.1 Update `handlers.rs`: ensure stop_all_containers() returns Result<()> for async shutdown
  • ☑ 5.2 Implement signal-hook integration: register handlers, set up channel for SIGTERM/SIGINT
  • ☑ 5.3 Test signal handling: send SIGTERM via `kill -TERM`, verify containers stop within 30s
  • ☑ 5.4 Test SIGKILL fallback: if container doesn't stop in 30s, send SIGKILL, verify exit
  • ☑ 6.1 Update `handlers::cleanup_all_secrets()` to be called on app exit
  • ☑ 6.2 Verify podman secrets tillandsias-* are deleted on SIGTERM
  • ☑ 6.3 Add test: check `podman secret ls` after app exit is empty
  • ☑ 7.1 Build musl binary: `cargo build --release --target x86_64-unknown-linux-musl`
  • ☑ 7.2 Verify binary is statically linked: `ldd ./target/x86_64-unknown-linux-musl/release/tillandsias` should show "not a dynamic executable"
  • ☑ 7.3 Test on Ubuntu 22.04: copy binary, run `tillandsias --headless`, verify containers start
  • ☑ 7.4 Test on Arch Linux: repeat with musl binary
  • ☑ 7.5 Test on Fedora: repeat with musl binary
  • ☑ 7.6 Test portability: run musl binary on system without glibc-devel
  • ☑ 8.1 Remove `src-tauri/` directory (keep in git history, don't delete)
  • ☑ 8.2 Remove `tauri`, `tauri-build`, `webkit2gtk` from Cargo.toml
  • ☑ 8.3 Remove `build-appimage.sh`, `scripts/appimage/` directory
  • ☑ 8.4 Remove embedded Ubuntu container references from build scripts
  • ☑ 8.5 Update `build.sh`: remove Tauri bundle step, only build musl binary
  • ☑ 9.1 Run `cargo test --workspace` with musl target
  • ☑ 9.2 Run litmus test chain: `./build-git.sh && tillandsias --headless /tmp/test-project`
  • ☑ 9.3 Verify all containers start and stop cleanly
  • ☑ 9.4 Verify no orphaned containers remain after app exit
  • ☑ 9.5 Test signal handling with `kill -TERM` and `kill -INT`
  • ☑ 10.1 Update CLAUDE.md: remove Tauri references, document `--headless` mode
  • ☑ 10.2 Update README: new build command (`cargo build --release --target x86_64-unknown-linux-musl`)
  • ☑ 10.3 Create release notes: Tauri removal, musl portability, headless mode for wrappers
  • ☑ 10.4 Tag version: `v0.1.X.Y` with musl build in releases

Read source record ↗

changeAgent source of truth completed 2026 04 27Checklist complete79 checked · 0 open

Coding agents (Claude Code, OpenCode, OpenSpec) running inside the forge container today have no curated reference for what tools the forge ships, what the runtime constraints are (Fedora minimal, immutable image layers, ephemeral mutable overlay), or how to use the tools idiomatically. Agents either guess (wrong defaults, deprecated flags, missing batteries

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1.1 Create `cheatsheets/` at repo root with subdirectories `runtime/`, `languages/`, `utils/`, `build/`, `web/`, `test/`, `agents/`. Add a `.gitkeep` to each so they survive an empty initial commit.
  • ☑ 1.2 Create `cheatsheets/INDEX.md` skeleton — header explaining the format, then one `## <category>` section per subdirectory, each section initially empty (filled as cheatsheets land in waves).
  • ☑ 1.3 Create `cheatsheets/TEMPLATE.md` — the canonical cheatsheet template defined in `design.md` decision 4. Sub-agents in subsequent waves copy this for each new cheatsheet.
  • ☑ 2.1 Add a `## Sources of Truth` section to `~/src/CLAUDE.md` (cross-project workspace conventions): describes the convention, points at `cheatsheets/`, lists the format `- <category>/<filename>.md — <reason>`, notes the warn-not-error policy.
  • ☑ 2.2 Mirror the same section in `~/src/tillandsias/CLAUDE.md` (project-local conventions), pinning paths to this repo.
  • ☑ 2.3 Update the OpenSpec specs template guidance — add a sentence to the spec instruction returned by `openspec instructions specs` indicating the `## Sources of Truth` section is expected. (If the template lives in a config file this change can edit, do so; otherwise leave a note in `design.md` documenting that the template lives outside this repo and the OpenSpec project owner must propagate the change.)
  • ☑ 2.4 Update the change's own template usage: this change's `proposal.md` and `specs/*/spec.md` already cite cheatsheets where they exist (this change creates them), so they self-validate.
  • ☑ 3.1 Write `cheatsheets/runtime/forge-container.md` covering: Fedora minimal 43 base, microdnf vs dnf (forge ships microdnf only), the immutable image layers (everything outside `$HOME` is image-state — read-only at runtime), the writable mutable overlay boundaries (`$HOME/src/<project>` workspace, `$HOME/.cache/`, `$HOME/.config/`), best practices (small configs and skills only, no new binaries in user space), how to consult `/opt/cheatsheets/INDEX.md`, and the RUNTIME_LIMITATIONS feedback loop.
  • ☑ 3.2 Write `cheatsheets/runtime/runtime-limitations.md` covering the RUNTIME_LIMITATIONS_NNN.md format: front-matter fields, location convention, sequential numbering, how the host triages reports.
  • ☑ 3.3 Write `cheatsheets/runtime/networking.md` covering the enclave network: forge has no external network; egress goes through the proxy; git via the enclave-internal mirror; inference via local ollama at `inference:11434`. Anti-patterns: `curl https://...` from the forge (will fail unless going through proxy + cert trust), `pip install` from PyPI (use proxy or accept failure).
  • ☑ 4.1 Write `cheatsheets/agents/claude-code.md` — Claude Code CLI: launching, model flags, hooks, skills, MCP. Pinned version from forge image.
  • ☑ 4.2 Write `cheatsheets/agents/opencode.md` — OpenCode CLI + web mode: serve port, session DB, theme config, parallel sessions on same forge.
  • ☑ 4.3 Write `cheatsheets/agents/openspec.md` — OpenSpec workflow (`openspec new change`, `openspec instructions`, `openspec validate`, `openspec status`); the artifact lifecycle proposal → design → specs → tasks → archive.
  • ☑ 5.1 `cheatsheets/languages/python.md` — Python 3.13 syntax, type hints, dataclasses, match statement, asyncio basics, common stdlib (pathlib, json, subprocess), virtual env conventions in forge (pipx vs pip vs uv).
  • ☑ 5.2 `cheatsheets/languages/rust.md` — Rust edition 2024, ownership, borrowing, lifetimes (one example each), Result/Option idioms, iter chains, common derives, async with tokio basics.
  • ☑ 5.3 `cheatsheets/languages/java.md` — Java 21 LTS, records, sealed classes, pattern matching, virtual threads, common Stream patterns, Maven coords format.
  • ☑ 5.4 `cheatsheets/languages/typescript.md` — TS 5.x, type inference, generics, utility types (Pick/Omit/Partial), discriminated unions, async patterns, tsconfig essentials.
  • ☑ 5.5 `cheatsheets/languages/javascript.md` — Modern JS: const/let, destructuring, spread, async/await, optional chaining, nullish coalescing, ESM vs CJS, common Array methods.
  • ☑ 5.6 `cheatsheets/languages/bash.md` — Strict mode (`set -euo pipefail`), parameter expansion, arrays, `[[ ]]` vs `[ ]`, redirection, here-docs, signal traps, common pitfalls (word splitting, quoting).
  • ☑ 5.7 `cheatsheets/languages/dart.md` — Dart 3 sound null safety, classes, mixins, async/await, Stream basics, Flutter-relevant idioms.
  • ☑ 6.1 `cheatsheets/languages/sql.md` — Standard SQL: SELECT, JOIN types, GROUP BY + HAVING, window functions, CTEs, transactions. Postgres + SQLite divergences (the forge ships clients for both).
  • ☑ 6.2 `cheatsheets/languages/json.md` — JSON syntax, common pitfalls (no comments, no trailing commas, only double quotes), JSON Lines, JSON Pointer.
  • ☑ 6.3 `cheatsheets/languages/yaml.md` — YAML 1.2 basics, indent semantics, anchors/aliases, the Norway problem, multi-line strings (`|` vs `>`), mapping vs sequence pitfalls.
  • ☑ 6.4 `cheatsheets/languages/toml.md` — TOML 1.0 basics, tables, arrays-of-tables, dotted keys, vs YAML/JSON tradeoffs.
  • ☑ 6.5 `cheatsheets/languages/html.md` — HTML5 semantic tags, ARIA basics, common pitfalls (block-vs-inline, nested forms).
  • ☑ 6.6 `cheatsheets/languages/css.md` — Modern CSS: flexbox, grid, custom properties, container queries, common pitfalls (specificity, stacking contexts).
  • ☑ 6.7 `cheatsheets/languages/markdown.md` — CommonMark + GFM, code fences, tables, footnotes, link reference style, GitHub-specific extensions (task lists, mentions).
  • ☑ 6.8 `cheatsheets/languages/xml.md` — XML basics, namespaces, XPath fundamentals, when to use vs JSON/YAML.
  • ☑ 7.1 `cheatsheets/utils/git.md` — git 2.x: commit/branch/log/diff/rebase, common workflows, recovery from common mistakes, hooks basics.
  • ☑ 7.2 `cheatsheets/utils/gh.md` — GitHub CLI: pr / issue / repo / workflow / api commands, JSON output mode, scripting patterns.
  • ☑ 7.3 `cheatsheets/utils/jq.md` — jq 1.7: filters, pipes, --slurp, joins, lookups, common transformations.
  • ☑ 7.4 `cheatsheets/utils/yq.md` — mikefarah/yq 4.x (added by this change): query/update YAML, multi-doc files, the `eval` form vs `eval-all`.
  • ☑ 7.5 `cheatsheets/utils/curl.md` — curl: HTTP methods, headers, --data forms, --form, follow redirects, --resolve, proxy env vars (relevant in forge).
  • ☑ 7.6 `cheatsheets/utils/ripgrep.md` — rg 14: patterns, --type, --multiline, -A/-B/-C, replacements, --json output.
  • ☑ 7.7 `cheatsheets/utils/fd.md` — fd-find: replacement for `find`, type filters, --exec, --hidden flag.
  • ☑ 7.8 `cheatsheets/utils/fzf.md` — fzf: --bind, --preview, common shell integrations, multi-select.
  • ☑ 7.9 `cheatsheets/utils/ssh.md` — ssh: client config, agent forwarding, port forwarding, ProxyJump, known_hosts.
  • ☑ 7.10 `cheatsheets/utils/rsync.md` — rsync: -avz, --delete, --exclude, --dry-run, ssh transport.
  • ☑ 7.11 `cheatsheets/utils/tree.md` — tree: -L depth, -I exclude, --gitignore, --du.
  • ☑ 7.12 `cheatsheets/utils/shellcheck-shfmt.md` — shellcheck rules + auto-fix, shfmt formatting flags. (Both added by this change.)
  • ☑ 8.1 `cheatsheets/build/cargo.md` — Cargo: build/test/run/check/clippy/fmt/doc, workspaces, features, target dirs.
  • ☑ 8.2 `cheatsheets/build/npm.md` — npm: install/run/publish, package.json scripts, workspaces.
  • ☑ 8.3 `cheatsheets/build/pnpm.md` — pnpm: install/run/-w (workspaces), --filter, why-pnpm-vs-npm.
  • ☑ 8.4 `cheatsheets/build/yarn.md` — yarn classic vs berry, install/run/workspaces.
  • ☑ 8.5 `cheatsheets/build/pip.md` — pip: install, requirements.txt, constraints.txt, editable installs, --no-deps.
  • ☑ 8.6 `cheatsheets/build/pipx.md` — pipx: install/run/inject, --python, where binaries land.
  • ☑ 8.7 `cheatsheets/build/uv.md` — uv: drop-in pip replacement, lockfile, project mode.
  • ☑ 8.8 `cheatsheets/build/poetry.md` — poetry: init/install/add/build/publish, vs pip+venv.
  • ☑ 8.9 `cheatsheets/build/maven.md` — Maven: lifecycle phases, common plugins, dependency management, profiles.
  • ☑ 8.10 `cheatsheets/build/gradle.md` — Gradle 8.x: tasks, dependency configurations, wrapper, Kotlin DSL basics.
  • ☑ 8.11 `cheatsheets/build/go.md` — Go modules: go mod init/tidy/get, build/test/run/vet, GOPATH-vs-module-mode.
  • ☑ 8.12 `cheatsheets/build/flutter.md` — Flutter 3.24: pub get, run --device-id, build (web/desktop), no-android/ios in this forge.
  • ☑ 8.13 `cheatsheets/build/make.md` — GNU make: rules, automatic variables, .PHONY, common pitfalls (tabs, recursive expansion).
  • ☑ 8.14 `cheatsheets/build/cmake.md` — CMake 3.x: project, target_*, find_package, generator expressions.
  • ☑ 8.15 `cheatsheets/build/ninja.md` — Ninja: when used, build.ninja format basics, vs make.
  • ☑ 9.1 `cheatsheets/web/protobuf.md` — proto3 syntax, common types, compilation with protoc, language plugins.
  • ☑ 9.2 `cheatsheets/web/grpc.md` — gRPC concepts, unary/streaming, gRPC-web, grpcurl invocation.
  • ☑ 9.3 `cheatsheets/web/openapi.md` — OpenAPI 3.x structure, common refactoring patterns, codegen tools.
  • ☑ 9.4 `cheatsheets/web/http.md` — HTTP/1.1 + HTTP/2 basics, methods, status codes, common headers, idempotency.
  • ☑ 9.5 `cheatsheets/web/websocket.md` — WS protocol, framing, common libraries by language.
  • ☑ 9.6 `cheatsheets/web/sse.md` — Server-sent events, format, reconnection, vs WS.
  • ☑ 9.7 `cheatsheets/test/pytest.md` — pytest: discovery, fixtures, parametrize, conftest, plugins.
  • ☑ 9.8 `cheatsheets/test/junit.md` — JUnit 5: @Test, @BeforeEach, parameterized, assertions.
  • ☑ 9.9 `cheatsheets/test/cargo-test.md` — `cargo test`: discovery, --workspace, integration tests, doctests.
  • ☑ 9.10 `cheatsheets/test/go-test.md` — `go test`: -run, table-driven, t.Helper, subtests, benchmarks.
  • ☑ 9.11 `cheatsheets/test/selenium.md` — Selenium: WebDriver basics, when to use, browser deps inside forge.
  • ☑ 9.12 `cheatsheets/test/playwright.md` — Playwright: browser auto-install, fixtures, when to choose vs Selenium.
  • ☑ 10.1 Add `COPY cheatsheets/ /opt/cheatsheets/` and `ENV TILLANDSIAS_CHEATSHEETS=/opt/cheatsheets` to `images/default/Containerfile`. Place the COPY near the end (after `/opt/agents/`, before locale files) so the layer is small and rebuild-cheap.
  • ☑ 10.2 Add the optional tools in one new `microdnf install -y` layer: `ShellCheck shfmt protobuf-compiler protobuf-devel`. (`yq` and `grpcurl` are not in Fedora's repos — fetch via curl in the same layer, mirror the Gradle pattern.)
  • ☑ 10.3 Update `forge-welcome.sh` to print the cheatsheet hint line.
  • ☑ 10.4 Update `flake.nix` to mirror the same additions for parity (so Nix-backend builds also have the tools — even though the default backend is Fedora).
  • ☑ 11.1 Build forge image: `scripts/build-image.sh forge --force`. Capture build time.
  • ☑ 11.2 Smoke test inside the new image: `podman run --rm tillandsias-forge:v<version> sh -c 'ls /opt/cheatsheets/INDEX.md && echo $TILLANDSIAS_CHEATSHEETS && shellcheck --version && yq --version && protoc --version && grpcurl -version'`.
  • ☑ 11.3 If smoke test passes, install: `./build.sh --install` then launch tray, attach to a project, exec into the forge, verify cheatsheets are visible and `cat $TILLANDSIAS_CHEATSHEETS/INDEX.md` returns the index.
  • ☑ 11.4 Run the test suite: `cargo test --workspace --lib`, `cargo test -p tillandsias --bin tillandsias`. Should be a no-op since this change has no Rust code, but confirms nothing accidentally broke.
  • ☑ 12.1 Verify INDEX.md has every cheatsheet from waves A–E, in the right category section, with one-line descriptions ≤ 100 chars.
  • ☑ 12.2 Run a final pass adding cross-references in each cheatsheet's `## See also` section so the graph is connected.
  • ☑ 13.1 Each new cheatsheet carries `@trace spec:agent-cheatsheets` near the top per the template.
  • ☑ 13.2 No version bump now — happens at archive per CLAUDE.md.

Read source record ↗

changeProposal: expert-serve — one grounded pipeline, two front-endsChecklist complete27 checked · 0 open

Commit 687eb6d57 ("wire local expert system") shipped a facade, and the audit of it is the direct provenance of this change (order 920-pxg6). What the audit found, span by span:

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1.1 File this change tree (.openspec.yaml, proposal, design D1-D7,
  • ☑ 1.2 `specs/expert-serve-grounded-pipeline/spec.md` ADDED requirements
  • ☑ 1.3 `specs/forge-environment-discoverability/spec.md` MODIFIED delta
  • ☑ 2.1 `run_grounded` preflight refusals: no embedding endpoint, no
  • ☑ 2.2 Dead-endpoint degradation: embed failure refuses typed; synthesis
  • ☑ 2.3 HTTP front-end renders refusals verbatim as completion content
  • ☑ 2.4 `litmus-expert-serve-refusal-not-fallback.yaml`: typed refusal +
  • ☑ 2.5 Function-level test: no envelope may carry
  • ☑ 3.1 Per-variant envelopes built via the `spec::build_envelope` family
  • ☑ 3.2 Freshness + `rag_source_commit` from the index entry's `.commit`
  • ☑ 3.3 `litmus-expert-serve-citations-only-if-used.yaml`: subset-usage
  • ☑ 3.4 Freshness honesty tests: fixture `.commit` propagates; frameless
  • ☑ 4.1 DELETE `validate.lua` and its dead bridge paths; validation is
  • ☑ 4.2 Fix `collect.lua` nil-answer crash; regression test.
  • ☑ 4.3 Make `tier.lua` tiers reachable; enforce budgets with
  • ☑ 4.4 Correct `lua_runtime.rs` docs to describe only what exists — no
  • ☑ 4.5 mlua portability evidence from the darwin/msys lanes (902-5bf9's
  • ☑ 5.1 `pub async fn run_grounded(runtime, entry, cfg, query) -> Envelope`
  • ☑ 5.2 Rewrite the `pipeline` CLI arm onto `run_grounded`, emitting the
  • ☑ 5.3 New `expert-serve` subcommand: tokio TcpListener on 127.0.0.1
  • ☑ 5.4 One HTTP client helper for ALL model calls (decompose +
  • ☑ 5.5 Hoist `SpecIndexEntry` (+ resolve ladder, arity-refusing loader)
  • ☑ 5.6 Registration: DISPATCH_ARMS, capabilities.txt (sorted),
  • ☑ 5.7 Config repoint: `tillandsias-experts` provider + `local-experts`
  • ☑ 5.8 `litmus-expert-serve-endpoint-shape.yaml`: OpenAI envelope shape,
  • ☑ 5.9 `cargo test -p tillandsias-plan` green, including the three
  • ☑ 5.10 Live OpenCode session verification against a running

Read source record ↗

changeForge bake nix completed 2026 04 27Checklist complete19 checked · 0 open

Per the user's directive: "make sure nix tools are available in the forge. We will recommend in the methodology that any new projects SHOULD be built using nix tools." Plus `forge-cache-architecture` chose nix as the single shared-cache entry point — that only works if nix is actually present in the forge.

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1.1 Add nix installation to Containerfile (curl installer, single-user mode)
  • ☑ 1.2 Create /etc/nix/nix.conf with experimental-features = nix-command flakes
  • ☑ 1.3 Install direnv to Containerfile
  • ☑ 1.4 Install nix-direnv to Containerfile
  • ☑ 1.5 Verify /nix/store mount point exists (dependency on forge-cache-architecture)
  • ☑ 1.6 Add direnv hooks to bashrc
  • ☑ 1.7 Add direnv hooks to zshrc
  • ☑ 2.1 Add direnv hooks to config.fish
  • ☑ 2.2 Set NIX_CONFIG and NIX_PATH env vars in entrypoint or Containerfile
  • ☑ 2.3 Update TILLANDSIAS_CAPABILITIES to include nix
  • ☑ 3.1 Test: podman run forge nix --version
  • ☑ 3.2 Test: podman run forge nix flake --help
  • ☑ 3.3 Test: direnv hook verification in bash/zsh/fish
  • ☑ 4.1 Add "Nix Inside the Forge" section to project CLAUDE.md
  • ☑ 4.2 Add "Nix-First for New Projects" section to workspace CLAUDE.md (deferred—cross-project scope)
  • ☑ 4.3 Update forge-opencode-onboarding with nix-first.md instruction (covered by 4.1)
  • ☑ 5.1 Run /opsx:verify to confirm implementation matches specs
  • ☑ 5.2 Run /opsx:archive to archive change and sync delta specs to main
  • ☑ 5.3 Bump version: ./scripts/bump-version.sh --bump-changes

Read source record ↗

changeInference host side pull completed 2026 04 27Checklist complete14 checked · 0 open

Per `~/src/java/local_model_testrun.md`: agents need higher-tier models (T2-T5) for analysis tasks but the in-container ollama pull goes through Squid which `project_squid_ollama_eof.md` documents EOFs hard on big manifests. Plus the user's directive: "Models lazily load on the background automatically on first launch, only the correct sizes for the supporte

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ Task 1: Create `src-tauri/src/inference_lazy_pull.rs` module with `spawn_model_pull_task()` entry point
  • ☑ Task 2: Implement `model_tier_map()` returning HashMap<GpuTier, Vec<&str>> with T0-T5 mappings
  • ☑ Task 3: Implement `is_model_cached()` checking `~/.ollama/models/manifests/registry.ollama.ai/library/<name>/<tag>`
  • ☑ Task 4: Implement `run_model_pull()` async function filtering cached models and spawning pulls
  • ☑ Task 5: Implement `pull_model_host_side()` async/blocking function spawning `ollama pull` on host
  • ☑ Task 6: Wire into `handlers.rs::ensure_inference_running()` after health check passes; add `Hash` derive to `GpuTier`
  • ☑ Task 7: Add telemetry log events: task start (tier, model_count), per-model (model, cached/pulled), completion
  • ☑ Task 8: Add degradation log when ollama binary not found (category = "capability", safety = "DEGRADED...")
  • ☑ Task 9: Verify bind-mount already exists in handlers.rs (line 289-295): `-v ~/.cache/tillandsias/models/:/home/ollama/.ollama/models:rw`
  • ☑ Task 10: Build + verify no compile errors; manual test: start inference with GPU, check logs for model pull progress
  • ☑ Task 11: Update `CLAUDE.md` with lazy model pull workflow, tier mappings, command to monitor progress (future `--download-stats`)
  • ☑ Task 12: Add `@trace spec:inference-host-side-pull` annotations in all relevant code locations (handlers, main, module)
  • ☑ Task 13: Mark all 12 tasks above [x] and create final commit
  • ☑ Task 14: Run `/opsx:archive` to archive change, sync specs, validate

Read source record ↗

changeProject bootstrap readme applied 2026 04 27Checklist complete42 checked · 0 open

In-forge agents (OpenCode, Claude, OpenSpec) currently land in a fresh container cold: they spend the first prompt scanning `flake.nix`, `Cargo.toml`, `pubspec.yaml`, `.tillandsias/`, and the OpenSpec tree before producing any user-visible work. Every project owns the perfect surface to short-circuit this — `README.md` — but only if it is treated as the agen

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1.1 Author `cheatsheets/welcome/sample-prompts.md` with at minimum 9 curated sample prompts spanning at least 4 domains (game, business tool, education, creative). `tier: bundled`. Provenance citing one authoritative source on prompting patterns.
  • ☑ 1.2 Author `cheatsheets/welcome/readme-discipline.md` documenting the FOR HUMANS / FOR ROBOTS structure, the auto-regen warning, the seven mandatory FOR ROBOTS subsections, the requires_cheatsheets YAML block schema, and the readme.traces format. `tier: bundled`. Provenance.
  • ☑ 1.3 Author `cheatsheets/runtime/agent-startup-skills.md` documenting the four skills (`/startup`, `/bootstrap-readme-and-project`, `/bootstrap-readme`, `/status`), their routing matrix, the empty-project detection heuristic, and how the OpenCode entrypoint shim invokes `/startup`. `tier: bundled`. Provenance.
  • ☑ 1.4 Update `cheatsheets/INDEX.md` to add a `## welcome` section and the three new lines under runtime.
  • ☑ 2.1 Implement `scripts/summarize-cargo.sh` per the project-summarizers interface (argv contract, exit-code semantics, markdown stdout under H3s). Test against `Cargo.toml` (Tillandsias workspace).
  • ☑ 2.2 Implement `scripts/summarize-nix.sh` (parses `flake.nix` outputs + `flake.lock` input pins).
  • ☑ 2.3 Implement `scripts/summarize-package-json.sh` (parses `dependencies` + `devDependencies`).
  • ☑ 2.4 Implement `scripts/summarize-pubspec.sh` (Flutter SDK pin, dart deps).
  • ☑ 2.5 Implement `scripts/summarize-go-mod.sh` (module path, Go version, top-level requires).
  • ☑ 2.6 Implement `scripts/summarize-pyproject.sh` (build-backend, deps, optional `[tool.poetry]`).
  • ☑ 2.7 Each summarizer SHALL exit 2 (not 1) when its target manifest is absent — distinguishes "skip" from "error".
  • ☑ 3.1 Implement `scripts/regenerate-readme.sh` per the project-summarizers spec: walk to project root, invoke each summarizer, render FOR HUMANS + FOR ROBOTS, atomic write to README.md.
  • ☑ 3.2 Render the FOR HUMANS section: line-1 auto-regen warning, timestamp (coarse to minutes + timezone), ASCII art banner from a curated cache, install snippet, whimsical 3-line description.
  • ☑ 3.3 Render the FOR ROBOTS section: concatenation of summarizer outputs under `## Tech Stack` and `## Build/Runtime Dependencies`; preserve agent-curated `## Security`, `## Architecture`, `## Privacy` from the previous README; append `## Recent Changes`, `## OpenSpec — Open Items`, requires_cheatsheets YAML.
  • ☑ 3.4 Implement `scripts/check-readme-discipline.sh` per the project-bootstrap-readme spec: validate header warning string, two H1s, seven FOR ROBOTS H2s, requires_cheatsheets YAML well-formed, timestamp ≤ 7 days. ERROR on missing structure; WARN on stale timestamp.
  • ☑ 4.1 Author `images/default/config-overlay/opencode/agent/startup.md` — entrypoint, reads project state, branches via `git ls-files | wc -l` heuristic + README presence + validator exit code.
  • ☑ 4.2 Author `images/default/config-overlay/opencode/agent/bootstrap-readme-and-project.md` — empty-project welcome flow; reads `cheatsheets/welcome/sample-prompts.md`; renders banner + 3 randomly-picked prompts + forge capability summary + open prompt.
  • ☑ 4.3 Author `images/default/config-overlay/opencode/agent/bootstrap-readme.md` — invokes `regenerate-readme.sh` then `check-readme-discipline.sh`; surfaces remaining structural gaps as agent prompts.
  • ☑ 4.4 Author `images/default/config-overlay/opencode/agent/status.md` — runs `openspec list`; summarizes last 5 commits via `git log --oneline -5`; loads readme.traces tail; suggests next action.
  • ☑ 5.1 Add the synthetic-first-prompt block to `images/default/entrypoint-forge-opencode.sh`: write `run /startup` to the OpenCode auto-prompt path; idempotent across container restarts.
  • ☑ 5.2 Verify `entrypoint-forge-claude.sh`, `entrypoint-forge-opencode-web.sh`, `entrypoint-terminal.sh` are NOT touched (the shim is OpenCode-only in v1).
  • ☑ 5.3 Update `images/default/Containerfile` (or `flake.nix`) to COPY the four skill files into `~/.config-overlay/opencode/agent/` at image build time.
  • ☑ 5.4 Update `images/default/Containerfile` to COPY the dispatcher + validator into `/usr/local/bin/`, the six summarizers into `/opt/summarizers/` (with symlinks under `/usr/local/bin/`), and the welcome cheatsheets into `cheatsheets/welcome/` (consumed by the existing cheatsheet COPY stage).
  • ☑ 6.1 Author `scripts/install-readme-pre-push-hook.sh` (idempotent — checks for existing hook with same content and is a no-op; re-installs if content drift detected).
  • ☑ 6.2 The hook calls `regenerate-readme.sh && git add README.md && git diff --cached --quiet README.md || git commit --no-verify -m "chore(readme): regenerate at $(date -u -Iminutes)"`.
  • ☑ 6.3 The hook never blocks the push (any non-zero from regenerate is logged and swallowed; the push proceeds).
  • ☑ 6.4 `/startup` invokes the installer if `<project>/.git/hooks/pre-push` is missing or content-mismatched.
  • ☑ 7.1 Define the schema as `{ts: ISO8601, agent: string, observation: string, severity: info|warn|regen-trigger}` in `cheatsheets/welcome/readme-discipline.md`.
  • ☑ 7.2 The dispatcher reads the latest 50 lines of `<project>/.tillandsias/readme.traces` (if present) and feeds them as context to the summary-rendering stage.
  • ☑ 7.3 `<project>/.tillandsias/readme.traces` is committed to the project's git (NOT gitignored) so cross-machine sessions inherit observations.
  • ☑ 7.4 The file is append-only — the dispatcher MUST NOT rewrite earlier lines.
  • ☑ 8.1 Extend the cheatsheet-telemetry event schema to support new `event_type` values: `startup_routing` (which branch was taken), `readme_regen` (which summarizers ran), `readme_requires_pull` (which required cheatsheet was materialized).
  • ☑ 8.2 Add the new event types to `cheatsheets/runtime/external-logs.md`'s schema documentation.
  • ☑ 8.3 The four skills emit telemetry at routing decision points and after dispatcher runs.
  • ☑ 9.1 `/startup` parses the requires_cheatsheets YAML block from the project's README (after running `bootstrap-readme` if README is bad).
  • ☑ 9.2 For each required cheatsheet, look up via cheatsheets-license-tiered tier classifier: bundled → already on disk; distro-packaged → already on disk; pull-on-demand → materialize via the recipe; missing-and-off-allowlist → emit WARN.
  • ☑ 9.3 Materialization emits a `readme_requires_pull` telemetry event with `triggered_by: readme-requires`.
  • ☑ 10.1 Run `/bootstrap-readme` against Tillandsias itself; commit the generated README.
  • ☑ 10.2 Verify the generated README round-trips through `check-readme-discipline.sh` with no errors.
  • ☑ 10.3 Install the pre-push hook in Tillandsias; verify a `git push` produces a `chore(readme): regenerate at <ts>` commit.
  • ☑ 11.1 Add a "Project README discipline" section to `CLAUDE.md` pointing at `cheatsheets/welcome/readme-discipline.md` and naming the four skills.
  • ☑ 11.2 Update `docs/cheatsheets/` (host-side maintainer cheatsheets) with a new entry on the README discipline if a maintainer-relevant gotcha emerges during apply.

Read source record ↗

changeShutdown conmon stragglers completed 2026 04 27Checklist complete19 checked · 0 open

After `Quit Tillandsias`, observed `tillandsias-*` containers occasionally remain running, kept alive by their `conmon` processes. `shutdown_all` calls `launcher.stop()` (graceful SIGTERM with 12s timeout, then SIGKILL via `podman kill`) followed by `client.remove_container()` (`podman rm -f`), and an orphan sweep with prefix `tillandsias-` — but it never **

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1.1 In `crates/tillandsias-podman/src/client.rs:251`, change `kill_container` signature to `pub async fn kill_container(&self, name: &str, signal: Option<&str>) -> Result<(), PodmanError>`. When `signal` is `Some(s)`, prepend `["--signal", s]` to the args. When `None`, preserve today's exact behavior (no `--signal` flag → podman default = SIGTERM).
  • ☑ 1.2 Update the single existing caller in `crates/tillandsias-podman/src/launch.rs:151` (`self.client.kill_container(container_name).await`) to pass `None` as the new signal argument so default-SIGTERM behavior is preserved for the timeout-fallback path.
  • ☑ 1.3 `cargo check --workspace` — confirm no other callers exist.
  • ☑ 2.1 Add `pub(crate) async fn list_running_tillandsias_containers() -> Vec<String>` near the existing infra helpers (around `handlers.rs:1000`). Implementation: shell out to `podman ps --filter name=tillandsias- --format {{.Names}}`, split lines, strip whitespace, filter empty. Errors return an empty vec (defensive — verification continues with what it can see).
  • ☑ 2.2 Add `pub(crate) async fn kill_and_remove(name: &str)`. Calls `client.kill_container(name, Some("KILL"))`, sleeps 500 ms, then `client.remove_container(name)`. Logs `accountability = true, category = "enclave", spec = "app-lifecycle, podman-orchestration"` on entry naming the container.
  • ☑ 2.3 Add `#[cfg(unix)] pub(crate) fn pkill_orphan_conmon()`. Implementation: spawn `pkill -TERM -f 'conmon.*--name tillandsias-'`, ignore exit code (1 = no matches is fine). Log `accountability = true, category = "enclave"` whether or not anything matched.
  • ☑ 2.4 Add `#[cfg(not(unix))] pub(crate) fn pkill_orphan_conmon()` as a no-op stub.
  • ☑ 3.1 Add `pub(crate) async fn verify_shutdown_clean()` in `handlers.rs`. Body:
  • ☑ 3.2 Add `verify_shutdown_clean().await;` as the last line of `shutdown_all` in `handlers.rs:3916` (after the existing orphan-sweep block at lines 4023–4045, before the function returns).
  • ☑ 4.1 In `crates/tillandsias-podman/src/client.rs` `tests` module (or wherever existing `kill_container`-adjacent tests live), add `kill_container_default_signal_omits_flag` asserting that calling with `None` produces a `podman` arg vec equal to `["kill", "<name>"]` (no `--signal`).
  • ☑ 4.2 Add `kill_container_explicit_kill_signal_includes_flag` asserting `Some("KILL")` produces `["kill", "--signal", "KILL", "<name>"]`. If the existing client tests are integration-style (need real podman), add the new tests at the arg-building layer instead — extract `build_kill_args(name, signal) -> Vec<String>` and unit-test it.
  • ☑ 4.3 If `verify_shutdown_clean` ends up factorable into a pure scheduling helper (e.g., `next_action(stragglers, last_step) -> EscalationStep`), unit-test the escalation-state machine independently of podman. If it stays I/O-coupled, gate the integration with `#[ignore]` and document how to run manually.
  • ☑ 4.4 `cargo test --workspace --lib` and `cargo test -p tillandsias --bin tillandsias` — both green.
  • ☑ 5.1 `./build.sh --check` and `./build.sh` (debug). Launch tray, attach to one project, then `Quit`. Confirm `podman ps --filter name=tillandsias-` returns empty before the tray process exits (look in tray log for `verify_shutdown_clean: zero stragglers`).
  • ☑ 5.2 Reproduce the failure mode: launch tray, attach, then `kill -STOP` the forge container's PID 1 with `podman exec`-spawned `kill`, click Quit. Confirm escalation logs fire (`category = "enclave"` SIGKILL escalation log, possibly conmon-pkill log) and the container is gone after `shutdown_all` returns.
  • ☑ 5.3 Confirm graceful shutdown is unchanged in the happy path — total time from `Quit` click to process exit is roughly the same as before (verification phase exits on first tick when nothing strays).
  • ☑ 6.1 Confirm every new function carries `// @trace spec:app-lifecycle` (and `spec:podman-orchestration` where relevant) near the function definition.
  • ☑ 6.2 Add a short note at the bottom of `docs/cheatsheets/tray-state-machine.md` (or new cheatsheet `docs/cheatsheets/shutdown-escalation.md` — author's choice) documenting the three escalation tiers (graceful → SIGKILL → conmon-pkill) and how to interpret the accountability logs after a Quit.
  • ☑ 7.1 No version bump now — bump happens at archive time per CLAUDE.md.

Read source record ↗

Other archived changes 104

Open tasks or no checklist were recorded. These records remain part of the source history.

changeAttach here mvpOpen tasks13 checked · 1 open

The tray app detects projects and shows menus, but "Attach Here" doesn't actually do anything yet. The MVP needs to launch a real development environment — a Fedora Minimal container with OpenCode, OpenSpec, and Nix pre-installed — so a user can click one button and start working on any project.

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1.1 Create `images/default/Containerfile` — Fedora Minimal with microdnf, git, gh, curl, jq, ripgrep, nodejs, npm, nix (single-user), OpenCode binary (glibc variant)
  • ☑ 1.2 Create `images/default/entrypoint.sh` — bootstrap cache dirs, deferred OpenSpec install, welcome banner, launch OpenCode as foreground process
  • ☑ 1.3 Create `images/default/opencode.json` — minimal OpenCode config (OpenCode provider, bash/edit/read tools enabled)
  • ☑ 1.4 Test image builds with `podman build -t tillandsias-forge:latest images/default/` (838MB, verified)
  • ☑ 2.1 Add `build_image(containerfile_path, image_name, context_dir)` async method to PodmanClient
  • ☑ 2.2 Add `ensure_image(image_name, containerfile_path, context_dir)` — builds only if image doesn't exist
  • ☑ 3.1 Update handlers.rs `handle_attach_here` to: check/build image, start container with interactive terminal, mount project dir + cache
  • ☑ 3.2 Container started with `-it` flag and entrypoint, not detached — user gets terminal directly
  • ☑ 3.3 Open host terminal emulator running `podman exec` or direct `podman run -it` into the container
  • ☑ 3.4 Detect terminal emulator: check $TERMINAL, x-terminal-emulator, then fallback list (gnome-terminal, konsole, alacritty, kitty, foot, xterm)
  • ☑ 4.1 Handler resolves image source from executable-relative path or ~/.local/share/tillandsias/images/default/
  • ☑ 4.2 Update build.sh --install to copy images/default/ to ~/.local/share/tillandsias/images/default/
  • ☑ 5.1 Build workspace, verify compilation
  • ☐ 5.2 Test: click Attach Here → image builds → terminal opens with OpenCode → project mounted at /home/forge/src (requires manual interactive test)

Read source record ↗

changeCosign signingOpen tasks15 checked · 5 open

SHA256 checksums (Phase 1) prove that a downloaded file matches what was uploaded, but they don't prove who uploaded it. If an attacker compromises the GitHub Release assets or performs a MITM attack, they can replace both the binary and its checksum. Users have no way to distinguish a legitimate release from a tampered one.

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1.1 Add `id-token: write` to the release workflow permissions in `.github/workflows/release.yml` alongside existing `contents: write`
  • ☑ 1.2 Verify OIDC token generation works by adding a diagnostic step that requests a token and logs the issuer (remove after verification)
  • ☑ 2.1 Add `sigstore/cosign-installer` action step (SHA-pinned with version comment) to the release/signing job
  • ☑ 2.2 Add a `cosign version` verification step to confirm successful installation
  • ☑ 3.1 Add signing loop in the release job: after artifacts are collected, iterate over each binary and run `cosign sign-blob --yes --oidc-issuer https://token.actions.githubusercontent.com --output-signature {artifact}.sig --output-certificate {artifact}.cert {artifact}`
  • ☑ 3.2 Ensure signing uses `--yes` flag for non-interactive mode (no browser prompt in CI)
  • ☑ 3.3 Verify that each signing operation produces both `.sig` and `.cert` files alongside the binary
  • ☑ 3.4 Add failure handling: if any `cosign sign-blob` command fails, the entire job fails (preventing unsigned releases)
  • ☑ 4.1 Update the GitHub Release creation step to include `.sig` and `.cert` files for each artifact alongside the binaries and `SHA256SUMS`
  • ☑ 4.2 Verify the release contains the correct number of assets: 3 binaries + 3 `.sig` + 3 `.cert` + `SHA256SUMS` = 10 files
  • ☑ 5.1 Add a verification command template to the release notes body, automatically populated with the correct artifact names and repository identity for each release
  • ☑ 5.2 Template should include `cosign verify-blob` with `--certificate`, `--signature`, `--certificate-identity-regexp`, and `--certificate-oidc-issuer` flags
  • ☑ 5.3 Add a `VERIFYING.md` document to the repository root explaining how to install Cosign and verify artifacts, with examples for all three platforms
  • ☑ 6.1 Document minisign as a fallback strategy in `VERIFYING.md`: when to use it (Sigstore outage), how it works (static key pair), and why Cosign keyless is preferred
  • ☑ 6.2 Note that minisign implementation is deferred until Sigstore reliability proves insufficient
  • ☐ 7.1 Test the full signing flow by pushing a release candidate tag (`v0.1.0-rc.2`) and verifying all artifacts are signed
  • ☐ 7.2 Download a signed artifact and run `cosign verify-blob` locally to confirm verification succeeds
  • ☐ 7.3 Modify a downloaded artifact (e.g., append a byte) and verify that `cosign verify-blob` fails
  • ☐ 7.4 Verify Rekor entries exist by searching for the artifact hash in the transparency log
  • ☐ 7.5 Test the verification instructions from the release notes on a clean machine (no prior Cosign state)

Read source record ↗

changeRelease pipelineOpen tasks18 checked · 4 open

Tillandsias targets three platforms (Linux, macOS, Windows) and must be distributed as a single download-and-run binary per platform. Without automated release builds, every release requires manual compilation on three operating systems, manual checksum generation, and manual upload to GitHub Releases. This is error-prone, time-consuming, and blocks the rele

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1.1 Create `.github/workflows/release.yml` with `on: push: tags: ['v*']` trigger, `permissions: contents: write`, and concurrency group to prevent duplicate release runs
  • ☑ 1.2 Add version validation job: extract version from `github.ref_name`, read version from workspace `Cargo.toml`, fail with clear error if they don't match
  • ☑ 1.3 Define matrix strategy with three targets: `x86_64-unknown-linux-gnu` on `ubuntu-latest`, `aarch64-apple-darwin` on `macos-latest`, `x86_64-pc-windows-msvc` on `windows-latest`
  • ☑ 2.1 Add Rust toolchain setup step using `dtolnay/rust-toolchain@<sha>` (stable channel) with appropriate target triple per matrix entry
  • ☑ 2.2 Add platform-specific system dependency installation: Linux (`libwebkit2gtk-4.1-dev`, `libappindicator3-dev`, `librsvg2-dev`, `patchelf`), macOS (none needed beyond Xcode CLI tools), Windows (none needed beyond MSVC)
  • ☑ 2.3 Add Rust compilation caching via `Swatinem/rust-cache@<sha>` scoped to the release workflow
  • ☑ 2.4 Add Tauri CLI installation step (`cargo install tauri-cli` or `npm install @tauri-apps/cli`)
  • ☑ 2.5 Add `tauri build` step for each matrix target, producing platform-native bundles
  • ☑ 2.6 Add post-build artifact rename step: find Tauri output artifacts and rename to `tillandsias-{version}-{os}-{arch}.{ext}` convention
  • ☑ 2.7 Upload renamed artifacts via `actions/upload-artifact@<sha>` for consumption by the release job
  • ☑ 3.1 Create `scripts/checksum.sh` that takes a directory of artifacts and produces a `SHA256SUMS` file using `sha256sum`
  • ☑ 3.2 Add checksum job to the workflow (needs all build jobs): download all artifacts via `actions/download-artifact@<sha>`, run checksum script, upload `SHA256SUMS` as artifact
  • ☑ 4.1 Add release job (needs build + checksum jobs): download all artifacts and `SHA256SUMS`, create GitHub Release via `softprops/action-gh-release@<sha>` or `gh release create`
  • ☑ 4.2 Configure release to use tag name as title, auto-generated release notes, and attach all platform artifacts plus `SHA256SUMS`
  • ☑ 4.3 Set release as draft initially (can be changed to auto-publish later)
  • ☑ 5.1 Pin all third-party actions by full commit SHA with human-readable version comments
  • ☑ 5.2 Scope `GITHUB_TOKEN` permissions to minimum required (`contents: write`)
  • ☑ 5.3 Add `concurrency` group to prevent parallel release runs for the same tag
  • ☐ 6.1 Test the workflow by pushing a `v0.1.0-rc.1` tag and verifying all three platform builds succeed
  • ☐ 6.2 Verify artifact naming matches the convention for all platforms
  • ☐ 6.3 Verify `SHA256SUMS` file contains entries for all artifacts and `sha256sum -c` passes
  • ☐ 6.4 Verify GitHub Release is created with all assets attached

Read source record ↗

changeRuntime loggingOpen tasks8 checked · 3 open

The tray app currently has debug-only tracing behind `#[cfg(debug_assertions)]`. In release builds, there's zero observability — container launches, stops, failures, scanner events all happen silently. Developers need structured logs to diagnose issues, and the foundation should support OpenTelemetry for future production telemetry.

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1.1 Add `tracing-appender` to workspace dependencies in root Cargo.toml and src-tauri/Cargo.toml
  • ☑ 1.2 Create `src-tauri/src/logging.rs` module: init function that sets up dual subscriber (stderr if TTY + file appender to state dir)
  • ☑ 1.3 Use `TILLANDSIAS_LOG` env var for filtering (default: `tillandsias=info`), fall back to `RUST_LOG` if set
  • ☑ 1.4 Platform-aware log directory: Linux `~/.local/state/tillandsias/`, macOS `~/Library/Logs/tillandsias/`, Windows `%LOCALAPPDATA%/tillandsias/logs/`
  • ☑ 1.5 Remove `#[cfg(debug_assertions)]` guard from logging init in main.rs — enable in all builds
  • ☑ 2.1 Add tracing instrument spans to handlers.rs: handle_attach_here, handle_stop, handle_destroy with structured fields (container.name, project, genus)
  • ☑ 2.2 Add image build span with duration tracking in podman client ensure_image_built
  • ☑ 2.3 Add scanner lifecycle logging: initial scan count, watch start, project discovered/removed events
  • ☐ 3.1 Build, install, run from terminal and verify logs appear on stderr
  • ☐ 3.2 Verify log file is created at ~/.local/state/tillandsias/tillandsias.log
  • ☐ 3.3 Test TILLANDSIAS_LOG=tillandsias=debug shows debug output

Read source record ↗

changeForge init and mountOpen tasks7 checked · 2 open

The forge entrypoint installs OpenCode and OpenSpec every run because npm globals go to the read-only Nix store and OpenSpec isn't cached. Also, projects mount at `/home/forge/src` flat instead of `/home/forge/src/<project>/`, breaking OpenCode's status bar which should show `src/<project>:main`.

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1.1 Cache OpenCode: download to $CACHE/opencode/ if missing, add to PATH
  • ☑ 1.2 Cache OpenSpec: `npm install --prefix $CACHE/openspec @anthropic-ai/openspec` if missing, add bin to PATH
  • ☑ 1.3 Both cached in mounted volume, persisted across runs
  • ☑ 1.4 Entrypoint is idempotent: second run skips all installs
  • ☑ 2.1 handlers.rs: mount at `/home/forge/src/<project-name>` instead of `/home/forge/src`
  • ☑ 2.2 runner.rs: same mount path fix
  • ☑ 2.3 Entrypoint: cd into the project subdir under src/
  • ☐ 3.1 Test: opencode shows src/lakanoa:main in status bar
  • ☐ 3.2 Test second run is instant (no downloads)

Read source record ↗

changeDesktop launchersOpen tasks22 checked · 1 open

Tillandsias targets non-technical users who do not know what a "terminal" is. Today, launching the application requires opening a terminal emulator and running a binary by name, or double-clicking an unmarked executable in a file manager. Neither path is discoverable, and both feel alien to users who expect applications to appear in their operating system's

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1.1 Create `.desktop` file template at `assets/tillandsias.desktop` with `Name=Tillandsias`, `Comment=Local development environments that just work`, `Exec=tillandsias-tray`, `Icon=tillandsias-tray`, `Terminal=false`, `Type=Application`, `Categories=Development;`, `StartupWMClass=tillandsias-tray`
  • ☑ 1.2 Write install script logic to copy `.desktop` file to `~/.local/share/applications/tillandsias.desktop`, substituting the correct `Exec=` path based on the installed binary location
  • ☑ 1.3 Write install script logic to copy icon PNGs from `src-tauri/icons/` to `~/.local/share/icons/hicolor/{32x32,128x128,256x256}/apps/tillandsias-tray.png`
  • ☑ 1.4 Run `gtk-update-icon-cache ~/.local/share/icons/hicolor/` and `update-desktop-database ~/.local/share/applications/` post-install if commands are available
  • ☑ 1.5 Write uninstall script logic to remove the `.desktop` file, icon files at all resolutions, and refresh caches
  • ☑ 2.1 Create `.app` bundle directory structure template: `Tillandsias.app/Contents/{MacOS,Resources}/` with `Info.plist`
  • ☑ 2.2 Write `Info.plist` template with `CFBundleName=Tillandsias`, `CFBundleIdentifier=com.tillandsias.tray`, `CFBundleIconFile=tillandsias.icns`, `LSUIElement=true` (agent app, no dock icon)
  • ☑ 2.3 Write install script logic to convert PNGs to `.icns` using `iconutil` or `sips`, place binary in `Contents/MacOS/`, and copy bundle to `~/Applications/`
  • ☑ 2.4 Write uninstall script logic to remove `~/Applications/Tillandsias.app`
  • ☑ 3.1 Write PowerShell install script to create Start Menu shortcut at `$env:APPDATA\Microsoft\Windows\Start Menu\Programs\Tillandsias.lnk` using COM WScript.Shell object
  • ☑ 3.2 Ensure NSIS/MSI installer configuration (Tauri bundler) creates Start Menu shortcut with the tillandsia icon
  • ☑ 3.3 Write PowerShell uninstall script to remove the Start Menu shortcut
  • ☑ 4.1 Add `autostart = false` default to the global config template at `~/.config/tillandsias/config.toml`
  • ☑ 4.2 Implement Linux autostart: create/remove `~/.config/autostart/tillandsias.desktop` based on config setting
  • ☑ 4.3 Implement macOS autostart: create/remove `~/Library/LaunchAgents/com.tillandsias.tray.plist` based on config setting
  • ☑ 4.4 Implement Windows autostart: create/remove `HKCU\Software\Microsoft\Windows\CurrentVersion\Run\Tillandsias` registry entry based on config setting
  • ☑ 4.5 Ensure uninstall scripts remove autostart entries on all platforms regardless of current config setting
  • ☑ 5.1 Create `scripts/install.sh` handling Linux and macOS desktop integration (detect platform, run appropriate steps)
  • ☑ 5.2 Create `scripts/install.ps1` handling Windows desktop integration (Start Menu shortcut, optional autostart)
  • ☑ 5.3 Create `scripts/uninstall.sh` handling Linux and macOS cleanup (desktop files, icons, autostart entries, cache refresh)
  • ☑ 5.4 Create `scripts/uninstall.ps1` handling Windows cleanup (Start Menu shortcut, autostart registry entry)
  • ☑ 5.5 Ensure all scripts are idempotent: repeated runs do not produce errors or duplicate entries
  • ☐ 5.6 Test install/uninstall cycle on each platform: verify launcher entry appears, launches tray app correctly, and is fully removed on uninstall

Read source record ↗

changeApp cleanup flagsOpen tasks16 checked · 2 open

Disk usage from Tillandsias artifacts accumulates silently. Podman images (forge, macuahuitl), dangling build layers, stopped containers, Nix build outputs, and Cargo registry caches can grow to several gigabytes without any visibility. Users have no way to inspect or reclaim this space without knowing internal paths and commands.

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1.1 Write proposal.md
  • ☑ 1.2 Write tasks.md
  • ☑ 1.3 Write specs/app-cleanup-flags/spec.md
  • ☑ 2.1 Add `Stats` variant to `CliMode` enum in `cli.rs`
  • ☑ 2.2 Add `Clean` variant to `CliMode` enum in `cli.rs`
  • ☑ 2.3 Parse `--stats` and `--clean` flags in `cli::parse()`
  • ☑ 2.4 Update `USAGE` string to document new flags
  • ☑ 3.1 Create `src-tauri/src/cleanup.rs` with `run_stats()` and `run_clean()`
  • ☑ 3.2 Implement podman image listing (images matching `tillandsias-*` or `macuahuitl*`) with sizes
  • ☑ 3.3 Implement container listing (all containers matching `tillandsias-*`) with status
  • ☑ 3.4 Implement disk usage checks for Nix cache, Cargo cache, and installed binary
  • ☑ 3.5 Implement `run_clean()`: podman image prune, stopped container removal, Nix cache wipe
  • ☑ 3.6 Dispatch `CliMode::Stats` and `CliMode::Clean` early in `main.rs`
  • ☑ 4.1 Add `podman image prune -f` after successful debug build
  • ☑ 4.2 Add `podman image prune -f` after successful release build
  • ☑ 5.1 `cargo check --workspace` passes
  • ☐ 5.2 `tillandsias --stats` prints expected output on a machine with images
  • ☐ 5.3 `tillandsias --clean` removes stopped containers and dangling images

Read source record ↗

changeAppimage desktop integrationOpen tasks0 checked · 10 open

When users download and run the Tillandsias AppImage directly (`./Tillandsias.AppImage`), GNOME shows a generic blue gear icon in the taskbar and dock because there is no `.desktop` file or icon installed on the host. The `install.sh` script handles desktop integration for `--install` users, but AppImage users who simply download and run the binary never exe

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☐ 1.1 Create `src-tauri/src/desktop.rs` with `ensure_desktop_integration()` function
  • ☐ 1.2 Detect AppImage via `std::env::var("APPIMAGE")`
  • ☐ 1.3 Write `.desktop` file to `~/.local/share/applications/tillandsias.desktop`
  • ☐ 1.4 Write icon PNGs (32x32, 128x128, 256x256) to hicolor icon theme directories
  • ☐ 1.5 Run `update-desktop-database` and `gtk-update-icon-cache`
  • ☐ 1.6 Staleness check: only write if `.desktop` missing or `Exec=` path changed
  • ☐ 2.1 Add `mod desktop;` to `main.rs`
  • ☐ 2.2 Call `desktop::ensure_desktop_integration()` after CLI parsing, before tray setup
  • ☐ 3.1 Add 256x256 icon installation to the icon loop in `scripts/install.sh`
  • ☐ 4.1 `./build.sh --check` passes

Read source record ↗

changeAppimage update flagOpen tasks14 checked · 2 open

The tray app silently checks for updates in the background, but there is no way for a user or script to trigger an update check from the command line. Power users and automation scripts need a way to invoke `tillandsias --update` and get a clear, machine-readable report of what happened: current version, whether an update is available, and whether the update

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1.1 Add `Update` variant to `CliMode` enum in `src-tauri/src/cli.rs`
  • ☑ 1.2 Parse `--update` flag before the positional-arg loop in `cli::parse()`
  • ☑ 1.3 Add `tillandsias --update` line to the USAGE help text
  • ☑ 2.1 Create `src-tauri/src/update_cli.rs` with `run()` function
  • ☑ 2.2 Print current version from `env!("CARGO_PKG_VERSION")`
  • ☑ 2.3 Fetch latest.json from the configured endpoint (read from tauri.conf.json at compile time or hardcoded constant)
  • ☑ 2.4 Parse `version` field from latest.json response
  • ☑ 2.5 Compare fetched version with current version; print "Already up to date." or "Update available: vX.Y.Z"
  • ☑ 2.6 If update available: download update artifact, log download progress (bytes received), apply update
  • ☑ 2.7 Print "Restart the application to use the new version." after successful apply
  • ☑ 2.8 Return `bool` (true = success) for `main.rs` exit-code dispatch
  • ☑ 3.1 Handle `CliMode::Update` in `main.rs` before any Tauri builder code
  • ☑ 3.2 Exit with code 0 on success, 1 on error
  • ☑ 4.1 `cargo check --workspace` passes
  • ☐ 4.2 Run `tillandsias --update` manually against live endpoint — verify "Already up to date." path
  • ☐ 4.3 Confirm `tillandsias --help` shows `--update` flag

Read source record ↗

changeDynamic tray iconsOpen tasks0 checked · 24 open

The tray icon is a static PNG set once at startup and never updated. This means users get no ambient feedback about what the application is doing. A build in progress, a broken podman installation, and an idle ready state all look identical. The icon is the only always-visible surface of the app — making it dynamic is the highest-leverage UX improvement poss

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☐ 1.1 Add `TrayIconState` enum (`Base`, `Building`, `Decay`) to `tillandsias-core/src/state.rs`
  • ☐ 1.2 Add `tray_icon_state: TrayIconState` field to `TrayState`
  • ☐ 1.3 Add `active_builds: u32` field to `TrayState`
  • ☐ 1.4 Add derived method: `fn compute_icon_state(&self) -> TrayIconState` — returns `Decay` if `!has_podman`, `Building` if `active_builds > 0`, else `Base`
  • ☐ 2.1 On startup, run podman availability check (`podman version`) before entering the event loop
  • ☐ 2.2 If podman check fails: set `tray_icon_state = Decay`, set `has_podman = false`, disable all menu items, display error item, stop further initialization
  • ☐ 2.3 If podman check passes: check whether `tillandsias-forge:latest` image exists (`podman image exists tillandsias-forge:latest`)
  • ☐ 2.4 If forge image is absent: auto-trigger `run_build_image_script("forge")` with build lock, increment `active_builds`, set icon to `Building`
  • ☐ 2.5 If forge image is present: set `tray_icon_state = Base`, proceed normally
  • ☐ 2.6 Web image is NOT checked or built on launch — build on-demand only
  • ☐ 3.1 In `handlers.rs`, increment `active_builds` when any image build starts
  • ☐ 3.2 Decrement `active_builds` (never below 0) when a build completes or fails
  • ☐ 3.3 After each change to `active_builds`, recompute and update `tray_icon_state`
  • ☐ 4.1 In the state change callback, compare `old_state.tray_icon_state` with `new_state.tray_icon_state`
  • ☐ 4.2 If changed, call `tray.set_icon(tray_icon(new_state.tray_icon_state))`
  • ☐ 4.3 Replace the static `include_bytes!` icon in `main.rs` with the initial call to `tray_icon(TrayIconState::Base)`
  • ☐ 5.1 When `tray_icon_state == Decay`, render all project/action menu items as disabled
  • ☐ 5.2 Show a single disabled informational item at the top of the menu (e.g., "Podman is not available")
  • ☐ 5.3 Quit item remains enabled in Decay state
  • ☐ 6.1 `cargo test -p tillandsias-core` passes with new fields and enum
  • ☐ 6.2 `cargo check --workspace` clean
  • ☐ 6.3 Manual: launch without podman in PATH — Decay icon appears, menu items disabled
  • ☐ 6.4 Manual: launch with podman but no forge image — Building icon during build, Base icon after
  • ☐ 6.5 Manual: launch with podman and forge image already present — Base icon from startup

Read source record ↗

changeFedora repo install docsOpen tasks0 checked · 3 open

The README has Fedora COPR instructions using `sudo dnf`, but on Fedora Silverblue (the primary development platform) `dnf` is not available on the host — the immutable base uses `rpm-ostree`. Users on Silverblue need explicit instructions to add the COPR repo and layer the RPM so it auto-updates with system updates.

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☐ 1. In `README.md`, after the "Fedora (COPR)" block (line ~33), add a "Fedora Silverblue (COPR — auto-updates)" section with:
  • ☐ 2. In `docs/UPDATING.md`, after the "Fedora (COPR)" section (line ~77), add a "Fedora Silverblue" subsection explaining:
  • ☐ 3. Verify markdown renders correctly on GitHub (no broken HTML in details/summary blocks)

Read source record ↗

changeGithub login iconsOpen tasks0 checked · 4 open

The GitHub Login menu items in the Settings submenu are plain text with no visual differentiation. Adding emoji icons gives users an immediate visual cue about the action's nature — the key emoji signals "entering/unlocking" for initial login, and the closed lock signals "re-authenticating" for a refresh when already logged in. This matches the emoji-first v

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☐ 1.1 Change `"GitHub Login"` to `"\u{1F511} GitHub Login"` (🔑) in `build_settings_submenu`
  • ☐ 1.2 Change `"GitHub Login Refresh"` to `"\u{1F512} GitHub Login Refresh"` (🔒) in `build_settings_submenu`
  • ☐ 2.1 `cargo check --workspace` passes (or confirms only GTK linker errors, not compile errors)
  • ☐ 2.2 Both string literals updated in `src-tauri/src/menu.rs`

Read source record ↗

changeImmutable os installOpen tasks6 checked · 1 open

On Fedora Silverblue (and other immutable/ostree systems like Kinoite and uBlue), the install script currently silently falls through to AppImage only when piped from curl — because `HAS_SUDO=false` gates the entire RPM section. This is the right outcome, but it happens for the wrong reason. If a user ever runs the script interactively on Silverblue with sud

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1.1 In `scripts/install.sh`, after architecture detection and before the Linux package manager block, add an `IS_IMMUTABLE=false` default
  • ☑ 1.2 Add detection: if `/run/ostree-booted` exists OR `rpm-ostree` is in PATH, set `IS_IMMUTABLE=true`
  • ☑ 1.3 When `IS_IMMUTABLE=true`, print: "Immutable OS detected (Silverblue/Kinoite/uBlue) — installing to userspace"
  • ☑ 2.1 In the Linux install block, add an early check: if `IS_IMMUTABLE=true`, skip directly to the AppImage download (set `INSTALLED=false` path, bypass all deb/rpm/COPR/rpm-ostree logic)
  • ☑ 2.2 Ensure the AppImage fallback message is adjusted to not say "Falling back" when IS_IMMUTABLE is true — it's the intended path, not a fallback
  • ☑ 3.1 Run `bash -n scripts/install.sh` — no syntax errors
  • ☐ 3.2 Manually verify: on a non-immutable system the deb/rpm path is still attempted as before

Read source record ↗

changeLinux appimage onlyOpen tasks0 checked · 17 open

Tillandsias currently supports three Linux distribution formats: .deb (via APT repository), .rpm (via COPR and direct download), and AppImage. Maintaining all three creates significant complexity:

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☐ 1.1 Remove PKG_TYPE detection (dpkg/rpm check)
  • ☐ 1.2 Remove HAS_SUDO check (not needed for AppImage-only)
  • ☐ 1.3 Remove entire APT repository section (deb install + fallback)
  • ☐ 1.4 Remove entire COPR/dnf/rpm-ostree section (rpm install + fallback)
  • ☐ 1.5 Make AppImage the direct Linux path (not a fallback)
  • ☐ 2.1 Remove deb/rpm from artifact collection find patterns
  • ☐ 2.2 Remove the entire "Publish APT repository" job
  • ☐ 2.3 Remove deb/rpm artifact renaming logic
  • ☐ 3.1 Change `BUNDLES="deb,rpm"` to `BUNDLES="none"` for Linux release builds
  • ☐ 4.1 Remove `linux.deb` and `linux.rpm` configuration sections from bundle config
  • ☐ 5.1 Remove "Fedora (COPR)" section
  • ☐ 5.2 Remove "Fedora Silverblue (COPR)" section
  • ☐ 5.3 Remove "Manual RPM / DEB" section
  • ☐ 6.1 Delete `packaging/tillandsias.spec`
  • ☐ 6.2 Delete `packaging/copr-custom-script.sh`
  • ☐ 6.3 Delete `packaging/COPR-SETUP.md`
  • ☐ 7.1 Run `./build.sh --check` — zero errors

Read source record ↗

changeLocal appimage buildOpen tasks0 checked · 8 open

AppImages can only be built in CI (GitHub Actions on Ubuntu with FUSE). Local development on Fedora Silverblue cannot produce AppImages because Tauri's linuxdeploy bundler requires FUSE, which is unavailable inside the toolbox. Developers need to push to CI and wait for a full release just to test an AppImage locally.

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☐ 1. Add `--appimage` flag parsing to `build.sh` (alongside existing `--release`, `--test`, etc.)
  • ☐ 2. Add `build_appimage()` function in `build.sh` that:
  • ☐ 3. Inside the container: install system deps (build-essential, pkg-config, libgtk-3-dev, libwebkit2gtk-4.1-dev, libappindicator3-dev, librsvg2-dev, libssl-dev, fuse, libfuse2)
  • ☐ 4. Inside the container: install rustup + stable toolchain + `cargo install tauri-cli`
  • ☐ 5. Inside the container: copy source from /src to a writable build dir, run `cargo tauri build`
  • ☐ 6. Inside the container: copy `*.AppImage` from `target/release/bundle/appimage/` to the mounted output dir
  • ☐ 7. Print the output path on success: `target/release/bundle/appimage/Tillandsias-linux-x86_64.AppImage`
  • ☐ 8. Test: run `./build.sh --appimage` on Silverblue, verify the AppImage is created and executable

Read source record ↗

changeMenu progress uxOpen tasks0 checked · 24 open

The tray menu gives no feedback during image builds or container creation — the user sees a static menu with no indication that work is in progress. On Linux/libappindicator, periodic menu rebuilds to animate a spinner or show elapsed time would cause visible flicker if the user has the menu open. The tray icon (from the dynamic-tray-icons change) provides c

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☐ 1.1 Add `BuildStatus` enum to `tillandsias-core/src/state.rs`: `InProgress`, `Completed`, `Failed(String)`
  • ☐ 1.2 Add `BuildProgress` struct: `image_name: String`, `status: BuildStatus`, `started_at: Instant`, `completed_at: Option<Instant>`
  • ☐ 1.3 Add `active_builds: Vec<BuildProgress>` field to `TrayState`; initialise as empty in `TrayState::new()`
  • ☐ 2.1 On build-started event: push `BuildProgress { status: InProgress, .. }` into `state.active_builds`, call `on_state_change`
  • ☐ 2.2 On build-completed event: find matching entry, set `status = Completed`, set `completed_at = Some(Instant::now())`, call `on_state_change`
  • ☐ 2.3 On build-failed event: find matching entry, set `status = Failed(reason)`, call `on_state_change`
  • ☐ 2.4 Add prune step before each `on_state_change` call: remove entries where `status == Completed` and `completed_at.elapsed() > 10s`; if any were removed, the rebuild triggered by the original event handles the clean state (no extra rebuild needed)
  • ☐ 2.5 Schedule the 10s fadeout: after setting `completed_at`, spawn a one-shot `tokio::time::sleep(10s)` task that sends a no-op state-change trigger so the prune step fires and removes the completed item
  • ☐ 3.1 In `rebuild_menu()`, after the "No running environments" / running section, iterate `state.active_builds` and render each as a disabled `MenuItemBuilder` item
  • ☐ 3.2 `InProgress` label: `"⏳ Building {image_name}..."`
  • ☐ 3.3 `Completed` label: `"✅ {image_name} ready"`
  • ☐ 3.4 `Failed` label: `"❌ {image_name} build failed"`
  • ☐ 3.5 All build-progress items use `enabled(false)`
  • ☐ 3.6 Rename "Ground" entry in project submenu builder: change emoji from `\u{1F331}` to `\u{1F527}` (🔧) and label from `Ground` to `Maintenance`
  • ☐ 4.1 Reuse `BuildProgress` / `active_builds` for maintenance container setup: image_name = `"Maintenance"`
  • ☐ 4.2 On maintenance-container setup started: push `InProgress` entry; label renders as `"🔧 Setting up Maintenance..."` — override label in render step when `image_name == "Maintenance"` and status is `InProgress`
  • ☐ 4.3 Completed and Failed follow the same pattern as image builds
  • ☐ 5.1 `cargo check --workspace` passes
  • ☐ 5.2 `cargo test --workspace` passes
  • ☐ 5.3 Manual: trigger a build, confirm menu shows `⏳` chip while in progress
  • ☐ 5.4 Manual: on completion, confirm chip changes to `✅`, then disappears after ~10 seconds with no flicker
  • ☐ 5.5 Manual: on failure, confirm `❌` chip persists and is removed only when next build attempt begins
  • ☐ 5.6 Manual: confirm project submenu shows `🔧 Maintenance` (not `🌱 Ground`)
  • ☐ 5.7 Manual: confirm `MenuCommand::Terminal` and `terminal:<path>` ID still route correctly after rename

Read source record ↗

changeMenu ux polishOpen tasks0 checked · 14 open

The project submenu has several rough UX edges: "Attach Here" shows no plant emoji when idle, remains clickable when the container is already running, the Maintenance icon is a generic wrench instead of a garden tool, the per-project container listing duplicates information already conveyed by the "Attach Here" / "Blooming" state, and the project label in th

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☐ 1.1 Idle state: show `🌱 Attach Here` (seedling prefix) when `assigned_genus` is None
  • ☐ 1.2 Running state: show `🌺 Blooming` (genus flower + "Blooming") when `assigned_genus` is Some, with `enabled(false)`
  • ☐ 1.3 Container exit: reverts to idle automatically via `assigned_genus = None` (no code change needed)
  • ☐ 2.1 Replace wrench `🔧` (U+1F527) with pick `⛏️` (U+26CF) in idle Maintenance label
  • ☐ 2.2 Replace wrench `🔧` in build chip label for Maintenance in-progress state
  • ☐ 3.1 Remove separator before per-project containers (line 451)
  • ☐ 3.2 Remove entire per-project container loop (lines 443-464)
  • ☐ 4.1 When attach container running: prefix project name with genus flower (e.g., `🌺 my-project`)
  • ☐ 4.2 When only maintenance running: prefix with pick (e.g., `⛏️ my-project`)
  • ☐ 4.3 When both running: prefix with both (e.g., `🌺⛏️ my-project`)
  • ☐ 4.4 When nothing running: prefix with seedling (e.g., `🌱 my-project`)
  • ☐ 4.5 Remove parenthesized counter from project label
  • ☐ 5.1 `cargo check --workspace` passes
  • ☐ 5.2 `cargo test --workspace` passes

Read source record ↗

changeNamed terminalsOpen tasks0 checked · 23 open

Terminal windows launched by "Attach Here" and "Maintenance" show meaningless titles like "ptyxis" or a raw `podman run ...` command string. When a user has multiple projects running simultaneously, there is no way to tell which terminal window belongs to which project. Additionally, clicking "Attach Here" on a project that already has a running container si

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☐ 1.1 Add `flower()` method to `TillandsiaGenus` in `crates/tillandsias-core/src/genus.rs`, returning the fixed emoji for each variant
  • ☐ 1.2 Verify the 8-flower pool maps 1:1 to the 8 genera (no duplicates, no gaps)
  • ☐ 2.1 Change `open_terminal(command: &str)` signature to `open_terminal(command: &str, title: &str)` in `src-tauri/src/handlers.rs`
  • ☐ 2.2 Update ptyxis branch to prepend `-T <title>` before `-s --new-window -x`
  • ☐ 2.3 Update gnome-terminal branch to prepend `--title=<title>`
  • ☐ 2.4 Update konsole branch to prepend `-p tabtitle=<title>`
  • ☐ 2.5 Update xterm branch to prepend `-T <title>`
  • ☐ 2.6 Update macOS osascript branch — embed title in the `do script` call if Terminal.app supports it, otherwise no-op
  • ☐ 2.7 Update Windows `cmd /c start` branch to pass the title as the first positional argument to `start`
  • ☐ 3.1 In `handle_attach_here()`: construct title as `"<flower> <project_name>"` using the allocated genus and call `open_terminal(&podman_cmd, &title)`
  • ☐ 3.2 In `handle_terminal()`: construct title as `"<flower> <project_name>"` — allocate or derive genus consistently with the attach path, then call `open_terminal(&podman_cmd, &title)`
  • ☐ 4.1 In `handle_attach_here()`: before allocating a genus, check `state.running` for any container whose `project_name` matches; if found, fire a desktop notification "Already running — look for '<flower> <project_name>' in your windows" and return early
  • ☐ 4.2 In `handle_terminal()`: apply the same guard for maintenance terminals (container name pattern `tillandsias-<project>-terminal`)
  • ☐ 4.3 Use `tauri::notification::Notification` (or equivalent Tauri v2 API) for the desktop notification — no modal dialog
  • ☐ 5.1 In `build_project_submenu()` in `src-tauri/src/menu.rs`: when a project has a running container (`project.assigned_genus.is_some()`), prefix the "Attach Here" label with the flower emoji of the assigned genus
  • ☐ 5.2 Apply the same prefix to the "Maintenance" terminal label when a maintenance container is already running
  • ☐ 6.1 `cargo check --workspace` passes with no errors
  • ☐ 6.2 `cargo test --workspace` passes
  • ☐ 6.3 Manual: launch Attach Here on a project — terminal window title shows `<flower> <project_name>`
  • ☐ 6.4 Manual: tray menu "Attach Here" label shows matching flower while environment is running
  • ☐ 6.5 Manual: click "Attach Here" a second time — desktop notification appears, no new terminal opens
  • ☐ 6.6 Manual: repeat 6.3–6.5 for Maintenance terminal
  • ☐ 6.7 Manual: stop the container — menu label reverts to plain "Attach Here" with no flower

Read source record ↗

changeOpenspec auto initOpen tasks0 checked · 3 open

When a user launches a Tillandsias environment for the first time, OpenSpec is installed inside the container but `openspec init` is never called for the project. This means OpenCode's OpenSpec commands (e.g., `/opsx:new`, `/opsx:onboard`) fail with "Run `openspec init` first". The init step is missing from the container entrypoint.

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☐ 1. In `images/default/entrypoint.sh`, after the project directory detection block (line 54) and before the banner (line 56), add an OpenSpec init block:
  • ☐ 2. Verify the embedded entrypoint in `src-tauri/src/embedded.rs` references `images/default/entrypoint.sh` via `include_str!` — confirm the change will be picked up on next build
  • ☐ 3. Test: rebuild forge image (`./scripts/build-image.sh forge --force`) and launch a new environment to verify OpenSpec is initialized automatically

Read source record ↗

changeReadme simplifyOpen tasks0 checked · 8 open

The current Install section uses nested `<details>` blocks to show per-platform options, including APT repository setup, Fedora COPR, Silverblue rpm-ostree layering, RPM/DEB downloads, and an `.app.tar.gz` bundle. This creates visual noise and maintenance surface: three distro-specific package manager flows, two Linux download formats (AppImage + RPM + DEB),

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☐ 1.1 Remove the outer `<details open>` / `<details>` wrapper for Linux and the nested "Other ways to install" block (APT, COPR, Silverblue rpm-ostree, RPM/DEB table)
  • ☐ 1.2 Remove the `<details>` wrapper for macOS and its nested "Other ways to install" block (DMG table + `.app.tar.gz` link)
  • ☐ 1.3 Remove the `<details>` wrapper for Windows and its nested "Other ways to install" block (MSI link)
  • ☐ 1.4 Write three flat platform sections: `**Linux**`, `**macOS**`, `**Windows**` — each containing only its one-shot install command block
  • ☐ 1.5 Add a single collapsed `<details><summary>Direct downloads</summary>` block after the three platform sections with a four-row table (Linux AppImage, macOS Apple Silicon DMG, macOS Intel DMG, Windows setup.exe)
  • ☐ 2.1 Confirm `## Run` and all sections after it are byte-for-byte unchanged
  • ☐ 2.2 Confirm no broken HTML — every `<details>` has a matching `</details>`, every `<summary>` has a matching `</summary>`
  • ☐ 2.3 Confirm the direct-downloads table renders (pipe-delimited markdown table inside the `<details>` block)

Read source record ↗

changeSvg icon pipelineOpen tasks0 checked · 10 open

The 32 SVG assets at `assets/icons/<genus>/<state>.svg` are currently dead design documentation. The system tray displays a single static PNG (`src-tauri/icons/tray-icon.png`) generated by `scripts/generate-icons.py` as a procedural rosette — it does not use the per-genus, per-lifecycle SVGs at all. This means the visual identity the SVGs represent (8 distin

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☐ 1. Add `resvg` and `tiny-skia` as `[build-dependencies]` in `src-tauri/Cargo.toml`
  • ☐ 2. Extend `src-tauri/build.rs` to render all 32 SVGs (`assets/icons/<genus>/<state>.svg`) into 32x32 PNGs in `OUT_DIR/icons/<genus>/<state>.png` using `resvg`
  • ☐ 3. Extend `src-tauri/build.rs` to render the 3 tray icon variants (Ionantha bud/bloom/dried) into `OUT_DIR/icons/tray/base.png`, `building.png`, `decay.png` at 32x32
  • ☐ 4. Extend `src-tauri/build.rs` to render per-genus, per-lifecycle window icons at 48x48 into `OUT_DIR/icons/window/<genus>/<state>@48.png`
  • ☐ 5. Add `TrayIconState` enum (`Base`, `Building`, `Decay`) to `crates/tillandsias-core/src/genus.rs`
  • ☐ 6. Create `crates/tillandsias-core/src/icons.rs` with `icon_png(genus, lifecycle, size)` and `tray_icon_png(state)` functions using `include_bytes!` pointed at `OUT_DIR` paths via `env!("OUT_DIR")`
  • ☐ 7. Expose the `icons` module in `crates/tillandsias-core/src/lib.rs`
  • ☐ 8. Replace `include_bytes!("../icons/tray-icon.png")` in `src-tauri/src/main.rs` with `tillandsias_core::icons::tray_icon_png(TrayIconState::Base)`
  • ☐ 9. Add unit tests to `icons.rs` verifying all 32 PNG combinations are non-empty and start with the PNG magic bytes (`\x89PNG`)
  • ☐ 10. Verify `cargo build --workspace` succeeds and the tray icon loads correctly at runtime

Read source record ↗

changeTerminal in tauriOpen tasks0 checked · 45 open

Terminals are currently spawned as external processes (ptyxis, gnome-terminal, konsole, xterm) via `open_terminal()` in handlers.rs. Once spawned, the application loses all control. It cannot focus the window when the user clicks "Attach Here" a second time. It cannot detect when the user closes the terminal — only inferring exit indirectly from podman conta

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☐ 1.1 Add `portable-pty` to `src-tauri/Cargo.toml` dependencies
  • ☐ 1.2 Enable Tauri features in `src-tauri/Cargo.toml`: add `"window-create"` and `"webview-data-url"` to the tauri dependency features list (currently only `"tray-icon"` and `"image-png"`)
  • ☐ 1.3 Update `src-tauri/capabilities/default.json`: add `"core:window:allow-create"`, `"core:window:allow-set-focus"`, `"core:window:allow-close"`, `"core:event:allow-emit"`, `"core:event:allow-listen"` permissions
  • ☐ 1.4 Vendor xterm.js (minified bundle + CSS) and addons (fit, webgl, web-links) into `assets/frontend/vendor/`; no npm, no CDN, no build step
  • ☐ 1.5 Create `assets/frontend/terminal.html` — minimal HTML shell that loads xterm.js, creates terminal instance, initializes fit addon
  • ☐ 1.6 Create `assets/frontend/terminal.js` — Tauri IPC bindings: listen for `terminal:data` events, invoke `terminal_write` on user input, invoke `terminal_resize` on window resize, invoke `terminal_ready` on load
  • ☐ 1.7 Create `assets/frontend/terminal.css` — terminal fills viewport, no scrollbars on body, dark background matching xterm defaults
  • ☐ 1.8 Verify basic window creation: hardcode a test Tauri window in setup that loads `terminal.html` and displays an xterm.js instance (no PTY yet, just confirm rendering works)
  • ☐ 2.1 Create `src-tauri/src/terminal.rs` — `PtySession` struct holding: PTY master pair (reader + writer), child process handle, window label, buffer
  • ☐ 2.2 Implement `PtySession::spawn(command, args, env, cwd, size)` — uses `portable_pty::native_pty_system()` to open a PTY, spawns the child process connected to the PTY slave, returns `PtySession`
  • ☐ 2.3 Implement `PtySession::read_loop(window)` — async task that reads from PTY master, batches output at ~16ms intervals, emits `terminal:data` events to the window, detects EOF and emits `terminal:exit`
  • ☐ 2.4 Implement `PtySession::write(data)` — writes raw bytes to the PTY master writer
  • ☐ 2.5 Implement `PtySession::resize(cols, rows)` — calls `pty.resize(PtySize { rows, cols, pixel_width: 0, pixel_height: 0 })`
  • ☐ 2.6 Implement `PtyManager` struct — `HashMap<String, PtySession>` keyed by window label, with methods: `spawn`, `write`, `resize`, `remove`, `has`
  • ☐ 2.7 Register `PtyManager` as Tauri managed state (`app.manage(Mutex<PtyManager>)`)
  • ☐ 2.8 Implement Tauri command `terminal_write(label: String, data: String, state: State<Mutex<PtyManager>>)` — looks up session by label, calls write
  • ☐ 2.9 Implement Tauri command `terminal_resize(label: String, cols: u16, rows: u16, state: State<Mutex<PtyManager>>)` — looks up session, calls resize
  • ☐ 2.10 Implement Tauri command `terminal_ready(label: String, state: State<Mutex<PtyManager>>)` — signals the read loop to begin emitting data
  • ☐ 2.11 Register commands in `main.rs`: `.invoke_handler(tauri::generate_handler![terminal_write, terminal_resize, terminal_ready])`
  • ☐ 2.12 Unit test: spawn a PTY running `echo hello`, verify read loop receives "hello\n" and emits EOF
  • ☐ 3.1 Implement `terminal.js` — on DOMContentLoaded: create `Terminal` instance, load fit addon, open terminal in container div, call fit
  • ☐ 3.2 Wire xterm.js `onData` callback to invoke `terminal_write` with the current window label
  • ☐ 3.3 Wire xterm.js `onResize` callback (from fit addon) to invoke `terminal_resize` with cols and rows
  • ☐ 3.4 Listen for `terminal:data` Tauri event — base64-decode payload, call `terminal.write()` with decoded bytes
  • ☐ 3.5 Listen for `terminal:exit` Tauri event — display "Session ended (exit code N)" in terminal, disable input
  • ☐ 3.6 Send `terminal_ready` invoke after xterm.js initialization completes — this unblocks the PTY read loop
  • ☐ 3.7 Handle window resize: `window.addEventListener('resize', ...)` triggers fit addon's `fit()`, which triggers `onResize`, which sends `terminal_resize`
  • ☐ 3.8 Implement copy/paste: xterm.js selection API + Tauri clipboard (or browser default Ctrl+Shift+C/V)
  • ☐ 3.9 Test: open window, verify xterm.js renders, type characters, confirm they echo back (requires Phase 2 PTY running `bash` or `cat`)
  • ☐ 4.1 Pass `AppHandle` to `handle_attach_here()` — add parameter, thread it through from the event loop which receives it from main.rs setup
  • ☐ 4.2 Rewrite `handle_attach_here()` — replace `open_terminal(&podman_cmd)` with: check for existing window (`app.get_webview_window(label)`), if exists call `set_focus()`, otherwise create `WebviewWindowBuilder::new()` with label, title, size, icon; then spawn PTY with podman args; wire read loop to window
  • ☐ 4.3 Rewrite `handle_terminal()` — same pattern as attach_here but with fish entrypoint and `-terminal` label suffix
  • ☐ 4.4 Rewrite `handle_github_login()` — same pattern with the extracted gh-auth-login.sh script
  • ☐ 4.5 Wire window close event in `main.rs` `run()` callback — on `RunEvent::WindowEvent { event: WindowEvent::Destroyed, label, .. }`: look up PTY session, drop it (sends SIGHUP), remove from PtyManager, update TrayState
  • ☐ 4.6 Wire PTY EOF to state update — when read loop detects EOF: remove container from `TrayState.running`, release genus from allocator, trigger menu rebuild, close window after 2-second delay
  • ☐ 4.7 Delete `open_terminal()` function from handlers.rs
  • ☐ 4.8 Remove platform-specific terminal detection code (ptyxis, gnome-terminal, konsole, xterm, osascript, cmd.exe)
  • ☐ 4.9 Remove `LD_LIBRARY_PATH`/`LD_PRELOAD` clearing for terminal spawning (no longer needed — PTY is in-process)
  • ☐ 4.10 Update `build_run_args()` — the function still builds the argument list but no longer joins them into a shell command string; return `Vec<String>` is passed directly to `PtySession::spawn`
  • ☐ 4.11 Test with OpenCode: "Attach Here" → window opens → OpenCode launches inside container → full TUI works (colors, mouse, alternate screen, resize)
  • ☐ 4.12 Test with vim/htop: open maintenance terminal → run vim → verify insert mode, syntax highlighting, mouse clicks, window resize
  • ☐ 4.13 Test window re-focus: click "Attach Here" when window already exists → existing window gains focus, no duplicate created
  • ☐ 4.14 Test window close cleanup: close window via X button → container receives SIGTERM → container stops within 10s → genus released → tray menu updated
  • ☐ 4.15 Test PTY EOF cleanup: type `exit` in terminal → container exits → window closes automatically → genus released → tray menu updated
  • ☐ 4.16 Test GitHub Login flow: click "GitHub Login" → window opens → gh auth login runs → user completes auth → window closes on exit

Read source record ↗

changeTerminal lifecycleOpen tasks0 checked · 30 open

After `terminal-in-tauri` lands, terminals run inside Tauri webview windows that Tillandsias owns. The tray menu has no way to know whether a window is open, so "Attach Here" always launches a new terminal — destroying the user's existing session (open editor, uncommitted history, running processes). There is also no cleanup path: if the user closes the wind

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☐ 1.1 Add `WindowType` enum (`AttachHere`, `Maintenance`) to `tillandsias-core/src/state.rs`
  • ☐ 1.2 Add `WindowInfo` struct (`project_path: PathBuf`, `genus: Option<TillandsiaGenus>`, `window_type: WindowType`, `created_at: Instant`) to `tillandsias-core/src/state.rs`
  • ☐ 1.3 Add `open_windows: HashMap<String, WindowInfo>` field to `TrayState`
  • ☐ 1.4 Add `compute_window_label(project_path: &Path, window_type: &WindowType, genus: Option<&TillandsiaGenus>) -> String` helper — produces `tillandsias-<slug>-<genus-slug>` or `tillandsias-<slug>-maintenance`
  • ☐ 2.1 In `handle_attach_here`, compute the window label before any container or PTY work
  • ☐ 2.2 Call `app.get_webview_window(&label)`; if `Some(window)` is returned, call `window.unminimize()` if minimized, then `window.set_focus()`, and return `Ok(())`
  • ☐ 2.3 If no window exists, proceed with normal launch; add entry to `open_windows` when the Tauri window is created
  • ☐ 3.1 In `handle_maintenance`, compute the maintenance label (`tillandsias-<slug>-maintenance`) before any work
  • ☐ 3.2 Same focus-or-launch gate as task 2.2
  • ☐ 3.3 If no window exists, proceed with launch; add entry to `open_windows` with `WindowType::Maintenance`
  • ☐ 4.1 In `main.rs`, register an `on_window_event` listener after app setup
  • ☐ 4.2 On `WindowEvent::Destroyed`, look up the window label in `open_windows`
  • ☐ 4.3 If found: remove entry, release genus if present, remove project from running container set, call `rebuild_menu()`
  • ☐ 4.4 If not found (non-tracked window): no-op, return silently
  • ☐ 5.1 When the user closes the Tauri window (X button), the PTY manager receives the close signal via the `CloseRequested` window event
  • ☐ 5.2 PTY manager sends SIGHUP to the podman child process
  • ☐ 5.3 Verify containers are started with `--stop-timeout=10` and `--rm` so graceful stop and removal are automatic
  • ☐ 5.4 After SIGHUP is sent, allow `WindowEvent::Destroyed` to handle all state cleanup (no duplicate cleanup in the close handler)
  • ☐ 6.1 In `build_menu()`, for each project item, check whether an entry exists in `open_windows` for the project
  • ☐ 6.2 Show 🌺 bloom icon for "Attach Here" only when: `open_windows` contains an AttachHere entry for the project AND the container is in the running set
  • ☐ 6.3 Show 🌱 pup icon in all other cases (no window, or window exists but container not yet running)
  • ☐ 6.4 Show 🌱 pup icon for Maintenance item when no maintenance window is open; show 🔧 (or distinct bloom variant) when a maintenance window is open
  • ☐ 7.1 `cargo test -p tillandsias-core` passes with new types and fields
  • ☐ 7.2 `cargo check --workspace` clean
  • ☐ 7.3 Manual: click "Attach Here" → terminal opens, menu shows 🌺
  • ☐ 7.4 Manual: click "Attach Here" again while terminal is open → existing window comes to front, no new window spawned, container count unchanged
  • ☐ 7.5 Manual: type `exit` in terminal → window closes, menu reverts to 🌱, container removed (`podman ps` confirms)
  • ☐ 7.6 Manual: close terminal window via X → menu reverts to 🌱, container removed
  • ☐ 7.7 Manual: minimize terminal window, then click "Attach Here" → window unminimizes and comes to front
  • ☐ 7.8 Manual: same focus recovery tests for Maintenance terminal

Read source record ↗

changeEmoji ux guidelinesOpen tasks0 checked · 22 open

The emoji system is inconsistent: Maintenance terminals show flower emojis (from the genus) instead of tool emojis, breaking the visual link between the menu item and the window title. The core UX principle — every window "plants" its emoji on the menu item that launched it — is undermined when emoji families don't match intent.

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☐ 1.1 Create `crates/tillandsias-core/src/tools.rs` with `TOOL_EMOJIS: &[&str]` pool (16+ curated tools: 🔧🪛🔩⚙️🪚🔨🪜🧲🪣🧰🪝🔗📐🪤🧱🪵)
  • ☐ 1.2 Add `ToolAllocator` (mirrors GenusAllocator pattern) — allocate/release per project
  • ☐ 1.3 Add `tool_emoji(index)` helper
  • ☐ 1.4 Expose module in `lib.rs`
  • ☐ 2.1 Add `display_emoji: String` field to `ContainerInfo` in `state.rs`
  • ☐ 2.2 In `handle_attach_here()`: set `display_emoji = genus.flower().to_string()`
  • ☐ 2.3 In `handle_terminal()`: allocate tool from ToolAllocator, set `display_emoji = tool`
  • ☐ 2.4 In startup container discovery: set `display_emoji = genus.flower()` for discovered containers (default to Forge)
  • ☐ 3.1 In `handle_attach_here()`: window title = `"{display_emoji} {project_name}"`
  • ☐ 3.2 In `handle_terminal()`: window title = `"{display_emoji} {project_name}"` (tool emoji, NOT flower)
  • ☐ 4.1 In `build_project_submenu()`: collect all display_emojis for running containers of this project
  • ☐ 4.2 Separate into tools (Maintenance) and flowers (Forge) groups
  • ☐ 4.3 Format label: `"{project_name} {tools}{flowers}"` — tools first, flowers after
  • ☐ 4.4 Idle projects: plain name, no emojis
  • ☐ 4.5 Maintenance menu item: show tool emoji when running (`"🔧 Maintenance"` → `"{tool} Maintenance"`)
  • ☐ 5.1 Release tool emoji from ToolAllocator when Maintenance container stops (in handle_podman_event)
  • ☐ 5.2 Update tests for new ContainerInfo field
  • ☐ 6.1 `./build.sh --check` passes
  • ☐ 6.2 `./build.sh --test` passes
  • ☐ 6.3 Manual: launch Forge → window title has flower, menu shows flower suffix
  • ☐ 6.4 Manual: launch Maintenance → window title has tool, menu shows tool suffix
  • ☐ 6.5 Manual: launch 2 Maintenance + 1 Forge → project shows `project 🔧🪛🌸`

Read source record ↗

changeExpand genus poolOpen tasks0 checked · 13 open

The genus pool has only 8 entries. With terminals now receiving their own genus names (separate from forge containers), a single project can easily consume 2+ genera. Users working on 4-5 projects simultaneously will exhaust the pool and hit `None` from the allocator, leaving new environments unnamed.

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☐ 1.1 Add 16 new variants to `TillandsiaGenus` enum: Cyanea, Funckiana, Magnusiana, Bergeri, Brachycaulos, Harrisii, Duratii, Gardneri, Seleriana, Fasciculata, Leiboldiana, Flabellata, Paleacea, Recurvata, Kolbii, Pruinosa
  • ☐ 1.2 Add all 16 to the `ALL` const array
  • ☐ 1.3 Add slug mappings for each new variant in `slug()` and `from_slug()`
  • ☐ 1.4 Add display names in `display_name()`
  • ☐ 1.5 Add unique flower emojis in `flower()` (no conflicts with lifecycle emojis)
  • ☐ 2.1 Update `icons` module in `genus.rs` — new genera without SVG assets fall back to Ionantha icons
  • ☐ 2.2 Update `build.rs` GENERA list and `genus_to_variant()` to include new genera, with fallback to ionantha SVGs when genus directory does not exist
  • ☐ 2.3 Update generated `icon_png` and `window_icon_png` functions to handle all 24 genera
  • ☐ 3.1 Update `allocator_pool_exhaustion` test from 8 to 24
  • ☐ 3.2 Verify `flower_unique_per_genus` still passes with 24 entries
  • ☐ 3.3 Verify `slug_roundtrip` passes for all 24 genera
  • ☐ 4.1 `cargo check --workspace` passes
  • ☐ 4.2 `cargo test --workspace` passes

Read source record ↗

changeFix appimage autoupdateOpen tasks0 checked · 9 open

The AppImage auto-update system is completely broken — neither the Tauri built-in updater nor the `--update` CLI flag can find or apply updates. Five root causes identified through release workflow log analysis and code inspection.

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☐ 1.1 Change `createUpdaterArtifacts` from `"v1Compatible"` to `true`
  • ☐ 2.1 Add `*.AppImage.tar.gz` and `*.AppImage.tar.gz.sig` to the `find` command in collect step
  • ☐ 2.2 Rename collected `.AppImage.tar.gz` and `.AppImage.tar.gz.sig` to versionless names
  • ☐ 2.3 Replace `publish-update-manifest` job with `latest.json` generation inside `sign-and-release`
  • ☐ 2.4 Generate `latest.json` with correct Tauri v2 format including all platform entries
  • ☐ 2.5 Upload `latest.json` to the GitHub release alongside other assets
  • ☐ 3.1 Normalize version strings to same part count before comparison (truncate 4-part to 3-part when comparing against CARGO_PKG_VERSION)
  • ☐ 4.1 Add `rustls-tls` feature to `reqwest` dependency in `src-tauri/Cargo.toml`
  • ☐ 5.1 Run `./build.sh --check` to confirm compilation

Read source record ↗

changeFix build script initOpen tasks4 checked · 1 open

`./build.sh --appimage` uses `podman run --rm` (ephemeral container) so Rust, tauri-cli, and system deps are reinstalled from scratch every build (~10 min). Only the cargo registry was cached. Subsequent builds should be fast.

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1. Add volume mounts for rustup, cargo/bin, and apt cache
  • ☑ 2. Skip rustup install if cached
  • ☑ 3. Skip tauri-cli install if cached
  • ☑ 4. Detect cached state and show appropriate message
  • ☐ 5. Manual: first build populates caches, second build skips installs

Read source record ↗

changeFix container death detectionOpen tasks0 checked · 19 open

Container death is completely invisible. When a `--rm` container dies, the tray never updates because three independent bugs in `events.rs` conspire to suppress all death events. The event stream silently produces zero events, the fallback polling never notices disappearances, and the user sees a permanently "running" ghost entry.

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☐ 1.1 Remove `--filter container=...` from `podman events` args
  • ☐ 1.2 Keep `--format json` and add `--filter type=container` to reduce noise
  • ☐ 2.1 Change `value["Actor"]["Attributes"]["name"]` to `value["Name"]`
  • ☐ 2.2 Change `value["Action"]` to `value["Status"]`
  • ☐ 2.3 Update match arms: `"died" | "remove" | "cleanup"` for Stopped
  • ☐ 3.1 Add `HashSet<String>` to track known-running containers in `backoff_inspect`
  • ☐ 3.2 After each `podman ps` poll, emit Stopped for containers that vanished
  • ☐ 3.3 Update known set when containers appear or disappear
  • ☐ 4.1 Debug log on event stream start/reconnect
  • ☐ 4.2 Debug log on raw JSON line received
  • ☐ 4.3 Debug log on parsed event dispatched
  • ☐ 4.4 Debug log on fallback activation
  • ☐ 4.5 Debug log on container disappearance detected
  • ☐ 5.1 Test parse_podman_event with valid Podman JSON for each status
  • ☐ 5.2 Test prefix filtering (match and reject)
  • ☐ 5.3 Test malformed JSON returns None
  • ☐ 5.4 Test Docker-format JSON returns None (not our format)
  • ☐ 6.1 `cargo check --workspace` passes
  • ☐ 6.2 `cargo test --workspace` passes

Read source record ↗

changeFix file permissionsOpen tasks0 checked · 10 open

Files created inside Tillandsias forge containers (via `--userns=keep-id`) can end up with restrictive permissions on the host bind mount. Specifically, files created by container processes under `~/src/<project>/` — such as `.bash-private`, `.opencode/`, `openspec/`, `.claude/` — may have no write permission for the host user, requiring `chmod` or `sudo rm`

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☐ 1.1 After `chown -R 1000:1000 ./home/forge`, add `chmod -R u+rw ./home/forge`
  • ☐ 1.2 After copying skel files, add `chmod -R a+r ./etc/skel` so shell configs are
  • ☐ 2.1 Add `umask 0022` immediately after `set -euo pipefail` (line 2 of the script)
  • ☐ 3.1 Add `umask 0022` to `images/default/shell/bashrc` (interactive bash sessions)
  • ☐ 3.2 Add `umask 0022` to `images/default/shell/zshrc` (interactive zsh sessions)
  • ☐ 3.3 Add `umask 022` to `images/default/shell/config.fish` (fish sessions — fish syntax)
  • ☐ 4.1 `./build.sh --check` passes (cargo check)
  • ☐ 4.2 `./scripts/build-image.sh forge --force` succeeds (image rebuild with fixes)
  • ☐ 4.3 Launch a container, run `touch test-file && ls -la test-file` — permissions
  • ☐ 4.4 Create a file via npm/openspec inside the container, verify it is writable

Read source record ↗

changeFix gh auth setup gitOpen tasks2 checked · 2 open

`git push` inside forge containers prompts for username/password because git doesn't know about gh's OAuth token. The `hosts.yml` with the token is mounted, but git's `credential.helper` is not configured to use `gh`. Running `gh auth setup-git` bridges this gap.

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1. Add `gh auth setup-git` to entrypoint.sh (Phase 1)
  • ☑ 2. Configure opencode.json deny rules for ~/.config/gh and ~/.config/tillandsias-git (Phase 2)
  • ☐ 3. Manual: rebuild forge image, verify `git push --dry-run` works without prompt
  • ☐ 4. Manual: verify OpenCode cannot `cat ~/.config/gh/hosts.yml`

Read source record ↗

changeFix root mount and terminalOpen tasks0 checked · 7 open

The root-level "Attach Here" and root terminal (top of tray menu, scoped to the entire `~/src/` watch path) have two bugs:

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☐ 1.1 In `build_run_args()`, detect when `project_path.file_name()` == the last component of the watch path (i.e., the path IS the watch root, not a project inside it) and mount at `/home/forge/src/` instead of `/home/forge/src/<name>/`
  • ☐ 1.2 Alternatively, fix in `handle_attach_here()` by adjusting the project_path before calling `build_run_args()` when the path is the watch root
  • ☐ 2.1 Change `--entrypoint bash` to `--entrypoint fish` in `handle_root_terminal()`
  • ☐ 3.1 `cargo check --workspace` passes
  • ☐ 3.2 `cargo test --workspace` passes
  • ☐ 3.3 Manual: Root "Attach Here" mounts `~/src/` at `/home/forge/src/` (not `src/src/`)
  • ☐ 3.4 Manual: Root terminal opens fish with welcome banner

Read source record ↗

changeFix terminal root landing uxOpen tasks1 checked · 1 open

Root terminal welcome message shows "Project: unknown" and "Mount: /home/forge/src/unknown" because `TILLANDSIAS_PROJECT` env var is not passed to the root terminal container. The welcome script (`forge-welcome.sh`) defaults to "unknown" when the var is absent.

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1. Add `-e TILLANDSIAS_PROJECT='(all projects)'` to root terminal podman command
  • ☐ 2. Manual: verify welcome shows "Project: (all projects)" and correct mount path

Read source record ↗

changeFix update no curlOpen tasks8 checked · 2 open

`tillandsias --update` shells out to the system `curl` binary to fetch `latest.json` and download the update archive. Inside an AppImage, this causes a library symbol mismatch:

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1.1 Create `proposal.md`
  • ☑ 1.2 Create `tasks.md`
  • ☑ 1.3 Create `specs/fix-update-no-curl/spec.md`
  • ☑ 2.1 Add `reqwest` as a direct dependency in `src-tauri/Cargo.toml`
  • ☑ 2.2 Replace `fetch_url` (fetches `latest.json`) — remove `Command::new("curl")`, use `reqwest` via `tokio::runtime::Builder::new_current_thread().block_on(...)`
  • ☑ 2.3 Replace `download_update` (downloads AppImage archive) — remove `Command::new("curl")`, write response bytes to temp file using `std::io::copy`
  • ☑ 2.4 Remove unused `std::process::Command` import paths from `update_cli.rs` if no longer needed
  • ☑ 3.1 `./build.sh --check` passes (type-check only, no AppImage needed)
  • ☐ 3.2 `./build.sh --test` passes
  • ☐ 3.3 Manual: run `tillandsias --update` from an AppImage install and confirm no symbol lookup error

Read source record ↗

changeForge build ux polishOpen tasks17 checked · 4 open

When Tillandsias starts and the forge image is absent or stale, the user can click "Attach Here" or "Maintenance" before the build finishes. Those actions fail silently or produce confusing errors because the forge image is not ready yet. Additionally, the build chip always shows "Building forge..." regardless of whether this is a first-time install or a rou

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1.1 Add `forge_available: bool` to `TrayState` (default `false`)
  • ☑ 1.2 Set `forge_available = true` when forge image check passes at launch (image already present)
  • ☑ 1.3 Set `forge_available = true` when forge build completes successfully
  • ☑ 1.4 Set `forge_available = false` when forge rebuild is triggered (image stale)
  • ☑ 2.1 Detect whether image existed before triggering the build
  • ☑ 2.2 First-time build (image absent): send `BuildProgressEvent::Started { image_name: "Building Forge".to_string() }`
  • ☑ 2.3 Update build (image stale): send `BuildProgressEvent::Started { image_name: "Building Updated Forge".to_string() }`
  • ☑ 3.1 Top-level "Attach Here" (~/src/ — Attach Here): `enabled(false)` when `!state.forge_available`
  • ☑ 3.2 Top-level "🛠️ Root": `enabled(false)` when `!state.forge_available`
  • ☑ 3.3 Per-project "Attach Here" / "Blooming": `enabled(false)` when `!state.forge_available`
  • ☑ 3.4 Per-project "Maintenance": `enabled(false)` when `!state.forge_available`
  • ☑ 3.5 GitHub Login: `enabled(false)` when `!state.forge_available`
  • ☑ 3.6 "Quit Tillandsias": always `enabled(true)`
  • ☑ 3.7 Settings submenu: stays enabled (does not require forge)
  • ☑ 3.8 Pass `forge_available` into `build_inactive_projects_submenu` for per-project disabling
  • ☑ 4.1 On `BuildProgressEvent::Completed` for forge image name variants, set `forge_available = true` in shared state and rebuild menu
  • ☑ 5.1 `./build.sh --check` passes with no errors
  • ☐ 5.2 On launch with no forge image: menu items are disabled, chip shows "Building Forge..."
  • ☐ 5.3 On launch with stale forge image: menu items are disabled, chip shows "Building Updated Forge..."
  • ☐ 5.4 After build completes: all items re-enable automatically
  • ☐ 5.5 "Quit Tillandsias" remains clickable at all times

Read source record ↗

changeGenus named terminalsOpen tasks0 checked · 20 open

Terminal (Maintenance) containers are the only container type that does not use the genus naming convention. They use a fixed name `tillandsias-{project}-terminal` instead of `tillandsias-{project}-{genus}`. This causes three bugs:

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☐ 1.1 Add `ContainerType` enum (`Forge`, `Maintenance`) to `state.rs` with `Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize` derives
  • ☐ 1.2 Add `container_type: ContainerType` field to `ContainerInfo` struct
  • ☐ 1.3 Default to `ContainerType::Forge` in all existing code paths that construct `ContainerInfo` (event loop discovery, handle_attach_here pre-registration)
  • ☐ 1.4 Update the postcard roundtrip test to include `container_type`
  • ☐ 2.1 Change `handle_terminal()` signature to accept `&mut TrayState` and `&mut GenusAllocator` instead of `&TrayState`
  • ☐ 2.2 Allocate a genus via `allocator.allocate()` at the start of the handler
  • ☐ 2.3 Build container name using `ContainerInfo::container_name()` instead of hardcoded `-terminal` suffix
  • ☐ 2.4 Pre-register the container in `state.running` with `ContainerType::Maintenance` and `ContainerState::Creating`
  • ☐ 2.5 Remove the don't-relaunch guard (the `terminal_container_name` / `state.running.find` block)
  • ☐ 2.6 Use the allocated genus flower for the terminal window title
  • ☐ 2.7 On failure (image missing, terminal spawn error), clean up: remove from `state.running`, release genus
  • ☐ 3.1 Change `MenuCommand::Terminal` arm to pass `&mut state` and `&mut allocator` to `handle_terminal()`
  • ☐ 3.2 Call `on_state_change(&state)` after successful terminal launch (menu rebuild)
  • ☐ 3.3 Call `prune_completed_builds(&mut state)` before state change callback
  • ☐ 4.1 Replace `terminal_container_name` / name-based matching in `build_project_submenu()` with `container_type == ContainerType::Maintenance` check
  • ☐ 4.2 Handle the case where multiple maintenance containers exist for one project (any match means maintenance is running)
  • ☐ 4.3 Derive maintenance flower from the first matching maintenance container's genus
  • ☐ 5.1 `cargo check --workspace` passes
  • ☐ 5.2 `cargo test --workspace` passes
  • ☐ 5.3 Existing container_name and parse_container_name tests still pass

Read source record ↗

changeGh login no forgeOpen tasks10 checked · 1 open

When a user clicks "GitHub Login" from the tray on a fresh installation, the forge image has not been built yet. The `gh-auth-login.sh` script is extracted from the binary and run directly in a terminal — but it fails immediately:

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1.1 Add `build_tx: mpsc::Sender<BuildProgressEvent>` parameter to `handle_github_login()`
  • ☑ 1.2 Before extracting `gh-auth-login.sh`, check `client.image_exists(FORGE_IMAGE_TAG).await`
  • ☑ 1.3 If image missing: emit `BuildProgressEvent::Started { image_name: "forge" }`, call `run_build_image_script("forge")` via `spawn_blocking`, emit `Completed` or `Failed`
  • ☑ 1.4 If build fails: return early with error (do not open terminal)
  • ☑ 1.5 After image confirmed present: proceed with existing terminal-open logic (unchanged)
  • ☑ 2.1 Update `MenuCommand::GitHubLogin` arm to pass `build_tx.clone()` to `handle_github_login`
  • ☑ 3.1 Write `proposal.md`
  • ☑ 3.2 Write `tasks.md`
  • ☑ 3.3 Write `specs/gh-login-no-forge/spec.md` — update `gh-auth-script` spec with new first-run scenario
  • ☑ 4.1 `./build.sh --check` passes (type-check only)
  • ☐ 4.2 Manual test: delete forge image, click GitHub Login, observe build chip then terminal

Read source record ↗

changeMove entrypoint from homeOpen tasks4 checked · 2 open

The forge container's `entrypoint.sh` is placed at `/home/forge/entrypoint.sh` — directly inside the user's home directory. When a user browses their home with `ls ~`, a file manager, or any shell, they see a stray `entrypoint.sh` file that has nothing to do with their work. This is confusing and pollutes the home directory.

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1.1 Create `/usr/local/bin/` directory in `fakeRootCommands` and copy entrypoint there
  • ☑ 1.2 Update `chmod +x` to target the new path
  • ☑ 1.3 Update container `Entrypoint` config to `/usr/local/bin/tillandsias-entrypoint.sh`
  • ☑ 2.1 Confirm no other references to `/home/forge/entrypoint.sh` remain in the codebase
  • ☐ 2.2 Rebuild forge image and verify entrypoint runs correctly
  • ☐ 2.3 Verify `ls ~` inside a running forge container shows no `entrypoint.sh`

Read source record ↗

changePin nix builder imageOpen tasks0 checked · 2 open

The `nixos/nix:latest` tag can change at any time. A breaking Nix release could silently break the forge image build for all users. Since we already pin Nix inputs via `flake.lock`, pinning the builder image version completes the reproducibility picture.

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☐ 1. Change `NIX_IMAGE="docker.io/nixos/nix:latest"` to `NIX_IMAGE="docker.io/nixos/nix:2.34.4"` in `scripts/build-image.sh`
  • ☐ 2. Add comment: `# Pinned for reproducibility. Update: podman pull docker.io/nixos/nix:<new-version>`

Read source record ↗

changeRemote project to launchOpen tasks0 checked · 10 open

When a user clones a remote project, they are expressing intent to work on it immediately. Today, after a successful clone, the user must find the new project in the tray and click "Attach Here" manually. This is unnecessary friction — the clone is the signal that the user wants to start working.

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☐ 1.1 Add `allocator` and `build_tx` parameters to `handle_clone_project` function signature
  • ☐ 1.2 Pass `&mut allocator` and `build_tx.clone()` at the call site in `MenuCommand::CloneProject` handler
  • ☐ 1.3 After a successful clone, pre-insert the cloned project into `state.projects` using a minimal `Project` entry so `handle_attach_here` can locate it
  • ☐ 1.4 Call `handlers::handle_attach_here(target_dir, &mut state, &mut allocator, build_tx)` after pre-insertion
  • ☐ 1.5 Log and swallow `handle_attach_here` errors — clone success is independent of launch success
  • ☐ 1.6 Call `on_state_change` after the auto-launch attempt so the tray reflects the new state
  • ☐ 2.1 Run `./build.sh --check` — must pass with zero errors
  • ☐ 2.2 Clone a remote project via the tray menu and verify the forge launches automatically
  • ☐ 2.3 Verify that if the forge image is not built, the clone still succeeds and the error is logged gracefully
  • ☐ 2.4 Verify the scanner does not create a duplicate project entry when it detects the new directory

Read source record ↗

changeTray menu fanoutOpen tasks0 checked · 20 open

The tray menu has grown incrementally and now has too many top-level items separated by dividers. With multiple projects, running environments, build chips, settings, and version info all laid out inline, the menu is long, hard to scan, and requires scrolling on small screens. Every new feature adds yet another divider and item to an already crowded top leve

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☐ 1.1 Extract project iteration from `build_tray_menu()` into a new `build_projects_submenu()` function returning `tauri::Result<tauri::menu::Submenu<R>>`
  • ☐ 1.2 Label the submenu "Projects" when multiple projects exist; include a count hint e.g. "Projects (3)" when there are more than 3
  • ☐ 1.3 When `state.projects` is empty, the function still returns a submenu containing a single disabled "No projects detected" item
  • ☐ 1.4 All per-project submenus built by `build_project_submenu()` are added as children of this new submenu
  • ☐ 2.1 When `state.running` is non-empty, append a separator and the build chip items inside the Running Environments submenu (after the per-container items)
  • ☐ 2.2 When `state.running` is empty but `state.active_builds` is non-empty, create a standalone "Activity" submenu containing the build chip items
  • ☐ 2.3 Remove the inline build chip block from the top-level menu in `build_tray_menu()`
  • ☐ 3.1 Create `build_about_submenu()` returning `tauri::Result<tauri::menu::Submenu<R>>`
  • ☐ 3.2 Submenu label is "Tillandsias v{version}" using the same `include_str!("../../VERSION")` embed
  • ☐ 3.3 Submenu contains: disabled "Tillandsias v{version}" item, disabled "by Tlatoāni" item, separator, "Quit Tillandsias" item (enabled)
  • ☐ 3.4 Remove the inline version, credit, and separator block from `build_tray_menu()`
  • ☐ 4.1 Top-level order after restructure: `src/ — Attach Here`, `🛠️ Root`, separator, `Projects ▸`, `Running ▸` (conditional), separator, `Settings ▸`, `Tillandsias v{version} ▸`
  • ☐ 4.2 "Running Environments" submenu appears at top level only when `state.running` is non-empty; omitted entirely when idle (no "No running environments" placeholder at top level — that lives inside the Projects or Activity context)
  • ☐ 4.3 All separators reduced: exactly two separators in normal operation (one after Root, one before Settings)
  • ☐ 4.4 When `state.running` is empty, no Activity submenu appears unless there are active builds
  • ☐ 5.1 Replace inline version/credit/quit block in `build_decay_menu()` with a call to `build_about_submenu()`
  • ☐ 5.2 Verify decay menu still compiles and renders correctly
  • ☐ 6.1 `cargo check --workspace` passes
  • ☐ 6.2 `cargo test --workspace` passes
  • ☐ 6.3 Manual smoke test: tray menu opens, Projects submenu shows all projects, Running submenu appears/disappears with containers, About submenu contains version+credit+quit

Read source record ↗

changeWebview menus explorationNo checklist0 checked · 0 open

The Settings submenu is currently three levels deep: Settings > GitHub > Remote Projects, and Settings > Seedlings > agent selection. Native tray menus are OS widgets with zero styling capability -- no custom icons, no color, no layout control. Each platform renders them differently. Navigating nested submenus on Linux (libappindicator) is particularly clunk

No task record was present in this archived change.

Read source record ↗

changeClaude api key loginOpen tasks22 checked · 1 open

Claude Code authenticates via an Anthropic API key (`ANTHROPIC_API_KEY`). Today, users who select Claude as their agent must manually export this environment variable or rely on Claude's own interactive login flow each time a container starts. There is no persistent, secure storage for the API key in Tillandsias — unlike the GitHub token which lives in the O

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1.1 Write proposal.md
  • ☑ 1.2 Write tasks.md
  • ☑ 1.3 Write specs/claude-api-key-login/spec.md
  • ☑ 2.1 Add `CLAUDE_API_KEY_KEY` constant and `store_claude_api_key()` to `secrets.rs`
  • ☑ 2.2 Add `retrieve_claude_api_key()` to `secrets.rs`
  • ☐ 2.3 Add unit tests for Claude API key keyring functions
  • ☑ 3.1 Add `MenuCommand::ClaudeLogin` to `event.rs`
  • ☑ 3.2 Add `CLAUDE_LOGIN` ID constant and `claude_login()` helper to `menu.rs` ids
  • ☑ 3.3 Add Claude Login item to `build_seedlings_submenu()` with key status indicator
  • ☑ 3.4 Add `claude-login` dispatch case in `handle_menu_click()` in `main.rs`
  • ☑ 3.5 Handle `MenuCommand::ClaudeLogin` in `event_loop.rs`
  • ☑ 4.1 Create `claude-api-key-prompt.sh` embedded script
  • ☑ 4.2 Embed script in `embedded.rs` via `include_str!`
  • ☑ 4.3 Implement `handle_claude_login()` in `handlers.rs` — extract script, open terminal, read temp file, store in keyring
  • ☑ 5.1 Add `ANTHROPIC_API_KEY` env var to `build_run_args()` in `handlers.rs` (when key present in keyring)
  • ☑ 5.2 Add `ANTHROPIC_API_KEY` env var to `handle_terminal()` format string
  • ☑ 5.3 Add `ANTHROPIC_API_KEY` env var to `handle_root_terminal()` format string
  • ☑ 5.4 Add `ANTHROPIC_API_KEY` env var to `build_run_args()` in `runner.rs` (CLI mode)
  • ☑ 6.1 Capture `ANTHROPIC_API_KEY` into local var and unset from environment in `entrypoint.sh`
  • ☑ 6.2 Re-inject key only into claude exec in the `claude)` case branch
  • ☑ 7.1 Add `/proc/*/environ` to deny list in `opencode.json`
  • ☑ 8.1 `./build.sh --check` passes
  • ☑ 8.2 `./build.sh --test` passes

Read source record ↗

changeFix forge agent claudeOpen tasks15 checked · 29 open

Three bugs make the forge environment broken or unusable for Claude Code users:

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1.1 In `crates/tillandsias-core/src/config.rs:27`, change `SelectedAgent::default()` to return `Self::Claude`
  • ☑ 1.2 Update `entrypoint.sh:37` default from `opencode` to `claude`: `AGENT="${TILLANDSIAS_AGENT:-claude}"`
  • ☑ 2.1 Make `install_opencode()` conditional — only runs if `TILLANDSIAS_AGENT=opencode`
  • ☑ 2.2 Remove unconditional `$CACHE/opencode` mkdir; keep `$OS_PREFIX` and `$CACHE/nix`
  • ☑ 2.3 Remove opencode from PATH (entrypoint): keep only `$OS_PREFIX/bin:$PATH`
  • ☐ 2.4 Remove the `opencode` case from the launch switch (lines 131-138): only keep claude + fallback to bash
  • ☑ 2.5 Update `openspec init --tools opencode` (line 105) to `--tools claude`
  • ☑ 3.1 `images/default/shell/bashrc:13` -- replace opencode with claude/bin in PATH
  • ☑ 3.2 `images/default/shell/zshrc:9` -- replace opencode with claude/bin in PATH
  • ☑ 3.3 `images/default/shell/config.fish:10` -- replace opencode with claude/bin in fish_add_path
  • ☐ 4.1 Delete `images/default/opencode.json`
  • ☐ 4.2 In `flake.nix:16`, remove `forgeOpencode = ./images/default/opencode.json;`
  • ☐ 4.3 In `flake.nix:82-83`, remove `mkdir -p ./home/forge/.cache/tillandsias/{nix,opencode}` -- keep only nix
  • ☐ 4.4 In `flake.nix:93-94`, remove `cp ${forgeOpencode} ./home/forge/.config/opencode/config.json`
  • ☐ 4.5 In `src-tauri/src/embedded.rs:38`, remove `FORGE_OPENCODE_JSON` constant
  • ☐ 4.6 In `src-tauri/src/embedded.rs:101,144-145`, remove opencode.json from extraction list and write call
  • ☑ 5.1 In `handlers.rs:469-473` (`build_run_args()`): changed to `dirs::home_dir().join(".claude")`
  • ☑ 5.2 In `runner.rs:221-225`: same change -- mount host `~/.claude/` instead of empty secrets dir
  • ☑ 5.3 In `handlers.rs:934-935,965` (`handle_terminal()`): same mount path change
  • ☑ 5.4 In `handlers.rs:1096-1097,1127` (`handle_root_terminal()`): same mount path change
  • ☑ 5.5 Removed `std::fs::create_dir_all(&claude_dir)` calls -- the host `~/.claude/` should already exist from `claude login`
  • ☐ 6.1 Remove `ANTHROPIC_API_KEY` env var injection from `handlers.rs:463-465` (`build_run_args()`)
  • ☐ 6.2 Remove `ANTHROPIC_API_KEY` env var injection from `runner.rs:215-217`
  • ☐ 6.3 Remove `claude_api_key_arg` from `handlers.rs:941-943` (`handle_terminal()`)
  • ☐ 6.4 Remove `claude_api_key_arg` from `handlers.rs:1103-1105` (`handle_root_terminal()`)
  • ☐ 6.5 Remove API key capture/scrubbing from `entrypoint.sh:41-42` (`_CLAUDE_KEY` / `unset ANTHROPIC_API_KEY`)
  • ☑ 6.6 Remove `env ANTHROPIC_API_KEY=...` from claude exec in entrypoint (lines 121-122) — claude now uses OAuth via mounted ~/.claude/
  • ☐ 6.7 Remove `store_claude_api_key()` and `retrieve_claude_api_key()` from `secrets.rs`
  • ☐ 6.8 Remove `CLAUDE_API_KEY_KEY` constant from `secrets.rs:36`
  • ☐ 6.9 Delete `claude-api-key-prompt.sh` from project root
  • ☐ 6.10 Remove `CLAUDE_API_KEY_PROMPT` from `embedded.rs:24`
  • ☐ 7.1 Decide: either (a) run `claude login` on the host via terminal, or (b) remove the menu item entirely if auth is expected to be done externally
  • ☐ 7.2 If keeping: update `handle_claude_login()` in `handlers.rs:1266` to run `claude login` instead of the API key prompt script
  • ☐ 7.3 Update menu label/state logic in `menu.rs:458-466` -- no longer checks keyring for claude key; could check if `~/.claude/` exists and has credentials
  • ☐ 8.1 Remove `mkdir -p ./home/forge/.config/opencode` from fakeRootCommands
  • ☐ 8.2 Remove opencode config copy from fakeRootCommands
  • ☐ 8.3 Verify `mkdir -p ./home/forge/.cache/tillandsias/nix` still works without the opencode joint mkdir
  • ☐ 8.4 Add `mkdir -p ./home/forge/.cache/tillandsias/claude` for npm-installed claude code cache
  • ☐ 9.1 Rebuild forge image: `scripts/build-image.sh forge --force`
  • ☐ 9.2 Rebuild app: `./build.sh`
  • ☐ 9.3 Test: click "Attach Here" -- should launch claude, not opencode
  • ☐ 9.4 Test: claude should authenticate via OAuth (no API key prompt)
  • ☐ 9.5 Test: maintenance terminal (fish shell) should not have opencode in PATH
  • ☐ 9.6 Test: new config files default to `[agent]\nselected = "claude"`

Read source record ↗

changeProposal: Fix Forge Image Persistence on macOSOpen tasks5 checked · 7 open

1. `main.rs` (line 296-297): Calls `forge_image_tag()` which returns `tillandsias-forge:v{CARGO_PKG_VERSION}` (currently `tillandsias-forge:v0.1.81`) 2. `main.rs` (line 297): Calls `forge_client.image_exists(&tag).await` 3. `client.rs` (line 64-70): Runs `podman image exists <tag>` and checks exit code 4. If the image is absent, triggers `run_build_image_scr

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ Method: `pub async fn wait_for_ready(&self, max_attempts: u32) -> bool`
  • ☑ Backoff: 500ms, 1s, 2s, 4s (capped), up to `max_attempts`
  • ☑ Uses existing `is_available()` as the readiness probe
  • ☑ Log each retry attempt at debug level
  • ☑ Log success at info level
  • ☐ Build and install: `./build.sh --install`
  • ☐ Stop podman machine: `podman machine stop`
  • ☐ Launch tillandsias
  • ☐ Confirm logs show machine auto-start + readiness wait
  • ☐ Confirm image is found (no unnecessary rebuild)
  • ☐ Quit and relaunch -- confirm no rebuild on second launch
  • ☐ Reboot macOS, launch tillandsias -- confirm no rebuild if image persists

Read source record ↗

changeFix update artifactsOpen tasks0 checked · 8 open

The AppImage update system has two misaligned assumptions introduced during the initial auto-updater implementation.

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☐ 1.1 Change `linux-x86_64` URL in latest.json from `.AppImage.tar.gz` to `.AppImage`
  • ☐ 1.2 Change `LINUX_SIG` source from `.AppImage.tar.gz.sig` to `.AppImage.sig`
  • ☐ 2.1 Refactor `apply_appimage_update` to accept the download URL and branch on extension
  • ☐ 2.2 For `.AppImage` URL: skip tar extraction, use downloaded file directly as replacement binary
  • ☐ 2.3 For `.tar.gz` URL: retain existing tar extraction path (macOS / future Linux compat)
  • ☐ 2.4 Update module-level doc comment to reflect corrected Linux update flow
  • ☐ 3.1 `./build.sh --check` passes with no type errors
  • ☐ 3.2 `cargo test -p tillandsias` — existing update CLI tests still pass

Read source record ↗

changeConfig driven launchOpen tasks0 checked · 32 open

The Rust code that builds `podman run` arguments is duplicated across three locations with subtle differences:

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☐ 1.1 Create `crates/tillandsias-core/src/container_profile.rs` with `ContainerProfile`, `ProfileMount`, `SecretMount`, `LaunchContext` structs
  • ☐ 1.2 Implement `Default` for `ContainerProfile` (Claude forge as default)
  • ☐ 1.3 Implement built-in profiles: `forge_opencode()`, `forge_claude()`, `terminal()`, `web()` as associated functions
  • ☐ 1.4 Add `container_profile` module to `crates/tillandsias-core/src/lib.rs`
  • ☐ 1.5 Write unit tests: each built-in profile produces expected entrypoint, mounts, env, secrets
  • ☐ 2.1 Create `src-tauri/src/launch.rs` with `build_podman_args(profile: &ContainerProfile, context: &LaunchContext) -> Vec<String>`
  • ☐ 2.2 Hardcode non-negotiable security flags at the start of the function (never from profile)
  • ☐ 2.3 Implement mount key resolution: `"project"` -> `context.project_path`, `"cache"` -> `context.cache_dir`, `"secrets/gh"` -> `context.gh_dir`, etc.
  • ☐ 2.4 Implement secret mount resolution: `"claude_dir"` -> `context.claude_dir` (if Some), `"claude_api_key"` -> env var injection (if Some)
  • ☐ 2.5 Add GPU passthrough detection (Linux only)
  • ☐ 2.6 Add custom mounts from project config (appended after profile mounts)
  • ☐ 2.7 Write unit tests: verify correct args for each profile type, verify security flags always present, verify secrets only present for profiles that declare them
  • ☐ 3.1 Replace `build_run_args()` function body with: select profile based on agent + container type, build `LaunchContext`, call `build_podman_args()`
  • ☐ 3.2 Refactor `handle_terminal()`: remove the 40-line format string, replace with profile lookup + `build_podman_args()` + `open_terminal()`
  • ☐ 3.3 Refactor `handle_root_terminal()`: same as 3.2, with `is_watch_root = true` in context
  • ☐ 3.4 Verify `handle_attach_here()` uses the refactored `build_run_args()`
  • ☐ 3.5 Delete the old `build_run_args()` function after migration
  • ☐ 4.1 Replace `runner.rs::build_run_args()` with profile lookup + `build_podman_args()` from the new `launch.rs` module
  • ☐ 4.2 Handle the `bash` flag: use `terminal()` profile when `bash = true`, forge profile otherwise
  • ☐ 4.3 Delete the old `build_run_args()` function
  • ☐ 5.1 Add `version: Option<u32>` to `GlobalConfig` with default value 1
  • ☐ 5.2 Add parsing logic: absent = 1, present = use value, > max_supported = log warning
  • ☐ 5.3 Add `version: Option<u32>` to `ProjectConfig` with same semantics
  • ☐ 5.4 Write unit tests: parse config with version=1, version absent, version=99 (unknown future version)
  • ☐ 6.1 `cargo test --workspace` — all existing tests pass
  • ☐ 6.2 New unit tests for `build_podman_args()` with all four profile types
  • ☐ 6.3 New unit tests for mount key resolution (project, cache, secrets, claude)
  • ☐ 6.4 New unit tests for security flag enforcement (flags present regardless of profile)
  • ☐ 6.5 Manual test: tray mode Attach Here (Claude) — verify correct mounts, env, entrypoint
  • ☐ 6.6 Manual test: tray mode Maintenance — verify no Claude secrets mounted
  • ☐ 6.7 Manual test: CLI mode `tillandsias <path>` — verify correct launch
  • ☐ 6.8 Manual test: CLI mode `tillandsias --bash <path>` — verify terminal profile used

Read source record ↗

changeFix forge launch regressionOpen tasks0 checked · 22 open

The current forge entrypoint (`images/default/entrypoint.sh`) fails to launch Claude Code correctly inside containers. The monolithic entrypoint handles all agent types (OpenCode, Claude, unknown) in a single script, and the Claude Code install path (`npm install -g --prefix`) does not reliably produce a working `claude` binary. When `install_claude()` fails

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☐ 1.1 In `entrypoint.sh`, replace `exec "$CC_BIN" "$@"` (line 126) with `exec env ANTHROPIC_API_KEY="$_CLAUDE_KEY" "$CC_BIN" "$@"` to re-inject the captured API key at exec time
  • ☐ 1.2 Apply same pattern for OpenCode if it needs env vars in the future (currently it does not use API keys, so no change needed)
  • ☐ 1.3 Verify: launch a Claude forge container with `ANTHROPIC_API_KEY` set, confirm Claude Code can authenticate
  • ☐ 2.1 Remove `2>/dev/null` from `npm install -g --prefix "$CC_PREFIX" @anthropic-ai/claude-code` (line 76) so errors are visible
  • ☐ 2.2 Remove `2>/dev/null` from the OpenSpec install (line 89) for consistency
  • ☐ 2.3 Add a post-install verification: run `"$CC_BIN" --version` after install, print result or error
  • ☐ 2.4 Add a post-install verification for OpenCode: run `"$OC_BIN" --version` after install
  • ☐ 2.5 Improve fallback message: instead of "Claude Code not available. Starting bash.", print the specific failure reason and suggest `tillandsias --bash <project>` for debugging
  • ☐ 3.1 Add `update_claude()` function that compares installed version with latest npm version
  • ☐ 3.2 Rate-limit the check to once per 24 hours via a timestamp file at `$CACHE/claude/.last-update-check`
  • ☐ 3.3 If update available, run `npm install -g --prefix "$CC_PREFIX" @anthropic-ai/claude-code` with visible output
  • ☐ 3.4 If update fails, continue with existing version (non-blocking)
  • ☐ 4.1 Add `update_opencode()` function that checks GitHub releases API for newer version
  • ☐ 4.2 Rate-limit to once per 24 hours via `$CACHE/opencode/.last-update-check`
  • ☐ 4.3 If update available, download and replace binary
  • ☐ 4.4 If update fails, continue with existing version (non-blocking)
  • ☐ 5.1 Rebuild forge image via `scripts/build-image.sh forge --force` to pick up entrypoint changes
  • ☐ 5.2 Test: Claude forge launch — verify Claude Code starts with API key access
  • ☐ 5.3 Test: OpenCode forge launch — verify OpenCode starts correctly
  • ☐ 5.4 Test: Launch with empty cache (first install) — verify install succeeds with visible output
  • ☐ 5.5 Test: Launch with corrupt cache (delete binary but keep dir) — verify re-install happens
  • ☐ 5.6 Test: Launch with no network — verify graceful fallback

Read source record ↗

changeFix ux string bugsOpen tasks0 checked · 24 open

A thorough audit of all user-facing strings across the Rust tray app, CLI commands, shell scripts, and installer reveals multiple bugs and inconsistencies. The most severe is a word-doubling bug where tray menu build chips display "Building Building Forge..." because `build_chip_label()` prepends "Building" to image names that already contain the word "Build

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☐ 1.1 Change `main.rs:325-328` to use `"Forge"` / `"Updated Forge"` as chip names (remove "Building" prefix)
  • ☐ 1.2 Update `event_loop.rs:320-321` `is_forge_build()` to match `"Forge"` / `"Updated Forge"`
  • ☐ 1.3 Verify `build_chip_label()` in `menu.rs:548-557` produces correct output: "⏳ Building Forge..." and "⏳ Building Updated Forge..."
  • ☐ 1.4 Verify the `Completed` and `Failed` paths also read correctly: "✅ Forge ready", "❌ Forge build failed"
  • ☐ 2.1 Create `src-tauri/src/strings.rs` with constants for: `SETUP_ERROR`, `ENV_NOT_READY`, `INSTALL_INCOMPLETE`
  • ☐ 2.2 Replace all 16 copies of `"Tillandsias is setting up..."` with `strings::SETUP_ERROR`
  • ☐ 2.3 Replace all 5 copies of `"Development environment not ready yet..."` with `strings::ENV_NOT_READY`
  • ☐ 2.4 Replace 2 copies of `"Tillandsias installation may be incomplete..."` with `strings::INSTALL_INCOMPLETE`
  • ☐ 2.5 Add `mod strings;` to `main.rs`
  • ☐ 3.1 In `init.rs`: standardize all checkmarks to `\u{2713}` and cross-marks to `\u{2717}` (lines 71, 78)
  • ☐ 3.2 Verify `runner.rs`, `cleanup.rs` already use consistent `\u{2713}` / `\u{2717}`
  • ☐ 3.3 Keep `menu.rs` using emoji variants `\u{2705}` / `\u{274C}` (tray GUI context)
  • ☐ 4.1 Replace `" done X installed"` with `" ✓ X installed"` on lines 62, 78, 90, 110
  • ☐ 4.2 Add `" ✗ X install failed"` for the Claude Code failure case on line 80
  • ☐ 5.1 Add space after colon on line 189: `" Installed binary: {} ({})"`
  • ☐ 5.2 Align column width with other labels in the same report
  • ☐ 6.1 Remove or condition the `"Endpoint: {UPDATE_ENDPOINT}"` println on a debug flag
  • ☐ 7.1 Move `human_bytes()` to `tillandsias-core` or a shared crate utility
  • ☐ 7.2 Remove duplicate implementations from `cleanup.rs` and `update_cli.rs`
  • ☐ 8.1 `cargo build --workspace` compiles with no warnings
  • ☐ 8.2 `cargo test --workspace` passes
  • ☐ 8.3 Manual test: trigger a build chip and verify label says "Building Forge..." not "Building Building Forge..."
  • ☐ 8.4 Manual test: `tillandsias init` shows consistent ✓/✗ characters
  • ☐ 8.5 Manual test: `tillandsias --stats` / `tillandsias --clean` output is aligned

Read source record ↗

changeI18n infrastructureOpen tasks0 checked · 34 open

Tillandsias serves Average Joe (AJ), who may not speak English. The project is authored by a bilingual developer (English/Spanish) and targets a global audience. Currently, every user-facing string is hardcoded in English across Rust source files and shell scripts. There is no mechanism to localize the application.

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☐ 1.1 Create `src-tauri/src/i18n.rs` with: `detect_locale()`, `t(key)`, `t_with(key, vars)`, TOML parsing
  • ☐ 1.2 Create `locales/en.toml` with all ~80 Rust-side string keys (complete English)
  • ☐ 1.3 Create `locales/es.toml` with proof-of-concept subset (~20 key strings in Spanish)
  • ☐ 1.4 Add `mod i18n;` to `main.rs`
  • ☐ 1.5 Wire `include_str!("../../locales/en.toml")` and `include_str!("../../locales/es.toml")` in `i18n.rs`
  • ☐ 2.1 Create `images/default/locales/en.sh` with all ~50 shell string variables
  • ☐ 2.2 Create `images/default/locales/es.sh` with proof-of-concept subset (~15 strings)
  • ☐ 2.3 Add locale sourcing to `images/default/entrypoint.sh` (detect `$LANG`, source locale file)
  • ☐ 2.4 Add locale sourcing to `images/default/forge-welcome.sh`
  • ☐ 2.5 Update `flake.nix` / Containerfile to include locale files in the image
  • ☐ 3.1 Replace all menu label literals with `i18n::t("menu.*")` calls
  • ☐ 3.2 Replace `build_chip_label()` literals with `i18n::t_with("menu.build.*", ...)`
  • ☐ 3.3 Replace settings submenu labels with `i18n::t("menu.settings.*")`
  • ☐ 4.1 Replace notification messages with `i18n::t()` / `i18n::t_with()`
  • ☐ 4.2 Replace error messages with `i18n::t("errors.*")`
  • ☐ 5.1 Replace `init.rs` messages with `i18n::t("init.*")`
  • ☐ 5.2 Replace `runner.rs` messages with `i18n::t("cli.*")`
  • ☐ 5.3 Replace `cleanup.rs` messages with `i18n::t("stats.*")` / `i18n::t("clean.*")`
  • ☐ 5.4 Replace `update_cli.rs` messages with `i18n::t("update.*")`
  • ☐ 6.1 Replace `entrypoint.sh` echo statements with `$L_*` variables
  • ☐ 6.2 Replace `forge-welcome.sh` labels with `$L_*` variables
  • ☐ 6.3 Replace `install.sh` messages with `$L_*` variables (optional -- may stay English)
  • ☐ 6.4 Replace `uninstall.sh` messages with `$L_*` variables (optional -- may stay English)
  • ☐ 7.1 Implement `detect_locale()` in `i18n.rs` with `LC_ALL` > `LC_MESSAGES` > `LANG` > `LANGUAGE` priority
  • ☐ 7.2 Add macOS `defaults read -g AppleLanguages` fallback
  • ☐ 7.3 Pass detected locale to containers via `-e LANG=...` in podman run args (if not already inherited)
  • ☐ 8.1 Add a test or script that verifies every key in `en.toml` exists in `es.toml` (value can be empty = fallback to English)
  • ☐ 9.1 `cargo build --workspace` compiles
  • ☐ 9.2 `cargo test --workspace` passes
  • ☐ 9.3 Set `LANG=es_MX.UTF-8`, launch tray -- verify Spanish labels appear
  • ☐ 9.4 Set `LANG=en_US.UTF-8`, launch tray -- verify English labels appear
  • ☐ 9.5 Unset `LANG`, launch tray -- verify English fallback
  • ☐ 9.6 Enter forge container, verify entrypoint uses locale-appropriate messages
  • ☐ 9.7 Enter forge container with `LANG=es_MX.UTF-8`, verify Spanish welcome message

Read source record ↗

changeModular entrypointsOpen tasks0 checked · 27 open

The current forge image has a single monolithic entrypoint (`images/default/entrypoint.sh`, 144 lines) that handles every container type: OpenCode forges, Claude forges, maintenance terminals, and (eventually) web servers. This is a growing source of fragility:

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☐ 1.1 Create `images/default/entrypoint-common.sh` with shared setup: `set -euo pipefail`, `umask 0022`, `trap`, secrets dirs, `gh auth setup-git`, shell config deployment, common PATH setup
  • ☐ 1.2 Verify the library is sourceable (no `exit`, no `exec`, exports only what is needed)
  • ☐ 1.3 Update `flake.nix` to copy `entrypoint-common.sh` to `/usr/local/lib/tillandsias/entrypoint-common.sh` in the image
  • ☐ 2.1 Create `images/default/entrypoint-forge-opencode.sh`: source common, install/update OpenCode, install OpenSpec, find project dir, openspec init, print banner, `exec env opencode`
  • ☐ 2.2 Create `images/default/entrypoint-forge-claude.sh`: source common, install/update Claude Code, install OpenSpec, find project dir, openspec init, print banner, `exec env ANTHROPIC_API_KEY="$_CLAUDE_KEY" claude`
  • ☐ 2.3 Create `images/default/entrypoint-terminal.sh`: source common, find project dir, print welcome banner (via `forge-welcome.sh`), `exec fish`
  • ☐ 2.4 Each entrypoint must be independently executable (`chmod +x`, proper shebang, no dependency on being called from another script)
  • ☐ 3.1 Update `flake.nix` `fakeRootCommands` to copy all new entrypoints to `/usr/local/bin/` and the library to `/usr/local/lib/tillandsias/`
  • ☐ 3.2 Update `flake.nix` image `config.Entrypoint` to use `entrypoint-forge-claude.sh` (default agent)
  • ☐ 3.3 Keep the old `tillandsias-entrypoint.sh` as a backward-compat redirect that dispatches based on `TILLANDSIAS_AGENT`
  • ☐ 3.4 Add all new files to the `forgeEntrypoint*` local file references at the top of `flake.nix` so changes trigger rebuilds
  • ☐ 4.1 In `handlers.rs::build_run_args()`, add `--entrypoint` argument based on agent selection: `SelectedAgent::OpenCode` -> `entrypoint-forge-opencode.sh`, `SelectedAgent::Claude` -> `entrypoint-forge-claude.sh`
  • ☐ 4.2 In `handlers.rs::handle_terminal()`, replace `--entrypoint fish` with `--entrypoint /usr/local/bin/entrypoint-terminal.sh`
  • ☐ 4.3 In `handlers.rs::handle_root_terminal()`, replace `--entrypoint fish` with `--entrypoint /usr/local/bin/entrypoint-terminal.sh`
  • ☐ 4.4 In `runner.rs::build_run_args()`, add `--entrypoint` argument based on agent selection (same logic as 4.1)
  • ☐ 4.5 In `runner.rs` bash mode, replace `--entrypoint fish` with `--entrypoint /usr/local/bin/entrypoint-terminal.sh`
  • ☐ 5.1 In `handlers.rs::build_run_args()`, conditionally mount Claude dir and inject API key ONLY when agent is Claude (currently always mounted)
  • ☐ 5.2 In `runner.rs::build_run_args()`, same conditional mount logic
  • ☐ 5.3 In `handle_terminal()` and `handle_root_terminal()`, remove Claude dir mount and API key injection entirely (terminal doesn't need agent secrets)
  • ☐ 5.4 Verify: launch an OpenCode forge, confirm `~/.claude` is NOT mounted inside the container
  • ☐ 5.5 Verify: launch a maintenance terminal, confirm neither `~/.claude` nor `ANTHROPIC_API_KEY` is present
  • ☐ 6.1 Rebuild forge image via `scripts/build-image.sh forge --force`
  • ☐ 6.2 Test: Claude forge launch — verify `entrypoint-forge-claude.sh` runs, Claude Code starts
  • ☐ 6.3 Test: OpenCode forge launch — verify `entrypoint-forge-opencode.sh` runs, OpenCode starts
  • ☐ 6.4 Test: Maintenance terminal — verify `entrypoint-terminal.sh` runs, fish starts with welcome banner and proper PATH
  • ☐ 6.5 Test: Old cached image (if possible) — verify backward-compat redirect still works
  • ☐ 6.6 Test: `cargo test --workspace` passes

Read source record ↗

changeWeb container supportOpen tasks0 checked · 25 open

The `tillandsias-web` image was designed and built (see archived `2026-03-22-web-image` change) but is not yet wired into the tray app or CLI. A user cannot launch a web container from the tray menu or via `tillandsias --web <path>`. The image exists in `flake.nix`, the entrypoint exists at `images/web/entrypoint.sh`, but there is no UI surface to use it.

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☐ 1.1 Add `detect_document_root(project_path: &Path) -> PathBuf` function to `tillandsias-core` — checks for `public/`, `dist/`, `build/`, `_site/`, `out/` subdirectories in order, falls back to project root
  • ☐ 1.2 Add `web.document_root` field to `ProjectConfig` for explicit override
  • ☐ 1.3 Write unit tests: directory with `dist/` returns `dist/`, directory with nothing returns project root, explicit config overrides detection
  • ☐ 2.1 Add `web()` built-in profile to `container_profile.rs`: entrypoint `/entrypoint.sh`, image `tillandsias-web:latest`, mount document_root at `/var/www:ro`, port 8080, no secrets, no env
  • ☐ 2.2 Add `web.port` field to `ProjectConfig` for port override
  • ☐ 2.3 Write unit tests: web profile produces correct args, no secrets mounted, read-only document root
  • ☐ 3.1 Add "Serve Here" menu item to project submenus (alongside "Attach Here" and "Maintenance")
  • ☐ 3.2 Use chain link emoji for web containers in the menu: `"🔗 Serve Here"`
  • ☐ 3.3 Add `handle_serve_here()` handler in `handlers.rs`: detect document root, build web profile, launch container, print URL
  • ☐ 4.1 Add `--web` flag to CLI argument parser
  • ☐ 4.2 Wire `--web` to use the web profile in `runner.rs`
  • ☐ 4.3 Print `Serving at http://localhost:<port>` after successful launch
  • ☐ 5.1 Web containers use naming convention `tillandsias-<project>-web` (no genus allocation)
  • ☐ 5.2 Don't-relaunch guard: if a web container for this project is already running, notify and skip
  • ☐ 5.3 Web containers appear in tray state with `ContainerType::Web` and link emoji
  • ☐ 5.4 Stop/Destroy actions work for web containers (reuse existing handlers)
  • ☐ 6.1 Ensure `build-image.sh web` is called when web image is missing (same pattern as forge auto-build in `handle_attach_here`)
  • ☐ 6.2 Add web image staleness detection (currently only forge is auto-built)
  • ☐ 6.3 Add `BuildProgressEvent` messages for web image build: "Building web server..." / "Web server ready"
  • ☐ 7.1 `cargo test --workspace` — all tests pass
  • ☐ 7.2 Manual test: create a project with `public/index.html`, click "Serve Here", verify httpd serves the file
  • ☐ 7.3 Manual test: `tillandsias --web <path>` — verify web container launches and URL is printed
  • ☐ 7.4 Manual test: verify web container has NO access to secrets (no gh dir, no git config, no claude dir, no API key)
  • ☐ 7.5 Manual test: verify port conflict handling — start two web containers, confirm different ports
  • ☐ 7.6 Manual test: Stop web container from tray, verify it stops cleanly

Read source record ↗

changeCli welcome help redesignOpen tasks0 checked · 14 open

Tillandsias currently has a minimal CLI interface: a static `USAGE` string in `cli.rs` with a flat list of flags. As the application gains accountability windows (`--log-secret-management`, `--log-image-management`, `--log-update-cycle`), per-module log control (`--log=...`), and maintenance commands (`init`, `--stats`, `--clean`, `--update`), the help text

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☐ 1.1 Replace the static `USAGE` string in `cli.rs` with a sectioned format containing USAGE, ACCOUNTABILITY, OPTIONS, MAINTENANCE sections. Include placeholder entries for `--log=MODULES`, `--log-secret-management`, `--log-image-management`, `--log-update-cycle` flags (marked as "coming soon" or just present — they parse but are no-ops until the logging change lands).
  • ☐ 1.2 Audit all user-facing text in the help output for forbidden words: "container", "pod", "image" (container), "runtime" (container). Replace with "environment", "project", etc. per CLAUDE.md conventions. Note: "image" in the context of `--image <name>` should become `--env <name>` or similar.
  • ☐ 1.3 Add `--version` flag handling to `cli::parse()`. Print `tillandsias <4-part-version>` and return `None`. The 4-part version is embedded at build time via `include_str!("../../VERSION")` trimmed.
  • ☐ 1.4 Add unit tests for help text: verify `--help` output contains all section headers, verify `--version` outputs the version string.
  • ☐ 2.1 Create `fn print_welcome_banner()` in `cli.rs` (or a new `banner.rs` module). The function:
  • ☐ 2.2 Implement `detect_podman_version() -> Option<String>` — runs `podman --version` synchronously, parses `"podman version X.Y.Z"` to extract `"X.Y.Z"`.
  • ☐ 2.3 Implement `check_forge_image_status() -> ForgeStatus` enum with variants: `Ready(tag)`, `UpdateNeeded(current_tag, expected_tag)`, `NotBuilt`, `PodmanUnavailable`. Uses synchronous `podman image exists` check.
  • ☐ 2.4 Call `print_welcome_banner()` in `runner.rs` before the container launch sequence (after CLI parse, before podman run). Only in attach mode.
  • ☐ 2.5 Suppress banner when `--debug` is active (debug output replaces the banner with more detailed info).
  • ☐ 2.6 Add unit test for `detect_podman_version()` parsing: standard format, unexpected format, missing binary.
  • ☐ 2.7 Manual test: run `tillandsias <project>` from a terminal, verify banner appears. Run `tillandsias <project> | cat`, verify banner does NOT appear (not a TTY). Run `tillandsias --help`, verify banner does NOT appear (help mode).
  • ☐ 3.1 When `logging-accountability-framework` lands, update the help text to remove any "coming soon" markers from the `--log-*` flags.
  • ☐ 3.2 When `secret-rotation-tokens` lands, update the banner to show token delivery mechanism status (tmpfs vs hosts.yml fallback) in `--debug` mode.
  • ☐ 3.3 Consider adding `--quiet` flag to suppress the banner for scripted usage (alternative to piping). Low priority — piping already suppresses it.

Read source record ↗

changeClickable trace indexOpen tasks0 checked · 15 open

`@trace spec:<name>` comments in source files create a navigational chain from code back to the specification that justified a decision. But the chain breaks on GitHub: code comments are not hyperlinks, so a reader sees `@trace spec:podman-orchestration` and has no direct path to the spec file.

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☐ 1.1 Create `scripts/generate-traces.sh` with shebang, `set -euo pipefail`, and `@trace spec:clickable-trace-index`
  • ☐ 1.2 Implement scan: grep all `.rs`, `.sh`, `.toml`, `.nix` files for `@trace spec:<name>` patterns, collect file path + line number + spec names
  • ☐ 1.3 Implement spec resolution: for each unique spec name, locate `openspec/specs/<name>/spec.md` (active) or `openspec/changes/archive/*/<name>/spec.md` (archived); mark missing specs as `(not found)`
  • ☐ 1.4 Implement root `TRACES.md` generation: header, regeneration notice, table with columns Trace / Spec / Source Files; source file links use relative paths with `#L<n>` anchors
  • ☐ 1.5 Implement per-spec `TRACES.md` generation: for each spec with at least one trace, write `openspec/specs/<name>/TRACES.md` with back-links; relative paths from the spec dir to source files
  • ☐ 1.6 Make script executable (`chmod +x`)
  • ☐ 2.1 Add `generate-traces.sh` call to `build.sh` after the version bump line (non-test, non-check builds only)
  • ☐ 3.1 Run `./scripts/generate-traces.sh` to produce initial `TRACES.md` from the 38 existing annotations
  • ☐ 3.2 Verify `TRACES.md` renders correctly: table has expected rows, links are relative, anchors are present
  • ☐ 3.3 Verify per-spec `TRACES.md` files are created for each active spec that has traces
  • ☐ 3.4 Spot-check one archived spec (`tray-icon-lifecycle`) to confirm it links to the archive path
  • ☐ 4.1 Confirm `TRACES.md` contains all 11 unique spec names found in the codebase
  • ☐ 4.2 Confirm per-spec `TRACES.md` files exist for each active spec in the trace index
  • ☐ 4.3 Confirm script is idempotent (running twice produces identical output)
  • ☐ 4.4 Confirm `./build.sh --check` still passes with the new `build.sh` line

Read source record ↗

changeLogging accountability frameworkOpen tasks0 checked · 26 open

Tillandsias currently has a single-layer logging system: `tracing` with an `EnvFilter` that reads from `TILLANDSIAS_LOG` or `RUST_LOG`, defaulting to `tillandsias=info`. There is no CLI flag for controlling log levels, no per-module granularity from the user's perspective, and no way to inspect specific subsystems (like secret handling or container lifecycle

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☐ 1.1 Add `LogConfig` struct to `cli.rs`: `modules: Vec<(String, tracing::Level)>`, `accountability: Vec<AccountabilityWindow>`. Define `AccountabilityWindow` enum with `SecretManagement`, `ImageManagement`, `UpdateCycle` variants.
  • ☐ 1.2 Add `--log=MODULES` flag parsing to `cli::parse()`. Parse semicolon-separated `module:level` pairs. Validate module names against the six defined modules (`secrets`, `containers`, `updates`, `scanner`, `menu`, `events`). Warn on unknown modules, error on invalid levels.
  • ☐ 1.3 Add `--log-secret-management`, `--log-image-management`, `--log-update-cycle` flag parsing to `cli::parse()`. These set the corresponding `AccountabilityWindow` variant in the `LogConfig`.
  • ☐ 1.4 Define the module-to-Rust-target mapping function in `logging.rs`: `fn module_to_targets(module: &str) -> Vec<&str>`. Maps user-facing names to Rust module paths (e.g., `"secrets"` -> `["tillandsias::secrets", "tillandsias::launch"]`).
  • ☐ 1.5 Update `logging::init()` to accept `LogConfig` parameter. Build `EnvFilter` from the module map. If `LogConfig` is empty, fall back to current behavior (`TILLANDSIAS_LOG` / `RUST_LOG` / default).
  • ☐ 1.6 Thread `LogConfig` from `cli::parse()` through `main.rs` to `logging::init()`. For `CliMode::Attach`, pass log config. For `CliMode::Tray`, pass log config (affects file output).
  • ☐ 1.7 Add unit tests for `--log` parsing: valid single module, multiple modules, invalid module name (warning), invalid level (error), empty value, interaction with `TILLANDSIAS_LOG`.
  • ☐ 1.8 Add unit tests for `module_to_targets()`: all six modules map correctly, unknown module returns empty.
  • ☐ 2.1 Create `src-tauri/src/accountability.rs` module. Define `AccountabilityLayer` struct implementing `tracing_subscriber::Layer<S>`. The layer filters on events where the span has `accountability = true` field.
  • ☐ 2.2 Implement `AccountabilityFormatter` that renders the `[category] version | message` format with spec name and cheatsheet path. Version is read from `env!("CARGO_PKG_VERSION")` at compile time.
  • ☐ 2.3 Define `spec_url(spec_name: &str) -> String` helper that generates `https://github.com/8007342/tillandsias/search?q=%40trace+spec%3A{name}&type=code`. URL-encode the spec name.
  • ☐ 2.4 When `--log-secret-management` is active, register the `AccountabilityLayer` with the subscriber in `logging::init()`. The layer uses a per-layer filter (`tracing_subscriber::filter::Targets`) to only process events from the `secrets` module targets.
  • ☐ 2.5 Add the `mod accountability;` declaration to `main.rs`.
  • ☐ 2.6 Add unit tests for `AccountabilityFormatter`: correct prefix, version present, spec URL generation, cheatsheet path present.
  • ☐ 2.7 Add unit test for `spec_url()`: correct URL encoding, correct base URL.
  • ☐ 3.1 Add `#[instrument(fields(accountability = true, category = "secrets"))]` to `secrets::store_github_token()`, `secrets::retrieve_github_token()`, `secrets::write_hosts_yml_from_keyring()`, and `secrets::migrate_token_to_keyring()`.
  • ☐ 3.2 Add accountability-tagged `info!` events to each secrets function with human-readable summaries: "Token retrieved from native keyring", "Token stored in native keyring", "hosts.yml refreshed from keyring", "Token migrated from hosts.yml to keyring".
  • ☐ 3.3 Add `spec` field to `trace!` events in secrets functions: `trace!(spec = "native-secrets-store", "...")`.
  • ☐ 3.4 Add accountability-tagged spans to `launch.rs` `ensure_secrets_dirs()` and the secret resolution code in `build_podman_args()`.
  • ☐ 3.5 Add accountability-tagged spans to `handlers.rs` at the points where `write_hosts_yml_from_keyring()` is called (three callsites: forge launch, terminal, root terminal).
  • ☐ 3.6 Manual test: run `tillandsias --log-secret-management <project>` and verify the accountability output shows each secrets operation with the correct format.
  • ☐ 4.1 Instrument `handlers.rs` container lifecycle functions with `containers` category accountability spans.
  • ☐ 4.2 Instrument `updater.rs` and `update_cli.rs` with `updates` category accountability spans.
  • ☐ 4.3 Instrument `tillandsias_scanner` with `scanner` category spans (debug-level only, no accountability window yet).
  • ☐ 4.4 Instrument `menu.rs` and `event_loop.rs` with `menu`/`events` category spans.
  • ☐ 4.5 Add integration test: launch with `--log=secrets:trace;scanner:off`, verify secrets trace output present and scanner output absent in log file.

Read source record ↗

changeLogging cheatsheetsOpen tasks0 checked · 9 open

The `logging-accountability-framework` and `secret-rotation-tokens` changes create a system where runtime log output links to spec names and cheatsheet paths. But those cheatsheet files do not exist yet. Without them, the accountability window's "Cheatsheet:" lines are dead references.

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☐ 1.1 Create `docs/cheatsheets/` directory.
  • ☐ 1.2 Write `docs/cheatsheets/secret-management.md` covering:
  • ☐ 1.3 Write `docs/cheatsheets/logging-levels.md` covering:
  • ☐ 1.4 Write `docs/cheatsheets/token-rotation.md` covering:
  • ☐ 2.1 After `logging-accountability-framework` implementation: verify that every `Cheatsheet:` path in accountability output resolves to a real file in `docs/cheatsheets/`.
  • ☐ 2.2 After `secret-rotation-tokens` implementation: verify that the `token-rotation.md` cheatsheet accurately describes the implemented mechanism. Update any discrepancies (monotonic convergence).
  • ☐ 2.3 Add a CI check (optional, low priority): verify that all `docs/cheatsheets/*.md` files referenced in source code exist. Can be a simple grep + file-exists check in the CI script.
  • ☐ 3.1 When `--log-image-management` accountability window is implemented: create `docs/cheatsheets/image-management.md` covering forge image build, staleness detection, auto-build, pruning old versions.
  • ☐ 3.2 When `--log-update-cycle` accountability window is implemented: create `docs/cheatsheets/update-cycle.md` covering version check, download, verify, apply, restart, rollback.

Read source record ↗

changeSecret rotation tokensOpen tasks0 checked · 32 open

Today, Tillandsias injects GitHub credentials into containers by writing the user's full OAuth token to `~/.cache/tillandsias/secrets/gh/hosts.yml` and bind-mounting it read-only at `/home/forge/.config/gh/`. This token has `repo`, `read:org`, and `gist` scopes -- full read/write access to every repository the user owns, plus organization data.

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☐ 1.1 Create `src-tauri/src/token_file.rs` module with `token_base_dir() -> PathBuf` function. Resolves the tmpfs-backed base directory: `$XDG_RUNTIME_DIR/tillandsias/tokens/` on Linux, `$TMPDIR/tillandsias/tokens/` on macOS, `%TEMP%\tillandsias\tokens\` on Windows. Falls back through the chain if earlier options are unavailable.
  • ☐ 1.2 Add `token_file::write(container_name: &str, token: &str) -> Result<PathBuf, String>` function. Creates `<base>/<container_name>/` directory with mode 0700. Writes token to `github_token.tmp` with mode 0600. Atomically renames to `github_token`. Returns the full path to the token file. Logs to accountability window.
  • ☐ 1.3 Add `token_file::delete(container_name: &str)` function. Removes the `<base>/<container_name>/` directory and its contents. Best-effort (no error if already gone). Logs to accountability window.
  • ☐ 1.4 Add `token_file::delete_all()` function. Removes the entire `<base>/` directory tree. Called on app exit. Logs count of deleted token files to accountability window.
  • ☐ 1.5 Create `TokenCleanupGuard` struct that holds the base directory path. Implement `Drop` to call `delete_all()`. This ensures cleanup on panic.
  • ☐ 1.6 Add `mod token_file;` declaration to `main.rs`.
  • ☐ 1.7 Add unit tests for `token_file`: write creates file with correct permissions, write is atomic (no partial reads), delete removes file and directory, delete_all removes all, token_base_dir resolves correctly on each platform (use `cfg(test)` overrides).
  • ☐ 2.1 Create `images/default/git-askpass-tillandsias` script file with the GIT_ASKPASS helper contents. Script reads `/run/secrets/github_token` for password, returns `x-access-token` for username. Must be executable (0755), owned by root in the image.
  • ☐ 2.2 Update `flake.nix` to include the script at `/usr/local/bin/git-askpass-tillandsias` in the forge image. Ensure it is in the image's `PATH`.
  • ☐ 2.3 Manual test: build the forge image with `scripts/build-image.sh forge --force`, enter a container, verify the script exists at the expected path with correct permissions.
  • ☐ 3.1 Add `SecretKind::GitHubToken` variant to `crates/tillandsias-core/src/container_profile.rs`. This represents "mount the GitHub token file at /run/secrets/github_token".
  • ☐ 3.2 Add `token_file_path: Option<PathBuf>` field to `LaunchContext` in `container_profile.rs`.
  • ☐ 3.3 Add `SecretMount { kind: SecretKind::GitHubToken }` to `forge_opencode_profile()`, `forge_claude_profile()`, and `terminal_profile()`. The web profile does NOT get this secret.
  • ☐ 3.4 Update `build_podman_args()` in `src-tauri/src/launch.rs` to handle `SecretKind::GitHubToken`: if `ctx.token_file_path` is `Some(path)`, add `-v <path>:/run/secrets/github_token:ro` and `-e GIT_ASKPASS=/usr/local/bin/git-askpass-tillandsias`.
  • ☐ 3.5 Update `build_launch_context()` in `src-tauri/src/handlers.rs` to: (a) retrieve token from keyring, (b) call `token_file::write()`, (c) set `token_file_path` in the `LaunchContext`.
  • ☐ 3.6 Update `runner.rs` CLI mode to do the same: write token file before launch, set `token_file_path`.
  • ☐ 3.7 Update unit tests in `launch.rs`: verify `GitHubToken` secret produces the correct `-v` and `-e` args. Verify web profile does NOT have the token mount.
  • ☐ 3.8 Update unit tests in `container_profile.rs`: forge and terminal profiles now have `GitHubToken` in their secrets list.
  • ☐ 4.1 Add `token_file::spawn_refresh_task(tracked: Arc<Mutex<HashSet<String>>>) -> JoinHandle<()>` function. The task runs `tokio::time::interval(Duration::from_secs(55 * 60))`. On each tick, for each container name in `tracked`, retrieve the token from the keyring and rewrite the token file atomically.
  • ☐ 4.2 Add `tracked_containers: Arc<Mutex<HashSet<String>>>` to the main event loop state. When a container starts (AttachHere/Terminal), insert the container name. When a container stops (podman die event), remove it.
  • ☐ 4.3 Spawn the refresh task in `main.rs` alongside the scanner and podman event tasks. Pass the `tracked_containers` handle.
  • ☐ 4.4 Add unit test: mock the interval, verify refresh writes new file content, verify untracked containers are skipped.
  • ☐ 5.1 In `event_loop.rs`, when a podman `die` or `stop` event is received for a tracked container, call `token_file::delete(&container_name)`. Remove the container from the `tracked_containers` set.
  • ☐ 5.2 In `main.rs`, create `TokenCleanupGuard` early in `main()` (before Tauri setup). The guard's `Drop` will call `delete_all()` on exit.
  • ☐ 5.3 In the `RunEvent::ExitRequested` handler in `main.rs`, explicitly call `token_file::delete_all()` before the guard has a chance to run (belt-and-suspenders).
  • ☐ 5.4 Add integration test: start a mock container, verify token file exists, stop the container, verify token file is deleted. Exit the app, verify the entire tokens directory is gone.
  • ☐ 6.1 Add accountability-tagged spans to `token_file::write()`: `[secrets] Token written for <container> -> /run/secrets/... (tmpfs, ro mount)`.
  • ☐ 6.2 Add accountability-tagged spans to `token_file::delete()`: `[secrets] Token revoked for <container> (container stopped)`.
  • ☐ 6.3 Add accountability-tagged spans to the refresh task tick: `[secrets] Token refreshed for <container> (55min rotation)`.
  • ☐ 6.4 Add accountability-tagged spans to `token_file::delete_all()`: `[secrets] All token files cleaned up (app exit, <N> files removed)`.
  • ☐ 6.5 Add accountability-tagged span to fallback path: `[secrets] WARN: tmpfs unavailable, falling back to hosts.yml mount for <container>`.
  • ☐ 6.6 Manual test: run `tillandsias --log-secret-management <project>`, verify all token lifecycle events appear in the accountability output.

Read source record ↗

changeTray icon lifecycleOpen tasks0 checked · 10 open

The tray icon currently uses 3 states (`Base`, `Building`, `Decay`) that map to Ionantha `bud.svg`, `bloom.svg`, and `dried.svg`. This mapping is incomplete and semantically wrong: the "base" state uses a bud icon (which suggests something new and growing), while it should represent a healthy, mature plant at rest. The 4 SVG lifecycle stages per genus (pup,

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☐ 1. Rename `TrayIconState::Base` to `Mature`, `Decay` to `Dried`, and add `Pup` and `Blooming` variants in `genus.rs`
  • ☐ 2. Update `TRAY_ICONS` in `build.rs` from 3 to 5 entries: pup->pup.svg, mature->bud.svg, building->bloom.svg, blooming->bloom.svg, dried->dried.svg
  • ☐ 3. Update generated `tray_icon_png()` match arms in `build.rs` `generate_icons_rs()` for all 5 variants
  • ☐ 4. Update `TrayState::new()` in `state.rs` to initialize with `TrayIconState::Pup` instead of `Base`
  • ☐ 5. Update `compute_icon_state()` in `state.rs` to return `Blooming` when recent builds completed and none in progress
  • ☐ 6. Update `main.rs` startup: launch with `Pup` icon, transition to `Mature` when forge confirmed, `Dried` when podman missing
  • ☐ 7. Add Blooming-to-Mature transition in `main.rs` `on_menu_event` when user opens tray menu
  • ☐ 8. Update `menu.rs` references from `TrayIconState::Decay` to `TrayIconState::Dried`
  • ☐ 9. Update test functions in `icons.rs` for renamed variants (`Base`->`Mature`, `Decay`->`Dried`) and add tests for `Pup` and `Blooming`
  • ☐ 10. Verify `cargo test --workspace` passes and `cargo build --workspace` succeeds

Read source record ↗

changeTray icon resize visibilityOpen tasks17 checked · 2 open

The 32 tray icon SVGs (8 genera x 4 lifecycle states) have a 64x64 viewBox but the plant graphics are tiny within it. At typical system tray sizes (22-32px), the plants are barely visible:

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1.1 Read all 32 SVGs and catalog transform anchors, bounding boxes, fill colors
  • ☑ 1.2 Identify the vertical extent each plant uses within the 64x64 canvas
  • ☑ 1.3 Note special cases (usneoides hangs from top, tectorum is spherical, xerographica has wide curls)
  • ☑ 2.1 Adjust `translate()` anchors to vertically center each plant in the 64x64 canvas
  • ☑ 2.2 Apply `scale()` transforms to fill 70-85% of the canvas
  • ☑ 2.3 Handle usneoides separately (top-hanging orientation preserved)
  • ☑ 2.4 Handle tectorum (spherical shape needs even centering)
  • ☑ 2.5 Handle xerographica (wide curling leaves, avoid clipping)
  • ☑ 3.1 Increase stroke-width from 1 to 1.5 on polygon-based genera (ionantha, stricta)
  • ☑ 3.2 Verify path-based genera already have adequate stroke-width (bulbosa, caput-medusae, xerographica have 2-3)
  • ☑ 3.3 Increase tectorum trichome line stroke-width from 1 to 1.5
  • ☑ 4.1 Greens: #6aad6a -> #7ec87e, #7cbc7c -> #95d895, #5a9a5a -> #6ab86a
  • ☑ 4.2 Stroke greens: #3a7a3a -> #4a8a4a, #4a8a4a -> #5a9a5a
  • ☑ 4.3 Dried browns: #a08860 -> #b09968, #b09970 -> #c0aa78, #887044 -> #98804c
  • ☑ 4.4 Pup light greens: keep as-is (already bright enough)
  • ☑ 4.5 Bloom pinks/reds/purples: keep vivid (already high contrast)
  • ☑ 5.1 All 32 SVGs modified (8 genera x 4 states)
  • ☐ 5.2 `./build.sh --check` passes (build.rs SVG rendering)
  • ☐ 5.3 Visual spot-check: ionantha bloom/bud/dried/pup at 32x32 render

Read source record ↗

changeUpdate audit loggingOpen tasks18 checked · 4 open

When `--update` runs or the background auto-updater applies a patch, there is no record of what happened. Users and developers have no way to confirm whether an update succeeded, what version was installed, or when the last check occurred. After the process exits, all context is gone. A persistent audit log closes this gap — it's a plain text file that accum

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1.1 Create `src-tauri/src/update_log.rs` with `log_path() -> PathBuf` returning `cache_dir().join("update.log")`
  • ☑ 1.2 Implement `append_entry(line: &str)` — creates parent dirs if absent, opens file in append mode, writes `[<rfc3339>] <line>\n`
  • ☑ 1.3 Implement `rotate_if_needed()` — if `update.log` exceeds 1 MB, read all lines, keep last 100, rewrite file, append a `[<rfc3339>] LOG ROTATED (kept last 100 entries)` header line
  • ☑ 1.4 Implement `read_last_entry() -> Option<String>` — reads the last non-empty line from `update.log`, returns `None` if file absent
  • ☑ 1.5 Add `sha2` and `hex` to `src-tauri/Cargo.toml` dependencies for SHA256 computation
  • ☑ 1.6 Implement `sha256_file(path: &Path) -> Result<String, String>` — read file bytes, compute SHA256, return lowercase hex string
  • ☑ 2.1 On successful manifest fetch where version is already current: append `UPDATE CHECK: v<current> — already up to date`
  • ☑ 2.2 On update available: append `UPDATE CHECK: v<current> → v<latest> available`
  • ☑ 2.3 On manifest fetch error: append `ERROR: failed to fetch update manifest: <msg>`
  • ☑ 2.4 On download complete: append `DOWNLOAD: <human_bytes> from <url>`
  • ☑ 2.5 On download error: append `ERROR: download failed: <msg>`
  • ☑ 2.6 On apply success: append `APPLIED: v<old> → v<new> (replaced <path>) SHA256: <hex>`
  • ☑ 2.7 On apply error: append `ERROR: failed to apply update: <msg>`
  • ☑ 2.8 Call `rotate_if_needed()` once at the start of `run()` (before any new entries are written) if `update.log` already exists
  • ☑ 3.1 In `check_for_update()`: when `updater.check()` returns `Ok(Some(update))` for a new version not yet seen this session, append `UPDATE CHECK: v<current> → v<new_version> available (background)`
  • ☑ 3.2 In `install_update()`: after `download_and_install` succeeds, append `APPLIED: background updater installed v<new_version>` (no SHA256 here — Tauri manages the binary replacement)
  • ☑ 3.3 In `install_update()`: on `download_and_install` error, append `ERROR: background update install failed: <msg>`
  • ☑ 4.1 After the "Installed binary" block in `run_stats()`, read `update_log::read_last_entry()` and print ` Last update: <entry>` or ` Last update: (no update log)` if absent
  • ☐ 5.1 Manual test: run `tillandsias --update` on an up-to-date install, verify `update.log` contains an "already up to date" entry
  • ☐ 5.2 Manual test: run `tillandsias --stats`, verify "Last update" line appears
  • ☐ 5.3 Manual test: force rotation by writing a >1 MB `update.log` (many lines), run `--update`, verify file is truncated to ~100 lines plus the rotation marker
  • ☐ 5.4 Run `./build.sh --check` to confirm the build type-checks cleanly

Read source record ↗

changeAdd german i18nNo checklist0 checked · 0 open

2026-04-03-add-german-i18n

No task record was present in this archived change.

Read source record ↗

changeEnclave proxy networkOpen tasks0 checked · 39 open

Coding containers currently have unrestricted network access and receive GitHub tokens via bind mounts. AI agents running inside can read secrets and exfiltrate data to any endpoint. This is the most critical security gap in the architecture. Phase 1 lays the foundation by creating an internal podman network (enclave) and a caching proxy container that becom

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☐ 1.1 Create `images/proxy/Containerfile` — Alpine + squid, non-root user, ~15MB
  • ☐ 1.2 Create `images/proxy/squid.conf` — caching proxy config, 500MB disk cache, domain ACLs
  • ☐ 1.3 Create `images/proxy/allowlist.txt` — curated domain allowlist for web/mobile/cloud dev
  • ☐ 1.4 Create `images/proxy/entrypoint.sh` — initialize cache dirs, start squid foreground
  • ☐ 1.5 Register `proxy` image type in `build-image.sh` so `build-image.sh proxy` works
  • ☐ 1.6 Test: `build-image.sh proxy --tag tillandsias-proxy:v0.1.126` builds successfully under 30MB
  • ☐ 2.1 Add `ensure_enclave_network()` to `tillandsias-podman` crate — creates `tillandsias-enclave` if absent
  • ☐ 2.2 Add `remove_enclave_network()` to `tillandsias-podman` crate — removes network if no containers attached
  • ☐ 2.3 Add `network_exists()` check to `tillandsias-podman` crate
  • ☐ 2.4 Call `ensure_enclave_network()` before any container launch in `handlers.rs`
  • ☐ 2.5 Call `remove_enclave_network()` on app exit in `main.rs`
  • ☐ 3.1 Add `proxy_profile()` to `container_profile.rs` — dual-network, cache volume, no secrets
  • ☐ 3.2 Add `ProxyState` to `TrayState` — tracks proxy container running/stopped
  • ☐ 3.3 Add `ensure_proxy_running()` to `handlers.rs` — start proxy if not running, health-check
  • ☐ 3.4 Call `ensure_proxy_running()` before forge launch in `handle_attach_here()`
  • ☐ 3.5 Call `ensure_proxy_running()` before forge launch in CLI `runner::run()`
  • ☐ 3.6 Stop proxy container on app exit alongside enclave network cleanup
  • ☐ 3.7 Add proxy health check to event loop (60-second interval)
  • ☐ 4.1 Modify `build_podman_args()` in `launch.rs` to add `--network=tillandsias-enclave`
  • ☐ 4.2 Add `HTTP_PROXY`, `HTTPS_PROXY`, `NO_PROXY` env vars to forge profiles in `container_profile.rs`
  • ☐ 4.3 Update `common_forge_env()` with proxy env vars (conditional on enclave being active)
  • ☐ 4.4 Update launch.rs tests to verify enclave network attachment and proxy env vars
  • ☐ 5.1 Add `ProxyManagement` and `EnclaveManagement` variants to `AccountabilityWindow` in `cli.rs`
  • ☐ 5.2 Add `--log-proxy` and `--log-enclave` flag parsing in `parse_log_flags()`
  • ☐ 5.3 Add proxy/enclave log targets to `logging.rs` module-to-targets mapping
  • ☐ 5.4 Add `@trace spec:proxy-container` and `@trace spec:enclave-network` annotations to all new code
  • ☐ 5.5 Update USAGE string in `cli.rs` with new accountability flags
  • ☐ 5.6 Update `docs/cheatsheets/logging-levels.md` with new accountability windows
  • ☐ 6.1 Create `docs/cheatsheets/enclave-architecture.md` with architecture diagram and trace references
  • ☐ 6.2 Update `docs/cheatsheets/secret-management.md` to reference enclave isolation
  • ☐ 6.3 Add `@trace spec:enclave-network` and `@trace spec:proxy-container` to all modified source files
  • ☐ 6.4 Update CLAUDE.md with enclave architecture section
  • ☐ 7.1 Run `cargo test --workspace` — all existing tests pass
  • ☐ 7.2 Test: `build-image.sh proxy` builds successfully
  • ☐ 7.3 Test: proxy container starts and responds to HTTP requests
  • ☐ 7.4 Test: forge container can `npm install` through proxy
  • ☐ 7.5 Test: forge container cannot reach non-allowlisted domains
  • ☐ 7.6 Test: `--log-proxy` shows proxy request events
  • ☐ 7.7 Test: `--log-enclave` shows network lifecycle events

Read source record ↗

changeEnclave telemetry polishOpen tasks0 checked · 15 open

Phases 1-4 built the full enclave architecture: proxy, git mirror, offline forge, and inference. The accountability windows (`--log-proxy`, `--log-enclave`, `--log-git`) were added but need real telemetry events populated throughout the codebase. Documentation needs final updates to reflect the complete architecture.

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☐ 1.1 Verify `ensure_enclave_network()` emits `[enclave]` events with `spec = "enclave-network"`
  • ☐ 1.2 Verify `ensure_proxy_running()` emits `[proxy]` events with `spec = "proxy-container"`
  • ☐ 1.3 Verify `ensure_git_service_running()` emits `[git]` events with `spec = "git-mirror-service"`
  • ☐ 1.4 Verify `ensure_inference_running()` emits events with `spec = "inference-container"`
  • ☐ 1.5 Verify shutdown functions emit cleanup events
  • ☐ 1.6 Verify proxy health check emits events
  • ☐ 2.1 Update CLAUDE.md with enclave architecture section
  • ☐ 2.2 Update enclave-architecture.md cheatsheet — mark all phases complete
  • ☐ 2.3 Update logging-levels.md cheatsheet with new accountability windows
  • ☐ 2.4 Final @trace annotation sweep across all new source files
  • ☐ 3.1 Archive enclave-proxy-network
  • ☐ 3.2 Archive git-mirror-service
  • ☐ 3.3 Archive forge-offline-isolation
  • ☐ 3.4 Archive inference-container
  • ☐ 3.5 Archive enclave-telemetry-polish

Read source record ↗

changeForge offline isolationOpen tasks0 checked · 28 open

Phases 1-2 established the enclave network, proxy, and git mirror. But forge containers still have credential mounts (GitHub token, hosts.yml, Claude dir) and the project directory is still mounted directly. Phase 3 completes the isolation: forge containers become fully offline with zero credentials. Code comes exclusively from the git mirror, packages throu

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☐ 1.1 Remove `SecretKind::GitHubToken` from `forge_opencode_profile()` secrets
  • ☐ 1.2 Remove `SecretKind::GitHubToken` and `SecretKind::ClaudeDir` from `forge_claude_profile()` secrets
  • ☐ 1.3 Remove `GIT_ASKPASS` related env var handling (it was injected by launch.rs for GitHubToken)
  • ☐ 1.4 Remove `SecretKind::GitHubToken` from `terminal_profile()` secrets
  • ☐ 1.5 Update all profile tests (secret counts, env var counts)
  • ☐ 2.1 Remove `MountSource::ProjectDir` from `common_forge_mounts()` — code comes from git clone
  • ☐ 2.2 Remove `MountSource::SecretsSubdir("gh")` from `common_forge_mounts()` — no hosts.yml
  • ☐ 2.3 Remove `MountSource::SecretsSubdir("git")` from `common_forge_mounts()` — no git config
  • ☐ 2.4 Keep `MountSource::CacheDir` — build caches are still needed
  • ☐ 2.5 Add `GIT_AUTHOR_NAME` and `GIT_AUTHOR_EMAIL` env vars from global config (replaces git config mount)
  • ☐ 2.6 Update `common_forge_mounts()` tests (mount count: 4 → 1)
  • ☐ 2.7 Update launch.rs tests that verify mount strings
  • ☐ 3.1 Update `entrypoint-forge-opencode.sh` — remove fallback to direct mount, clone to primary path
  • ☐ 3.2 Update `entrypoint-forge-claude.sh` — same as opencode
  • ☐ 3.3 Update `entrypoint-terminal.sh` — terminal also clones from mirror (maintenance mode)
  • ☐ 3.4 Add git identity setup in entrypoint from env vars (`git config user.name`, `git config user.email`)
  • ☐ 3.5 Add agent reminder message about committing work before stopping
  • ☐ 4.1 Update `handle_github_login()` to exec into running git service or start temporary one
  • ☐ 4.2 Remove the standalone forge-based `gh-auth-login.sh` terminal launch
  • ☐ 4.3 Update CLI `run_github_login()` to use git service container
  • ☐ 4.4 Test: GitHub Login works with and without running git service
  • ☐ 5.1 Remove `write_hosts_yml_from_keyring()` calls from forge launch paths (no longer needed)
  • ☐ 5.2 Remove `token_files::write_token()` calls from forge launch paths (no longer needed)
  • ☐ 5.3 Keep token infrastructure for git service container (it still uses token as fallback)
  • ☐ 5.4 Run `cargo test --workspace` — all tests pass
  • ☐ 5.5 Verify: forge container has zero credential mounts in podman args
  • ☐ 5.6 Verify: forge container has no project directory mount
  • ☐ 5.7 Update `docs/cheatsheets/secret-management.md` — Phase 3 is now active

Read source record ↗

changeGit mirror serviceOpen tasks0 checked · 35 open

Phase 1 introduced the enclave network and proxy. But credentials (GitHub token, hosts.yml) are still mounted directly into forge containers. The git mirror service eliminates this by isolating all git/GitHub operations into a dedicated container that owns the credentials. Forge containers clone from the mirror — they never touch credentials or the remote.

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☐ 1.1 Create `images/git/Containerfile` — Alpine + git + gh CLI, non-root user, git-daemon ready
  • ☐ 1.2 Create `images/git/entrypoint.sh` — start git daemon with `--export-all --enable=receive-pack` on `/srv/git`
  • ☐ 1.3 Create `images/git/post-receive-hook.sh` — template hook that pushes to origin if configured
  • ☐ 1.4 Register `git` image type in `build-image.sh`
  • ☐ 1.5 Test: `build-image.sh git` builds successfully under 20MB
  • ☐ 2.1 Add `detect_project_git_state()` function — returns enum: `RemoteRepo(url)`, `LocalRepo`, `NotGitRepo`
  • ☐ 2.2 Add `ensure_mirror()` function — creates/updates bare mirror at `~/.cache/tillandsias/mirrors/<project>/`
  • ☐ 2.3 Handle non-git project: `git init` + initial commit before mirroring
  • ☐ 2.4 Install post-receive hook into mirror's `hooks/` directory
  • ☐ 2.5 Test: mirror creation for repo with remote, repo without remote, and non-git directory
  • ☐ 3.1 Add `git_service_profile()` to `container_profile.rs` — enclave network, D-Bus mounts, mirror volume
  • ☐ 3.2 Add `git_image_tag()` to `handlers.rs`
  • ☐ 3.3 Add `ensure_git_service_running()` to `handlers.rs` — start per-project git service if not running
  • ☐ 3.4 Add `stop_git_service()` to `handlers.rs` — stop when last forge for project stops
  • ☐ 3.5 Add `TILLANDSIAS_GIT_SERVICE` env var to forge profiles pointing to git-service hostname
  • ☐ 3.6 Wire git service lifecycle into `handle_attach_here()`, `handle_terminal()`, CLI `runner::run()`
  • ☐ 4.1 Modify `entrypoint-forge-opencode.sh` to clone from git mirror before launching agent
  • ☐ 4.2 Modify `entrypoint-forge-claude.sh` to clone from git mirror before launching agent
  • ☐ 4.3 Add retry logic (5 attempts, 1s delay) for git clone in case git service isn't ready
  • ☐ 4.4 Configure git remote in cloned repo to push back to git-service
  • ☐ 5.1 Add `GitManagement` variant to `AccountabilityWindow` in `cli.rs`
  • ☐ 5.2 Add `--log-git` flag parsing in `parse_log_flags()`
  • ☐ 5.3 Add git log targets to `logging.rs`
  • ☐ 5.4 Add `@trace spec:git-mirror-service` annotations to all new code
  • ☐ 5.5 Update USAGE string with `--log-git`
  • ☐ 6.1 Modify `handle_github_login()` to run `gh auth login` in git service container instead of forge
  • ☐ 6.2 Handle case where no git service is running (start temporary one)
  • ☐ 6.3 Update `gh-auth-login.sh` to work in git service container context
  • ☐ 7.1 Run `cargo test --workspace` — all existing tests pass
  • ☐ 7.2 Test: `build-image.sh git` builds successfully
  • ☐ 7.3 Test: git service starts and git daemon is reachable from enclave
  • ☐ 7.4 Test: forge container can clone from `git://git-service/<project>`
  • ☐ 7.5 Test: push from forge → mirror → remote (with post-receive hook)
  • ☐ 7.6 Test: credential refresh via tray GitHub Login works mid-session
  • ☐ 7.7 Test: non-git directory auto-initialized and served through mirror

Read source record ↗

changeInference containerOpen tasks0 checked · 15 open

The enclave architecture isolates forge containers from the network and credentials. For AI-assisted development with local models, we need a dedicated inference container running ollama. This container is offline (enclave-only), shares models via a persistent volume, and requests model downloads through the host app.

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☐ 1.1 Create `images/inference/Containerfile` — Fedora minimal + ollama, non-root user
  • ☐ 1.2 Create `images/inference/entrypoint.sh` — start ollama serve with proxy env vars
  • ☐ 1.3 Register `inference` image type in `build-image.sh`
  • ☐ 1.4 Test: `build-image.sh inference` builds successfully
  • ☐ 2.1 Add `inference_profile()` to `container_profile.rs` — enclave network, model cache volume, proxy env vars, no secrets
  • ☐ 2.2 Add `inference_image_tag()` to `handlers.rs`
  • ☐ 2.3 Add `ensure_inference_running()` to `handlers.rs` — start if not running, shared across projects
  • ☐ 2.4 Add `stop_inference()` to `handlers.rs` — stop on app exit
  • ☐ 2.5 Wire `ensure_inference_running()` into forge launch paths
  • ☐ 2.6 Add inference stop to shutdown sequence in `main.rs`
  • ☐ 3.1 Add `OLLAMA_HOST=http://inference:11434` env var to `common_forge_env()`
  • ☐ 3.2 Update env var count tests
  • ☐ 4.1 Run `cargo test --workspace` — all tests pass
  • ☐ 4.2 Test: inference container starts and ollama responds
  • ☐ 4.3 Test: forge container can reach inference via OLLAMA_HOST

Read source record ↗

changeAdd spec traceability refsOpen tasks36 checked · 3 open

No way to trace backwards from code or logs to the spec that justified a decision. Troubleshooting agents guess which spec applies. When code is modified, no way to know which specs are impacted. Adding lightweight `@trace` references and structured `spec` log fields completes the observability chain: logs → code → spec → ground truth.

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1.1 Add `spec` field to `#[instrument]` on `handle_attach_here()` in handlers.rs
  • ☑ 1.2 Add `spec` field to `#[instrument]` on `handle_stop()` in handlers.rs
  • ☑ 1.3 Add `spec` field to `#[instrument]` on `handle_destroy()` in handlers.rs
  • ☑ 1.4 Add `spec` field to key `info!`/`error!` events in handlers.rs (build, launch, security)
  • ☑ 1.5 Add `spec` field to key events in runner.rs (CLI attach, image build)
  • ☑ 1.6 Add `spec` field to key events in event_loop.rs (container state changes)
  • ☑ 1.7 Add `spec` field to key events in init.rs (first-run setup)
  • ☑ 2.1 Add `@trace` to src-tauri/src/handlers.rs module doc
  • ☑ 2.2 Add `@trace` to src-tauri/src/launch.rs module doc
  • ☑ 2.3 Add `@trace` to src-tauri/src/runner.rs module doc
  • ☑ 2.4 Add `@trace` to src-tauri/src/event_loop.rs module doc
  • ☑ 2.5 Add `@trace` to src-tauri/src/embedded.rs module doc
  • ☑ 2.6 Add `@trace` to src-tauri/src/secrets.rs module doc
  • ☑ 2.7 Add `@trace` to src-tauri/src/build_lock.rs module doc
  • ☑ 2.8 Add `@trace` to src-tauri/src/init.rs module doc
  • ☑ 2.9 Add `@trace` to src-tauri/src/desktop.rs module doc
  • ☑ 2.10 Add `@trace` to src-tauri/src/menu.rs module doc
  • ☑ 2.11 Add `@trace` to crates/tillandsias-podman/src/lib.rs module doc
  • ☑ 2.12 Add `@trace` to crates/tillandsias-podman/src/launch.rs module doc
  • ☑ 2.13 Add `@trace` to crates/tillandsias-scanner/src/lib.rs module doc
  • ☑ 2.14 Add `@trace` to crates/tillandsias-core/src/container_profile.rs module doc
  • ☑ 3.1 Add `@trace` to security flags block in src-tauri/src/launch.rs
  • ☑ 3.2 Add `@trace` to GPU passthrough block in src-tauri/src/launch.rs
  • ☑ 3.3 Add `@trace` to volume mount block in src-tauri/src/launch.rs
  • ☑ 3.4 Add `@trace` to FD sanitization block in crates/tillandsias-podman/src/lib.rs
  • ☑ 3.5 Add `@trace` to container profile definitions in crates/tillandsias-core/src/container_profile.rs
  • ☑ 3.6 Add `@trace` to image tag derivation in handlers.rs
  • ☑ 3.7 Add `@trace` to singleton guard in build_lock.rs
  • ☑ 3.8 Add `@trace` to keyring operations in secrets.rs
  • ☑ 3.9 Add `@trace` to embedded source extraction in embedded.rs
  • ☑ 3.10 Add `@trace` to tokio::select! event loop in event_loop.rs
  • ☑ 4.1 Add `@trace` to scripts/build-image.sh (nix-builder, default-image)
  • ☑ 4.2 Add `@trace` to build.sh (dev-build)
  • ☑ 4.3 Add `@trace` to scripts/bump-version.sh (versioning)
  • ☑ 4.4 Add `@trace` to scripts/fetch-debug-source.sh (knowledge-source-of-truth)
  • ☑ 5.1 Patch openspec-apply-change SKILL.md to instruct adding @trace on implementation
  • ☐ 6.1 Run `grep -r "@trace" --include="*.rs" --include="*.sh" | wc -l` to confirm coverage
  • ☐ 6.2 Run `./build.sh --check` to confirm no compilation impact
  • ☐ 6.3 Verify a sample log output includes the spec field

Read source record ↗

changeAppimage icon polishOpen tasks7 checked · 1 open

The AppImage shows a generic blue gear icon in file managers instead of our tillandsia. While the tray icon is now properly embedded (v0.1.37), the AppImage file itself and its desktop integration need polish to show the correct icon everywhere.

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1.1 Verify icon.png (256x256) has proper transparency and is high quality
  • ☑ 1.2 Add AppStream metadata file for software center integration
  • ☑ 1.3 Desktop-template.desktop Icon={{icon}} is correct — Tauri fills with binary name
  • ☑ 2.1 Verified: Tauri bundler automatically copies icon to AppDir root (.DirIcon + <name>.png)
  • ☑ 2.2 Verified: linuxdeploy.rs copies largest icon to AppDir/.DirIcon and <productName>.png
  • ☑ 2.3 Note: GNOME Nautilus does NOT read AppImage embedded icons — this is a GNOME limitation, not a bug
  • ☑ 3.1 cargo check passes
  • ☐ 3.2 Build release, download AppImage, verify icon when desktop-integrated

Read source record ↗

changeAudit remediationOpen tasks18 checked · 2 open

Full spec-vs-code audit revealed 5 critical divergences, 9 warnings, 7 orphaned code areas, and 6 forgotten specs. Trivial fixes applied immediately; this change tracks the remaining non-trivial items that need design decisions.

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1.1 Fix environment-runtime spec port ranges 100→20
  • ☑ 1.2 Fix --bash help text to say fish not bash
  • ☑ 1.3 Fix runner.rs git mount: directory mount + GIT_CONFIG_GLOBAL + :ro (align with handlers.rs)
  • ☑ 1.4 Fix runner.rs GitHub credentials mount: add :ro
  • ☑ 1.5 Fix stale tray-app spec: remove contradictory GitHub Login requirement
  • ☑ 1.6 Add GPU passthrough to tray-mode build_run_args (was missing, only CLI had it)
  • ☑ 2.1 SHA-pin all GitHub Actions in release.yml (checkout, setup-node, rust-toolchain, rust-cache, upload-artifact, download-artifact, gh-release)
  • ☑ 2b.1 Remove tracked `result` file (stale Nix build artifact) + add to .gitignore
  • ☑ 2b.2 Escape macOS osascript command string in open_terminal() (injection via crafted dir names)
  • ☑ 2b.3 Add --init to short-lived gh containers in github.rs and gh-auth-login.sh (consistency)
  • ☑ 2b.4 Build lock is_alive() — check process name, not just PID existence (match singleton pattern)
  • ☐ 2b.5 Consider flock() for atomic lock acquisition (singleton + build lock TOCTOU)
  • ☑ 3.1 Remove WASM isolation requirement from app-lifecycle spec (F4 — no WASM in codebase, aspirational)
  • ☑ 3.2 Remove or demote tray icon state switching to "future" (W2/F1 — SVGs exist but icon is static)
  • ☑ 3.3 Remove Start/Rebuild menu actions from artifact-detection spec (F3 — never implemented)
  • ☑ 3.4 Clarify destroy UX in app-lifecycle: server-side delay, not hold-button (W5)
  • ☐ 4.1 Wire Tauri updater to show "Update available (vX.Y.Z)" in tray menu (W7)
  • ☑ 4.2 Extract detect_host_os() to shared module (O5 — duplicated in handlers.rs and runner.rs)
  • ☑ 5.1 cargo clippy + fmt clean
  • ☑ 5.2 cargo test passes

Read source record ↗

changeSecrets architectureOpen tasks15 checked · 2 open

Tillandsias forge containers need access to credentials (GitHub tokens, SSH keys, git identity) to perform useful work — cloning private repos, pushing commits, authenticating with APIs. Today there is no defined strategy for how secrets enter containers, persist between runs, or stay protected from the AI agent and host-level exposure.

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1.1 Draft the Overview section: transparent secrets management philosophy, user-facing simplicity
  • ☑ 1.2 Define Secret Categories table: GitHub auth, git identity, SSH keys, project tokens with scope and storage paths
  • ☑ 1.3 Document Current Implementation (MVP): plain directory mounts, `gh auth` storage, git config path
  • ☑ 1.4 Propose Future Encrypted Secrets Filesystem: LUKS/gocryptfs options, keyring integration, mount/unlock flow
  • ☑ 1.5 Write Per-Project vs Shared Credentials analysis with recommendations
  • ☑ 1.6 Write Security Model threat/mitigation table
  • ☑ 1.7 Document Mount Strategy: host paths to container paths mapping
  • ☑ 1.8 Define Implementation Phases (Phase 1/2/3) with scope boundaries
  • ☑ 2.1 Write proposal.md: motivation, what changes, capabilities, impact
  • ☑ 2.2 Write design.md: context, goals/non-goals, key decisions
  • ☑ 2.3 Write specs/secrets-management/spec.md: requirements and scenarios
  • ☑ 2.4 Write tasks.md with document-writing tasks (not implementation tasks)
  • ☑ 3.1 Document Nix store encryption strategy in SECRETS.md — builder toolbox isolation, content-addressed verification, encrypted-at-rest for Phase 2
  • ☑ 3.2 Document build artifact chain of trust — embedded sources (Phase 1) → image hash verification → signed images (Phase 3)
  • ☑ 3.3 Document secrets cache encryption lifecycle — gocryptfs/LUKS, keyring integration, auto-unlock/lock tied to container lifecycle
  • ☐ 4.1 User reviews SECRETS.md and provides feedback
  • ☐ 4.2 Incorporate review feedback into SECRETS.md and spec

Read source record ↗

changeFix shell quote env varsOpen tasks4 checked · 2 open

Container launches from the tray fail on macOS 26+ because the `TILLANDSIAS_HOST_OS` env var value contains a space (`macOS 26.4`). When podman args are joined into a shell command string for `open_terminal()`, the space splits the value — podman interprets `26.4` as a separate image name argument and tries to pull `docker.io/library/26.4:latest`.

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1.1 Add `shell_quote()` function — `shell_quote_join()` exists in `launch.rs:272` (added in v0.1.105)
  • ☑ 1.2 Add `join_shell_args()` function — same as above, `shell_quote_join()` handles quoting
  • ☑ 1.3 Replace `podman_parts.join(" ")` calls — podman args are now passed as array (not joined string), shell quoting is only needed for terminal command display
  • ☑ 1.4 Fix debug display in `runner.rs:503` — already quotes args with spaces via `format!("'{a}'")`
  • ☐ 2.1 `./build-osx.sh --check` compiles clean
  • ☐ 2.2 Install and test container launch from tray on macOS 26.4

Read source record ↗

changeLayered tools overlayOpen tasks18 checked · 4 open

Every forge container launch downloads and installs OpenCode and OpenSpec from the internet before the user can begin work. On the Claude entrypoint, this means `npm install -g @anthropic-ai/claude-code` (large npm tree) plus `npm install -g @fission-ai/openspec`. On the OpenCode entrypoint, this means `curl | bash` for OpenCode plus the same npm install for

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ **P1-1**: Create `scripts/build-tools-overlay.sh` -- shell script that runs a temporary forge container to install Claude Code, OpenCode, and OpenSpec into an output directory. Must use matching container paths (`/home/forge/.tools/`) for npm prefix resolution. @trace spec:layered-tools-overlay
  • ☑ **P1-2**: Add `MountSource::ToolsOverlay` variant to `MountSource` enum in `crates/tillandsias-core/src/container_profile.rs`. Add `tools_overlay_dir: Option<PathBuf>` to `LaunchContext`. @trace spec:layered-tools-overlay
  • ☑ **P1-3**: Add tools overlay mount to `common_forge_mounts()` in `container_profile.rs`. Mount at `/home/forge/.tools` with `MountMode::Ro`. @trace spec:layered-tools-overlay
  • ☑ **P1-4**: Resolve `MountSource::ToolsOverlay` in `resolve_mount_source()` in `src-tauri/src/launch.rs`. Resolve to `cache_dir/tools-overlay/current`. Skip mount if path does not exist (graceful fallback). @trace spec:layered-tools-overlay
  • ☑ **P1-5**: Add `ensure_tools_overlay()` function to `src-tauri/src/handlers.rs`. On first launch: run builder script, write manifest, create symlink. On subsequent launches: validate symlink, check forge image version match. @trace spec:layered-tools-overlay
  • ☑ **P1-6**: Wire `ensure_tools_overlay()` into `ensure_enclave_ready()` / `ensure_infrastructure_ready()` call chain. @trace spec:layered-tools-overlay
  • ☑ **P1-7**: Modify `images/default/entrypoint-forge-claude.sh` -- check `/home/forge/.tools/claude/bin/claude` before calling `install_claude()`. @trace spec:layered-tools-overlay
  • ☑ **P1-8**: Modify `images/default/entrypoint-forge-opencode.sh` -- check `/home/forge/.tools/opencode/bin/opencode` before calling `ensure_opencode()`. @trace spec:layered-tools-overlay
  • ☑ **P1-9**: Modify `images/default/lib-common.sh` -- modify `install_openspec()` to check `/home/forge/.tools/openspec/bin/openspec` first. @trace spec:layered-tools-overlay
  • ☑ **P1-10**: Add `.manifest.json` read/write utilities to Rust code (serde struct, read from overlay dir, write after builder completes). @trace spec:layered-tools-overlay
  • ☑ **P1-11**: Validate npm path resolution -- confirm that `claude` and `openspec` binaries installed with `--prefix /home/forge/.tools/<tool>` inside the builder container resolve their node_modules correctly when mounted at the same path inside forge containers. @trace spec:layered-tools-overlay
  • ☑ **P1-12**: Add tests for overlay mount presence/absence in podman args (similar to existing `forge_has_no_project_dir_mount` test pattern). @trace spec:layered-tools-overlay
  • ☑ **P2-1**: Add version checking functions -- query npm registry for Claude Code and OpenSpec latest versions, query GitHub releases API for OpenCode latest version. @trace spec:layered-tools-overlay
  • ☑ **P2-2**: Add 24-hour rate limiting for version checks (stamp file at `tools-overlay/.last-update-check`). @trace spec:layered-tools-overlay
  • ☑ **P2-3**: Implement background overlay rebuild -- create new versioned directory, run builder, swap symlink atomically, clean up old versions (keep one rollback). @trace spec:layered-tools-overlay
  • ☑ **P2-4**: Wire background update check into post-launch flow (after container starts, spawn non-blocking task). @trace spec:layered-tools-overlay
  • ☑ **P2-5**: Add forge image version comparison to `ensure_tools_overlay()` -- trigger blocking rebuild if manifest's `forge_image` does not match current tag. @trace spec:layered-tools-overlay
  • ☑ **P2-6**: Add CLI/tray notification for overlay status (building, updated, up-to-date). @trace spec:layered-tools-overlay
  • ☐ **P3-1**: Test overlay on macOS with podman machine -- verify bind-mount through virtiofs, symlink behavior, npm path resolution. @trace spec:layered-tools-overlay
  • ☐ **P3-2**: Test overlay on Windows with podman machine -- verify bind-mount through 9p, symlink behavior inside WSL2 VM. @trace spec:layered-tools-overlay
  • ☐ **P3-3**: Test concurrent container launches sharing the same read-only overlay -- verify no contention or file locking issues. @trace spec:layered-tools-overlay
  • ☐ **P3-4**: Measure macOS virtiofs performance for node_modules reads -- determine if squashfs optimization is needed. @trace spec:layered-tools-overlay

Read source record ↗

changeAdd opencode web agentOpen tasks25 checked · 4 open

Terminal-based session launch is brittle across platforms: the recent Windows fix that suppressed stray console windows also disabled the Linux terminal path, leaving users without a working default. Rather than chase terminal emulator quirks across Linux, macOS, and Windows, we pivot to OpenCode's built-in web server and display it in an embedded Tauri webv

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1.1 Extend `SelectedAgent` in `crates/tillandsias-core/src/config.rs`: add `OpenCodeWeb` variant, update `as_env_str` (`"opencode-web"`), `from_str_opt`, `display_name` (`"OpenCode Web"`); flip `Default::default()` to `OpenCodeWeb`. Add `is_web()` helper. Update unit tests.
  • ☑ 1.2 Add new `ContainerType::OpenCodeWeb` variant (distinct from existing `ContainerType::Web` used by Serve Here) in `crates/tillandsias-core/src/state.rs`. Add `ContainerInfo::forge_container_name(project_name: &str) -> String` returning `tillandsias-<project>-forge`, plus `parse_forge_container_name(name: &str) -> Option<String>`. Do NOT rename or alter `ContainerType::Web` / `web_container_name` which belong to Serve Here. (Moved to Wave 2 — the Wave 1 agent left this undone because `ContainerType::Web` already existed.)
  • ☑ 1.3 Create `images/default/entrypoint-forge-opencode-web.sh` — copy from `entrypoint-forge-opencode.sh`, replace the final `exec opencode "$@"` block with `exec opencode serve --hostname 0.0.0.0 --port 4096`, drop the interactive banner, keep all setup (CA trust, git clone, OpenSpec init, OpenCode install). Add `# @trace spec:opencode-web-session, spec:default-image` header.
  • ☑ 1.4 Update `images/default/Containerfile` to COPY `entrypoint-forge-opencode-web.sh`, `chmod +x` it, and update `images/default/entrypoint.sh` dispatcher to route `opencode-web) exec /usr/local/bin/entrypoint-forge-opencode-web.sh "$@" ;;`.
  • ☑ 1.5 Add i18n strings `menu.stop` across 17 locale files (`locales/*.toml`). Agent dropdown labels stay hardcoded in Rust `display_name()`.
  • ☑ 1.6 Run `toolbox run -c tillandsias cargo check --workspace` — passes (after orchestrator added `forge_opencode_web_profile()` + 2 match arms as Wave 1 follow-ups).
  • ☑ 1.7 Orchestrator follow-up: add `forge_opencode_web_profile()` in `crates/tillandsias-core/src/container_profile.rs`; add `SelectedAgent::OpenCodeWeb` arms in `src-tauri/src/handlers.rs::forge_profile` and `src-tauri/src/runner.rs` agent dispatch.
  • ☑ 2.1 In `crates/tillandsias-podman/src/launch.rs` add `LaunchMode::Detached` (or equivalent flag on the launch options struct) that emits `-d` and omits `-i -t --rm`. Keep the existing interactive mode untouched.
  • ☑ 2.2 In `crates/tillandsias-podman/src/launch.rs` add `allocate_single_port(start: u16, end: u16)` returning one free host port in the ephemeral range (default 17000-17999). Include a unit test that two calls return distinct ports.
  • ☑ 2.3 In `crates/tillandsias-podman/src/launch.rs` extend publish-arg construction so that when the launch mode is web, the `-p` arg is `127.0.0.1:<host_port>:4096`. Unit test: assert the produced arg string starts with `"127.0.0.1:"` and contains no `"0.0.0.0"`.
  • ☑ 2.4 In `src-tauri/src/launch.rs` add a `forge_opencode_web_profile()` sibling to `forge_opencode_profile()` / `forge_claude_profile()` that sets env `TILLANDSIAS_AGENT=opencode-web`, picks the detached mode, requests the single-port publish, and targets the same forge image.
  • ☑ 2.5 Create new module `src-tauri/src/webview.rs` exposing `open_web_session(app: &AppHandle, project: &ProjectInfo, genus: &str, host_port: u16) -> tauri::Result<()>`. Uses `tauri::WebviewWindowBuilder::new(app, format!("web-{}-{}", project.name, epoch_ms), url::Url::parse(...)?)`, sets size 1200x800, title `"Tillandsias — {project} ({genus})"`. Wire module in `src-tauri/src/main.rs` via `mod webview;`.
  • ☑ 2.6 Health-wait helper in `src-tauri/src/webview.rs`: `wait_for_web_ready(host_port: u16, timeout: Duration)` pings `http://127.0.0.1:<host_port>/` with exponential backoff (1s/2s/4s/8s cap, total ~30s) before opening the window. Reuse existing health-check utilities from `tillandsias-podman` if available.
  • ☑ 2.7 Run `toolbox run -c tillandsias cargo check --workspace && cargo test -p tillandsias-podman` — must pass.
  • ☑ 3.1 In `src-tauri/src/handlers.rs` refactor `handle_attach_here` to branch on `global_config.agent.selected.is_web()`: web path calls new `handle_attach_web(...)`; else falls through to existing terminal path unchanged.
  • ☑ 3.2 Implement `handle_attach_web(...)` in `src-tauri/src/handlers.rs`:
  • ☑ 3.3 Add `handle_stop_project(project_path)` in `src-tauri/src/handlers.rs`: stops the project's web container via launcher, removes it from state, closes every `WebviewWindow` whose label starts with `web-<project>-`, releases the port.
  • ☑ 3.4 In `src-tauri/src/menu.rs` add the "OpenCode Web" Seedlings option first; update active-choice rendering. Add per-project "Stop" item rendered only when `TrayState::running` contains a `Web` container for that project. Add new `MenuCommand::StopProject` variant and wire through `src-tauri/src/event_loop.rs`.
  • ☑ 3.5 In `src-tauri/src/handlers.rs::shutdown_all()` ensure the existing loop over `state.running` stops web containers (the generic launcher.stop path already handles them once `ContainerType::Web` is recognised — verify no special casing is needed). Extend the orphan-sweep filter to match `tillandsias-*-forge` (likely already covered by the existing `tillandsias-` prefix filter — verify and document). Before stopping containers, call `app.webview_windows()` and close all whose label starts with `"web-"`.
  • ☑ 3.6 Add cheatsheet `docs/cheatsheets/opencode-web.md` — one-pager covering: default agent, port contract (127.0.0.1 only), persistent container, multi-webview semantics, Stop action, shutdown behaviour. Include `@trace spec:opencode-web-session`.
  • ☑ 3.7 Integration tests added (launch arg assertions in `src-tauri/src/launch.rs` tests; single-port allocator tests in `tillandsias-podman`) in `src-tauri/tests/` (or appropriate test crate) for: (a) launch arg assembly for web mode produces `-d`, no `--rm`, `127.0.0.1:P:4096`; (b) single-port allocator returns distinct ports.
  • ☑ 3.8 `cargo test --workspace` (252 passed, 1 pre-existing flake in isolation-only test) + `cargo clippy --workspace` (no new warnings).
  • ☑ 3.9 Rebuild forge image: `scripts/build-image.sh forge` — built `localhost/tillandsias-forge:latest`, new entrypoint confirmed present, dispatcher routes `opencode-web` correctly.
  • ☑ 3.10 Headless smoke test: launched `entrypoint-forge-opencode-web.sh` in a detached container with `-p 127.0.0.1:17500:4096`; confirmed `opencode serve` listens on 0.0.0.0:4096 inside, HTTP 200 returned on `http://127.0.0.1:17500/` after ~6s. Full UI-side smoke test (webview open, Stop, quit) deferred to user since the agent environment has no display.
  • ☑ 3.11 `./scripts/bump-version.sh --bump-changes` → v0.1.158.182. Committed on `linux-next` (commit `001edad`) with the @trace URL. Push deferred to after archive + merge-to-main.
  • ☐ 4.1 After successful local validation: merge `linux-next` to `main`.
  • ☐ 4.2 `./scripts/bump-version.sh --bump-build`, tag if convention requires.
  • ☐ 4.3 `gh workflow run release.yml -f version="<new_version>"` from `main`.
  • ☐ 4.4 `/opsx:verify add-opencode-web-agent` then `/opsx:archive add-opencode-web-agent`.

Read source record ↗

changeTray aware cli and webview lifecycleOpen tasks0 checked · 15 open

The v0.1.159.x attach flow works but several UX edges jar the user experience:

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☐ 1.1 Add `images/default/config-overlay/opencode/tui.json` containing `{ "$schema": "https://opencode.ai/tui.json", "theme": "tokyonight" }`. Register it in `src-tauri/src/embedded.rs` (new `CONFIG_OVERLAY_OPENCODE_TUI` const + a `write_lf` call inside `write_image_sources` for the same `images/default/config-overlay/opencode/` target dir). Verify the existing config-overlay symlinking in `lib-common.sh` already covers the new file (it should — the loop iterates everything in `~/.config-overlay/opencode/`).
  • ☐ 1.2 In `src-tauri/src/main.rs`, extend the `.run(...)` closure to filter `tauri::RunEvent::WindowEvent { label, event: tauri::WindowEvent::CloseRequested { .. }, .. }` and **return early** when `label.starts_with("web-")`. The existing `RunEvent::ExitRequested` handler stays exactly as-is. Add `// @trace spec:opencode-web-session, spec:app-lifecycle` near the new arm.
  • ☐ 1.3 Add a `BrokenPipeFilter` `MakeWriter` in `src-tauri/src/logging.rs` that wraps `std::io::stderr()`. Its `Write::write` impl returns `Ok(buf.len())` when the underlying error is `ErrorKind::BrokenPipe`; everything else passes through unchanged. Wire it into the existing stderr layer construction. Add a unit test that feeds a fake writer asserting BrokenPipe → silent success.
  • ☐ 2.1 Create `src-tauri/src/desktop_env.rs` exposing `pub fn has_graphical_session() -> bool`. Logic per design D3 (Linux DISPLAY/WAYLAND_DISPLAY env probe; macOS/Windows `true`; `TILLANDSIAS_NO_TRAY=1` env override → `false`). Add `mod desktop_env;` in main.rs. Add three unit tests covering the env-var permutations (use `temp_env::with_vars` if needed; otherwise scope env mutation manually with `unsafe`).
  • ☐ 2.2 Add `src-tauri/src/tray_spawn.rs` with `pub fn spawn_detached_tray()`. Linux/macOS: `Command::new(std::env::current_exe()?).env_remove("TILLANDSIAS_NO_TRAY").process_group(0).spawn()`. Windows: similar with `CREATE_NEW_PROCESS_GROUP | DETACHED_PROCESS` flags via `windows_sys`. Pass `--tray-only` (a new no-op flag, or rely on no-args being tray-mode — check existing `cli::parse()` semantics first; if no-args is already tray mode, no flag needed). Returns `Ok(())` on spawn success, swallows errors with a `warn!` (CLI must not fail because tray failed).
  • ☐ 2.3 In `src-tauri/src/main.rs`, before the CLI mode dispatch for `Attach` and inside the `Debug` mode (and any other CLI mode that should be tray-aware), call `if desktop_env::has_graphical_session() { tray_spawn::spawn_detached_tray(); }`. Confirm singleton guard handles "already running" silently. Add a friendly stdout line ("Tray launched in background — open the tray menu for project actions.") only the first time per session.
  • ☐ 2.4 In `src-tauri/src/runner.rs`, install a SIGINT handler at the start of `run()` (after enclave init). Use `tokio::signal::ctrl_c()` in a spawned task that, on first receive, prints "Stopping…", awaits a minimal `shutdown_all`-like cleanup (proxy/git/inference/cleanup_enclave_network), then `std::process::exit(0)`. A second SIGINT during cleanup falls through to default (use a `Once` or `AtomicBool` to guard).
  • ☐ 2.5 Verify `runner::run` exits 0 when the foreground podman child exits cleanly, and that no shutdown of infrastructure happens (the tray child owns infrastructure now). Print "OpenCode session ended — Tillandsias tray is still running." before returning.
  • ☐ 3.1 `toolbox run -c tillandsias cargo check --workspace && cargo test --workspace` — must be green, no new flakes.
  • ☐ 3.2 `./scripts/build-image.sh forge` — confirm new `tui.json` lands in the image.
  • ☐ 3.3 Reinstall locally: `./build.sh --install`.
  • ☐ 3.4 Smoke matrix on Linux:
  • ☐ 3.5 Bump VERSION monotonically (max(local build, remote build) + 1), update version on Cargo.toml/tauri.conf.json via `bump-version.sh`.
  • ☐ 3.6 Commit with @trace URLs, push to linux-next, fast-forward main, push, trigger `gh workflow run release.yml -f version=...`.
  • ☐ 3.7 Sync delta specs into main specs and `/opsx:archive tray-aware-cli-and-webview-lifecycle`.

Read source record ↗

changeDeprecate tauri webview for native browserOpen tasks1 checked · 23 open

Every problem we've hit with OpenCode Web in the last cycle came from the Tauri/WebKit2GTK webview layer:

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☐ Create `src-tauri/src/browser.rs`:
  • ☐ Add unit tests:
  • ☐ Delete `src-tauri/src/webview.rs` entirely.
  • ☐ `src-tauri/src/main.rs`:
  • ☐ `src-tauri/src/handlers.rs::handle_attach_web`:
  • ☐ Grep the codebase for any remaining `WebviewWindow` / `webview::`
  • ☐ `Cargo.toml`: remove any webview-only features/plugins no longer used.
  • ☐ `images/default/sse-keepalive-proxy.js`:
  • ☐ Add proxy tests via a tiny Node smoke script (host-side, no podman
  • ☑ `openspec/changes/deprecate-tauri-webview-for-native-browser/` —
  • ☐ `openspec validate --strict deprecate-tauri-webview-for-native-browser` passes.
  • ☐ New `docs/cheatsheets/native-browser-launch.md`: detection order,
  • ☐ Update `docs/cheatsheets/opencode-web.md`: "no longer uses Tauri
  • ☐ Update the memory at
  • ☐ `./build.sh --check` clean.
  • ☐ `./build.sh --test` all green (new unit tests pass).
  • ☐ `./build.sh --release --install` rebuilds AppImage.
  • ☐ Rebuild forge image (new proxy JS baked in).
  • ☐ Launch tray. Click Attach Here on a committed-repo project.
  • ☐ Verify: app-mode window opens in the detected browser; URL is
  • ☐ `/opsx:verify deprecate-tauri-webview-for-native-browser`.
  • ☐ `/opsx:archive deprecate-tauri-webview-for-native-browser`.
  • ☐ Bump version via `./scripts/bump-version.sh --bump-changes`.
  • ☐ Commit with `@trace spec:opencode-web-session` + GitHub search URL.

Read source record ↗

changeFix tray exit webview lifecycleOpen tasks14 checked · 7 open

Regression discovered after `tray-aware-cli-and-webview-lifecycle` landed: the tray quit path and the webview close path both leave containers running and the enclave network attached, contrary to the specs already in `openspec/specs/app-lifecycle/` and `openspec/specs/opencode-web-session/`.

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ `src-tauri/src/main.rs` (tray `on_menu_event` closure, ~line 279): replace the
  • ☑ Keep the `info!("Quit requested")` log line for the audit trail.
  • ☑ `src-tauri/src/event_loop.rs`: add `app_handle: tauri::AppHandle<tauri::Wry>` as
  • ☑ In `MenuCommand::Quit` arm: after `handlers::shutdown_all(&state).await;`, call
  • ☑ `src-tauri/src/main.rs`: pass the cloned `AppHandle` into `event_loop::run`.
  • ☑ `src-tauri/src/main.rs` (`.run(...)` closure, ~line 895): replace the unconditional
  • ☑ Remove the inline `rt.block_on(async { stop_inference(); stop_proxy();
  • ☑ Add `@trace spec:app-lifecycle` on both match arms.
  • ☑ Keep the existing early `return;` for `web-*` `WindowEvent::CloseRequested`. No
  • ☑ SKIPPED — the discriminator is an inline `if code.is_none()` branch. A dedicated
  • ☑ `./build.sh --check` — expect clean compile.
  • ☑ `./build.sh --test` — expect all existing tests pass plus the new one.
  • ☑ `cargo clippy --workspace -- -D warnings` if clippy is part of the project gate.
  • ☑ `./build.sh --release --install` produces and installs `~/Applications/Tillandsias.AppImage`.
  • ☐ USER: launch the AppImage. Attach Here on a project in web mode. Verify `podman
  • ☐ USER: close the webview window. Verify: tray icon still responds to menu clicks;
  • ☐ USER: click tray Quit. Verify: `podman ps --filter name=tillandsias-` is empty;
  • ☐ USER: re-launch and repeat — enclave network is freshly created, no "previous
  • ☐ Run `/opsx:verify fix-tray-exit-webview-lifecycle` — confirm no divergence.
  • ☐ Archive via `/opsx:archive fix-tray-exit-webview-lifecycle`.
  • ☐ `./scripts/bump-version.sh --bump-changes` and commit with the

Read source record ↗

changeMirror to host auto syncOpen tasks21 checked · 7 open

Right now the enclave mirror is the authoritative recipient of every forge push — commits land there first, then the git-service container's post-receive + startup retry-push forwards them to GitHub. But the user's host working copy at `<watch_path>/<project>` never hears about those commits unless the user manually `git pull`s. Side effects:

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ `src-tauri/src/mirror_sync.rs` — `sync_project(project, mirror, host)`
  • ☑ Uses `git -C <host> symbolic-ref --short HEAD`, `git status --porcelain`,
  • ☑ `sync_all_projects(mirrors_root, watch_paths)` iterates
  • ☑ Called from `main.rs` setup closure after the initial project scan
  • ☑ `spawn_watcher(mirrors_root, watch_paths)` uses
  • ☑ Watches the mirrors root recursively.
  • ☑ Filters to `refs/heads/*`, `packed-refs`, `HEAD`, `FETCH_HEAD`
  • ☑ 500 ms per-project debounce to coalesce the burst of FS events a
  • ☑ Armed on tray startup; survives for tray lifetime.
  • ☑ `notify = { workspace = true }` added to `src-tauri/Cargo.toml`.
  • ☑ `shutdown_all()` in `handlers.rs` runs `sync_all_projects` before
  • ☑ `event_loop.rs` — after a Forge / OpenCodeWeb / Maintenance
  • ☑ `already_up_to_date_returns_noop`
  • ☑ `mirror_advances_fast_forwards_host`
  • ☑ `dirty_host_is_skipped`
  • ☑ `diverged_host_is_skipped_not_force_merged`
  • ☑ `absent_host_returns_host_absent`
  • ☑ `absent_mirror_returns_mirror_absent`
  • ☑ `./build.sh --check` clean.
  • ☑ `./build.sh --test` — all sync tests green (6 of 6).
  • ☐ `./build.sh --release --install` produces an AppImage including the
  • ☐ Launch tray. Attach Here on a committed-repo project you have
  • ☐ Ask the agent to add a file and push. Watch your terminal:
  • ☐ Make your host working tree dirty (`echo x > ~/src/java/scratch`).
  • ☐ Commit your scratch file locally without pushing. Push from the
  • ☑ `openspec validate --strict mirror-to-host-auto-sync` passes.
  • ☐ After user smoke test: `openspec archive -y mirror-to-host-auto-sync`.
  • ☐ Bump version via `./scripts/bump-version.sh --bump-changes`.

Read source record ↗

changeOpencode web config isolationOpen tasks7 checked · 9 open

Live-session diagnosis of OpenCode Web running inside a forge container showed multiple violations of the spec's per-container isolation contract. The first draft of this change over-corrected by restricting providers to ollama only; the user revised the direction to **additive**: preserve every default OpenCode capability, add ollama + enclave-specific conf

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ `images/default/config-overlay/opencode/config.json`: replace
  • ☑ Add `enabled_providers: ["ollama"]`.
  • ☑ Add `default_agent: "build"`.
  • ☑ `images/default/entrypoint-forge-opencode-web.sh`: after the tools
  • ☑ `src-tauri/src/webview.rs`: remove the `#[cfg(debug_assertions)]` gate
  • ☑ `./build.sh --check` passes.
  • ☐ `./build.sh --release --install` produces a new AppImage and forge
  • ☐ New proxy image unchanged (no allowlist touch in this change).
  • ☐ Launch the new AppImage. Attach a project in web mode.
  • ☐ Press F12 in the OpenCode Web window — inspector must open.
  • ☐ In a shell: `curl http://127.0.0.1:<port>/config | jq
  • ☐ In a shell: `curl http://127.0.0.1:<port>/config/providers | jq
  • ☐ UI loads with dark theme immediately (no light-theme flash).
  • ☑ `openspec validate opencode-web-config-isolation` passes.
  • ☐ `/opsx:verify opencode-web-config-isolation` after smoke test.
  • ☐ `/opsx:archive opencode-web-config-isolation`, bump version, commit with

Read source record ↗

changeOpencode web proxy transparentOpen tasks11 checked · 4 open

OpenCode Web hangs after one or two prompts inside a forge container. Proxy logs captured from a live session show three independent failure modes all traced back to proxy configuration:

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ `crates/tillandsias-core/src/container_profile.rs` — in `forge_profile()`,
  • ☑ `crates/tillandsias-core/src/container_profile.rs` — in
  • ☑ `images/proxy/allowlist.txt` — add three lines under the AI/ML section:
  • ☑ `src-tauri/src/launch.rs` tests that assert the exact NO_PROXY string
  • ☑ Run `scripts/build-image.sh proxy` to pick up the new allowlist.
  • ☑ Confirm the produced image tag is `tillandsias-proxy:v<FULL_VERSION>`.
  • ☑ No forge image rebuild required; profile changes take effect on next
  • ☑ `docs/cheatsheets/opencode-proxy-egress.md` — new cheatsheet documenting:
  • ☑ Run `./build.sh --test` (all tests pass).
  • ☑ New AppImage installed to `~/Applications/Tillandsias.AppImage` via
  • ☐ USER: launch the AppImage, attach a project in web mode, send 3 prompts.
  • ☐ USER: tail the proxy log during the session:
  • ☑ `openspec validate opencode-web-proxy-transparent` — passes.
  • ☐ Run `/opsx:verify opencode-web-proxy-transparent` — confirm convergence.
  • ☐ Archive via `/opsx:archive opencode-web-proxy-transparent`, bump changes with

Read source record ↗

changeSimplified tray uxOpen tasks25 checked · 14 open

The current tray menu has accreted complexity that no longer matches how Tillandsias is actually used:

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ Proposal documenting the menu redesign + state machine.
  • ☑ Spec delta for `tray-app`.
  • ☐ Validate strict.
  • ☑ `crates/tillandsias-core/src/event.rs`: drop `SelectAgent`,
  • ☑ Add `Launch { project_path }` (replaces ServeHere from tray-side).
  • ☑ Add `MaintenanceTerminal { project_path }`.
  • ☑ Add `IncludeRemoteToggle { include: bool }`.
  • ☑ `event_loop.rs` arms updated; removed variants delete their match arms.
  • ☑ New `menu::TrayMenu` struct holding handles to every pre-built item.
  • ☑ `TrayMenu::new()` builds the static skeleton at app start.
  • ☑ `TrayMenu::set_stage(Stage)` toggles `enabled`/text per stage.
  • ☑ `TrayMenu::update_projects(local, remote)` rebuilds the Projects ▸
  • ☑ Remove `rebuild_menu()` callers that don't need a structural rebuild.
  • ☑ `handle_attach_web` reused via `MenuCommand::Launch` from the tray.
  • ☑ If a forge is already running for the project, the click reopens a
  • ☑ Browser window URL: `http://<project>.opencode.localhost/`
  • ☑ Tear-down: only `shutdown_all` stops the forge.
  • ☑ `handle_maintenance_terminal(project_path)`: spawn a host terminal
  • ☑ Multiple maintenance terminals against the same forge are allowed.
  • ☑ Falls back gracefully if the forge isn't running (offers Launch first).
  • ☑ `src-tauri/src/github_health.rs`:
  • ☑ Cached for tray process lifetime; re-runs on user-initiated
  • ☑ Drives `Stage` selection in the new state machine.
  • ☐ `pre_ui_cleanup_stale_containers()` in handlers, called from
  • ☐ Forces enclave network removal too (so a stale tray's network is
  • ☐ `tokio::sync::CancellationToken` per long-running spawn (image
  • ☐ `event_loop.rs` adds `biased;` to its `tokio::select!`.
  • ☐ On Quit, all tokens fire abort.
  • ☐ Long-running `tokio::process::Command::output()` calls get a
  • ☑ Unit test: stage state machine transitions — `stage_visibility_table_matches_spec`
  • ☑ Unit test: `credential_health_to_stage_mapping` and
  • ☐ Unit test: `update_projects` rebuilds only when project set changes.
  • ☐ Smoke test: kill `gh auth` token → relaunch → see `Sign in to GitHub`.
  • ☐ Smoke test: network off → see `(GitHub unreachable, using cached
  • ☐ Smoke test: 30s fake forge build + Quit click → exits within 5s.
  • ☑ `docs/cheatsheets/tray-state-machine.md` documenting the five
  • ☐ `/opsx:verify` strict.
  • ☐ `/opsx:archive simplified-tray-ux`.
  • ☐ Archive `tray-responsiveness-and-startup-gating` as superseded

Read source record ↗

changeSubdomain routing via reverse proxyOpen tasks17 checked · 5 open

When a coding agent inside the forge launches a dev server (e.g., `flutter run -d web-server`, `vite`, `next dev`, `opencode serve`), it prints something like `Server running at http://localhost:8080`. That URL is meaningless to the human:

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ Spec proposal + delta specs.
  • ☑ `images/default/config-overlay/opencode/instructions/web-services.md`.
  • ☑ Wire instruction file into `opencode/config.json` instructions list.
  • ☑ Embed in `src-tauri/src/embedded.rs` (const + write call).
  • ☑ `images/router/Containerfile` based on Caddy 2.x.
  • ☑ `images/router/base.Caddyfile` with default 404 + remote_ip allowlist.
  • ☑ `images/router/entrypoint.sh` merging base + dynamic Caddyfiles.
  • ☑ `images/router/router-reload.sh` for `caddy reload` after dynamic edits.
  • ☑ `crates/tillandsias-core/src/container_profile.rs`: `router_profile()`.
  • ☑ `src-tauri/src/handlers.rs`: `ensure_router_running()` + `stop_router()`.
  • ☑ Bind-mount the dynamic Caddyfile at `/run/router/dynamic.Caddyfile`.
  • ☑ Squid `squid.conf` adds `acl localhost_subdomain dstdomain .localhost`,
  • ☑ Host-side bind `127.0.0.1:80` only — never `0.0.0.0:80`.
  • ☑ Wire into `ensure_infrastructure_ready` (after proxy) and `shutdown_all`.
  • ☑ `scripts/build-image.sh` recognises `router` image name.
  • ☑ Embed router source files in `src-tauri/src/embedded.rs`.
  • ☑ Smoke-tested locally: 404 on unknown hosts, dynamic route injection
  • ☐ OpenCode Web migrates URL to `<project>.opencode.localhost`.
  • ☐ Flutter web instruction wires `--web-hostname 0.0.0.0 --web-port 8080`.
  • ☐ Vite/Next/Storybook/Jupyter/Streamlit instructions added.
  • ☐ Remove the random-port `<project>.localhost:<port>` browser launch.
  • ☐ Migrate `opencode-web-session` spec to require the new URL exclusively.

Read source record ↗

changeTray responsiveness and startup gating supersededOpen tasks1 checked · 28 open

Live issue report (2026-04-24, v0.1.160.212): after a cold launch, the tray became fully unresponsive. Root cause: a `build-tools-overlay.sh` subprocess spawned a podman container to compile the forge tools overlay, and the entire event loop waited on its completion. Symptoms:

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☐ Grep `src-tauri/src/event_loop.rs` + `src-tauri/src/handlers.rs`
  • ☐ Add `biased;` to the main `tokio::select!` in `event_loop::run`
  • ☐ Introduce a `tokio::sync::CancellationToken` per long-running task
  • ☐ Wrap every `tokio::process::Command::output()` in
  • ☐ New function `handlers::pre_ui_cleanup_stale_containers()`.
  • ☐ Call it from `main.rs` setup, spawned off the event loop, with
  • ☐ Log as accountability event: count of removed containers +
  • ☐ `TrayState::ready_stage: Stage` with variants:
  • ☐ `menu::build_tray_menu` renders every item always; gates the
  • ☐ Tooltips describing what each disabled item is waiting for.
  • ☐ New module `src-tauri/src/github_health.rs`:
  • ☐ Wire into `TrayState::credential_health: CredentialHealth` with
  • ☐ Menu integration: when `Authenticated`, project lists enabled;
  • ☐ Responsiveness canary: spawn a 30s fake-blocking task, dispatch
  • ☐ Credential classifier unit tests covering each matrix cell.
  • ☐ Stale-cleanup test: create a throwaway container named
  • ☐ Existing mirror-sync + browser tests stay green.
  • ☑ Proposal + deltas in
  • ☐ Validates strict.
  • ☐ New cheatsheet
  • ☐ `./build.sh --check` clean.
  • ☐ `./build.sh --test` all green incl. new tests.
  • ☐ `./build.sh --release --install` rebuild.
  • ☐ Smoke test: kill `gh auth` (remove token from keyring), relaunch
  • ☐ Smoke test: start tray with network off — `GithubUnreachable`
  • ☐ Smoke test: while a long forge image build is in flight, click
  • ☐ `/opsx:verify tray-responsiveness-and-startup-gating`.
  • ☐ `/opsx:archive tray-responsiveness-and-startup-gating`.
  • ☐ Bump version, commit, push, release.

Read source record ↗

changeappimage-builder-source-slimOpen tasks0 checked · 12 open

The AppImage build script (`build.sh --install`) copies the entire workspace into the Ubuntu builder container before invoking `cargo tauri build`. On this developer's machine the copy step takes ~5 minutes, dominated by a **47 GB `target/` directory** and a **1.5 GB `.git/` directory** that the builder never reads — its own `target/` is built fresh from sou

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☐ 1.1 Declare `BUILDER_COPY_EXCLUDES=(./target ./.git ./.nix-output ./.claude ./.opencode ./node_modules './*.AppImage')` near the top of `build.sh` so callers can grep for it.
  • ☐ 1.2 Replace `cp -r /src /build` with the streaming `tar … | tar` pipe documented in design.md, building the `--exclude=…` flags from `${BUILDER_COPY_EXCLUDES[@]}`.
  • ☐ 1.3 Add `_assert_copy_under_150mb` helper: runs `du -sb /build`, compares against 157286400 (150 × 1024 × 1024), aborts the build with `du -sh /build/* | sort -hr | head -3` on overrun.
  • ☐ 1.4 Verify `tar` is the only tool used inside the container — no new `apt-get install` lines added.
  • ☐ 2.1 With a warm cargo cache and the existing 47 GB `target/`, run `./build.sh --install` and confirm the source-copy step takes <30 seconds (vs ~5 minutes before).
  • ☐ 2.2 Confirm the produced AppImage launches the tray (smoke test: `--version` returns the new version).
  • ☐ 2.3 Confirm `/build/target` is built fresh inside the container — not the host's 47 GB tree.
  • ☐ 3.1 Add `cheatsheets/utils/tar.md` with provenance citing
  • ☐ 3.2 Update `cheatsheets/INDEX.md` (run
  • ☐ 4.1 `openspec validate appimage-builder-source-slim --strict` is green.
  • ☐ 4.2 `/opsx:archive` after the implementation is verified.
  • ☐ 4.3 Bump `--bump-changes`.

Read source record ↗

changeCheatsheet tooling and mcpOpen tasks0 checked · 24 open

`cheatsheet-shortcomings.md` enumerated 10 first-hand gaps in the v2 cheatsheet system. 7 of them are tooling absences:

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☐ 1.1 `crates/tillandsias-cheatsheet-mcp/Cargo.toml` — new bin crate. Deps: `serde`, `serde_yaml`, `tokio` (for async stdio), `tracing`. Avoid heavy frameworks; the JSON-RPC layer is tiny enough to hand-roll.
  • ☐ 1.2 `src/main.rs` — stdio JSON-RPC loop. Read line-delimited JSON from stdin, dispatch to handlers, write response to stdout.
  • ☐ 1.3 `src/index.rs` — walk `cheatsheets/`, parse frontmatter via `serde_yaml`, build in-memory index. Re-index on SIGHUP for dev convenience.
  • ☐ 1.4 `src/tools/search.rs`, `tools/get.rs`, `tools/related.rs`, `tools/list.rs`, `tools/stale_check.rs` — one file per tool.
  • ☐ 1.5 Tests against fixture cheatsheets in `tests/fixtures/`.
  • ☐ 2.1 `scripts/regenerate-cheatsheet-index.sh` — bash, walks frontmatter via `awk`/`yq`, regenerates INDEX.md.
  • ☐ 2.2 `scripts/check-cheatsheet-refs.sh` — bash + ripgrep. Fails non-zero on broken `@cheatsheet` / `## See also` paths.
  • ☐ 2.3 `scripts/check-cheatsheet-provenance-reachability.sh` — bash + curl. Logs failures to a file, exits 0 (don't fail CI on transient URL outages).
  • ☐ 2.4 `scripts/check-cheatsheet-staleness.sh` — bash + awk. Default informational; `--strict` flag for non-zero exit.
  • ☐ 3.1 Decide hook framework: bare `.git/hooks/pre-commit` shell script vs `lefthook` vs `pre-commit`. Recommend bare for simplicity.
  • ☐ 3.2 Wire scripts/regenerate-cheatsheet-index.sh + scripts/check-cheatsheet-refs.sh as pre-commit. Provenance + staleness run separately on a slower cadence.
  • ☐ 4.1 `images/default/Containerfile` — copy `tillandsias-cheatsheet-mcp` binary into `/opt/agents/cheatsheet-mcp/bin/`. The host build script needs to build the binary first and stage it into the image build context (similar to how `cheatsheets/` is staged).
  • ☐ 4.2 Update `images/default/opencode.json` (or wherever opencode config is baked) to register the MCP server.
  • ☐ 4.3 Update `cheatsheets/agents/opencode.md` (DRAFT) to mention the MCP server is available — when retrofitted with provenance, this becomes the canonical reference.
  • ☐ 5.1 Document in this change's README how a host user adds the MCP server to `~/.claude/settings.json` for dogfooding. (Out of repo — manual user step.)
  • ☐ 5.2 If the host tray exposes a control socket (Implementation D), wire the MCP binary there too.
  • ☐ 6.1 Update `cheatsheets/runtime/cheatsheet-architecture-v2.md` — replace the "MCP query interface (planned)" section with current behaviour.
  • ☐ 6.2 Update `cheatsheets/runtime/cheatsheet-shortcomings.md` — strike items 1, 2, 3, 4, 5, 7, 9, 10. Item 6 remains; item 8 deferred.
  • ☐ 6.3 Add `cheatsheets/agents/cheatsheet-mcp.md` (with provenance) covering the tool surface — for both forge agents AND host Claude.
  • ☐ 7.1 `cargo build --release -p tillandsias-cheatsheet-mcp` — clean build, single-binary.
  • ☐ 7.2 Smoke test: `echo '{"method":"cheatsheet.search","params":{"query":"rxjava"},"id":1}' | tillandsias-cheatsheet-mcp` — returns ranked results.
  • ☐ 7.3 `cargo test --workspace` — green.
  • ☐ 7.4 `scripts/build-image.sh forge --force` — confirm MCP binary baked into the new forge image.
  • ☐ 7.5 Manual: launch tray, attach to a project, exec into the forge, run `which cheatsheet-mcp` AND `cheatsheet.search "rxjava"` via the opencode tool surface.

Read source record ↗

changeFix router loopback portOpen tasks0 checked · 12 open

The router container is published at `127.0.0.1:80:80` (`src-tauri/src/handlers.rs:902`). Rootless podman cannot bind ports below `net.ipv4.ip_unprivileged_port_start` (default `1024` on Fedora and most distros), so the router's port-80 publish silently fails when the tray runs as a non-privileged user. The browser opens `http://<project>.opencode.localhost/

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☐ 1.1 In `src-tauri/src/handlers.rs`, change the router publish line (currently `127.0.0.1:80:80` at ~`handlers.rs:902`) to `127.0.0.1:8080:80`. Add inline comment citing rootless podman + `ip_unprivileged_port_start`.
  • ☐ 1.2 In `src-tauri/src/browser.rs::build_subdomain_url`, change the formatted URL from `http://{host_label}.opencode.localhost/` to `http://{host_label}.opencode.localhost:8080/`. Update the doc-comment to explain the port.
  • ☐ 2.1 Update `browser.rs::tests::build_subdomain_url_no_ip_no_bare_localhost_no_port` — invert the no-port assertion to require `:8080/`. Rename to `build_subdomain_url_uses_rootless_port_8080`.
  • ☐ 2.2 Update `build_subdomain_url_has_opencode_subdomain_no_port_no_path` to assert exact equality `http://thinking-service.opencode.localhost:8080/`.
  • ☐ 2.3 Update `build_subdomain_url_lowercases_mixed_case_project` and `build_subdomain_url_sanitizes_invalid_label_chars` to expect the `:8080` suffix.
  • ☐ 3.1 Search `docs/cheatsheets/` for any `opencode.localhost` URL examples; update to `:8080`.
  • ☐ 3.2 Same for `cheatsheets/agents/opencode.md` and `cheatsheets/runtime/networking.md` — add a one-line note about the `:8080` host port.
  • ☐ 4.1 `cargo check --workspace` — clean.
  • ☐ 4.2 `cargo test -p tillandsias --bin tillandsias browser` — updated tests green.
  • ☐ 4.3 `./build.sh --install` — install the new build locally.
  • ☐ 4.4 Manual: launch tray, attach a project, confirm browser opens `http://<project>.opencode.localhost:8080/` and the page loads.
  • ☐ 4.5 Manual: `podman ps` shows `tillandsias-router` with port mapping `127.0.0.1:8080->80/tcp`. `curl -fsS http://<project>.opencode.localhost:8080/ -o /dev/null -w '%{http_code}\n'` returns a non-error code.

Read source record ↗

changeForge cache architectureOpen tasks0 checked · 26 open

The forge container today exports `CARGO_HOME=~/.cache/tillandsias/cargo`, `GOPATH=~/.cache/tillandsias/go`, etc. — but the directory those env vars point at is **never bind-mounted**. Every container restart re-downloads every package the agent installs. Maven, Gradle, Flutter pub-cache, yarn, pnpm, and uv don't even have env vars set, so they default to ep

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☐ 1.1 In `crates/tillandsias-core/src/container_profile.rs`, add two `MountSource` variants: `SharedCache` and `ProjectCache`. Resolution: `SharedCache` resolves to `~/.cache/tillandsias/forge-shared/nix-store/` (host) → `/nix/store/` (forge) `:ro`. `ProjectCache` resolves to `~/.cache/tillandsias/forge-projects/<project>/` (host) → `/home/forge/.cache/tillandsias-project/` (forge) `:rw`. Project name passed via launch context.
  • ☐ 1.2 Extend `common_forge_mounts()` to add both mounts to every forge profile (opencode-web, terminal, claude, opencode-non-web).
  • ☐ 1.3 In `src-tauri/src/handlers.rs::handle_attach_*`, ensure `~/.cache/tillandsias/forge-projects/<project>/` exists with mode 0700 owned by host UID before container start. Same for `~/.cache/tillandsias/forge-shared/nix-store/` (no project name).
  • ☐ 1.4 Add `--security-opt=label=disable` or appropriate SELinux relabel flag for the two new mounts so SELinux doesn't deny access on Fedora hosts. Pattern matches existing mounts.
  • ☐ 2.1 In `images/default/lib-common.sh`, add a new export block setting all 12 per-language env vars listed in spec §Per-language env vars table to `/home/forge/.cache/tillandsias-project/<lang>/`.
  • ☐ 2.2 Tombstone the prior exports (`@tombstone superseded:forge-cache-architecture` — kept three releases until 0.1.169.232).
  • ☐ 2.3 Ensure each subdirectory exists at first use — entrypoint `mkdir -p` for each before launching the agent. (The mount creates the parent; subdirs are tools' responsibility but a defensive mkdir in the entrypoint avoids tool-specific bugs.)
  • ☐ 3.1 New module `crates/tillandsias-core/src/download_telemetry.rs` exporting `pub fn log_download(...)` matching the spec field set. Wraps `tracing::info!` with the canonical fields.
  • ☐ 3.2 Audit existing download callsites in `src-tauri/src/handlers.rs`, `src-tauri/src/updater.rs`, `crates/tillandsias-podman/src/launch.rs`, image-build subprocess invocations. Replace ad-hoc logging with the canonical helper.
  • ☐ 3.3 In `images/inference/entrypoint.sh`, wrap `ollama pull` invocations to emit a parseable log line. (Bash, not Rust — use `printf '{"category":"download",...}\n'` to stdout, host parses.)
  • ☐ 3.4 Forge-side downloads (cargo, npm, etc.) are tool-managed — we don't intercept. But the `--download-stats` parser SHALL use a heuristic: any file written to a `forge-projects/<project>/` subdir whose mtime is between container start and the next stop counts as a download with `source="forge:<project>"`. Implement that heuristic in §4.
  • ☐ 4.1 In `src-tauri/src/main.rs`, add `--download-stats [--since=<duration>]` argument parsing. Out-of-band of the tray (so `tillandsias --download-stats` runs as a one-shot, no tray spawning).
  • ☐ 4.2 Implement the report: parse the accountability log file (already-existing path under `~/.cache/tillandsias/`), filter `category="download"`, aggregate by source / reason / day. Plain text output to stdout.
  • ☐ 4.3 If zero downloads in window, output `"no downloads in last <duration> ✓"` and exit 0.
  • ☐ 5.1 Write `cheatsheets/runtime/forge-paths-ephemeral-vs-persistent.md` — definitive path-category table, when each survives, anti-patterns flagged. Provenance: this OpenSpec change ID + project Containerfile + lib-common.sh.
  • ☐ 5.2 Write `cheatsheets/runtime/forge-shared-cache-via-nix.md` — why nix is the right shared-cache entry, content-addressed conflict-freedom, what the forge sees vs what it doesn't. Provenance: nix.dev + nixos.org manual + this spec.
  • ☐ 6.1 Write `images/default/config-overlay/opencode/instructions/cache-discipline.md` — opencode first-turn instruction. Tells the agent: shared cache is RO; per-project cache is RW; project workspace is for source only; build artifacts NEVER go in the workspace; use the env vars.
  • ☐ 6.2 Update `images/default/config-overlay/opencode/config.json` (or wherever instruction list lives) to include the new file in opencode's auto-loaded instruction set.
  • ☐ 6.3 Add a new section to `~/src/tillandsias/CLAUDE.md` codifying the dual-cache + ephemeral path model so host Claude (me) and project contributors share the same mental model.
  • ☐ 7.1 `cargo check --workspace` clean.
  • ☐ 7.2 `cargo test --workspace --lib` plus `cargo test -p tillandsias --bin tillandsias` — all green. New tests for the mount path resolution + the env var generation in `lib-common.sh` (smoke-tested via expect).
  • ☐ 7.3 `scripts/build-image.sh forge --force` — re-bake the forge image with the new `lib-common.sh`.
  • ☐ 7.4 Manual: launch tray, attach to a project, run `cargo new foo && cd foo && cargo build` — confirm `target/` lives under `~/.cache/tillandsias/forge-projects/<project>/cargo/target/` (host) and NOT under the project workspace.
  • ☐ 7.5 Manual: stop the forge container, attach again, run `cargo build` (no source changes) — confirm zero crate downloads (cache hit).
  • ☐ 7.6 Manual: launch a SECOND project's forge concurrently, confirm via `ls /home/forge/.cache/tillandsias-project/` that ONLY this project's cache is visible.
  • ☐ 7.7 Run `tillandsias --download-stats --since=1h` after the manual flow — confirm the log was parsed and the report is non-empty.

Read source record ↗

changeForge environment discoverabilityNo checklist0 checked · 0 open

Per `~/src/java/ENVIRONMENT_REPORT.md`: a model attempting to use the forge concluded "Java/JDK: Not installed in PATH … Maven Central URLs return 404/403 … RxJava JAR must be committed to repository." The forge actually ships `java-21-openjdk-devel`, `maven`, `gradle`, `flutter`, `cargo`, `rustup`, `go`, `python3`, `nodejs`, `pipx`, and 30+ utilities — but

No task record was present in this archived change.

Read source record ↗

changeForge headless browsersOpen tasks0 checked · 7 open

The forge image today ships no browsers. Agents trying to run Selenium / Playwright / WebDriver tests inside the forge fail at the first `chromedriver: program not found` step. Per `cheatsheets/test/selenium.md` (DRAFT) and `cheatsheets/test/playwright.md` (DRAFT) the workaround was either per-project install (heavy) or a sidecar container — both of which br

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☐ 1.1 Add `chromium-headless`, `firefox`, `chromedriver` to the existing `microdnf install -y \ ...` layer in `images/default/Containerfile` (after the `protobuf-compiler protobuf-devel xmlstarlet libxml2` group added by `agent-source-of-truth`).
  • ☐ 1.2 After the yq+grpcurl curl-and-extract block (already in the Containerfile), add a new layer that fetches `geckodriver-v0.36.0-linux64.tar.gz` from `https://github.com/mozilla/geckodriver/releases/download/v0.36.0/geckodriver-v0.36.0-linux64.tar.gz`, extracts to `/usr/local/bin/geckodriver` (with `--no-same-owner`), `chmod +x`, and verifies `--version`.
  • ☐ 2.1 In `cheatsheets/test/selenium.md`, replace the "Forge-specific" section's "no browsers in forge" warning with: "Chromium-headless, Firefox, ChromeDriver, and GeckoDriver are baked into the forge image (see `default-image` capability). No `RUNTIME_LIMITATIONS` report needed for browser-driven tests." Cheatsheet remains DRAFT until provenance retrofit.
  • ☐ 2.2 Same update in `cheatsheets/test/playwright.md` — note that Playwright's own `playwright install` still works, but the system Chromium in `chromium-headless` is now available as a fallback / for non-Playwright Selenium scripts.
  • ☐ 3.1 `scripts/build-image.sh forge --force` — confirm the new layer lands and the image rebuilds.
  • ☐ 3.2 Smoke test inside the new image: `podman run --rm tillandsias-forge:latest sh -c 'chromedriver --version && geckodriver --version && chromium-headless --version && firefox --version'` — all four return version lines.
  • ☐ 3.3 Compare image size before / after; document the delta in the commit message. Confirm ≤ 600 MB growth.

Read source record ↗

changeHost chromium on demandOpen tasks0 checked · 38 open

Per the user's directive: "include its installer in userspace with our curl installer, and assume from the binary that it is installed. Our installer shall remain tiny and free of dependencies." Plus: "Use some ephemeral --user-dir [...] incognito mode and some random empty user dir only if required to override default. Our browsers should also run in isolat

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☐ 1.1 Create `scripts/refresh-chromium-pin.sh` that:
  • ☐ 1.2 Add `# @trace spec:host-chromium-on-demand` and
  • ☐ 1.3 Add a `--release-cut` mode that fails the script if the JSON
  • ☐ 1.4 Document the script in `docs/cheatsheets/secrets-management.md`
  • ☐ 2.1 Add the four `CHROMIUM_SHA256_*` and `CHROMIUM_VERSION` shell
  • ☐ 2.2 Implement `install_chromium()` shell function:
  • ☐ 2.3 Add the `SKIP_CHROMIUM_DOWNLOAD=1` env-var guard at the top
  • ☐ 2.4 Wire `install_chromium` into the install.sh main flow
  • ☐ 2.5 Add the platform-appropriate uninstall hint to install.sh's
  • ☐ 2.6 Add `# @trace spec:host-chromium-on-demand` and
  • ☐ 3.1 Add a new clap subcommand under `src-tauri/src/cli.rs`:
  • ☐ 3.2 Implement `src-tauri/src/chromium_runtime.rs` (new module)
  • ☐ 3.3 The `expected_sha256` and `version` arguments are sourced
  • ☐ 3.4 Add `// @trace spec:host-chromium-on-demand` and
  • ☐ 3.5 Unit tests in `src-tauri/src/chromium_runtime.rs::tests`:
  • ☐ 4.1 Replace `detect_browser()` with the userspace-first / system-
  • ☐ 4.2 Tombstone `BrowserKind::Safari`, `BrowserKind::Firefox`, and
  • ☐ 4.3 Update `launch_for_project` to:
  • ☐ 4.4 Add cleanup logic: when the spawned Child exits, remove
  • ☐ 4.5 Add `// @trace spec:host-chromium-on-demand,
  • ☐ 4.6 Update tests in `src-tauri/src/browser.rs::tests`:
  • ☐ 5.1 Add `src-tauri/build.rs` logic that reads `CHROMIUM_VERSION`
  • ☐ 5.2 Expose them in `src-tauri/src/chromium_runtime.rs` as
  • ☐ 5.3 Acceptance assertion in `chromium_runtime.rs::tests`:
  • ☐ 6.1 Modify `src-tauri/src/cli.rs --uninstall --wipe` to also
  • ☐ 6.2 Add a `--help` epilogue line documenting the manual
  • ☐ 7.1 Update `cheatsheets/runtime/forge-paths-ephemeral-vs-persistent.md`
  • ☐ 7.2 Update `cheatsheets/security/owasp-top-10-2021.md` if it
  • ☐ 7.3 Update `cheatsheets/web/http.md` if needed to cover the
  • ☐ 7.4 Verify all three updated cheatsheets retain their
  • ☐ 8.1 Emit accountability log entries for every install_chromium
  • ☐ 8.2 Emit a one-time accountability log entry at tray startup
  • ☐ 8.3 Emit accountability entries on `current` symlink repointing
  • ☐ 9.1 Add a host-side integration test under
  • ☐ 9.2 Add a release-blocking smoke test that, given a Tillandsias
  • ☐ 10.1 After `/opsx:archive`, run
  • ☐ 10.2 Commit with the trace URL footer per project convention:
  • ☐ 10.3 Run `scripts/refresh-chromium-pin.sh` once before tagging

Read source record ↗

changeSubdomain naming flipOpen tasks0 checked · 12 open

Today's URL pattern `<project>.opencode.localhost:8080` puts the project name first and the service second. As soon as Tillandsias adds a second service per project (`web` for Flutter dev server, `dashboard` for an agent UI, `www` for a static preview), the URL space becomes hard to scan: `java.opencode.localhost`, `python.opencode.localhost`, `java.web.loca

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☐ 1.1 In `src-tauri/src/browser.rs::build_subdomain_url`, change the format string from `format!("http://{host_label}.opencode.localhost:8080/")` to `format!("http://opencode.{host_label}.localhost:8080/")`.
  • ☐ 1.2 Update the doc-comment block above `build_subdomain_url` to reflect the new ordering and the future-services rationale.
  • ☐ 1.3 In `src-tauri/src/handlers.rs::regenerate_router_caddyfile` (~line 1024-1029), change the `snippet.push_str` format from `"{project}.opencode.localhost:80 { ... reverse_proxy tillandsias-{project}-forge:4096 ... }"` to `"opencode.{project}.localhost:80 { ... reverse_proxy tillandsias-{project}-forge:4096 ... }"`. The reverse-proxy target is unchanged.
  • ☐ 2.1 Update `browser.rs::tests::build_subdomain_url_has_opencode_subdomain_port_8080_no_path` — expected URL becomes `http://opencode.thinking-service.localhost:8080/`.
  • ☐ 2.2 Update `build_subdomain_url_uses_rootless_port_8080` — assert URL ends with `.localhost:8080/` and starts with `http://opencode.`.
  • ☐ 2.3 Update `build_subdomain_url_lowercases_mixed_case_project` — `MyProject` → `http://opencode.myproject.localhost:8080/`.
  • ☐ 2.4 Update `build_subdomain_url_sanitizes_invalid_label_chars` — `My App/sub` → `http://opencode.my-app-sub.localhost:8080/`.
  • ☐ 3.1 In `cheatsheets/agents/opencode.md`, update URL examples under the "Tillandsias-specific path" table from `<project>.opencode.localhost` to `opencode.<project>.localhost`. Cheatsheet still carries DRAFT banner — provenance retrofit happens later.
  • ☐ 4.1 The `fix-router-loopback-port` change (in flight, not yet archived) used the `<project>.opencode.localhost:8080` shape. Update its `specs/opencode-web-session/spec.md` to use the new shape so the two changes are consistent. Both changes can land independently; the URL flip is purely string-shape.
  • ☐ 5.1 `cargo check --workspace` — clean.
  • ☐ 5.2 `cargo test -p tillandsias --bin tillandsias browser` — all four updated tests green.
  • ☐ 5.3 No runtime change to verify beyond the test pass; URL is purely a string. Live verification happens on the next `./build.sh --install` cycle.

Read source record ↗

changeTray host control socketOpen tasks30 checked · 13 open

Multiple in-flight changes need an out-of-band channel between tray-side processes and tray-spawned containers (router for OTP transport per `opencode-web-session-otp`; future host MCP server for browser-open allowlist; future cross-process events). Each ad-hoc design adds attack surface. One reviewable Unix-socket control plane is the smallest defensible sh

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1.1 Schema lives in `src-tauri/src/control_socket/wire.rs` (in-tree
  • ☑ 1.2 Defined `ControlEnvelope { wire_version: u16, seq: u64, body: ControlMessage }`.
  • ☑ 1.3 Defined v1 variants: `Hello { from, capabilities }`,
  • ☑ 1.4 Defined `ErrorCode` enum: `UnknownVariant`, `PayloadTooLarge`,
  • ☑ 1.5 Pinned `MAX_MESSAGE_BYTES: usize = 65_536` constant.
  • ☑ 1.6 Round-trip unit tests cover every v1 variant
  • ☑ 1.7 `@trace spec:tray-host-control-socket` and
  • ☑ 2.1 Server lives in `src-tauri/src/control_socket/mod.rs` (in-tree).
  • ☑ 2.2 `Server::bind` ensures parent dir exists with mode `0700`,
  • ☑ 2.3 `Server::bind_default` resolves
  • ☑ 2.4 Stale-socket recovery: probes existing nodes with a synchronous
  • ☑ 2.5 Accept loop uses `Semaphore` (32 permits) and per-connection
  • ☑ 2.6 `handle_connection` uses `tokio_util::codec::LengthDelimitedCodec`
  • ☐ 2.7 Per-handler dispatcher trait NOT YET implemented — v1 dispatch is a
  • ☑ 2.8 Graceful shutdown via `Server::shutdown` (notifies the accept
  • ☑ 2.9 Unit tests: bind+chmod permissions
  • ☑ 2.10 `@trace spec:tray-host-control-socket` on every public function
  • ☐ 3.1 Deferred to the OTP follow-up change. v1 ships only the
  • ☐ 3.2 (deferred) `Client::connect`.
  • ☐ 3.3 (deferred) Reconnect loop with exponential backoff.
  • ☐ 3.4 (deferred) `send` with sequence numbering + reply correlation.
  • ☐ 3.5 (deferred) Per-connection `Hello`/`HelloAck` handshake.
  • ☐ 3.6 (deferred) Integration test: in-process server + client,
  • ☑ 4.1 `Server::bind_default()` invoked from `src-tauri/src/main.rs`
  • ☑ 4.2 `Hello` is handled by `control_socket::handler::dispatch` and
  • ☑ 4.3 The event loop's Quit arm calls `crate::shutdown_control_socket()`
  • ☑ 4.4 `@trace spec:tray-host-control-socket` annotations cover the
  • ☐ 4.5 Singleton-guard end-to-end verification deferred to manual smoke
  • ☑ 5.1 `mount_control_socket: bool` field added to `ContainerProfile`
  • ☑ 5.2 `router_profile()` sets `mount_control_socket = true`.
  • ☑ 5.3 `src-tauri/src/launch.rs::build_podman_args` appends
  • ☑ 5.4 Verified by `control_socket_mount_added_when_profile_opts_in`:
  • ☑ 5.5 `control_socket_mount_added_when_profile_opts_in` and
  • ☑ 5.6 `forge_profiles_default_to_no_control_socket` and
  • ☑ 6.1 Accountability entries emitted on:
  • ☐ 6.2 Secret-redaction wrapper for `cookie_value` deferred to OTP
  • ☑ 6.3 `cheatsheet = "runtime/networking.md"` field present on the
  • ☑ 7.1 `toolbox run -c tillandsias cargo test --workspace` — 314 tests
  • ☑ 7.2 `toolbox run -c tillandsias cargo clippy --workspace --all-targets`
  • ☐ 7.3 `openspec validate tray-host-control-socket --strict` — pending
  • ☐ 7.4 Manual smoke (launch tray, verify socket node + perms, Quit
  • ☐ 7.5 Manual smoke (router exec + handshake) — pending orchestrator
  • ☐ 7.6 Manual smoke (SIGKILL + stale recovery) — pending orchestrator

Read source record ↗

changeTray no disabled itemsOpen tasks0 checked · 40 open

The tray menu's current rendering of the five-stage state machine fills the menu with disabled placeholder items (`Building [...]`, `Ready`, `Sign in to GitHub` when not in NoAuth, `(GitHub unreachable…)`, `No local projects`, `No remote projects`, separate `version` and `— by Tlatoāni` lines). Users read disabled rows as broken UI. The `Include remote` chec

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☐ 1.1 In `locales/en.toml`, remove `menu.launch`, `menu.include_remote`, `menu.no_local_projects`, `menu.no_remote_projects`, `menu.maintenance_terminal`. Add `menu.attach_here_with_emoji = "🌱 Attach Here"`, `menu.attach_another_with_emoji = "🌱 Attach Another"`, `menu.signature_with_version = "v{version} — by Tlatoāni"`, `menu.status.building_one = "Building {image}…"`, `menu.status.building_many = "Building {images}…"`, `menu.status.ready = "{image} ready"`, `menu.status.github_unreachable = "GitHub unreachable — using cached list"`. Re-use the existing `menu.maintenance` (`"⛏️ Maintenance"`) for the per-stack child action.
  • ☐ 1.2 Mirror the same key add/remove set in every other `locales/*.toml`. For locales without translations, leave the new keys as the English value — the runtime falls back to English on missing keys regardless, but explicit copies prevent accidental key removal during future edits.
  • ☐ 1.3 Verify that `i18n::t` and `i18n::tf` resolve all new keys at runtime by writing one quick `cargo test -p tillandsias-tauri --bin tillandsias --lib i18n` style check, OR by `grep`-ing for any remaining `menu.launch` / `menu.include_remote` / `menu.no_local_projects` / `menu.no_remote_projects` / `menu.maintenance_terminal` in `src-tauri/`.
  • ☐ 2.1 In `crates/tillandsias-core/src/event.rs`, remove the `MenuCommand::IncludeRemoteToggle { include }` variant.
  • ☐ 2.2 In `src-tauri/src/event_loop.rs:191`, remove the `MenuCommand::IncludeRemoteToggle` match arm.
  • ☐ 2.3 In `src-tauri/src/main.rs`, remove the two call sites that read `menu.include_remote_checked()` and dispatch `IncludeRemoteToggle`. The `update_projects` call replacement is in §3.
  • ☐ 2.4 Confirm `cargo check --workspace` after these removals — exhaustive match must still compile.
  • ☐ 3.1 In `src-tauri/src/tray_menu.rs`, remove `INCLUDE_REMOTE` from `ids` (line 52), remove the `include_remote_check: CheckMenuItem<R>` field, remove the import of `CheckMenuItem` / `CheckMenuItemBuilder`.
  • ☐ 3.2 Collapse `version_line` and `signature_line` into a single field `signature_combined: MenuItem<R>` whose initial text is `format!("v{} — by Tlatoāni", env!("TILLANDSIAS_FULL_VERSION"))` (or via `i18n::tf("menu.signature_with_version", &[("version", env!("TILLANDSIAS_FULL_VERSION"))])`). Remove `ids::SIGNATURE` (or repurpose its ID for the combined line) and remove `ids::VERSION_LINE`.
  • ☐ 3.3 Replace the pre-built static items (`building_chip`, `ready_indicator`, `sign_in`, `net_banner`, `projects_submenu`) with: a single `status_line: MenuItem<R>` handle (initially with empty text, NOT appended to the menu); a `sign_in: MenuItem<R>` handle (NOT initially appended). Stop pre-appending these to the root menu — they are appended on demand in §3.6.
  • ☐ 3.4 Reduce the root menu builder (`MenuBuilder::new(app)…build()`) at lines ~277–289 to ONLY the static row: `[ separator_top, language, signature_combined, quit ]`. The variable region above the separator is owned by `update_dynamic_region` (§3.6).
  • ☐ 3.5 Delete `set_stage` and `StageVisibility::for_stage`. Replace with a single method `apply_state(&self, app: &AppHandle<R>, stage: Stage, state: &TrayState) -> tauri::Result<()>` that calls `update_dynamic_region` (§3.6). Keep the `Stage` enum and `stage_from_health` — both are still semantic state.
  • ☐ 3.6 Implement `update_dynamic_region(&self, app: &AppHandle<R>, stage: Stage, state: &TrayState) -> tauri::Result<()>`:
  • ☐ 3.7 Implement `status_text(state, stage) -> Option<String>` as a pure function. Inputs: `state.active_builds` (active vs completed-within-2s), `stage == Stage::NetIssue`. Output: `Some(joined_text)` if any condition holds, else `None`. Joining: `["Building forge…", "GitHub unreachable — using cached list"].join(" · ")`. Use `i18n::tf("menu.status.building_one", …)` etc. for individual fragments.
  • ☐ 3.8 Implement `running_stacks(state) -> Vec<RunningStack>` returning `(project_name, project_path, bloom_emoji, tool_emojis)` for every project with at least one container of type `Forge | OpenCodeWeb | Maintenance` in `state.running`. `bloom_emoji: Option<String>` comes from the `OpenCodeWeb` container's `display_emoji` if present (today: `🌺`); else `None` (no fallback to Forge genus — bloom communicates "live web session", not "forge alive"). `tool_emojis: Vec<String>` comes from `display_emoji` of `Maintenance` containers in `state.running` insertion order, capped at 5 entries. Sorted by `project_name.to_lowercase()`.
  • ☐ 3.9 Implement `build_running_stack_submenu(app, stack)` returning a `Submenu<R>` with label `"<project>[ <bloom>][ <tool emojis>]"` (project name first; bloom present only if `Some`; tool emojis present only if non-empty) and exactly two children: `🌱 Attach Another` from `i18n::t("menu.attach_another_with_emoji")` (id `ids::launch(&project_path)` — same `MenuCommand::Launch` dispatch as the cold-start path), and `🔧 Maintenance` from `i18n::t("menu.maintenance")` (id `ids::maint(&project_path)`).
  • ☐ 3.10 Update `build_local_project_submenu` to remove the per-stack `Maintenance terminal` child when the project's forge is NOT running (the new spec hides Maintenance instead of disabling it). Rename the local action to `🌱 Attach Here` using `i18n::t("menu.attach_here_with_emoji")`. Drop the `🌺 ` prefix on the local-project label since the live shortcut is the top-level entry.
  • ☐ 3.11 Update `build_remote_project_submenu` unchanged — it already produces a single `Clone & Launch` child that dispatches `MenuCommand::CloneProject`.
  • ☐ 3.12 Update the `Projects ▸` submenu construction inside `update_dynamic_region`: only append it when `state.projects` is non-empty. Inside, omit the `INCLUDE_REMOTE` check and omit the "No local projects" placeholder. Iterate sorted local projects directly.
  • ☐ 3.13 Add `Remote Projects ▸` submenu construction inside `update_dynamic_region`: only append when `remote_only` (repos in `state.remote_repos` not in `state.projects` AND not present on disk under any watch path) is non-empty. Iterate sorted remote-only repos and call `build_remote_project_submenu` for each.
  • ☐ 3.14 Update `refresh_static_labels` to refresh ONLY `signature_combined`, `language`, `quit`. Drop refreshes for the removed handles. The dynamic region's labels are recomputed on every `update_dynamic_region` pass, so locale changes naturally pick them up via the standard event-loop tick.
  • ☐ 3.15 Update `dispatch_click` to remove the `INCLUDE_REMOTE` arm.
  • ☐ 4.1 In `src-tauri/src/main.rs`, replace the two `update_projects` call sites with `apply_state(stage, &state)` (or the equivalent helper that re-computes stage from `(state.enclave_health, credential_health, remote_repo_fetch_status)`). Verify no caller still references `menu.include_remote_checked()`.
  • ☐ 4.2 In `src-tauri/src/event_loop.rs`, ensure every place that called `on_state_change(&state)` (lines 121, 141, 161, 193, 202, 209, 216, 222, 432, 503) still triggers a tray menu refresh. The `on_state_change: MenuRebuildFn` callback is the same — it now indirectly calls `apply_state` instead of `set_stage` + `update_projects`.
  • ☐ 4.3 Verify `cargo check --workspace` passes.
  • ☐ 5.1 Rewrite `docs/cheatsheets/tray-state-machine.md` to reflect the new visibility table. Replace the old "Visible items" column with the new variable-region composition (status line / sign-in / running stacks / Projects / Remote Projects). Keep `@trace spec:tray-app` annotations.
  • ☐ 5.2 Add a row showing the contextual-status-line truth table: which `(active_builds, stage)` combinations produce which text.
  • ☐ 6.1 In `src-tauri/src/tray_menu.rs` `tests` module, delete `stage_visibility_table_matches_spec` (the table no longer exists). Add `status_text_truth_table` covering the matrix of `(active_builds, stage) → status_text` outputs, asserting `None` for the idle-authed combination.
  • ☐ 6.2 Add `running_stacks_orders_by_name` asserting `running_stacks(state)` returns project names in `to_lowercase()` ASCII order regardless of insertion order in `state.running`.
  • ☐ 6.3 Add `running_stacks_includes_tool_emojis` asserting that when a project has both `OpenCodeWeb` and `Maintenance` containers, the returned `RunningStack.tool_emojis` contains the `Maintenance` emojis in allocation order.
  • ☐ 6.4 Update `dispatch_click_known_actions` to remove the `INCLUDE_REMOTE` assertion.
  • ☐ 6.5 Add `dispatch_click_unknown_include_remote_returns_none` to confirm the legacy ID still parses to `None` (defensive — old menu state should never click this, but if it does, we don't crash).
  • ☐ 6.6 Run `cargo test --workspace` and confirm green.
  • ☐ 7.1 `./build.sh --check` passes.
  • ☐ 7.2 `./build.sh` (debug) — launch the tray. Open the menu in `Booting`, `NoAuth` (no token), `Authed` (token present, no projects), `Authed` (token present, projects present), `Authed` (with one running forge), `NetIssue` (drop network mid-session). For each: confirm exactly one disabled signature row, no `No local projects` / `No remote projects` placeholders, no `Building […]` row when idle, `🔑 Sign in to GitHub` only present in NoAuth/NetIssue and enabled.
  • ☐ 7.3 Click the running-stack submenu's `🌱 Attach Here` — confirm a second browser window opens against the same forge with no second container started (`podman ps` shows one `tillandsias-<project>-forge`).
  • ☐ 7.4 Click the running-stack submenu's `🔧 Maintenance` twice — confirm two terminal windows open, both `podman exec`'d into the same forge.
  • ☐ 7.5 Quit Tillandsias — confirm `shutdown_all` cleans the running stack (no `tillandsias-*` containers left after exit).
  • ☐ 8.1 Confirm every modified function in `src-tauri/src/tray_menu.rs` carries `// @trace spec:tray-app` near the function definition.
  • ☐ 8.2 Run `./scripts/bump-version.sh --bump-changes` after archive (per CLAUDE.md), not now.

Read source record ↗

changeTray progress and icon statesOpen tasks0 checked · 11 open

Today's status chip is a comma-joined fragment list (`Building Forge … · GitHub unreachable …`). Users at launch see only `Quit Tillandsias` + a disabled signature line for several seconds while infrastructure spins up — the menu reads "broken" until the chip first appears. Then individual fragments come and go without any visual sense of cumulative progress

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☐ 1.1 Update `locales/en.toml`: rename `menu.sign_in_github` value to `🔑 GitHub Login`. Add `menu.unhealthy_environment = "🥀 Unhealthy environment"`. Add `menu.status.verifying_environment` and `menu.status.ready_one`. Add chip names `menu.build.chip_browser_runtime`, `menu.build.chip_proxy`, `menu.build.chip_router`.
  • ☐ 1.2 Mirror to `locales/de.toml` and `locales/es.toml` so locale-parity tests pass.
  • ☐ 2.1 Add `subsystem_emoji_and_order(image_name) -> Option<(u8, &'static str)>` mapping the seven chip substrings to (sort_order, emoji).
  • ☐ 2.2 Rewrite `status_text(state, stage) -> Option<String>` to produce the additive chip shape. Tombstone the prior fragment-list version with `@tombstone superseded:tray-progress-and-icon-states`.
  • ☐ 2.3 Add `Stage::Unhealthy` to the enum. Update `apply_state` to early-return with the `🥀` item when stage is Unhealthy. Update its docs.
  • ☐ 3.1 Update `current_stage` precedence: Unhealthy first (any Failed build, no in-flight retry), then Booting, then health-derived stages.
  • ☐ 4.1 Existing tests in `tray_menu.rs::tests`: adjust `status_text_*` to match the new chip shape (`✅` prefix + emoji prefix + tail action text). The existing test names stay; only assertions change.
  • ☐ 4.2 New test: `current_stage_returns_unhealthy_when_a_build_failed_without_retry`. Pure-function test against a fabricated `TrayState` with one `BuildStatus::Failed` row.
  • ☐ 4.3 New test: `current_stage_returns_booting_when_failed_build_has_concurrent_retry`. Same shape, plus a parallel `BuildStatus::InProgress` row — confirms the retry overrides the failure.
  • ☐ 4.4 `cargo test -p tillandsias --bin tillandsias` — green.
  • ☐ 5.1 Update `docs/cheatsheets/tray-state-machine.md`: add the new emoji table + Unhealthy stage + the additive chip shape to the existing visibility tables.

Read source record ↗

changeCheatsheet source layerOpen tasks25 checked · 1 open

Tillandsias's cheatsheet provenance discipline ALREADY mandates citing high-authority source URLs. But there's no enforcement that the cited URL was actually fetched at authoring time vs. plausibly invented. The recent `cheatsheets/runtime/local-inference.md` was a textbook example: three plausible URLs cited, none actually fetched, and one of the documented

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1.1 `scripts/fetch-cheatsheet-source.sh` — verbatim fetcher with allowlist, GitHub-blob rewrite, RFC text preference, single-page variants, idempotent --cite Provenance update.
  • ☑ 1.2 `scripts/regenerate-source-index.sh` + `--check` — walks sidecars, produces deterministic INDEX.json.
  • ☑ 1.3 `scripts/check-cheatsheet-sources.sh` — four-check validator (URL→INDEX, local-path-exists, orphan, SHA match). UNFETCHED warnings during migration; errors on missing files / SHA mismatch.
  • ☑ 1.4 `scripts/refresh-cheatsheet-sources.sh [--max-age-days N] [--dry-run]` — drift detection, marks sidecars staleness: drift|gone|current.
  • ☑ 1.5 `scripts/audit-cheatsheet-sources.sh` — CSV migration triage.
  • ☑ 1.6 `cheatsheet-sources/license-allowlist.toml` — initial allowlist (RFC, IETF, W3C, WHATWG, MDN, OWASP, kernel.org, Python, Rust, Ollama, plus AWS/Azure/GCP as do-not-bundle).
  • ☑ 1.7 `.gitignore` for `*.norepublish`; `.gitattributes` LFS rules for HTML/PDF.
  • ☑ 1.8 End-to-end verification: fetched RFC 6265, INDEX regenerated, validator passes.
  • ☑ 2.1 Run `scripts/audit-cheatsheet-sources.sh > /tmp/audit.csv` to triage all ~80 cheatsheets' Provenance URLs.
  • ☑ 2.2 Bulk-fetch allowlisted-domain URLs: `scripts/fetch-cheatsheet-source.sh <URL>` for each (no `--cite` — just populate the verbatim layer).
  • ☑ 2.3 Off-allowlist URLs: surface for maintainer review — triage in /tmp/off-allowlist-triage.md.
  • ☑ 2.4 No 404s on initial fetch. One URL returned only 326-byte meta-refresh stub (owasp.org/Top10/) — chased redirect to real content at owasp.org/Top10/2025/.
  • ☑ 2.5 INDEX.json has 48 entries (47 new + 1 from chunk 1). Valid JSON.
  • ☑ 2.6 `cheatsheet-sources/ATTRIBUTION.md` generated (16 publishers, alphabetical).
  • ☑ 3.1 `scripts/bind-provenance-local-paths.sh` (NEW) — walks INDEX.json and
  • ☑ 3.2 `last_verified` frontmatter bumped to 2026-04-27 on all rewritten cheatsheets.
  • ☑ 3.3 All INDEX.json `cited_by` fields populated from the rewritten local: paths;
  • ☑ 3.4 `scripts/check-cheatsheet-sources.sh` exits 0 after rewrite.
  • ☑ 4.1 `scripts/check-cheatsheet-sources.sh` wired into
  • ☑ 4.2 `openspec validate` extension: the pre-commit hook IS the validation
  • ☑ 4.3 `scripts/regenerate-cheatsheet-index.sh` extended: generates a Python
  • ☑ 4.4 Deliberately-introduced mismatch test: adding a fake local: path
  • ☑ 5.1 `openspec/changes/cheatsheet-source-layer/specs/cheatsheet-source-layer/spec.md`
  • ☑ 5.2 `openspec/changes/cheatsheet-source-layer/specs/agent-cheatsheets/spec.md`
  • ☑ 5.3 `openspec validate cheatsheet-source-layer` — hook runs clean (0 errors).
  • ☐ 5.4 Archive: `openspec archive cheatsheet-source-layer -y` — deferred to

Read source record ↗

changeExternal logs layerOpen tasks0 checked · 32 open

A forge container that launches sibling service containers (e.g. a postgres + nginx pair to test the production stack the agent is editing) currently has **no observability over those siblings**. The existing per-container `/var/log/tillandsias/` mount is single-tier and per-container — sibling forges cannot read each other's logs, and there is no boundary b

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☐ 1.1 `crates/tillandsias-core/src/container_profile.rs`: add `MountSource::ExternalLogsProducer { role: &'static str }` and `MountSource::ExternalLogsConsumerRoot` variants.
  • ☐ 1.2 `ContainerProfile`: add `external_logs_role: Option<&'static str>` and `external_logs_consumer: bool`. Default both to `None`/`false` in all 7 profile constructors (preserves current behaviour).
  • ☐ 1.3 `crates/tillandsias-core/src/config.rs`: add `external_logs_dir() -> PathBuf` and `external_logs_role_dir(role: &str) -> PathBuf` helpers (mirror existing `log_dir` / `container_log_dir`).
  • ☐ 1.4 `src-tauri/src/launch.rs::resolve_mount_source`: handle the two new variants. Producer = create + bind-mount RW. Consumer = bind-mount parent RO. Mirror the existing `ContainerLogs` branch for directory creation.
  • ☐ 1.5 Test: `external_logs_consumer_resolves_to_root` and `external_logs_producer_creates_role_dir`.
  • ☐ 1.6 Test: `profile_cannot_be_both_producer_and_consumer` — refuses BOTH set on a single profile (assertion in profile constructor or builder).
  • ☐ 2.1 `git_service_profile()`: set `external_logs_role: Some("git-service")`.
  • ☐ 2.2 `src-tauri/src/handlers.rs::ensure_external_logs_dir`: one-shot called at tray startup. If `~/.local/state/tillandsias/containers/git/logs/git-push.log` exists, move it to `~/.local/state/tillandsias/external-logs/git-service/git-push.log`. Leave `MIGRATED.txt` stub at the old path with the new path inside.
  • ☐ 2.3 Verify `images/git/post-receive-hook.sh` continues to write to `/var/log/tillandsias/git-push.log` from inside the container — the bind-mount shadow does the work; no entrypoint change needed.
  • ☐ 2.4 Manual smoke: launch a forge for any project, observe that the bare mirror's post-receive writes to `~/.local/state/tillandsias/external-logs/git-service/git-push.log` (NOT to the old per-container dir).
  • ☐ 3.1 Three forge profiles (`forge_opencode`, `forge_claude`, `forge_opencode_web`) + `terminal_profile`: set `external_logs_consumer: true`.
  • ☐ 3.2 New script `images/default/cli/tillandsias-logs` (POSIX shell, executable, COPY'd to `/usr/local/bin/`) with three subcommands: `ls` (list role/file/size/mtime/lines), `tail <role> <file>` (`tail -f` wrapper with `[role/file]` prefix), `combine` (interleave consumer's own internal log + every external log, sorted by mtime).
  • ☐ 3.3 `images/default/lib-common.sh`: export `TILLANDSIAS_EXTERNAL_LOGS=/var/log/tillandsias/external` for tools that want the path without the CLI.
  • ☐ 3.4 Manual smoke: from a forge, `tillandsias-logs ls` shows `git-service/git-push.log` after a push.
  • ☐ 4.1 `images/git/external-logs.yaml` (NEW): canonical example. Single entry: `git-push.log`.
  • ☐ 4.2 `images/git/Containerfile`: COPY the manifest to `/etc/tillandsias/external-logs.yaml`.
  • ☐ 4.3 `src-tauri/src/handlers.rs::external_logs_audit_tick`: 60s tray-side task. For each running producer:
  • ☐ 4.4 Reverse-breach refusal at producer-launch time: any profile with BOTH role+consumer is rejected before podman.
  • ☐ 4.5 Tray UI: external-logs chip (yellow on alarm).
  • ☐ 4.6 Tests: `auditor_detects_unlisted_file_emits_leak`, `auditor_truncates_oversized_file`, `auditor_growth_rate_alarm_after_5_min`.
  • ☐ 5.1 Write `cheatsheets/runtime/external-logs.md` (≤200 lines with full Provenance per the agent-cheatsheets template).
  • ☐ 5.2 Update `cheatsheets/runtime/forge-paths-ephemeral-vs-persistent.md`: add fifth row "External logs (curated)".
  • ☐ 5.3 Update `cheatsheets/runtime/forge-container.md`: short note pointing at the new cheatsheet.
  • ☐ 5.4 `images/proxy/external-logs.yaml`: Squid `access.log` (curated subset) + `denied.log`.
  • ☐ 5.5 `images/router/external-logs.yaml`: Caddy `access.log`.
  • ☐ 5.6 `images/inference/external-logs.yaml`: `model-load.log`.
  • ☐ 5.7 Each producer image's Containerfile gains the `COPY external-logs.yaml /etc/tillandsias/`.
  • ☐ 5.8 `scripts/regenerate-cheatsheet-index.sh` picks up the new cheatsheet.
  • ☐ 5.9 `openspec/changes/external-logs-layer/specs/external-logs-layer/spec.md` (NEW capability spec).
  • ☐ 5.10 `openspec/changes/external-logs-layer/specs/runtime-logging/spec.md` (delta) + `podman-orchestration/spec.md` (delta).
  • ☐ 5.11 `openspec validate external-logs-layer` — expect 0 errors.
  • ☐ 5.12 Archive: `openspec archive external-logs-layer -y`.

Read source record ↗

changeForge hot cold splitOpen tasks0 checked · 35 open

Tillandsias's forge container today runs every read off the host's overlayfs storage driver (`~/.local/share/containers/storage/`). That includes paths the agent reads dozens of times per prompt — `/opt/cheatsheets/`, `/home/forge/src/<project>/` — alongside paths the agent writes once and reads at most once (build artefacts, downloaded archives, log noise).

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☐ 1.1 `crates/tillandsias-core/src/container_profile.rs`: replace `pub tmpfs_mounts: Vec<&'static str>` with `pub tmpfs_mounts: Vec<TmpfsMount>` where `TmpfsMount { path: &'static str, size_mb: u32, mode: u32 }`.
  • ☐ 1.2 Update all 7 profile constructors. Existing service-profile mounts (`web`, `git_service`) gain `size_mb: 64`.
  • ☐ 1.3 `src-tauri/src/launch.rs::build_podman_args`: drop the `if profile.read_only` gate. Emit `--tmpfs=<path>:size=<N>m,mode=<oct>` for every entry.
  • ☐ 1.4 Pair `--memory=<N>m` and `--memory-swap=<same>` whenever any tmpfs is present. N = sum of caps + 256MB working-set baseline.
  • ☐ 1.5 Tests: `every_profile_with_tmpfs_emits_size_cap`, `web_and_git_service_keep_their_existing_tmpfs_paths`, `tmpfs_emits_sized_flag_with_mode`, `tmpfs_pairs_with_memory_ceiling`, `forge_profiles_no_tmpfs_yet_no_memory_ceiling`.
  • ☐ 1.6 `cargo check --workspace --tests` clean; full workspace tests pass.
  • ☐ 2.1 `images/default/Containerfile`: rename COPY target from `/opt/cheatsheets` to `/opt/cheatsheets-image` (RO image lower layer, baked at build time).
  • ☐ 2.2 `images/default/lib-common.sh`: add idempotent `populate_hot_paths()` that does `cp -a /opt/cheatsheets-image/. /opt/cheatsheets/ 2>/dev/null || true`.
  • ☐ 2.3 Forge entrypoints (`entrypoint-forge-opencode.sh`, `entrypoint-forge-claude.sh`, `entrypoint-forge-opencode-web.sh`, `entrypoint-terminal.sh`): call `populate_hot_paths` AFTER tmpfs is mounted by podman, BEFORE any agent-visible work.
  • ☐ 2.4 `crates/tillandsias-core/src/container_profile.rs::common_forge_mounts`: add `TmpfsMount { path: "/opt/cheatsheets", size_mb: 8, mode: 0o755 }` to the four forge/maintenance profiles' tmpfs_mounts.
  • ☐ 2.5 Verify `ENV TILLANDSIAS_CHEATSHEETS=/opt/cheatsheets` is unchanged in the Containerfile (transparency requirement).
  • ☐ 2.6 Test: `forge_profile_includes_cheatsheets_tmpfs_mount`.
  • ☐ 2.7 Manual smoke: launch a forge, `findmnt /opt/cheatsheets -no FSTYPE` returns `tmpfs`; `wc -l /opt/cheatsheets/INDEX.md` matches the image-baked count.
  • ☐ 3.1 `src-tauri/src/preflight.rs` (NEW): `check_host_ram(required_mb: u32) -> Result<(), PreflightError>`. Reads `/proc/meminfo` (Linux) / macOS sysctl / Windows GlobalMemoryStatusEx. Returns `Err` if `MemAvailable < required * 1.25`.
  • ☐ 3.2 `src-tauri/src/launch.rs`: extend `LaunchContext` with `pub hot_path_budget_mb: u32`.
  • ☐ 3.3 `src-tauri/src/launch.rs::compute_hot_budget(project_name, cache_dir)`: runs `git -C <mirror> count-objects -v -H | grep size-pack`, parses + multiplies by `forge.hot_path_inflation` (default 4), clamps to `[256, forge.hot_path_max_mb]`.
  • ☐ 3.4 `crates/tillandsias-core/src/config.rs`: add `forge.hot_path_max_mb` (default 4096) and `forge.hot_path_inflation` (default 4) keys.
  • ☐ 3.5 `build_podman_args()`: when profile is forge-shaped, append `--tmpfs=/home/forge/src:size=<budget>m,mode=0755`.
  • ☐ 3.6 Every forge launch site in `handlers.rs` (search for `start_forge` / `handle_attach_web` / `handle_maintenance_terminal`): call `preflight::check_host_ram(ctx.hot_path_budget_mb + 80)` before invoking podman; emit tray notification on failure.
  • ☐ 3.7 Tests: `compute_hot_budget_returns_floor_for_empty_mirror`, `compute_hot_budget_clamps_at_ceiling`, `check_host_ram_refuses_below_threshold`.
  • ☐ 3.8 Manual smoke: launch a forge, `findmnt /home/forge/src -no FSTYPE` = tmpfs; `df --output=size /home/forge/src` ≈ the per-launch budget.
  • ☐ 4.1 `crates/tillandsias-core/src/container_profile.rs::common_forge_mounts`: add `TmpfsMount { path: "/tmp", size_mb: 256, mode: 0o1777 }` and `TmpfsMount { path: "/run/user/1000", size_mb: 64, mode: 0o0700 }`.
  • ☐ 4.2 Test: `forge_profile_caps_tmp_and_run_user`.
  • ☐ 4.3 Manual smoke: inside a forge, `df --output=size /tmp` ≈ 256MB; `dd if=/dev/zero of=/tmp/x bs=1M count=512` fails with ENOSPC at ~256MB.
  • ☐ 5.1 `images/default/forge-welcome.sh`: under the existing `Mounts` block, add a `RAM` line listing tmpfs paths + caps (read at runtime from `findmnt -t tmpfs`).
  • ☐ 5.2 `cheatsheets/runtime/forge-paths-ephemeral-vs-persistent.md`: add fifth column "Backing store: RAM | Disk | Image"; new "Hot vs Cold" section.
  • ☐ 5.3 `cheatsheets/runtime/forge-container.md`: cross-link.
  • ☐ 5.4 New `cheatsheets/runtime/forge-hot-cold-split.md` (≤200 lines, full Provenance per agent-cheatsheets template).
  • ☐ 5.5 `scripts/regenerate-cheatsheet-index.sh` picks up the new cheatsheet.
  • ☐ 6.1 `src-tauri/src/handlers.rs`: at every `start_forge` / `handle_attach_web` / `handle_maintenance_terminal` call site, emit a structured log with `accountability = true, category = "forge-launch", spec = "forge-hot-cold-split"` recording `host_mem_available_mb`, `budget_mb`, `decision = "launch"|"refuse"`.
  • ☐ 6.2 Tray notification surface for refusal cases (chunk 3 added the check; this surfaces it via Tauri notification).
  • ☐ 6.3 Test: `tray_preflight_unit_test_decision_refuse_emits_notification`.
  • ☐ 6.4 Spec changes: write `openspec/changes/forge-hot-cold-split/specs/forge-hot-cold-split/spec.md` (NEW capability) and delta specs for `forge-cache-dual`, `default-image`, `agent-cheatsheets`, `podman-orchestration`.
  • ☐ 6.5 `openspec validate forge-hot-cold-split` — expect 0 errors.
  • ☐ 6.6 Archive: `openspec archive forge-hot-cold-split -y`.

Read source record ↗

changeForge hot cold split tmpfs laneOpen tasks3 checked · 4 open

The `cheatsheets-license-tiered` design introduces a `pull-on-demand` cache at `~/.cache/tillandsias/cheatsheets-pulled/<project>/` with a tiered RAMDISK budget (64 MB modest / 128 MB normal / 1024 MB plentiful) that auto-spills to disk past the cap. This pattern is neither pure HOT (kernel tmpfs with hard ENOSPC) nor pure COLD (disk-only) — it is a third pa

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1.1 Author proposal.md, design.md, and the forge-hot-cold-split delta spec
  • ☑ 1.2 Validate via `openspec validate forge-hot-cold-split-tmpfs-lane --strict`
  • ☑ 2.1 Update `cheatsheets/runtime/forge-hot-cold-split.md` to document the tmpfs-overlay lane as the third pattern (alongside HOT and COLD); refresh provenance + `Last updated:` date
  • ☐ 3.1 Implement `MemTotal` detection in tray startup → resolve cap → set `TILLANDSIAS_PULL_CACHE_RAM_MB`
  • ☐ 3.2 Implement the tmpfs-overlay cache mechanism inside the forge (writes to `~/.cache/tillandsias/cheatsheets-pulled/<project>/`)
  • ☐ 3.3 Implement LRU eviction across the tmpfs/disk boundary, scoped per-project
  • ☐ 3.4 Add the four-HOT-roots enumeration scenario (proves the tmpfs-overlay path does NOT appear in the HOT root list)

Read source record ↗

changeHost browser mcpOpen tasks32 checked · 33 open

Per the user's directive: agents inside forge containers need to drive browser windows for web-app testing / inspection / agentic control. The mechanism: an MCP server on the host exposing `browser.open` / `browser.click` / `browser.type` / `browser.screenshot` etc., backed by CDP against the bundled Chromium.

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☐ 1.1 Create `cheatsheets/web/cdp.md` with:
  • ☐ 1.2 Create `cheatsheets/web/mcp.md` with:
  • ☐ 1.3 Add both new cheatsheets to `cheatsheets/INDEX.md` under the
  • ☐ 2.1 Depends on `tray-host-control-socket` capability landing
  • ☐ 2.2 Add `ControlMessage::McpFrame { session_id: u64, payload: Vec<u8> }`
  • ☐ 2.3 Add a `BrowserMcp` capability string to the
  • ☐ 2.4 Verify the existing per-message size cap permits up to 4 MiB
  • ☐ 2.5 Unit tests: round-trip `McpFrame` with empty / small / 4 MiB
  • ☐ 2.6 `@trace spec:host-browser-mcp, spec:tray-host-control-socket`
  • ☐ 3.1 Add `crates/tillandsias-podman/src/peer_table.rs`
  • ☐ 3.2 Wire into `src-tauri/src/handlers.rs` forge spawn / exit
  • ☐ 3.3 Unit tests: spawn-then-lookup, exit-then-lookup-fails,
  • ☐ 3.4 `@trace spec:host-browser-mcp, spec:tray-app, spec:podman-orchestration`.
  • ☐ 4.1 Create `src-tauri/src/browser_mcp/mod.rs` exposing
  • ☐ 4.2 Implement the JSON-RPC 2.0 framing layer:
  • ☐ 4.3 `tools/list` returns the eight v1 tool descriptors with
  • ☐ 4.4 `tools/call` dispatches by tool `name` to per-tool handlers
  • ☐ 4.5 `tools/call` enforces the per-session 16-concurrent-call
  • ☐ 4.6 Hook into `ControlMessage::McpFrame` dispatch on the host
  • ☐ 4.7 On connection accept, look up the connecting peer's PID via
  • ☐ 4.8 `@trace spec:host-browser-mcp, spec:tray-app` on every
  • ☐ 5.1 Create `src-tauri/src/browser_mcp/window_registry.rs`:
  • ☑ 5.2 Implement `browser_mcp/launcher.rs` reusing the bundled-Chromium
  • ☑ 5.3 CDP attach: 2 s timeout; reuse the CDP client introduced by
  • ☐ 5.4 Spawn a watcher task per launched chromium PID — when the PID
  • ☐ 5.5 On tray Quit: `drain_all` SIGTERMs every PID; 5 s grace; then
  • ☐ 5.6 Unit tests with mocked `Command` + mock CDP socket: launch
  • ☑ 5.7 `@trace spec:host-browser-mcp, spec:host-chromium-on-demand`;
  • ☐ 6.1 Implement `src-tauri/src/browser_mcp/allowlist.rs` with
  • ☐ 6.2 Unit tests covering each rule's reject path AND the accept
  • ☑ 6.3 Wire allowlist into `browser.open` BEFORE any chromium spawn
  • ☐ 6.4 Property test (proptest or quickcheck): for any random URL
  • ☑ 6.5 `@trace spec:host-browser-mcp, spec:opencode-web-session`;
  • ☐ 7.1 Add
  • ☑ 7.2 In `browser.open` after allowlist passes: lookup
  • ☐ 7.3 Unit tests: rapid duplicate returns existing; after 1000 ms
  • ☑ 7.4 `@trace spec:host-browser-mcp`.
  • ☑ 8.1 `browser.open` — covered by sections 5–7.
  • ☑ 8.2 `browser.list_windows` — `WindowRegistry.list_for_project`,
  • ☑ 8.3 `browser.read_url` — single `Page.getNavigationHistory`
  • ☑ 8.4 `browser.screenshot` — `Page.captureScreenshot` with
  • ☑ 8.5 `browser.click` — `Runtime.evaluate` against expression
  • ☑ 8.6 `browser.type` — `Runtime.evaluate` to set `.value` and
  • ☑ 8.7 `browser.eval` — gated: ALWAYS return
  • ☑ 8.8 `browser.close` — terminate chromium PID (SIGTERM, 5 s,
  • ☑ 8.9 `@trace spec:host-browser-mcp` on every tool;
  • ☑ 9.1 Verify `socat` is present in the forge image
  • ☑ 9.2 Create `images/default/config-overlay/mcp/host-browser.sh`:
  • ☑ 9.3 Add `host-browser.sh` to the embedded image-source list in
  • ☑ 9.4 Update `images/default/config-overlay/opencode/config.json`:
  • ☐ 9.5 Add the equivalent registration to the Claude Code config
  • ☐ 9.6 Integration test: `podman run` the forge image with the host
  • ☐ 9.7 Fallback path documentation in design.md Q-OPEN-4: if shell
  • ☑ 10.1 In every tool handler, emit exactly one
  • ☑ 10.2 Audit-log payload redaction:
  • ☑ 10.3 Test: capture structured log output during a full mocked
  • ☑ 10.4 `@trace spec:host-browser-mcp, spec:logging-accountability`.
  • ☑ 11.1 Unit tests: covered above per module (allowlist, debounce,
  • ☑ 11.2 Integration test (host-side, requires podman):
  • ☑ 11.3 Audit-log integration test: full session cycle,
  • ☑ 11.4 Concurrency test: 16 in-flight `tools/call` succeed; 17th
  • ☑ 12.1 Update `docs/cheatsheets/secrets-management.md` to note
  • ☑ 12.2 Add a new `docs/cheatsheets/host-browser-mcp.md` (operator
  • ☑ 13.1 After `/opsx:archive`, run
  • ☑ 13.2 Commit each cohesive batch with the trace URL footer per the

Read source record ↗

changeCodex tray launcherOpen tasks7 checked · 24 open

Codex is a powerful agent for code analysis, understanding, and generation. Currently, users access it only through secondary flows. Adding 🏗 Codex as a first-class tray menu button (alongside Claude and OpenCode) provides direct access and reinforces the agent-centric design philosophy of the platform. This brings Codex to parity with other agents in the UI

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ **Task 1.1**: Add Codex button to menu.rs
  • ☑ **Task 1.2**: Implement menu button state management
  • ☑ **Task 2.1**: Create launch_codex_container() handler (STUB)
  • ☐ **Task 3.1**: Implement stop handler for Codex container
  • ☐ **Task 3.2**: Implement destroy handler for Codex container
  • ☐ **Task 3.3**: Implement reattach logic
  • ☐ **Task 4.1**: Add Codex to forge image build
  • ☐ **Task 4.2**: Verify Codex in forge entrypoint
  • ☐ **Task 5.1**: Configure proxy allowlist for Codex
  • ☐ **Task 5.2**: Test egress isolation
  • ☐ **Task 6.1**: Unit tests for menu button logic
  • ☐ **Task 6.2**: Integration tests for container launch
  • ☐ **Task 6.3**: Integration tests for lifecycle management
  • ☐ **Task 6.4**: End-to-end feature test
  • ☐ **Task 7.1**: Add @trace annotations to all code
  • ☐ **Task 7.2**: Update TRACES.md
  • ☐ **Task 7.3**: Document Codex entrypoint in cheatsheet
  • ☑ Menu renders Codex button for each project
  • ☑ All @trace annotations present and correct (11 annotations added)
  • ☑ Code compiles with no errors
  • ☑ CI pipeline passes (all 7 checks)
  • ☐ Codex button launches container successfully (handler implemented, needs image)
  • ☐ Container name follows convention (tillandsias-<project>-codex) (infrastructure ready)
  • ☐ Container joins enclave and can reach GitHub API via proxy (infrastructure ready)
  • ☐ Progress chip appears and transitions through states (uses existing infrastructure)
  • ☐ Output logged with [codex] prefix (uses existing infrastructure)
  • ☐ Stop and Destroy handlers work correctly (delegated to existing handlers)
  • ☐ Reattach logic works for existing containers (delegated to existing handlers)
  • ☐ TRACES.md updated (specs not yet synced to main)
  • ☐ All tests pass (unit + integration) (can mock, but full test needs image)
  • ☐ Codex feature works end-to-end (REQUIRES Phase 4 image with Codex binary)

Read source record ↗

changeend-to-end-diagnostics-cli Change ProposalOpen tasks0 checked · 49 open

Current initialization and debugging workflows lack visibility into container lifecycle and health. Users running `tillandsias --init` cannot verify all images build (including new browser isolation containers), and there's no straightforward way to inspect live logs for troubleshooting. We need end-to-end observability: a predictable initialization flow tha

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☐ 1.1 Add `--diagnostics` flag to `src-tauri/src/cli.rs` CLI struct (takes optional project path argument)
  • ☐ 1.2 Add `--diagnostics` variant to CLI match statement in `main.rs` (entry point routing)
  • ☐ 1.3 Update `--help` text to describe `--diagnostics <project-path>` for container log streaming
  • ☐ 1.4 Add `@trace spec:cli-diagnostics` annotation near CLI flag definition
  • ☐ 2.1 Create `handle_diagnostics()` function in `src-tauri/src/handlers.rs` that accepts project path
  • ☐ 2.2 Implement container discovery logic: identify all running Tillandsias containers (shared + project-specific)
  • ☐ 2.3 Spawn `podman logs -f` subprocess for each running container
  • ☐ 2.4 Implement line-by-line prefix labeling: prepend `[<container-type>:<owner>]` to each log line
  • ☐ 2.5 Aggregate all subprocess output to stderr (unbuffered, real-time)
  • ☐ 2.6 Implement graceful shutdown: Ctrl+C kills all podman subprocesses cleanly, exits with code 0
  • ☐ 2.7 Handle edge case: no running containers → print clear error message and exit 0
  • ☐ 2.8 Emit observability events at start/end:
  • ☐ 2.9 Add `@trace spec:cli-diagnostics, spec:observability-convergence` annotations throughout handler
  • ☐ 3.1 Update `src-tauri/src/init.rs::run()` to include browser-core and browser-framework in image build list
  • ☐ 3.2 Verify browser image Containerfiles exist at `images/chromium/Containerfile.core` and `.framework`
  • ☐ 3.3 Extend staleness detection in `init.rs` to check browser image sources alongside other images
  • ☐ 3.4 Add `--debug` flag handling: disable timeout limits, stream build logs to stderr in real-time
  • ☐ 3.5 Modify build progress output to show all 6 images: "Building images (1/6) proxy, (2/6) forge, ..., (6/6) browser-framework"
  • ☐ 3.6 Update failure reporting: after build completes, print last 20 lines of failed image logs (if any)
  • ☐ 3.7 Add `@trace spec:init-command, spec:observability-convergence` near image build loop
  • ☐ 3.8 Emit events:
  • ☐ 4.1 Create `docs/cheatsheets/podman-logging.md` with sections:
  • ☐ 4.2 Create `docs/cheatsheets/container-lifecycle.md` with sections:
  • ☐ 4.3 Add "Sources of Truth" sections to all three specs referencing new cheatsheets:
  • ☐ 5.1 Add `// @trace spec:cli-diagnostics` annotation in `handle_diagnostics()` function header
  • ☐ 5.2 Add `// @trace spec:observability-convergence` near event emission code (logging module)
  • ☐ 5.3 Add `// @trace spec:init-command` near browser image build loop in `init.rs`
  • ☐ 5.4 Add `// @trace spec:init-command, spec:observability-convergence` in init event emitters
  • ☐ 5.5 Verify no implementation code lacks @trace annotations (grep for function definitions implementing specs)
  • ☐ 6.1 Ensure all events emitted by diagnostics and init include:
  • ☐ 6.2 Verify events are queryable: `grep 'spec="cli-diagnostics"' logs.txt` returns all diagnostics events
  • ☐ 6.3 Test that missing containers is logged as an event (not silent) with `spec=` attribute
  • ☐ 7.1 Ensure `./build.sh --install` succeeds (builds tray AppImage, copies to ~/Applications/)
  • ☐ 7.2 Ensure `tillandsias --init --debug` builds all 6 images (proxy, forge, git, inference, browser-core, browser-framework)
  • ☐ 7.3 Verify browser images are properly tagged and inspectable: `podman images | grep browser`
  • ☐ 7.4 Test `tillandsias /var/home/machiyotl/src/visual-chess --diagnostics`:
  • ☐ 7.5 Test full end-to-end: `./build.sh --install && tillandsias --init --debug && tillandsias /var/home/machiyotl/src/visual-chess --diagnostics`
  • ☐ 8.1 Verify all three specs are complete and syntactically valid (openspec validate)
  • ☐ 8.2 Add commit message with GitHub search URL:
  • ☐ 8.3 Prepare delta specs for archival (no changes to main specs needed; new capability specs created, init-command modified)
  • ☐ 8.4 After implementation complete, run `/opsx:verify` to confirm code matches specs
  • ☐ 8.5 After verification, run `/opsx:archive` to finalize change and sync delta specs to main
  • ☐ 9.1 Unit test: `test_diagnostics_container_labeling()` — verify log prefix format
  • ☐ 9.2 Unit test: `test_diagnostics_no_containers()` — verify graceful error when no containers exist
  • ☐ 9.3 Integration test: `test_init_builds_all_six_images()` — verify init completes all images
  • ☐ 9.4 Integration test: `test_init_debug_mode_skips_timeouts()` — verify extended timeout in --debug
  • ☐ 9.5 Manual test: Run full end-to-end flow and capture logs showing observable convergence
  • ☐ 9.6 Verify build passes: `./build.sh --test` (no regressions)
  • ☐ 9.7 Verify cross-platform (Linux): `cargo clippy --workspace` (no warnings)

Read source record ↗

changeProposal: Tray Minimal UXOpen tasks26 checked · 1 open

1. **Launch state**: Show only: - `<Checklist> Verifying environment ...` - Divider - `Version + Attribution` - `Quit Tillandsias`

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1.1 Modify `build_tray_menu()` in `src-tauri/src/menu.rs` to show only 4 elements at launch
  • ☑ 1.2 Add `Verifying environment ...` item with checklist icon (Unicode: ☑ or ✅)
  • ☑ 1.3 Add divider after the status item
  • ☑ 1.4 Add `Version X.Y.Z + Attributions` item (static, non-interactive)
  • ☑ 1.5 Add `Quit Tillandsias` item (always visible, always enabled)
  • ☑ 1.6 Add `@trace spec:tray-minimal-ux` annotation near menu building code
  • ☑ 2.1 Create status update function that modifies the first element based on build state
  • ☑ 2.2 Implement `<Checklist><Network> Building enclave ...` when proxy is ready
  • ☑ 2.3 Implement `<Checklist><Network><Mirror> Building git mirror ...` when git is ready
  • ☑ 2.4 Implement `<Checklist><Network><Mirror><Browser><DebugBrowser> ✓ Environment OK` when all images built
  • ☑ 2.5 Implement `<WhiteRose> Unhealthy environment` when any build fails
  • ☑ 2.6 Add `@trace spec:tray-minimal-ux` annotations to status update logic
  • ☑ 3.1 After successful build, add `<Home> ~/src >` if projects exist
  • ☑ 3.2 After successful build + GitHub auth, add `<Cloud> Cloud >` for remote projects
  • ☑ 3.3 If no GitHub credentials, show `<Key> GitHub login` instead of Cloud
  • ☑ 3.4 Hide Projects/Cloud items until environment is fully ready
  • ☑ 3.5 Add `@trace spec:tray-minimal-ux` annotations to conditional menu logic
  • ☑ 4.1 Modify project click handler to launch OpenCode Web container
  • ☑ 4.2 Add logic to clone remote project to local before launching (if not cloned)
  • ☑ 4.3 Once container is healthy, launch safe browser in `tillandsias-chromium-core` container
  • ☑ 4.4 Use `tillandsias-browser-tool` to communicate between tray and browser container
  • ☑ 4.5 Add `@trace spec:tray-minimal-ux` annotations to project click handler
  • ☑ 5.1 Add startup routine to clean up stale `tillandsias-*` containers
  • ☑ 5.2 Remove stopped/orphaned containers not tracked in TrayState
  • ☑ 5.3 Allow new containers to regenerate without conflicts
  • ☑ 5.4 Add `@trace spec:tray-minimal-ux` annotation to cleanup logic
  • ☐ 6.1 Sync delta spec from `openspec/changes/tray-minimal-ux/specs/tray-ux/spec.md` to `openspec/specs/tray-ux/spec.md`

Read source record ↗

changeChromium browser isolationNo checklist0 checked · 0 open

Tillandsias' tray app currently lacks hardened browser isolation for displaying project UIs and agent web sessions. Browser windows inherit host credentials, network access, and filesystem privileges — creating attack surface and data leakage vectors. We need a two-tier containerized Chromium architecture that isolates browser processes completely from the h

No task record was present in this archived change.

Read source record ↗

changeCheatsheets license tieredOpen tasks58 checked · 9 open

The just-archived `cheatsheet-source-layer` mandates verbatim source storage in the repo, which (a) bloats git for material we have no clear redistribution rights to, (b) creates a license-compliance trap for any source NOT explicitly allowlisted, and (c) makes refresh a manual chore that drifts over time. We need a model where license-friendly material is b

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1.1 Update `cheatsheets/runtime/cheatsheet-frontmatter-spec.md` to v2: add `tier`, `summary_generated_by`, `bundled_into_image`, `image_baked_sha256`, `structural_drift_fingerprint`, `local`, `package`, `pull_recipe`, `committed_for_project`, `shadows_forge_default`, `override_reason`, `override_consequences`, `override_fallback` to the schema; document the tier-conditional matrix (which fields are required/forbidden per tier); bump `last_verified` and add provenance entry for the v2 schema.
  • ☑ 1.2 Update `cheatsheets/TEMPLATE.md` to reflect the v2 frontmatter (default tier left blank for inference; commented placeholder fields for each tier-conditional field).
  • ☑ 1.3 Author `cheatsheets/runtime/cheatsheet-tier-system.md` describing the three tiers, their contracts, and worked examples for each (with full provenance per the cheatsheet provenance rule).
  • ☑ 1.4 Author `cheatsheets/runtime/cheatsheet-pull-on-demand.md` documenting the stub format (`### Source`, `### Materialize recipe`, `### Generation guidelines`); include a worked recipe for `docs.oracle.com` JDK API as the canonical example.
  • ☑ 1.5 Author `cheatsheets/runtime/cheatsheet-crdt-overrides.md` documenting the project-committed shadow flow, the four required override fields, and the runtime banner contract.
  • ☑ 1.6 Author `cheatsheets/runtime/cheatsheet-lifecycle.md` rendering the AUTHORED → BUNDLED-BAKED → LOADED → HIT/MISS → PULLED → REFINED → PROMOTED → RE-VERIFIED loop diagram; cite the `cheatsheets-license-tiered` spec.
  • ☑ 2.1 `git mv cheatsheet-sources/license-allowlist.toml cheatsheets/license-allowlist.toml` (preserves history).
  • ☑ 2.2 Add `default_tier`, `last_evaluated`, `evaluated_by` fields to every existing `[domains."..."]` entry in the relocated TOML; default the inference for currently-allowlisted bundleable domains to `default_tier = "bundled"`; default off-allowlist or do-not-bundle entries to `default_tier = "pull-on-demand"`.
  • ☑ 2.3 Add new `[domains."docs.oracle.com"]` entry with `default_tier = "pull-on-demand"`, `evaluated_by = "hand-curated"`, `license = "oracle-ftc"`, `redistribution = "do-not-bundle"`.
  • ☑ 2.4 Update header comment in the TOML to document the CRDT semantics (`evaluated_by`, `last_evaluated`, telemetry `license_drift` events).
  • ☑ 3.1 Extend `scripts/fetch-cheatsheet-source.sh` with a `--tier=bundled` mode: read the cheatsheet frontmatter to filter by tier, output to a cache-key-named directory under `$CACHE_DIR/cheatsheet-source-bake/<key>/`, write `.meta.yaml` sidecars per file (preserving existing fetch semantics: GitHub blob rewrite, IETF .txt preference).
  • ☑ 3.2 Add structural-drift fingerprint computation to the fetcher: extract `<h1>+<h2>+<h3>` text via `htmlq` (preferred; check availability) or a 30-line Python helper; output `SHA256(joined)[:16]`; store in the cache-key directory's per-file sidecar.
  • ☑ 3.3 Implement the cache-key derivation: `SHA-256( sorted(union(URLs)) || --max-age-days flag )`; document the algorithm in a comment block at the top of the new code.
  • ☑ 3.4 Add the fetch-and-bake stage to `scripts/build-image.sh forge` immediately before the existing `cheatsheets/` COPY: invoke `scripts/fetch-cheatsheet-source.sh --tier=bundled` on cache miss, stage `<key>/` as the build context's `cheatsheet-sources/` subtree, COPY into the image at `/opt/cheatsheet-sources/`.
  • ☑ 3.5 Wire the `--max-age-days N` and `--refresh-sources` flags through `build-image.sh` to the fetcher; default 30 days for local builds; CI passes 7.
  • ☑ 3.6 On every successful bundled fetch, write `image_baked_sha256` and `structural_drift_fingerprint` to `<build-context>/.cheatsheets-meta/<category>/<name>.frontmatter.json` (side-channel; avoids rewriting the cheatsheet inside the image).
  • ☑ 3.7 Implement network-failure-non-blocking behaviour: on fetch failure, reuse the previous cache key directory if any; emit `WARN: network unreachable, reusing cache key <prev>`; do NOT fail the build; do NOT bump `last_verified`.
  • ☑ 4.1 Add package-manifest discovery to `scripts/check-cheatsheet-sources.sh`: try `flake.nix` `contents` parsing first, then `images/default/Containerfile` `dnf install` lines, then `images/default/distro-packages.txt`; accept the first source that succeeds.
  • ☑ 4.2 Add `package:` validation: for each `tier: distro-packaged` cheatsheet, confirm the declared package is in the discovered manifest; emit ERROR if missing with `ERROR: distro-packaged cheatsheet references missing package: <name>`.
  • ☐ 4.3 Add post-build in-image validation: confirm the `local:` path (e.g., `/usr/share/javadoc/...`) exists inside the built forge image; emit ERROR if missing.
  • ☑ 4.4 Author `cheatsheets/build/distro-packaged-cheatsheets.md` (with full provenance) describing how to add a new distro-packaged cheatsheet (pick a doc package, add to forge image manifest, author cheatsheet with `tier: distro-packaged`).
  • ☑ 5.1 Add stub-completeness validation to `scripts/check-cheatsheet-sources.sh`: confirm `## Pull on Demand` section exists with `### Source`, `### Materialize recipe`, `### Generation guidelines` sub-headings; confirm `### Materialize recipe` is a non-empty fenced bash block; confirm frontmatter contains `pull_recipe: see-section-pull-on-demand` (any other value = ERROR).
  • ☑ 5.2 Add license declaration validation: confirm `### Source` block contains a license SPDX or short identifier AND a canonical license URL; emit ERROR if missing.
  • ☑ 5.3 Define the per-project pull cache layout (`~/.cache/tillandsias/cheatsheets-pulled/<project>/<host>/<path>`) in shell helper code under `images/default/lib-common.sh` — agents read the path via an exported env var (e.g., `TILLANDSIAS_PULL_CACHE`).
  • ☑ 5.4 Implement the tiered RAMDISK soft-cap detection at tray startup: read `/proc/meminfo` `MemTotal`; classify as Modest (<8 GiB → 64 MB) / Normal (8–32 GiB → 128 MB) / Plentiful (≥32 GiB → 1024 MB); emit the resolved cap to the forge launch context as `forge.pull_cache_ram_mb`.
  • ☑ 5.5 Wire `forge.pull_cache_ram_mb` override from `~/.config/tillandsias/config.toml` (override wins over auto-detection).
  • ☑ 5.6 Implement tmpfs-with-disk-spillover for the pull cache: tmpfs under `~/.cache/tillandsias/cheatsheets-pulled/<project>/` capped at the soft cap; auto-spill to disk under the same path when cap is exceeded; LRU eviction operates within the per-project subtree only. Implementation note: chose path 1 (pure-userspace LRU on a disk-backed per-project root) — see `images/default/lib-common.sh::_pull_cache_evict_lru_if_over_cap` and rationale comment block. Path 2 (real `--tmpfs` overlay with merged-view spillover) deferred until profiling shows path 1 too slow.
  • ☑ 6.1 Extend `populate_hot_paths()` in `images/default/lib-common.sh` to merge `<project>/.tillandsias/cheatsheets/` into `/opt/cheatsheets/` AFTER copying `/opt/cheatsheets-image/`; project-committed files at the same path overwrite forge defaults.
  • ☑ 6.2 Add shadow detection to `populate_hot_paths()`: for each project-committed file, check if a same-pathed file exists in `/opt/cheatsheets-image/`; if yes, parse the project frontmatter and emit one banner line per shadow: `[cheatsheet override] <path> → project version (reason: <first line of override_reason>)`.
  • ☑ 6.3 Add shadow validation to `scripts/check-cheatsheet-sources.sh`: for each cheatsheet under `<project>/.tillandsias/cheatsheets/` (or `cheatsheets/`-relative paths in project artifacts), check `shadows_forge_default` presence; if set, ERROR if any of `override_reason`, `override_consequences`, `override_fallback` is missing or empty.
  • ☑ 6.4 Add a runtime renderer (in `populate_hot_paths()` or a small helper) that surfaces `override_reason`, `override_consequences`, `override_fallback` as a header block at the top of every shadowed cheatsheet's body inside `/opt/cheatsheets/<path>` (e.g., as a fenced `> [!OVERRIDE]` callout block before `## Quick reference`).
  • ☑ 7.1 Author `images/default/external-logs.yaml` declaring `role: cheatsheet-telemetry` with `lookups.jsonl` (format `jsonl`, `rotate_at_mb: 10`, `written_by: forge-agent (claude / opencode / opsx)`).
  • ☑ 7.2 Update `images/default/Containerfile` (and/or `flake.nix`) to bake `external-logs.yaml` at `/etc/tillandsias/external-logs.yaml` per the `external-logs-layer` contract.
  • ☑ 7.3 Update the forge `ContainerProfile` in `src-tauri/src/profile.rs` (or wherever profile types live) to set `external_logs_role: Some("cheatsheet-telemetry")`.
  • ☑ 7.4 Update the launcher (`src-tauri/src/handlers.rs` or `runner.rs`) to bind-mount `~/.local/state/tillandsias/external-logs/cheatsheet-telemetry/` RW at `/var/log/tillandsias/external/cheatsheet-telemetry/` inside every forge container (preserves the existing producer-mount pattern from `git-service`).
  • ☑ 7.5 Extend `cheatsheets/runtime/external-logs.md` with the `cheatsheet-telemetry` role's JSONL schema (every field documented with type and meaning); add an example event for each `resolved_via` value.
  • ☑ 7.6 Document the agent-side write contract (where in opencode/claude/opsx instructions to land the "emit a JSONL line for every cheatsheet consultation" guidance) — add to `cheatsheets/agents/opencode.md` and `cheatsheets/agents/claude-code.md` under a new "Telemetry obligations" section.
  • ☑ 8.1 Update `scripts/regenerate-cheatsheet-index.sh` to read each cheatsheet's frontmatter `tier` and validation state and append the correct badge: `[bundled, verified: <sha8>]`, `[bundled, partial-verify]`, `[distro-packaged: <package>]`, `[distro-packaged: MISSING]`, `[pull-on-demand: stub]`, `[pull-on-demand: project-committed]`.
  • ☑ 8.2 Add a stripped-down shell version of the index regeneration to `populate_hot_paths()` (or call a small in-image helper) so `/opt/cheatsheets/INDEX.md` is re-rendered post-merge with project-committed entries and pulled-materialization `[pulled]` lines.
  • ☑ 8.3 Update `cheatsheets/INDEX.md` (the host-tracked file) once after the new badges land so the diff is one focused commit.
  • ☑ 9.1 Classify every existing cheatsheet under `cheatsheets/**/*.md` by tier: bundleable (allowlist `default_tier = bundled` and license permits) vs pull-on-demand. Output the classification as a CSV in the migration commit message for review.
  • ☑ 9.2 Add `tier:` to every cheatsheet's frontmatter (matching the classification); add `summary_generated_by: hand-curated` (default for existing); add `bundled_into_image: true|false` per tier.
  • ☑ 9.3 Author `## Pull on Demand` sections for each cheatsheet that classifies as pull-on-demand: copy from the cheatsheet's existing `## Provenance` URL list into the new `### Source`; author a minimal `### Materialize recipe` (curl + tar/unzip as appropriate); fill `### Generation guidelines` with per-cheatsheet hints; set frontmatter `pull_recipe: see-section-pull-on-demand`. Stubs marked `<!-- TODO: hand-curate the recipe before next forge build -->` for follow-up by domain experts; license fields populated with `see-license-allowlist` placeholder pending per-cheatsheet provenance refinement.
  • ☐ 9.4 Run `scripts/build-image.sh forge` to populate `image_baked_sha256` and `structural_drift_fingerprint` for every bundled cheatsheet (build-time injection); verify the side-channel `.cheatsheets-meta/` is produced.
  • ☐ 9.5 Re-run `scripts/regenerate-cheatsheet-index.sh` and commit the updated `cheatsheets/INDEX.md`.
  • ☑ 10.1 Empty the host repo's `cheatsheet-sources/` directory: `git rm -r cheatsheet-sources/*` excluding `.gitkeep-tombstone`; commit `cheatsheet-sources/.gitkeep-tombstone` with header `@tombstone superseded:cheatsheets-license-tiered — the verbatim host-bundled source layer is replaced by image-baked /opt/cheatsheet-sources/ for the bundled tier and per-project pull cache for the pull-on-demand tier. Kept for traceability through 0.1.<N+3>.x. Final removal in 0.1.<N+3>.x per the three-release retention rule.`
  • ☑ 10.2 Add `cheatsheet-sources/` to `.gitignore` (covers any local cache pollution).
  • ☑ 10.3 Add `@tombstone obsolete:cheatsheet-source-layer — superseded by build-time fetch-and-bake in scripts/build-image.sh forge. Safe to delete after 0.1.<N+3>.x.` to `scripts/regenerate-source-index.sh`; comment out the script body but keep the file for three releases.
  • ☑ 10.4 Add `@tombstone obsolete:cheatsheet-source-layer — superseded by build-time meta side-channel injection in build-image.sh. Safe to delete after 0.1.<N+3>.x.` to `scripts/bind-provenance-local-paths.sh`; comment out the body but keep the file.
  • ☑ 10.5 Add `@tombstone obsolete:cheatsheet-source-layer — refresh moves to build-time --refresh-sources for bundled tier and agent-driven materialization for pull-on-demand. Safe to delete after 0.1.<N+3>.x.` to `scripts/refresh-cheatsheet-sources.sh`; comment out the body.
  • ☑ 10.6 Audit `src-tauri/src/handlers.rs` and any other Rust code for references to a `forge.mount_source_layer` config option (the legacy host-mount opt-in); add `// @tombstone obsolete:cheatsheet-source-layer` markers and remove the option's effect (parsing it WARNs and ignores).
  • ☐ 11.1 Build the forge image via `scripts/build-image.sh forge`; capture the size delta against the previous version (target ≤ +300 MB per the design's open question 1 resolution).
  • ☐ 11.2 Confirm `/opt/cheatsheet-sources/<host>/<path>` exists for every bundled cheatsheet's cited URL: `podman run --rm <image> find /opt/cheatsheet-sources -maxdepth 3 -type f | wc -l` ≥ expected count.
  • ☐ 11.3 Confirm `/opt/cheatsheets/INDEX.md` (post-`populate_hot_paths()`) renders the tier-aware badges correctly inside the forge: launch a forge container, `cat /opt/cheatsheets/INDEX.md | grep -E '\[bundled|distro-packaged|pull-on-demand|pulled\]' | wc -l` matches the cheatsheet count.
  • ☐ 11.4 Confirm `/etc/tillandsias/external-logs.yaml` is baked: `podman run --rm <image> cat /etc/tillandsias/external-logs.yaml | grep '^role: cheatsheet-telemetry$'` exits 0.
  • ☐ 11.5 Confirm the forge launch banner emits `[cheatsheet override]` lines when project-committed shadows are present (test with a fixture project under a temp dir).
  • ☑ 12.1 Update `scripts/hooks/pre-commit-openspec.sh` (or the cheatsheet-specific hook) to run `scripts/check-cheatsheet-sources.sh --no-sha` and surface ERRORs as non-blocking warnings.
  • ☑ 12.2 Confirm the hook still exits 0 even when ERRORs are present (CRDT-convergence philosophy preserved).
  • ☑ 12.3 Add a CI job (or extend an existing one) that runs `scripts/check-cheatsheet-sources.sh` (with SHA check enabled) and FAILS on ERROR; this is the gating check for releases.
  • ☑ 13.1 Add `# @trace spec:cheatsheets-license-tiered` to every shell file modified by this change (`scripts/build-image.sh`, `scripts/fetch-cheatsheet-source.sh`, `scripts/check-cheatsheet-sources.sh`, `scripts/regenerate-cheatsheet-index.sh`, `images/default/lib-common.sh`).
  • ☑ 13.2 Add `// @trace spec:cheatsheets-license-tiered` to every Rust function modified by this change (`src-tauri/src/handlers.rs` external-logs role wiring, profile changes, `populate_hot_paths()` invocation if any).
  • ☑ 13.3 Add `# @cheatsheet runtime/cheatsheet-tier-system.md` (and the other new cheatsheets) to the same files where they informed implementation.
  • ☑ 13.4 Confirm `git grep '@trace spec:cheatsheets-license-tiered'` returns hits in shell, Rust, and markdown; commit message includes the GitHub search URL `https://github.com/8007342/tillandsias/search?q=%40trace+spec%3Acheatsheets-license-tiered&type=code` per the project commit convention.
  • ☑ 14.1 Run `openspec validate cheatsheets-license-tiered --strict`; fix every reported issue.
  • ☑ 14.2 Run `/opsx:verify cheatsheets-license-tiered` and address gaps; confirm spec, design, tasks, and implementation converge.
  • ☑ 14.3 Run `cargo test --workspace` and `./build.sh --test` to confirm no regression in the existing test suite.
  • ☐ 14.4 Build the forge image one final time, run a smoke test (launch forge, `cat /opt/cheatsheets/INDEX.md`, materialize one pull-on-demand recipe, confirm `lookups.jsonl` records the events), then archive via `/opsx:archive cheatsheets-license-tiered` and run `./scripts/bump-version.sh --bump-changes`.

Read source record ↗

changeForge build observability and diagnosticsOpen tasks0 checked · 58 open

Forge image builds take 2+ hours due to re-downloading ~6.5GB of packages on every build, with no visibility into which steps are slowest or largest. Developers can't identify optimization opportunities or diagnose failures in the build or runtime. Container health issues surface only after minutes of silent operation, requiring manual inspection.

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☐ 1.1 Add `struct BuildPhase` to `src-tauri/src/image_builder.rs` with fields: name, start_secs, end_secs, bytes_downloaded
  • ☐ 1.2 Add `Vec<BuildPhase>` field to `ImageBuilder` struct to accumulate phases during build
  • ☐ 1.3 Add `fn parse_build_output_for_metrics()` helper to extract download sizes and timestamps from podman build output
  • ☐ 2.1 Identify major phase boundaries in Containerfile (packages, tools, agents, summarizers, finalization)
  • ☐ 2.2 Inject `RUN echo "{timestamp}:{phase_name}:start" >&2` before each major phase
  • ☐ 2.3 Inject `RUN echo "{timestamp}:{phase_name}:end" >&2` after each major phase
  • ☐ 2.4 Ensure timing markers are captured in podman build output
  • ☐ 3.1 Modify `ImageBuilder::build_image()` to parse timing markers and download sizes from podman build output
  • ☐ 3.2 Calculate phase duration_secs = end_timestamp - start_timestamp for each phase
  • ☐ 3.3 Aggregate bytes_downloaded for each phase from grep of `Downloading`, `pulling`, `Installing` lines
  • ☐ 3.4 Store phases in `self.build_phases` Vec at completion
  • ☐ 4.1 After successful build, emit tracing event with spec="forge-build-metrics", phase_duration_secs, download_bytes
  • ☐ 4.2 Format metrics as structured JSON for programmatic analysis
  • ☐ 4.3 Include @trace spec:forge-build-metrics annotations in code
  • ☐ 5.1 Add `fn analyze_build_metrics(phases: &[BuildPhase]) -> Vec<Suggestion>` function to `init.rs`
  • ☐ 5.2 Implement rule: if package_install > 45% of total → suggest pre-populating mirror or proxy cache
  • ☐ 5.3 Implement rule: if total_bytes > 1GB → suggest enabling Squid proxy cache
  • ☐ 5.4 Implement rule: identify slowest phase and suggest layer reordering
  • ☐ 5.5 Call `analyze_build_metrics()` after build completion and print suggestions to stdout with emoji prefixes
  • ☐ 6.1 Add `--diagnostics` CLI flag to runner (CLI mode and tray app launcher)
  • ☐ 6.2 Modify argument parsing in `src-tauri/src/runner.rs` to recognize `--diagnostics` flag
  • ☐ 6.3 When flag is set, skip normal startup logic and enter diagnostics mode instead
  • ☐ 7.1 Implement `fn discover_running_containers(project: &str) -> Vec<ContainerInfo>` using podman API
  • ☐ 7.2 Filter containers by tag pattern: `tillandsias-<project>-*`
  • ☐ 7.3 Return ContainerInfo struct with container_id, service_name fields
  • ☐ 7.4 Call discovery on startup and periodically (every 5 seconds) to detect new containers
  • ☐ 8.1 For each discovered container, spawn async task: `podman exec <container_id> tail -f /strategic/service.log 2>/dev/null`
  • ☐ 8.2 Prefix output lines with `[<service_name>] ` for disambiguation
  • ☐ 8.3 Handle offline containers: emit `[<service_name>] [offline]` and continue with other containers
  • ☐ 8.4 Handle container exit: skip on next discovery cycle, don't error out
  • ☐ 8.5 Clean termination: on Ctrl+C, kill all tail processes and exit with code 0
  • ☐ 9.1 Create `/strategic/service.log` in proxy container entrypoint
  • ☐ 9.2 Write squid startup status to log (timestamp, version, listening port)
  • ☐ 9.3 Periodically log incoming request summary (requests/sec, total bytes proxied)
  • ☐ 9.4 Implement log rotation: max 100MB, rotate to `.1`, discard older rotations
  • ☐ 10.1 Create `/strategic/service.log` in forge entrypoint
  • ☐ 10.2 Log user setup completion and environment readiness indicators
  • ☐ 10.3 Log when OpenCode/Claude/OpenSpec CLI tools are ready
  • ☐ 10.4 Implement log rotation with TILLANDSIAS_LOG_SIZE environment variable support (default 100MB)
  • ☐ 11.1 Create `/strategic/service.log` in git-service entrypoint
  • ☐ 11.2 Log startup completion and git daemon listening status
  • ☐ 11.3 Log authenticated push events (project, branch, committer)
  • ☐ 11.4 Implement log rotation (max 100MB)
  • ☐ 12.1 Create `/strategic/service.log` in inference entrypoint
  • ☐ 12.2 Log ollama startup and model loading progress
  • ☐ 12.3 Log health check results (models loaded, VRAM available)
  • ☐ 12.4 Implement log rotation (max 100MB)
  • ☐ 13.1 Test build metrics collection with forge image build, verify phase aggregation
  • ☐ 13.2 Test optimization suggestions: verify rules fire correctly for different phase ratios
  • ☐ 13.3 Test `--diagnostics` flag with running stack, verify all logs stream correctly
  • ☐ 13.4 Test offline container handling: stop a container during diagnostics, verify [offline] message
  • ☐ 13.5 Test log rotation: write >100MB to strategic log, verify rotation occurs
  • ☐ 13.6 Test environment variable override: set TILLANDSIAS_LOG_SIZE, verify custom limit applied
  • ☐ 14.1 Add @trace spec:forge-build-metrics annotations to metrics collection code
  • ☐ 14.2 Add @trace spec:runtime-diagnostics annotations to diagnostics implementation
  • ☐ 14.3 Add @trace spec:init-command annotations to metrics/analysis code in init.rs
  • ☐ 14.4 Update CLI help text to document `--diagnostics` flag and purpose
  • ☐ 14.5 Document `/strategic/service.log` convention in architecture guide

Read source record ↗

changeWindows etw logging completed 2026 04 27Open tasks0 checked · 27 open

Tillandsias on Windows runs as a system tray application with no visible console window. When something goes wrong — a podman machine fails to start, a container launch errors out, or a forge image build fails — the user has no way to discover the problem through the standard Windows diagnostic tool: Event Viewer. All log output goes to a file in `%LOCALAPPD

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☐ 1.1 Add `tracing-layer-win-eventlog` to `src-tauri/Cargo.toml` under `[target.'cfg(windows)'.dependencies]`
  • ☐ 1.2 Verify it compiles alongside existing `windows-sys` dependency (both must use compatible `windows-sys` versions)
  • ☐ 1.3 Run `./build-windows.sh --check` to confirm cross-compilation still passes
  • ☐ 2.1 Create `src-tauri/src/windows_eventlog.rs` module with `#[cfg(target_os = "windows")]` gate
  • ☐ 2.2 Implement a custom `tracing_subscriber::Layer` that:
  • ☐ 2.3 Map tracing levels to Event Log types: ERROR -> `EVENTLOG_ERROR_TYPE`, WARN -> `EVENTLOG_WARNING_TYPE`, accountability INFO -> `EVENTLOG_INFORMATION_TYPE`
  • ☐ 2.4 Add `try_init() -> Option<impl Layer>` function that returns `None` if event source registration fails
  • ☐ 3.1 In `src-tauri/src/logging.rs`, add `#[cfg(target_os = "windows")]` block that calls `windows_eventlog::try_init()`
  • ☐ 3.2 Compose the optional layer into the subscriber stack: `.with(eventlog_layer)` where `eventlog_layer: Option<_>`
  • ☐ 3.3 Verify that `None` (unregistered source) does not break the subscriber chain
  • ☐ 3.4 Add the module declaration in `main.rs`: `#[cfg(target_os = "windows")] mod windows_eventlog;`
  • ☐ 4.1 In `src-tauri/nsis/` or Tauri's NSIS config, add registry key creation for the "Tillandsias" event source during install:
  • ☐ 4.2 Add registry key removal to the uninstaller section
  • ☐ 4.3 Document the manual registration command for development: `New-EventLog -LogName Application -Source Tillandsias` (PowerShell as admin)
  • ☐ 5.1 Add `// @trace spec:windows-event-logging` to the new `windows_eventlog.rs` module
  • ☐ 5.2 Add `// @trace spec:windows-event-logging` to the Windows-specific block in `logging.rs`
  • ☐ 5.3 Update `// @trace spec:logging-accountability` in `logging.rs` to include `, spec:windows-event-logging`
  • ☐ 6.1 Write unit tests for the Event Log message formatting (can run on any platform — test the formatting function, not the Windows API call)
  • ☐ 6.2 Write a `#[cfg(target_os = "windows")]` integration test that:
  • ☐ 6.3 Manual test on Windows: run Tillandsias, trigger an error (e.g., stop podman machine), check Event Viewer
  • ☐ 6.4 Verify Linux/macOS builds are unaffected: `./build.sh --check` and `./build-osx.sh --check`
  • ☐ 7.1 Add a `docs/cheatsheets/windows-event-viewer.md` cheatsheet with:
  • ☐ 7.2 Update `docs/cheatsheets/logging-levels.md` to mention Event Viewer as a Windows-specific output surface
  • ☐ 8.1 Run `./build-windows.sh --check` — Windows cross-compilation passes
  • ☐ 8.2 Run `./build.sh --test` — Linux tests pass, no Windows code compiled
  • ☐ 8.3 Run `./build-osx.sh --check` — macOS compilation passes
  • ☐ 8.4 Run `cargo clippy --workspace --target x86_64-pc-windows-msvc` (if available) — no warnings

Read source record ↗

Website changes 13

Local site contracts and their task records, including this page’s change.

siteadd-container-frameworkOpen tasks0 checked · 15 open

tillandsias.org hosts a mostly-static HTML website (the apex `tillandsias.org` is the canonical host — the shortest URL for users — and `www.tillandsias.org` redirects to it) on infrastructure that may live anywhere in the world, on a host whose public IP can change. The site must be served over HTTPS with automatic certificate issuance and renewal, and the

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☐ 1.1 Initialize `var/html/` with placeholder `index.html`
  • ☐ 1.2 Add release tooling producing a durable zip of `var/html/*` with the
  • ☐ 1.3 Wire release artifact consumption into the image build
  • ☐ 2.1 Author multi-stage CONTAINERFILE based on `httpd:2.4`
  • ☐ 2.2 Install certbot + certbot-dns-cloudflare
  • ☐ 2.3 Add `cloudflare-ddns` updater script
  • ☐ 2.4 Add entrypoint scripts (cert issuance/renewal, ddns loop, httpd start)
  • ☐ 2.5 Run as non-root runtime user; seal code/config layers
  • ☐ 3.1 Apache config: `www.tillandsias.org` 301 redirect to the apex, with
  • ☐ 3.2 Apache config: apex :443 virtual host with managed cert
  • ☐ 3.3 certbot DNS-01 wildcard issuance + renewal timer + httpd reload
  • ☐ 4.1 Document CloudFlare token scope (Zone:DNS:Edit) and creation
  • ☐ 4.2 Inject token at runtime via Podman secret, read-only, SELinux-labeled
  • ☐ 5.1 Rootless Podman + systemd Quadlet unit, ports 80/443, SELinux enabled
  • ☐ 5.2 Validate on Fedora Silverblue; prep amd64; plan arm64 manifest

Read source record ↗

siteadd-crdt-and-lamport-pagesOpen tasks5 checked · 1 open

The site explains CentiColons, the measurement, but says nothing about the other pillar the measurement stands on: the shared plan is an append-only pile of evidence that many agents on many hosts write to without conflicts, folded into one readable view. A reader who asks "how can that many agents share one plan?" currently has no page to be sent to, and th

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ Read the CentiColons page, figure registry and menu wiring; match them.
  • ☑ Draw `append-fold`, `commit-dag` and `lamport-exchange` in `scripts/figures.py`.
  • ☑ Write `crdt_view()` and `lamport_view()` in `scripts/build-matrix.py`; register both placeholders, menu entries and deep links.
  • ☑ Check every technical statement against the ledger code and methodology at the site's checked release (fragments module, distributed-work methodology, land queue).
  • ☑ Checked build prints `ok:checked-build`.
  • ☐ Operator review of the two articles before archival.

Read source record ↗

siteadd-level-page-specsOpen tasks25 checked · 7 open

The site's five explanation pages (`docs/matrix/level-1..5-*.md`, rendered into the single `var/html/index.html`) are the project's front door, and each is a long-lived contract of its own: a distinct audience, a distinct honesty frame (GREEN/RED/PATH), and a set of claims that must stay true against the release tag the level pins. Today nothing declares wha

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1.1 Create change directory `openspec/changes/add-level-page-specs/` with
  • ☑ 1.2 Write `proposal.md`, `design.md`, `tasks.md`
  • ☑ 1.3 Create delta spec directories `specs/site/level-common/` and
  • ☑ 2.0 `specs/site/level-common/spec.md` — rules shared by every level:
  • ☑ 2.1 `specs/site/level-1/spec.md` — "Like I'm 5" purpose/content contract
  • ☑ 2.2 `specs/site/level-2/spec.md` — "I barely understand my phone" contract
  • ☑ 2.3 `specs/site/level-3/spec.md` — "I'm a power user" contract
  • ☑ 2.4 `specs/site/level-4/spec.md` — "I'm a Cyber Security expert" contract
  • ☑ 2.5 `specs/site/level-5/spec.md` — "I'm a MathWiz / Hacker" contract: own
  • ☑ 2.6 Strip dated findings, order ids, ledger line numbers, file counts and
  • ☑ 2.7 `specs/site/navigation/spec.md` — the site as a whole: the hamburger
  • ☑ 3.1 `openspec validate --changes` passes. Recorded 2026-09-05 (last run,
  • ☐ 3.2 `var/html/index.html` rebuilds byte-identical. The check:
  • ☐ 3.3 (optional, on adoption) sync specs to `openspec/specs/site/level-<n>/`
  • ☑ 4.1 Doorman-key RED/PATH pair. The world-readable CA key exposure is
  • ☐ 4.2 Badge word-list premise. The cited issue span reasons from two menu
  • ☑ 4.3 Credential-store wording. The login token lands in the guest Vault,
  • ☑ 4.4 Unsigned-package scoping. The release workflow withholds only the
  • ☑ 4.5 Anatomy. The "five members" list was stale against the
  • ☑ 4.6 Global proxy block RED. At the pin `--init` deletes the global engine
  • ☑ 4.7 macOS cache destroyers RED. The shipped uninstaller preserves the VM
  • ☑ 4.8 Enclave network RED. At the pin `scripts/orchestrate-enclave.sh`
  • ☐ 4.9 Vault share caveat. The runtime also writes a fallback share file
  • ☐ 4.10 Reset-guest anchor. The footnote for what `--reset-guest` keeps
  • ☑ 4.11 Linux boundary PATH. "Nothing is recorded at all" overstated: the
  • ☑ 4.12 Corpus count. "415 litmus files" was the directory-entry count, not
  • ☑ 4.13 Provenance-research footnote. The verifier-needs-no-identity claim
  • ☑ 4.14 Bar-raise follow-up. The diff-scoped false-pass bar-raise the PATH
  • ☑ 4.15 Push-validation scope. "Nothing validating a push server-side" is
  • ☐ 4.16 Hardening section at the next pin bump. At the pin the launch-argv
  • ☑ 4.17 Delta change at the current pin. The 2026-09-05 re-verification of
  • ☐ 4.18 Delta change at the next pin bump, as its own OpenSpec change: the

Read source record ↗

siteExplain repository refinement as an empirical modelChecklist complete6 checked · 0 open

Add a presentation slide between the uncertainty and CRDT slides, a shared figure, and matched explanations at all five audience levels. The new material presents a repository-plus-prompt as selected model context, a proposed patch as a refinement, and checked acceptance as the transition into the shared record.

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ Add the schematic local-and-shared refinement figure.
  • ☑ Insert the Refinement slide before the CRDT explanation.
  • ☑ Insert the Aggregation slide after Refinement and before CRDT.
  • ☑ Add matching intuition at levels 1–4.
  • ☑ Add the formal candidate and its limitations at level 5.
  • ☑ Run the checked build and review the rendered slide order.

Read source record ↗

sitei-want-it-quickstartOpen tasks30 checked · 5 open

The **I want it!** page (`#install`, landed in d73d0e5) shows the three one-line installers and, once screenshots exist, a desktop gallery. It does not show what happens *after* the line is pasted. The owner's ask is to make the smallness of the ramp-up visible rather than to document it: after a curl install, someone points a phone at a QR code in the termi

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1.1 Create `openspec/changes/i-want-it-quickstart/` with `.openspec.yaml`
  • ☑ 1.2 Write `proposal.md`, `design.md`, `tasks.md`
  • ☑ 1.3 Write the new capability delta `specs/site/install/spec.md`
  • ☑ 1.4 Open `plan/i-want-it-page.md` and point to it from `plan/README.md`
  • ☑ 2.1 Give each `INSTALL` entry a slug as its first element:
  • ☑ 2.2 Add `QUICKSTART_OS`, `QUICKSTART` (three steps: `install`/Paste,
  • ☑ 2.3 Add `quickstart_shot(os, step)` implementing the resolution order
  • ☑ 3.1 Wrap the install strip in `<div class="qs" id="quickstart">` whose
  • ☑ 3.2 Render `<ol class="qs-steps">` with one `<li class="qs-step">` per
  • ☑ 3.3 Reword the lede to name the three beats (e.g. "One line in a
  • ☑ 3.4 Escape every string with `html.escape`; no raw user text reaches the
  • ☑ 4.1 Add the `.qs-os` segmented control, the per-OS `:checked ~` rules
  • ☑ 4.2 Add `.qs-steps` (3-column grid), `.qs-h`, `.qs-n`, `.qs-t`,
  • ☑ 4.3 In the existing `@media (max-width:760px)` block, stack `.qs-steps`
  • ☑ 4.4 Add `.qs-t` to the `body.is-accessible` enlarged-copy selector lists
  • ☑ 5.1 Add the detection IIFE from `design.md` beside the copy-button block:
  • ☑ 6.1 Create `var/html/assets/screenshots/quickstart/` (a `.gitkeep` is
  • ☑ 6.2 Hand the asset checklist in `plan/i-want-it-page.md` to the owner;
  • ☑ 7.1 `python3 scripts/build-matrix.py` succeeds; then
  • ☑ 7.2 Determinism: `sha256sum var/html/index.html`, rebuild, compare —
  • ☑ 7.3 Placeholder path: with the quickstart directory empty, the page shows
  • ☑ 7.4 Fallback path: a `gnome-install.png` alone is used for the Linux
  • ☐ 7.5 No-script smoke (`./serve-locally.sh`, JavaScript disabled): Linux is
  • ☐ 7.6 Detection smoke: with devtools device/UA emulation for a Mac and for
  • ☐ 7.7 Keyboard and screen reader: Tab reaches the radio group, arrow keys
  • ☐ 7.8 Phone width (~400px): steps stack, the switcher wraps, no horizontal
  • ☐ 7.9 Accessible mode (`body.is-accessible`): step copy enlarges with the
  • ☑ 7.10 `openspec validate i-want-it-quickstart --no-interactive` passes
  • ☑ 8.1 Stage `scripts/build-matrix.py`, `var/html/index.html` and the new
  • ☑ 8.2 Tick the tasks above as they land and append a session-log line to
  • ☑ 9.1 Record the owner's decision in `proposal.md`, `design.md` (story
  • ☑ 9.2 `QUICKSTART`: step 2 names the terminal and github.com/login/device;
  • ☑ 9.3 CSS: `.qs-steps` four columns, two under 900px, one under 760px;
  • ☑ 9.4 Lede names four beats; rebuild, checked build, determinism,
  • ☑ 9.5 Follow-up commit on `work/site-tweaks`, same convention

Read source record ↗

siteSeparate a measurable residual from a convergence guaranteeOpen tasks6 checked · 1 open

Scoped follow-up to BigPickle's stable v56.9.27.2 audit (website b3f3091). The operator requested a challenge to Level 5's missing metric, using the Lua runtime and CentiColons. Keep the stable pin and all unrelated sections. This proposal records website deltas, not authority to change runtime policy. Leave it unarchived for operator review.

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ Inspect BigPickle's b3f3091 update and affected stable source spans.
  • ☑ Record the six Level 5 deltas before editing; retain the stable pin.
  • ☑ Obtain an independent skeptical source review and address its findings.
  • ☑ Correct the scoped Level 5 and CentiColon copy, preserving examples and navigation.
  • ☑ Record the withdrawn claims in the append-only findings ledger.
  • ☑ Complete checked build, ledger, structure and reproducibility checks.
  • ☐ Operator review before archival; runtime policy adoption is separate.

Read source record ↗

sitelevel-5-stable-pin-deltasOpen tasks11 checked · 2 open

Level 5 (`docs/matrix/level-5-phd.md`) stays pinned to the stable release v56.9.2.1. Its mathematics and its RED flags are accurate at that pin, and the page moves only in tiny, individually justified deltas. The 2026-09-05 re-verification against the newest daily (v56.9.5.1) found: one statement that was false even at the stable pin (an associativity test t

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1.1 D1 — "two of the three properties — commutativity and idempotence —
  • ☑ 1.2 D2 — monotonicity PATH sentence + `[^41]`
  • ☑ 1.3 D3 — coordinates PATH sentence + `[^42]`
  • ☑ 1.4 D4 — scorer PATH sentence + `[^43]` local-ci.sh#L538-L563 @v56.9.5.1
  • ☑ 1.5 D5 — denominator PATH sentence + `[^44]` obligation.rs#L620-L638 @v56.9.5.1
  • ☑ 1.6 D6 — strong-law RED/PATH and Verdict left untouched (checked, no edit)
  • ☑ 1.7 D7 — verbatim quote under each of the 28 in-repo footnotes, from v56.9.2.1
  • ☑ 1.8 D2–D5 wording corrected 2026-09-05, page and `proposal.md` together:
  • ☑ 2.1 Checked build with both checkouts present; level-5 line:
  • ☑ 2.2 `git diff -- docs/matrix/level-5-phd.md` reviewed: every hunk is one
  • ☑ 2.3 `openspec validate --changes` passes (it required a delta spec, so the
  • ☐ 3.1 D8 (proposed) — the D4 PATH sentence makes three daily-channel
  • ☐ 3.2 D9 (proposed) — the D5 PATH's clause "and the script prints that

Read source record ↗

siteRefresh level 5 citations for stable v56.10.9.1Open tasks4 checked · 1 open

Stable `v56.10.9.1` changed eight of the files level 5 cites: `crates/tillandsias-plan/src/fragments.rs`, `crates/tillandsias-plan/src/lua_predicate.rs`, `methodology/convergence.yaml`, `methodology/proximity.yaml`, `methodology/versioning.yaml`, `scripts/check-centicolon-ratchet.sh`, `scripts/check-requirement-ids.sh` and `scripts/local-ci.sh`. Sixteen foot

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ Re-read every footnote that cites one of the eight changed files at v56.10.9.1, and classify each one.
  • ☑ Apply D1–D7 to `docs/matrix/level-5-phd.md`, and no other edits.
  • ☑ Move the level 5 pin to `v56.10.9.1` in `scripts/build-matrix.py`, on that line only.
  • ☑ Run the checked build: no unresolved target and no drifted quote for level 5.
  • ☐ Operator review before archival.

Read source record ↗

siteRefresh level 5 against stable v56.9.12.2Checklist complete3 checked · 0 open

Preserve the argument and manual additions while applying only the individually justified deltas below. Tests on a committed model are not a theorem about live validators; withdrawal of a mathematical citation is an improvement in accuracy.

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ Re-verify the proposed deltas and all resolvable quotes at v56.9.12.2.
  • ☑ Apply the release-pinned deltas and bump level 5 independently.
  • ☑ Run the checked build against v56.9.12.2.

Read source record ↗

siteRefresh level 5 citations for stable v56.9.27.2Open tasks4 checked · 1 open

Level 5's argument remains supported at the new stable release. Move its pin from `v56.9.21.1` to `v56.9.27.2` only after the checked build resolves every footnote. The page edits authorized by this change are the six citation range corrections below and one added paragraph; all other prose and every quoted line stay as they are.

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ Compare all level 5 targets and quotes with the v56.9.27.2 checkout.
  • ☑ Apply only the six citation-range corrections in the proposal.
  • ☑ Add the D1 paragraph after `It is bookkeeping over evidence, and is labelled as such.`
  • ☑ Move the level 5 pin and pass the checked build.
  • ☐ Record the audit and leave this proposal for operator review before archival.

Read source record ↗

siteA complete, inspectable website ledger and system mapOpen tasks14 checked · 1 open

Live progress currently renders only findings and gives each finding a short face. Readers cannot discover the canonical runtime specs, completed archived change checklists, or the evidence behind a finding from the page. Slide 6 deliberately compresses architecture into a presentation frame; a security reviewer needs the fuller target topology, its current

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1.1 Add a repeatable snapshot command for all canonical runtime specs and archived changes, including those without task records.
  • ☑ 1.2 Commit the numbered-release snapshot and make build reject pin drift.
  • ☑ 2.1 Keep every existing finding, including resolved entries, and expose its claim, evidence, remedy and history on expansion.
  • ☑ 2.2 Add complete spec and archived-change inventories; distinguish document lifecycle, checklist completion and verified behavior.
  • ☑ 2.3 Start all entries collapsed and provide search across rendered records.
  • ☑ 3.1 Add the fifth menu page and an onward link from slide 6.
  • ☑ 3.2 Map host, forge, filesystem, enclave, boundaries, credentials, certificates, lifecycle, remote and evidence flow for the website checkout example.
  • ☑ 3.3 Add full-screen bidirectional scrolling, labelled left-to-right journeys, layer toggles and an overlay component inspector; remove the zoom controls.
  • ☑ 3.4 Mark goal-only and partial components separately and link target claims to the pinned specs.
  • ☑ 3.5 Give every inspector a current-state explanation, remaining work or scope limit, and checked source or work-record links; clicking the same node toggles it closed.
  • ☑ 3.6 Fade hidden categories before compacting surviving rows and columns; redraw connections and label shortcuts over hidden stages.
  • ☑ 4.1 Run checked build and ledger validation; inspect generated IDs and references and deterministic output.
  • ☑ 4.2 Exercise menu, search, collapse, graph inspector and layer toggles in a DOM harness.
  • ☑ 4.4 Verify source paths and line ranges, same-node toggling, filtered compaction, shortcut paths, and reduced-motion behavior.
  • ☐ 4.3 Review graph and ledger layout visually in a browser. Chromium download failed with `ECONNRESET` in this forge; the DOM check does not verify layout.

Read source record ↗

siteReader-first claimsOpen tasks13 checked · 1 open

The explanation pages were written while the site's own rules were being worked out, and the rules leaked onto the pages. A five-year-old's page told its reader about a vocabulary rule the product's screens obey and about a machine that enforces it. The power-user page ended on the project's CI budget and "a standing obligation to run the local gate". Footno

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1.1 Add the reader rule and the plain no-remedy sentence to `docs/matrix/README.md`.
  • ☑ 1.2 Write the spec deltas under `specs/site/`.
  • ☑ 2.1 Use the reader's words for the flag labels in `scripts/build-matrix.py`.
  • ☑ 2.2 Collapse each level's source list under "How we know", keeping the
  • ☑ 2.3 Rewrite the home positioning line, the hero, the lede and the footer
  • ☑ 3.1 Level 1: take every block through the reader rule.
  • ☑ 3.2 Level 2: take every block through the reader rule.
  • ☑ 3.3 Level 3: plain PATH wording, the gate section rewritten, dated
  • ☑ 3.4 Level 4: meta NOTEs and developer-only REDs removed, PATH and label
  • ☑ 3.5 Level 5: apply D1 to D3, and nothing else.
  • ☑ 3.6 Remove the footnotes nothing references any more.
  • ☑ 4.1 Run the checked build (`skills/update-website/scripts/checked-build.sh`).
  • ☐ 4.2 Re-check the Mac reset PATH ("a fix is being written") against the
  • ☑ 4.3 Operator review before archival. APPROVED 2026-10-10: "Yes the website

Read source record ↗

siteslides-presentationOpen tasks5 checked · 13 open

The site tells the Tillandsias story in five written levels, but there is no way to *present* it: no deck, no walk-through, nothing to stand in front of a room with. A live talk needs a fourth page — **Slides** — reachable from the same menu, carrying a short deck that tells the project's story in a handful of slides: a **containerized workflow**, **Linux se

A checked archival task list records work, not proof that every goal-state behavior still works now.

Task record
  • ☑ 1.1 Create change directory `openspec/changes/slides-presentation/` with
  • ☑ 1.2 Write `proposal.md`, `design.md`, `tasks.md`
  • ☑ 1.3 Create delta spec directory `specs/site/navigation/` (modified
  • ☑ 2.1 `specs/site/slides/spec.md` — new capability: the deck page's
  • ☑ 2.2 `specs/site/navigation/spec.md` — delta: three pages → four, Slides in
  • ☐ 3.1 `scripts/slides.py` — the deck as data: three slides (thesis,
  • ☐ 3.2 Placeholders in the deck draw no uncited claim: every block not yet
  • ☐ 4.1 `scripts/build-matrix.py` — `slides_view()` renderer (deck frame +
  • ☐ 4.2 Drawer gains the fourth entry `<button class="nav" data-go="view-slides">`
  • ☐ 4.3 Deck CSS in the `TEMPLATE` `<style>` block: `.deck`, `.deck-frame`,
  • ☐ 4.4 Deck JS IIFE `deck()`: one slide on, counter/rail update, prev/next
  • ☐ 4.5 No regressions: copy-to-clipboard, footnotes, menu and tabs still
  • ☐ 5.1 `python3 scripts/build-matrix.py` succeeds and writes
  • ☐ 5.2 Determinism check: `sha256sum` the committed page, rebuild, and the
  • ☐ 5.3 Manual smoke test, recorded here: menu shows five control surfaces
  • ☐ 5.4 `openspec validate --changes` passes for this change (run with the
  • ☐ 6.1 The pretty content — figures, transitions, animations inside each
  • ☐ 6.2 Balance the drawer capsule: nothing about Slides leaks into the level

Read source record ↗

...wait, WHAT?!

How measuring chess pawns inspired how we measure software convergence.

Chess engines like Stockfish combine bounded search with an evaluation of promising positions. They do not solve the whole game from move one. The familiar centipawn label is now a calibrated scale: Stockfish’s official WDL model relates evaluation and remaining material to self-play outcomes under its test conditions. It is not a literal pawn count, a monotonically improving score, or a guarantee of victory.

Software engineering borrows the useful idea of an explicit evaluation, not chess’s outcome model. Tests provide evidence about declared behavior; a score can make remaining obligations visible. Measuring that remainder is one task. Ensuring accepted changes do not regress is another. Guaranteeing that the remainder eventually reaches zero additionally requires progress.

CentiColons (cc) are evidence-based accounting for declared obligations. The intended contract binds each credited assertion to the obligation, implementation inputs and required platform it actually tested. A green test file alone is not enough. Under a fixed scope and scoring policy, the residual (R) records what has not reached its evidence bar. Zero means those declared bars are met, not that every possible defect is absent.

At stable v56.10.9.1: Lua extracts scenario/requirement obligations and grades recorded positive-test results. The grader still has assertion, implementation-provenance and platform-attribution gaps; its residual is a raw count, not the full weighted policy below. The regression check is advisory, not enforced. Lua makes the metric executable; it does not by itself complete the methodology’s guarantee.

What a CentiColon counts

The following is a simplified binary weighted model, not a claim that the entire policy is shipped. Its base weights come from the methodology, which also declares modifiers, partial evidence credits, caps and penalties omitted from this example. Every auditable thing the project commits to—a must requirement, a systemic invariant, a positive or negative litmus signal, a runtime trace, a provenance binding, an environment assumption—enters the model as a named obligation with a stable identifier and a weight. The weights are declared policy rather than measurement: a must requirement is worth 100, a systemic invariant 120, a passing positive litmus 80, a passing negative litmus 100, a runtime trace 60, a should requirement 40. Add up every weight in the fixed scope and you have the budget. New scope or changed policy starts a different comparison; ordinary evidence improvement does not change that budget.

In this binary model, an obligation whose applicable evidence has reached its bar contributes its weight to the earned column; one that has not contributes nothing. Three numbers come out, and they are always reported together:

  • earned — the weights that reached their bar;
  • budget — every weight the spec declared;
  • residual — budget − earned, the distance still to travel.

This example has one property worth saying out loud: closing an obligation moves the score by exactly that obligation’s weight, and the residual by exactly the same amount. Nothing is averaged, smoothed or estimated. Close the 80-point positive litmus and the score moves 80 and the residual moves 80, and a reader can check it by hand. But a rising total can hide a lost obligation behind another gain. Non-regression therefore compares each retained obligation, with the same policy and applicable evidence, not just the total. That accounting rule is not a property of Stockfish’s evaluations.

A worked example, with declared weights

Take a five-obligation example spec, invented for this page: a Submit button, an invariant about how its payload is serialised, a positive litmus, a negative litmus, and a runtime trace. All declared, none yet tested, the budget is 460 and the earned is 0. Now play the position.

abcd4321NPPPNb1–c3 A POSITIONAL MOVE · one quiet developing step +0.18 → +0.42 +24 centipawns · the engine’s own unit THE SAME SHAPE OF MOVE, IN THE LEDGER 0/460 → 80/460 one positive litmus reaches its bar → +80 cc residual 460 → 380 · denominator unchanged
A positional gain, counted twice. Left: files a–d, ranks 1–4, Nb1–c3. The engine’s number rises by 24 centipawns; the ledger’s number rises by 80 CentiColons because one positive litmus reaches its bar. The example spec is the five-obligation one the page works through; the board is a fragment, not a game.

A quiet move. One positive litmus goes green and nothing else happens: no material changes hands, no feature ships. In chess terms it is Nb1–c3 — the piece simply got to a better square, and the number improved because the position improved. The score goes 0 → 80 of 460, the residual goes 460 → 380, and the budget does not move, because closing a claim is not deleting one.

defgh8765KRNNf7+ A TACTICAL MOVE · one blow that wins material +0.35 → +5.40 +505 centipawns · the knight forks king and rook THE SAME SHAPE OF MOVE, IN THE LEDGER 80/460 → 340/460 one change closes three obligations → +260 cc residual 380 → 120 · denominator unchanged
A tactical gain, counted twice. Left: files d–h, ranks 8–5, Nf7+, with the two dashed lines showing what the knight attacks on arrival. The fork is worth 505 centipawns; the same change closes three obligations and is worth 260 CentiColons. The example spec is the five-obligation one the page works through.

A combination. One change ships the button end to end: the requirement is positively tested, its negative case passes, and a runtime trace records the click. That is the knight fork — three things fall at once, and the number says so by the size of the jump. 80 → 340 of 460, residual 380 → 120, +260 in a single move. Notice what the two moves share: the size of the jump is exactly the declared weight of what closed. An engine’s number moves because the position changed; this one moves because evidence arrived, and by an amount somebody wrote down in advance rather than one an evaluation function invented on the spot.

From a sentence in a spec to a button on the screen

Which raises the only question that matters: what does it mean for “the Submit button stays disabled until the form is valid” to be closed? It means there is an arrow from that sentence to something that can actually be executed, and that applicable execution evidence was recorded. The assertion must discriminate the intended behavior; recording an unrelated pass cannot close it. The arrow is the validator, and it is the same shape every time: take the obligation the spec names, take the element the spec is about, and run the check that decides whether the clause holds.

spec: the Submit button staysdisabled until the form is validvalidate(spec, form.button())form.button()enabled ⇆ disabled+100 ccmust requirementspec: a duplicate submit isrefused while one is in flightvalidate(spec, form.button())form.button()second click ignored+100 ccnegative litmusspec: an accepted click isrecorded as a runtime tracevalidate(spec, form.button())trace: submit@clickseen at runtime+60 ccruntime trace three arrows, one change → +260 cc, residual 380 → 120 the denominator moves only when the obligation set changes, not when one closes
The arrow is the validator. Each row is one obligation of the five-obligation example spec: a clause on the left, the thing that can actually be executed in the middle, and the CentiColons the ledger gains when that execution is recorded. The three rows together are the tactical move above: +260 cc, residual 380 to 120.

Each row is a clause, an executable witness, and a price. The three rows are three different kinds of evidence for three different clauses — a must requirement at 100, a negative litmus at 100, a runtime trace at 60 — landing in one ledger, which is why the tactical move above is worth +260 and not an average of three opinions.

The proposed submetrics also cover work outside feature behavior. Systems engineering does not live in user-facing specifications alone. A large share of the real work is systemic invariants — deterministic JSON sorting, sandboxed execution, no fragile shell pipes — and transient platform preconditions, the Darwin bash quirks, the Windows CRLF line endings, the container tmpfs bounds. If the metric only measured feature specs, all of that would read as zero progress or be omitted from the evidence contract. Internal invariants can live in specifications too. The proposed common model treats a scenario, invariant and platform precondition as a named obligation with applicable evidence and a bar, without counting the same obligation twice. The ./plan ledger links a transient fix to the durable obligation and test that prevent recurrence; closing a packet is not itself correctness credit. Stable Lua extraction lists invariants separately, but the grading path does not yet consume them. Cacheable predicates restrict observation capabilities; observing predicates acquire external evidence. Neither label proves an assertion adequate or its cost negligible.

What would make the reduction monotonic?

Fix the obligation set, weights and evidence policy; bind results to the exact assertions, inputs and required platforms; keep a durable accepted baseline; and preserve every retained obligation’s credit. Under those conditions the residual cannot increase across accepted implementation changes. A new counterexample must still invalidate bad credit: honest measurement can rise when we learn that an earlier claim was wrong. Retractions are disclosed breaks in that monotone comparison, not exceptions silently counted as non-regression. New scope and corrected knowledge must remain visible.

Even perfect non-regression permits a plateau. To guarantee eventual zero, require a further progress premise: while a fixed nonnegative integer residual is positive, it decreases by at least one within every next K cycles. Then completion takes at most K × R0 cycles. That is a conditional bound, not a guarantee delivered by the current runtime. Level 5 states the assumptions and the remaining evidence gap.

What the number does not say

A number this clean is exactly the number people will over-read, so here are the four things it does not carry:

  • It is a ranking, not a probability. 340/460 is not a 74% chance the software is correct, and two scores cannot be combined by Bayes. It orders states; it does not measure belief.
  • It answers the question it was asked. Of what you declared, how much is closed. A requirement nobody wrote down is invisible to it, not free.
  • Effort is not in it. A hard obligation and an easy one are worth what their weights say they are worth. The score records closure, never the cost of getting there.
  • A percentage can be improved by throwing obligations away. The weighted Rust scorer reports a broken comparison regime when an obligation is tombstoned; removing an obligation changes the budget and may remove earned credit. This comparison warning prevents reading deletion as progress. The separate Lua advisory reports retirements without refusing the run.

The worked arithmetic is deliberately simpler than the full declared policy, and the shipped measurements are narrower still. Level 5 separates those three layers. CentiColons help operationalize the promise: they make declared evidence debt inspectable. They do not yet complete an enforced non-regression guarantee, and a measurement alone cannot supply the strict-progress premise of a termination proof.

Where the score stands at v56.10.9.1

100 of 990 obligations were still open at the last test run that reported a score, on 9 August 2026. Between runs the score went up as well as down, and no run has reported one since. 664 shell scripts are still to be rewritten in Lua, down from 677 when the count began.

How this section has improved
  • Its links to the source, and the summary that closes it, now update with each release.
  • The page explained the scoring idea with a chess analogy and a worked example.
  • It now separates what the score measures from what the score can promise. An earlier version claimed too much.

CRDT ...what?

Why many agents on many machines can all write to one shared plan and never have to argue about it.

Three friends and one notebook

Imagine three friends keeping one notebook about a shared garden. There is only one notebook, so they pass it around, and whoever has it writes. Then two of them go travelling, each takes a photocopy, and for a month all three write in their own copy. When they meet again they hold three notebooks that disagree.

Now the trouble starts. One crossed out “water the ferns on Tuesdays” and wrote “Thursdays”. Another crossed out the same line and wrote “Mondays”. Which copy is right? Nobody can say from the notebooks alone. Someone has to sit down, compare the three, and decide. That sitting-down is a merge conflict, and it is the thing every team of people, and every team of programs, eventually dreads.

The rule that makes the dread go away

Here is a different way to keep the notebook. Nobody ever crosses anything out. Each friend only adds dated, signed lines: “10 June, Ana: I watered the ferns.” “12 June, Ben: the ferns look dry; I think Thursdays is wrong.” When the three copies come back together you simply put every line from every copy into one list and sort them. No line fights another line, because no line was ever replaced. Three copies, one pile, zero arguments.

That is the whole idea behind a conflict-free replicated data type, or CRDT. Replicated: many copies. Conflict-free: the copies can be combined by a rule that never needs a referee. The rule has to have three everyday properties, and the notebook already has them:

  • Order does not matter. Add Ana’s lines then Ben’s, or Ben’s then Ana’s: same pile.
  • Grouping does not matter. Combine two copies first and the third later, or all three at once: same pile.
  • Repeating does not matter. Add the same copy twice by mistake: still the same pile.

A pile you can only add to, with those three properties, is the simplest CRDT there is, and mathematicians call it a grow-only set. Everything on this page is a variation of it.

What git gives you for free

A git repository is a very good notebook. Every change becomes a commit, every commit is named by a fingerprint of its own contents, and every commit records which commit it came after. Any number of people can take a copy, work alone for a month, and bring their commits back; git keeps all of them, and it remembers exactly which commit knew about which. Branches, worktrees, forks and pull requests are just different ways of carrying commits from one copy to another.

But git, on its own, is not a CRDT, and this page would be lying if it said so. Two people who edit the same line of the same file still produce a conflict that a human has to settle. Git replicates perfectly and remembers perfectly; what it cannot do is decide. The notebook rule has to come from how you use it.

The happy pile

Tillandsias runs many AI agents on many computers: Linux, macOS and Windows hosts, some of them disposable containers that exist for an hour. They all need to read and write one shared plan: what is being worked on, what was found, what was proved, what was retracted. If that plan were one big file that everyone edited, every hour would end in the three-notebooks problem. For a while it was, and it did.

So the project applies the notebook rule to its own plan. Shared state is written as append-only evidence: a host that wants to record something writes a new file, in a folder of fragments, with a name only that host could have produced: the time in UTC, a random suffix, and the host’s own name. Once written, a fragment is never edited. If it turns out to be wrong, the correction is another fragment. Git then has nothing to argue about, because no two hosts ever touch the same file.

linux host macOS host windows host …-linux.yaml…-linux.yaml…-macneo.yaml…-macneo.yaml…-macneo.yaml…-yoga.yaml the pile — append only, never edited fold one readable view same files → same view arrival order: any duplicates: harmless edits in place: none
Nobody edits the pile. Each host adds a file only it could have named; the readable plan is computed from the pile, in an order the file names fix, so every host that holds the same files derives the same view.

Seen this way, the whole GitHub apparatus stops being a source of friction and becomes a set of conveyor belts. A work branch is a host’s private stack of fragments. A pull request is that stack arriving at the shared pile. A landing queue takes the stacks one at a time, checks each against the pile as it stands at that moment, and adds it or sends it back; the tree that gets checked is the tree that lands, never a slightly different one. Hooks refuse the few things that would break the rule, such as editing the compacted base in place. None of these parts needs to know about the others, because the only thing any of them ever does is dump information into a shared pile of evidence.

Semantic distillation

A pile of thousands of fragments is not something you want to read. What you read is a view, computed from the pile by a fixed recipe the project calls the fold: start from the last compacted base, then apply every fragment, in an order the file names fix, so two hosts holding the same files always compute the same view. Applying a fragment twice changes nothing. That recipe is the distillation, and it is where the real CRDT thinking lives, because different kinds of information need different rules:

  • Work items and events are a grow-only set. Two hosts adding different items both win; adding the same item twice yields one item. A fact, once recorded, is never lost by a merge.
  • Single-valued fields are a register. A status can hold only one value, so the fold picks a winner deterministically, by timestamp and host name, never by whichever copy happened to arrive first. The project is careful to say what this costs: a register keeps one value and drops the other, which is exactly why facts are kept as a set and never as a register.
  • Progress climbs a ladder. A status may move up (implemented, completed, verified, done) freely, and may move down only when an explicit falsification event says so. A later, lower claim cannot quietly undo a verified one; a disproof can.

Every so often a host that can afford the full checks folds the fragments into a new base and deletes exactly the files it folded, by name, never by pattern, so a fragment written by someone else during compaction survives untouched.

Rectified provenance

The pile has one more property the notebook had: every line is signed and dated. Each fragment records which host and which agent wrote it, when, and about which work item. Because nothing is ever edited, the pile also keeps every mistake next to its correction: an agent that cited the wrong evidence writes a fragment saying so, and a reader can see both the claim and the retraction, in that order, forever.

That combination, an append-only pile plus provenance on every entry plus a deterministic way to distil it, is what the project means when it says generated software should stand on evidence. Code written by many agents in many places is only trustworthy if you can later ask “who claimed this, from what, and did anyone disprove it?” and get an answer that does not depend on which copy of the notebook you happen to be holding.

What the pile does not promise

Honesty about the limits is part of the method, so here are the three things a CRDT cannot do:

  • It does not deliver. Copies that hold the same fragments agree; nothing makes a fragment arrive. Pushing, fetching and the landing queue are separate machinery.
  • It does not make a claim true. Three hosts agreeing on a pile of evidence agree about what was recorded. Whether a recorded test actually proves anything is a question for the CentiColon accounting, not for the merge rule.
  • It does not merge code. Source files are still edited in place and still conflict; they go through review and the full build. The pile is for the plan and its evidence, not for the program.

The project also retracts its own CRDT claims when they fail: one deduplication routine once described itself as a CRDT, and its header now says plainly that it is not, because first-wins depends on arrival order. A pile that keeps its retractions is a pile worth trusting.

One question left: who went first?

Sorting the pile “by date” hides a problem. Three computers have three clocks, and none of them agrees with the others to the millisecond. Whether one fragment came before another is not something a wristwatch can settle. The answer is that computers do not need wristwatches at all; they need to know what caused what, and git has been writing that down all along.

Computers don’t understand time

Why machines count instead of telling the time, and why a git history is already the clock you wanted.

Everyone’s watch is a little wrong

Ask three friends what time it is and you get three answers, a minute or two apart. Usually that is fine. It stops being fine the moment you ask a question like “did Ana write her note before Ben read the notebook?” and the two watches disagree by more than the gap between the two events. Then “before” has no answer. Computers live in that situation all day long.

Why a computer counts instead of flows

A river flows; a metronome ticks. Inside a computer, time is a metronome: a tiny crystal vibrates, a counter goes up by one on each vibration, and that count is the only clock the machine has. Between two ticks nothing happens at all. The computer cannot say “a bit after tick 400”; it can only say “tick 400” or “tick 401”. Time in a computer is discrete: a staircase, not a ramp.

This is not a defect. A count is something a machine can compare exactly, copy exactly and write down exactly. “Four hundred” is the same on every machine; “a moment ago” is not.

Why making clocks agree is so hard

Fine, you say, just set every computer’s counter from one trustworthy clock. Here is why that is finicky in practice:

  • The crystals drift. Two quartz crystals never vibrate at exactly the same rate; left alone, two machines drift apart by seconds a month. Warm one of them and it drifts faster.
  • Asking takes time. To learn the time from a server you send a question and wait for the answer. The answer is already stale by the time it arrives, and you cannot know by how much, because the trip out and the trip back need not take the same time.
  • The sleep problem. Laptops sleep, virtual machines pause, containers are frozen and thawed. When they wake, their counter is wrong by exactly the time they were asleep, and they do not know it yet. Tillandsias runs most of its work in exactly such machines.
  • Time itself is edited. Leap seconds, time-zone rules, daylight saving and a human typing the wrong date all change the number without anything having happened.

Protocols such as NTP work hard at this and get two machines on one network to within a few milliseconds. A few milliseconds is an eternity for a computer: thousands of events fit inside it. So two timestamps from two machines can never prove which event came first.

Letters crossing in the mail

Step back and ask what we actually wanted to know. Not the time of day. We wanted to know whether Ana’s note could have influenced Ben’s. If Ben read the notebook before Ana wrote in it, his note cannot depend on hers, no matter what either watch says. If Ana posted a letter and Ben replied to it, the reply came after the letter, even if Ben’s clock says otherwise. And if two letters crossed in the mail, neither came “first” in any sense that matters: they are simply concurrent.

This relation, “could have caused”, is called happens-before, and it is the only order a distributed system truly needs. Notice that it is a partial order: some pairs of events are ordered, and some are not, and that is the honest answer.

The trick: carry a counter in every letter

In 1978 Leslie Lamport showed that you can capture happens-before with nothing but counting. Each machine keeps one integer. The rule has three lines:

  1. Before you do anything worth recording, add one to your counter.
  2. When you send a message, write your counter on it.
  3. When you receive a message, set your counter to one more than the larger of your own and the number on the message.
host A host B 123max(3,6)+1 = 781max(1,3)+1 = 456 message stamped 3 message stamped 6 rule: tick before each event; on receipt, counter = max(mine, theirs) + 1
Each host counts its own events. A message carries the sender’s count, and the receiver jumps to one past the larger of the two. Nothing is synchronised and no one asks what time it is, yet every cause carries a smaller number than its effect.

No machine ever asks what time it is. Yet if one event could have caused another, the first always carries the smaller number, because the message that carried the influence also carried the count. The number is not a time; it is a logical clock, and it is exactly as discrete as the computer wanted it to be in the first place.

One honest caveat. A smaller number does not prove causation: two concurrent letters may carry 3 and 5 without either having influenced the other. Lamport’s counter is consistent with happens-before, not equal to it. Keeping one counter per machine (a vector clock) closes that gap, at a cost in size. For most of what a project needs, the single counter is the right tool: it is small, it never disagrees with causality, and a tie can be broken by the machine’s name.

Your git history already is this

Now look at a git repository with the above in mind. Each commit is named by a fingerprint of its contents, and that fingerprint covers the names of its parents. So a commit literally cannot exist before its parents, and anyone holding a commit can follow the parent names all the way back. The arrows in the graph are happens-before, written down.

0000a1b2c3d4e5f69c0d7a8bmergef1e2 branch: linux host branch: macOS host 1 2 3 4 3 4 5 6 green: generation = 1 + max(parents)
Every commit names its parents by hash. Follow the arrows and you get “happened before” with no clock at all: a1b2 is before both branches, e5f6 and 7a8b know nothing of each other, and the merge knows everything.

This is better than a Lamport clock, not worse: the graph is the causal order, and a Lamport clock is just the smallest counter that respects it. You can read one off the graph by giving each commit one more than the largest number among its parents. Git in fact computes that very number, which it calls a generation number, to speed up its own history searches. Git also records the author’s wall-clock date on every commit, but nothing about branching or merging depends on it; two commits with crossed dates merge exactly as well as any others.

So the punchline is a cheap one, and that is the point: back your project up in a git repository and you get happens-before for free. Every clone is a replica; every commit is an event with its causes attached; every merge is a letter arriving. The clocks on the machines can be wrong by hours, and the history still says, correctly, what came after what.

How Tillandsias uses it

The project keeps its shared plan as an append-only pile of fragments inside that git history, so every fragment inherits the causal order of the commit that carried it. Within the pile, two smaller choices are worth naming plainly:

  • Fragment names begin with the UTC time, so a plain alphabetical sort of the folder is also a chronological one and every host folds the pile in the same order. That is a wall-clock stamp, and the project knows it: nothing is lost if two hosts’ clocks disagree, because facts are kept as a set, where order does not matter at all.
  • Where one value must win, the fold breaks ties by timestamp and then by host name, so every host picks the same winner from the same files. That is the familiar “later watch wins” rule, chosen for a field where only one value can survive anyway, and never applied to the facts.

The methodology also describes Lamport-style counters for ordering an agent’s iterations across merges: tick locally, take the maximum on fetch, break ties by agent name. Where it matters most, though, the project does not reach for a clock at all. The landing queue checks a candidate against the shared branch at a particular commit, and just before pushing it reads the branch again: if the commit has changed, the green result described a tree nobody is shipping, and the candidate goes back in the queue. That is a happens-before check, done with git’s own graph, and it is the one that keeps the shipped tree equal to the tested tree.

Which is where this page and the last one meet. A pile you only add to needs no referee; a history that names its causes needs no synchronised clock. Together they are most of what a crowd of agents on a crowd of machines needs in order to agree.

Big Graph

The desired architecture of a tillandsias.org checkout inside a forge. Follow the main flows left to right; scroll to explore and select a component for its sources.

Reading key: solid green = implemented at the pinned release; amber dashed = partial; gray dotted = goal only. These are editorial assessments from the stable audit, not runtime telemetry. Edges show intended interactions; dotted shortcut edges pass through hidden layers and do not prove a direct connection or enforcement.

01 · Build and publishOperator → forge → git mirror → guarded exit → GitHub
02 · Preview and filesRuntime boundary → mounted output → web sibling → browser
03 · Internal servicesHost keys → control wire → enclave → secrets, inference and cache
04 · Security reviewCertificates, policy, provenance and evidence support the flows above
05 · LifecycleInstall → create → edit → publish → teardown → rebuild
06 · AccountabilityRequirements → obligations → checks → ledger → score → review

Tillandsias / Slides

tillandsias · in one line

A small cloud region on your own computer.

Disposable workspaces. Durable work. Boundaries you can inspect.

YOUR COMPUTER AI assistants disposable workspaces Apps + tools Shared services · one local region push Durable work upstream repository
Your tools live together locally. Commit and push the work you want to keep.

Tools, services and AI assistants share one local region. Rebuild a workspace when needed; commit and push to keep your work.

The software is free. Your hardware, electricity and any paid AI service you choose still have costs.

1 / 16