TacidOS
DevelopersGitHub

Decision 0046

A project is what its files say, and Tacid writes nothing into it

Architecture decision record 0046: A project is what its files say, and Tacid writes nothing into it

Status
accepted
Date
2026-09-05

Context

The plan’s developer workflow starts with a project the machine understands: detection of Cargo, npm, CMake, PlatformIO, Zephyr, ESP-IDF, Python, Go and Nix projects; generated development shells loaded by direnv; and a rule above them — never rewrite an existing flake.nix, shell.nix or devenv.nix. The machine had none of it: a person’s project was a directory it could read file by file.

ADR-0044 settled how a person’s things are looked at: by their executor, as them, with the home read-only. A development shell written into a project is a write. So the reading half comes first here too, and the shell Tacid would write is shown rather than written.

Prior decisions

ADR-0035 gives every person an executor that reads their files as them and writes none of them. ADR-0029 names the project kinds and the never-rewrite rule. ADR-0010 says content from the world is never an instruction.

Decision

A project is detected from its files, its root is the nearest directory that says what it is, its own shell is kept, and the shell Tacid would write is shown for the person to save.

executor.project takes a path in the person’s home and finds the project it is in: the nearest directory, walking up no further than the home, with a file that names a kind — Cargo.toml, package.json, platformio.ini, west.yml or prj.conf, sdkconfig or idf_component.yml, CMakeLists.txt, pyproject.toml or setup.py or requirements.txt, go.mod, flake.nix or default.nix. A directory can be several kinds; a CMake file inside a Zephyr or ESP-IDF tree is those projects’ own and not a second kind. It says whether the project brings its own shell, in which file, and whether direnv already loads it; it reads the two keys of .tacid/project.toml a person may have written — the project’s name and its embedded target — and nothing Tacid would act on unasked; and, for a project with no shell of its own, it returns the flake.nix Tacid would write, with the tools each kind needs, as text.

The daemon’s project.look reaches the person’s executor for a home and looks at a project anywhere else as itself; the rules model turns “what kind of project is ~/x” and “set up a dev shell for ~/x” into a look. What comes back is untrusted: a file named the kind, and a name in a declaration is the world’s.

Writing the shell — and the per-project policy the plan sketches — waits for an executor that may write, as ADR-0044 says of commits.

Consequences

  • The never-rewrite rule holds by construction here: nothing is written, so nothing is rewritten. When writing arrives it will be the rule the writer checks first, and this record names the files it protects.
  • The suggested shell is nixpkgs attribute names a person can read, in a flake they can save; it is a suggestion in the plan’s own words, and it is not run.
  • The lifecycle test makes a Cargo project in a person’s home, asks what it is, reads the suggestion, and finds no flake.nix written.

Alternatives rejected

  • Writing the shell, with the never-rewrite rule as the safeguard. The executor cannot, and should not until a policy says what it may write.
  • A TOML parser for .tacid/project.toml. Two keys on two lines; a parser would be a dependency for the sake of a file nobody has written yet.