feat: publish releases with this repository's own SDK code
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>
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user