01fb5a2703
There is no CI: the instance has no act_runner and none is planned, so releases are cut by hand. That makes `make check` the only thing standing between a mistake and the tracker, and it is one command: gofmt, vet, the suite with the cache defeated, `go mod verify`, a vendored build, and `kettle gen skills --check`. The last one is the invariant worth having — the plugin's SKILL.md command reference is generated from the binary's registry, so a flag that changed cannot ship with documentation that recommends the old one. `cli/cmd/release` publishes to Gitea using the same SDK the binary already vendors, which is a pleasing thing to be able to say: nothing third-party handles the artifacts. It is a second binary rather than a `kettle` subcommand on purpose — `kettle`'s command tree is what generates the plugin's skills, so a verb there ships to every operator, and publishing a release is build infrastructure. It is idempotent end to end: an existing release for the tag is reused, an asset of the same name is replaced rather than doubled, and a retried run converges instead of duplicating. `make release` refuses three things, each with its own message: a dirty working tree, a TAG that is not what `git describe` reports, and a tag the remote does not have. A release built from uncommitted code is unreproducible and nobody finds out until they need to reproduce it. `kettle version` reports the stamp, the toolchain and the VCS revision. The default is `dev`, and a hand build says so and means it — a binary out of somebody's working tree is not a release and must not claim to be one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
108 lines
5.4 KiB
Markdown
108 lines
5.4 KiB
Markdown
# 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
|
|
`<super>/.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("") // <root>/.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.
|