From 78db9a341212352b3800e1ca8471c758df8d7e50 Mon Sep 17 00:00:00 2001 From: Raj Nakarja Date: Fri, 14 Aug 2026 11:09:36 +0200 Subject: [PATCH] Move the agent guidance to the server repository The server's CLAUDE.md imported this file, so both loaded together and stated the same coding principles and development cycle twice. One file now covers all three repositories. A CONTRIBUTING.md is the right home for contributor guidance here once the project takes contributions. --- CLAUDE.md | 237 ------------------------------------------------------ README.md | 4 +- 2 files changed, 1 insertion(+), 240 deletions(-) delete mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index 1c3bd2c..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1,237 +0,0 @@ -# CLAUDE.md - -Guidance for working in this repository. This file holds **only** high-level -coding and operational principles plus the architectural overview as it takes -shape. It must never describe product functionality or per-feature behaviour. -The README.md holds the project-structure overview and the setup instructions -for local development and releasing. - -**This repository is public.** Never commit secrets, tokens, or customer data: -the entire git history ships. - -**Built in tandem with the server.** The server lives at -`~/projects/superstack-server` (github.com/siliconwitchery/superstack-server) and -owns the JSON API this binary speaks. A change on either side of the wire -usually implies one on the other, so read its CLAUDE.md before changing -anything that crosses it. The two files share the coding principles and the -development cycle below verbatim; a change to one belongs in both. - -## Coding principles - -General and meant to be reused verbatim across projects. - -- **Complete names.** Use descriptive, whole-word names for non-trivial - variables (`tuiWidth`, not `boxW`). Short names are acceptable only for - receivers, loop indices, `err`, `ok`, and a framework's own idiomatic short - names: `w` for an `http.ResponseWriter`, `r` for an `*http.Request`. Keep - those reserved for that exact type: a reader that is not an `*http.Request` - is `reader`, not `r`. -- **Breathing room.** Separate a statement that produces a value from the - statement that consumes it with a blank line. For example: an assignment, a - blank line, then the `if err != nil` check. Group code into readable - paragraphs. -- **Guard clauses.** Handle edge cases and errors first and return early, so the - happy path stays unindented and reads straight down the function. -- **No comments.** Code must explain itself through naming and structure. - (Struct tags are not comments.) Two exceptions. The first is a constraint the - code cannot express on its own: an external or internal protocol, not a - restatement of what the code does. For example, noting that a flag exists - only because another process invokes it. The second is one-line step headings - that break a long procedural function into navigable sections; a step - heading is a plain sentence with no `Step:` style prefix, saying what the - paragraph below it is for, never how it works. -- **Procedural code.** Always inline simple logic so readers don't have to jump - around to see what small functions do. 1-3 line functions shouldn't exist - unless there's a very good reason, such as wrapping something that could - change like a hardcoded filepath. Never create a function that is only used - once. Long procedural functions are fine; reading one top to bottom should - describe its entire behaviour with minimal jumping around. -- **Functional code.** Prefer functional, stateless code. Some libraries demand - statefulness and it's fine to follow their style, but everywhere else avoid - mutable state. -- **Switch over ladders.** Prefer a `switch` (including a type switch) to a long - `if` / `else if` chain. -- **Co-location.** A self-contained unit lives entirely in its own file: its - config, state, behaviour, and rendering together. Its only references from - elsewhere are where it is wired in at the composition root. Removing it means - deleting its file and that one line of wiring, nothing scattered across the - project. -- **Table-driven tests.** Express tests as a table of input → expected cases - iterated in a loop, not as repeated near-identical assertions. -- **Exact scope.** Build precisely what was asked for, nothing broader. When - an extra input shape, mechanism, or option seems useful, present it as a - choice rather than building it; extras arrive only when asked for - explicitly. -- **No assumptive defaults.** A required input is required: a command missing - one errors and says what it takes, it never guesses what was probably - meant. -- **User-facing words, never internals.** Text shown to a user describes what - they did or must do, never the machinery: no keys, hashes, tokens, or other - implementation nouns. The product is invisible plumbing and its messages - keep it that way. -- **No personal data in code.** Tests and fixtures use invented neutral - identities, never a real name or address. Repositories publish. - -## Operational principles - -- Build and run with cgo disabled for a static, dependency-free binary: - `CGO_ENABLED=0 go build`. Run `go vet` and `go test` with `CGO_ENABLED=0` - too, so neither reaches for a C toolchain that need not exist. -- Keep the dependency set small: nothing a distribution's packager would balk - at. -- Development happens on `dev` in both repositories, created fresh from `main` - at the start of every cycle and never resumed: merging squashes the branch - and deletes it, so any local copy left behind is permanently diverged. -- The CLI version changes only in a release pull request. The server's minimum - version is an independent compatibility floor, not the current CLI version; - raise it only for an incompatible API change, after the required CLI release - is available through its package channels. - -## Development cycle - -One request, one branch, one pull request, and the branch never outlives it. -Raj owns steps 1, 5, 7 and 8; the rest happen here. - -1. **Raj asks for something.** A cycle starts from a request, never from - picking up where the last one stopped. -2. **Create `dev` fresh from `main`:** `git fetch origin`, then - `git switch -C dev origin/main`. Never resume an existing `dev`. Merging - squashes the branch and deletes it, so a local copy left behind is - permanently diverged, and its next pull request conflicts on every line it - touches. -3. **Work, then iterate on the feedback.** -4. **Open the pull request.** Product work does not change the CLI version or - the server's minimum-version gate. If an incompatible API change needs a - higher floor, keep the server compatible until the release in step 8 is - installable, then raise the gate in a server pull request. -5. **Raj reviews and merges on GitHub.** Squash only. -6. **Return to `main`:** `git switch main`, `git pull`, `git branch -D dev`, - so nothing stale is left to resume. -7. **Raj may ask for a release.** A merged cycle is not automatically one. -8. **Cut releases independently.** For the CLI, open and merge a release pull - request that changes its version, then tag that commit. A server-only - release needs no CLI release. For an incompatible API change, publish the - compatible CLI first, let its package channels catch up, and only then - merge and deploy the higher server gate. - -The production deploy refuses a gate ahead of the newest published CLI. The -branch invariant rests on step 2's `-C`, which resets a leftover `dev` onto -`origin/main` rather than resuming it, and on both repositories being set to -squash-only merges with Automatically delete head branches enabled. - -## Architecture - -`main.go` is the composition root. It holds one table of sections and commands, -and that table is the single source of truth for both the help output and -dispatch: a command cannot exist in one and not the other. Names may be several -words, and dispatch takes the longest match, so a two-word command always wins -over a one-word command that prefixes it. - -The layout mirrors the server: `main.go` at the repository root is the -composition root, everything else lives under `internal`. - -A command whose `run` is nil reports that it is not implemented yet and exits -non-zero. Filling one in means writing its own file in `internal/commands`, -holding everything that command needs, and pointing the table entry at it. -Removing one means deleting that file and its line in the table. - -`internal/commands/client.go` holds the one seam every command shares: the -server base URL, the stored key location, the request builder that stamps the -CLI version into User-Agent for the server's version negotiation, the -reachability check `main` runs before dispatching any command that talks to -the server, and the hidden `--server ` flag development uses to aim a -run at another server. The flag is deliberately absent from the help. - -Targets are positional. A fleet is named by the id `fleet list` shows, a -device by its IMEI, and a verb that can act on either takes one argument -accepting both. There is no default target and no bypass flag: a command -missing its target errors, and the destructive verbs ask for interactive -confirmation before acting. `internal/commands/fleets.go` holds the fleet -fetch the fleet-reading commands share. - -## Releases - -The version lives in one place, `version` in `main.go`. Change it in a release -pull request, then tag the commit that merges it. Product pull requests leave -it alone. Nothing injects the version at build time, because `-ldflags -X` -cannot write to a Go const; the release workflow refuses to build when the tag -and the const disagree. - -The workflow also refuses to build a tag that does not sit on `main`. GitHub -rulesets cannot express that, because a tag rule can restrict who creates a -tag but not which commit it points at, so the check lives in the workflow -where it can read the history. - -Pushing a `v*` tag is the whole release. GoReleaser builds the static binaries -for Linux, macOS, and Windows, publishes the GitHub release with checksums, -pushes the Homebrew cask to `siliconwitchery/homebrew-tap`, pushes the Scoop -manifest to `siliconwitchery/scoop-bucket`, and pushes the `superstack-bin` -PKGBUILD to the AUR. There is deliberately no winget package (its pull -requests review too slowly for the version gate's forced upgrades, and scoop -is the standard channel for developer tools), no `install.sh` (manual -installs are a download from the releases page, unpacked onto the PATH), and -no self-update command in the CLI (each channel updates itself; the server's -version gate is what prompts users to do so). - -The flake is the one distribution path not driven by a tag. It builds from -source at whatever commit the user points it at, so it needs no release to -work. Superstack is not in nixpkgs and will not be until the project has -traction, so the flake is how Nix users install until then. `flake.nix` -reads the version straight out of `main.go`, which keeps the single source -of truth intact, and renames the binary in `postInstall` because Go names it -after the module path rather than after the command. - -`.goreleaser.yaml` is the only description of the build matrix. Nothing else -may restate it, because a second copy drifts. CI proves the release path by -running `goreleaser release --snapshot` rather than by rebuilding the same -targets by hand, so the release workflow is never the first thing to exercise -that config. - -Every publisher carries `skip_upload: auto`, so a tag with a prerelease suffix -publishes a GitHub release and touches no package manager. That is how the -pipeline gets exercised without shipping. It leaves the three publisher pushes -themselves untested, which only a real tag proves. - -Two credentials sit behind the release, and the workflow's guard checks only -that they are present, never that they work. `TAP_GITHUB_TOKEN` is -fine-grained and scoped to the tap and the bucket. `AUR_KEY` is a -passphraseless SSH key registered with an AUR account. - -The generated changelog is deliberately disabled, so a fresh release starts -with an empty body. Write the notes into it afterwards; GoReleaser keeps an -existing body and will not overwrite them on a re-run. - -Release notes are written for someone deciding whether to upgrade, not for -someone reading the log. Lead with what changed for them, and never just list -commits. Order the bullets by what a user would notice first. Follow this -shape: - -``` -**Headline description** {Emoji} - -- Top feature/change -- Top feature/change -- Top feature/change -- Other notable/meaningful changes for users -- Security fixes/issues addressed - -**Breaking changes** - -- Change - solution if available -- ... -``` - -Omit the breaking changes section entirely when there are none. Omit the -security bullet when nothing was fixed. - -## Maintenance - -After adding or changing a major feature, re-read this file and update it so -the principles and the architectural overview stay accurate. - -Keep the README minimal as the project grows: the structure overview and the -setup steps, nothing else. Numbered imperative steps, self-contained command -blocks, constraints stated bare. No rationale, no explanation of how something -works, no troubleshooting. Anything that explains rather than instructs belongs -in this file, and a step that needs a paragraph to justify it is a sign the -step itself is wrong. diff --git a/README.md b/README.md index c50cf44..cd16d11 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,6 @@ a separate project; this repo is the CLI only. It is laid out as follows: ├── .github/workflows # CI on pull requests, release on v* tags ├── .gitignore ├── .goreleaser.yaml # Build matrix and every publishing target -├── CLAUDE.md # Coding principles and architectural overview ├── flake.lock # Pins nixpkgs ├── flake.nix # The superstack package, and the dev shell ├── go.mod @@ -142,8 +141,7 @@ Do everything below once. git tag "$tag" && git push origin "$tag" ``` -1. Write the release notes into the empty release body on GitHub, following the - shape in CLAUDE.md. +1. Write the release notes into the empty release body on GitHub. A tag carrying a prerelease suffix, `v0.0.2-rc1`, publishes a GitHub prerelease and skips every package manager. Tags cannot be moved or deleted.