# AGENTS.md — internal/project (ROOT) **One question: which directory is the project.** Everything that is a fact about a project — the issue store, the request-payload scratchpad, the tracker config — is resolved from the answer, and the answer is found by one walk written once. This package **depends on nothing** but the standard library, and it is the only one in the tree with no other package below it. | file | what is in it | |---|---| | `project.go` | `Marker`, `Anchors`, `Parents`, `GitDirOf`, `MainWorktree`, `Root`, and the paths resolved from it — `StoreRoot`, `PayloadRoot`, `ConfigPath` — plus `NotFoundError` | | `init.go` | `Init`: creates the marker, migrates an older layout in, gitignores `.kettle/`. `ClashError` is its refusal | | `project_test.go` | the walk, including the worktree hop and the "no marker anywhere" answer | ## The walk Anchors, first hit wins: `$CLAUDE_PROJECT_DIR`, then the working directory. Each is searched up its parent chain for a `.kettle/` marker, and then — **only if that found nothing** — up the parent chain of the **main working tree of any linked worktree** met on the way, reached by reading `gitdir:` out of a `.git` *file* and following `commondir`. A marker, not a fixed number of `..` hops: how deep a caller sits below the root is an implementation detail of the layout, and the layout is not a promise. Walking up means every command sees one store from anywhere inside the project — including from inside the store itself — while a `cd` into a *different* project correctly answers with that project's store. The worktree hop is one level of indirection, never two: a main checkout is not itself a linked worktree, so it cannot chain and cannot cycle. Only a `.git` *file* is a pointer; in an ordinary clone `.git` is a directory and there is nothing to follow. A submodule's `.git` is a pointer too, but it points into `/.git/modules/…`, and `MainWorktree` refuses it on the `.git` basename check — the tree it belongs to is already on the parent chain. ## Two rules that are not negotiable **Nothing here resolves from the executable's own location.** Where an installation keeps its files is a fact about the installation; whose issues a tree has is a fact about the tree, and a binary installed in one place and pointed at another must answer from the one it was pointed at. This is the whole reason the package exists — the Python version resolved its store from `__file__` and wrote issues into a versioned plugin cache. **The marker is created by `kettle init`, never inferred.** `.git` was tried and is in every clone, including this repository's own, which is how a plugin came to resolve its store inside itself. No marker anywhere is an *answer*, not a fallback: `NotFoundError` names the anchors the search began from — not the whole chain, because an operator who sees the two places it started knows immediately whether it started where they meant it to. ## Init, and the migration `Init` is idempotent and every step announces itself, so `--dry-run` is the same code path with the writes turned off: - creates `.kettle/issues/` and `.kettle/payload/`; - migrates an older store in, oldest layout first — `tmp/issues`, then `.tea/issues`, and the same pair for `payload` — so a tree that skipped a generation still lands in one place; - adds `.kettle/` to `.gitignore`, unless some line already ignores it. **Each migration is a move, never a copy.** Two stores is the state the marker exists to prevent, and a store left behind at an old path is a store somebody will edit by accident months later. When both sides hold a file of the same name it stops with a `ClashError` naming up to five of them and changes nothing: two versions of one issue, and which survives is not a decision a migration makes quietly. The old `.tea` marker is removed only when the migration emptied it — anything else parked in there is somebody's. `.kettle/` is gitignored because an `origin: local` issue is the only copy of that work and what goes into a shared history is the operator's call. Committing the store is a legitimate choice; drop the line if the team makes it. ## Usage ```go root := project.Root("") // "" when there is no project store := project.StoreRoot("") // /.kettle/issues if store == "" { return project.NotFoundError("") // names the directories it searched } ``` A non-empty `start` overrides both anchors and exists so resolution can be exercised against a scratch tree — which is what the test suite does, and why every fixture also strips `CLAUDE_PROJECT_DIR`. ## What does not belong here Anything that reads or writes an issue, a config file or a socket. This package hands out **paths** and one answer about directories; the store is [`issue`](../issue/AGENTS.md)'s, the config is [`config`](../config/AGENTS.md)'s, and the scratchpad is filled by [`gitea`](../gitea/AGENTS.md). ## Keeping this file true - **Scope:** `project.go`, `init.go`, `project_test.go` — the walk, the marker, the paths derived from it, and the migration. - **Update it when** an anchor is added or reordered, the marker name changes, a new path is resolved under the marker (the file table and the walk section both name them), a legacy layout is added to or dropped from the migration list, or the worktree rule changes. - **Do not** document what any resolved path is *used for*; that belongs to the package that uses it.