This document is the family-wide standard for the static shape of a CLI
repository — directory structure, the files it must contain, the Go-version
policy, the Makefile contract, the lint config, and the repository settings.
It is the foundation the other repo-axis docs build on: ci.md, release.md,
and distribution.md all consume primitives defined here.
This is normative for new CLIs.
Companion pillars:
ci.md— invokes the Makefile targets (§4) and lint config (§5) defined here.release.md— readsversion.txt(§2) and the commit conventions (§7).distribution.md— reads.goreleaser.*(§2).agent-implementation.md— owns how agent entrypoint files are used; this doc owns that the files exist in the repo skeleton.- The behavior-axis docs (
command-surface.md,output-and-rendering.md,working-with-secrets.md,working-with-state.md,scriptability.md) govern what lives inside the directories below. This doc owns the skeleton; they own the contents.
repo-layout.md is foundational among the repo-axis docs — when another
repo-axis doc appears to conflict on a primitive defined here (Go version,
Makefile targets, lint config, version.txt), this doc wins. See
docs/README.md for the full conflict-resolution order.
cmd/<binary>/main.go # entry point — one per shipped binary
internal/
cmd/
root/ # root command + Options struct (the DI seam)
initcmd/ # <tool> init (scriptability.md §1)
setcred/ # <tool> set-credential (working-with-secrets.md §1.5.2)
config/ # <tool> config {show,clear,test}
me/ # <tool> me (scriptability.md §4)
<feature>/ # one package per domain command group
config/ # config-file load/save (working-with-state.md §3)
client/ # the data layer — API client (output-and-rendering.md §9)
output/ (or view/) # the presentation layer — pure render fns
version/ # version string wiring (release.md §2)
noleak/ # secret no-leak test helper (working-with-secrets.md)
testutil/ # shared test fixtures
api/ # OPTIONAL: exported public client library
This is the shape slck (the cli-common pilot) and nrq follow. A new CLI
starts from this tree. The three-layer seam — client (data) → output
(presentation) → internal/cmd (command glue) — is mandated by
output-and-rendering.md §9; this section only fixes where those layers live.
| File | Status | Notes |
|---|---|---|
README.md |
MUST | install + usage |
LICENSE |
MUST | MIT |
AGENTS.md |
MUST | thin agent entrypoint index; peer of CLAUDE.md |
CLAUDE.md |
MUST | thin agent entrypoint index; peer of AGENTS.md |
docs/development.md |
MUST | repo-local facts; tool-local equivalent allowed in monorepos |
.golangci.yml |
MUST | §5 |
Makefile |
MUST | §4 |
version.txt |
MUST | release.md §2 source-of-truth |
.gitignore / .gitattributes |
MUST | |
.goreleaser.{yml,yaml} |
MUST if distributed | distribution.md §1 |
packaging/identity.yml |
MUST if distributed | distribution.md §8 — required by identity-check |
CONTRIBUTING.md |
SHOULD | |
CHANGELOG.md |
SHOULD | release-generated |
Agent entrypoints are indexes, not containers for copied policy. Keep
repo-specific facts in docs/development.md; keep shared standards in
cli-common/docs; keep shared automation in open-cli-collective/.github.
See agent-implementation.md for the full source-of-truth and reference-link
policy.
Monorepo (atlassian-cli): per-tool files live under tools/<tool>/
(version.txt, .goreleaser-<tool>.yml); repo-root carries the shared
go.work, LICENSE, and CI. Tool-local agent entrypoints and
docs/development.md equivalents are allowed where they reduce navigation cost.
A repo that ships no binary and publishes through no package channel —
cli-common itself — follows this doc under a reduced profile:
- Required files:
README.md,LICENSE,AGENTS.md,CLAUDE.md,docs/development.md,.golangci.yml,Makefile,.gitignore/.gitattributes. Agent entrypoints are required in every repo agents work in, library or not. - Not required:
version.txt,.goreleaser.*,packaging/identity.yml,CHANGELOG.md— distribution files exist only where something ships. - Makefile: the §4 library exemption applies, with
check=tidy+lint+test+buildso a green localcheckstill predicts a green CI run. - Release/tagging: semver tags are cut manually.
release.md's auto-release and run-number patch scheme do NOT apply — a library with co-evolving consumers must gate its tags on the consumer matrix being green against the candidate SHA, and automating the tag would defeat that gate. (working-with-state.md§6 is the concrete instance of this rule for cli-common's state components.) - CI:
ci.mdapplies — build/test/lint across the three OSes,go-version-file: go.mod, andpr-titleon the same terms as every repo (i.e., when the shared composite ships in the family rollout,ci.md§8).identity-checkis N/A (nothing ships). This profile creates no new CI-restructuring obligation beyondci.md's existing rules.
- The floor is Go 1.26. New repos pin the floor.
- The single source is the
godirective ingo.mod. Everything else (ci.md§3, local dev) reads from there. Do not duplicate the version into a workflow literal that can drift. - Bump across the family in lockstep.
salesforce-cliis on1.24;hubspot-clideclares1.23.0ingo.modbut pins1.22in CI (a mismatch — sync both) (§8). - Monorepo: each tool has its own
go.mod; all sit on the floor.
CI and release invoke the Makefile, so the target names are a contract. Every repo's Makefile MUST provide:
| Target | Does |
|---|---|
build |
compile the binary/binaries |
test |
go test ./... |
test-cover |
tests with coverage profile |
lint |
golangci-lint run |
fmt |
gofmt/goimports |
tidy |
go mod tidy |
deps |
fetch/verify deps |
check |
tidy + fmt + lint + test + build — the local pre-push gate |
install |
install to a local bin |
release / snapshot |
goreleaser wrappers |
clean |
remove build artifacts |
make check MUST mirror what CI gates on, so a green local check predicts a
green CI run.
Library/shared-module exemption. The full target set above applies to
shipped-binary CLI repos. A library or shared module that produces no binary
and ships through no package channel — e.g. cli-common itself (§2.1) — needs
only tidy/lint/test/build, with check running all four, and is exempt
from install/release/snapshot/test-cover. Do not force distribution
targets onto a repo that distributes nothing.
.golangci.yml, format version: "2". The canonical floor linter set every
repo MUST enable:
errcheck govet ineffassign staticcheck unused misspell revive gosec errorlint exhaustive
This is already the de-facto family set — google-readonly and all three
atlassian-cli configs (shared/, tools/cfl, tools/jtk) ship it. gosec
and errorlint matter for CLIs that handle credentials, which is all of them.
Tests are excluded from errcheck (and from errorlint/gosec where noisy). A
repo MAY add more linters but MUST NOT drop below the floor; the leaner repos
(slack-chat-api, salesforce-cli) sit below it today (§8). Running on golangci
defaults (no config file) is non-conformant.
- Require a PR with 1 approval.
- Required status checks:
build,test,lint,pr-title, andidentity-check(ci.md§2).pr-titleis required because it gates release eligibility (release.md§1);identity-checkbecause it guards the single-source manifest (distribution.md§8.2).buildis the required aggregate over the non-requiredbuild-platformOS matrix (ci.md§2/§7) — requirebuild, never the per-OS legs (their names shift with the matrix). Current repos require onlytest+lint— fold the rest in when migrating. Because CI is shared via composite actions (ci.md§7), these are bare check names and stay bare on migration — no branch-protection rewrite (the context-rename hazard only applies to the reusable-workflow form, which CI does not use). - Sequencing (load-bearing): add a required check to branch protection only
after the workflow that produces it exists and has run green on the repo —
pr-titleafter the composite is wired,identity-checkafter the composite ships andpackaging/identity.ymlexists. A required check that no workflow ever reports stays permanently pending and silently blocks every PR. - Require signed commits.
- Require linear history.
- No force pushes.
- Squash merge only (no merge commits, no rebase merge). This is why the PR
title must be a conventional commit (
release.md§1). - Set
squash_merge_commit_title=PR_TITLEandsquash_merge_commit_message=PR_BODY.COMMIT_OR_PR_TITLEis prohibited: for a one-commit squash PR it can select the lone commit subject instead of the validated PR title and bypass thepr-titlecheck.PR_BODYkeeps the validated PR body as the squash commit body. - Delete branch on merge.
-
Conventional commits drive releases (
release.md§1). -
Commit messages MUST NOT mention AI tooling (Claude, Anthropic, ChatGPT, Copilot, etc.). Enforce with a
commit-msghook that greps a blocklist and rejects on match. The hook alone is insufficient under squash merge: the requiredPR_TITLE/PR_BODYsettings build the landing commit from the PR title and body, neither of which the local hook sees. The CIpr-titlecheck therefore greps both against the same blocklist (ci.md§2). Reference implementation (track it asscripts/hooks/commit-msgand wire viagit config core.hooksPath scripts/hooks):#!/bin/sh if grep -iEq 'claude|anthropic|chatgpt|copilot|codex' "$1"; then echo "commit message references AI tooling — remove it (repo-layout.md §7)" >&2 exit 1 fi
.golangci.ymlmissing incodereview-clionly — it is the single repo without one.atlassian-cliis not missing it: as ago.workmonorepo it carries three configs (shared/,tools/cfl,tools/jtk), all on the §5 set.- Linter floor not yet met by
slack-chat-apiandsalesforce-cli(lean 6-linter core, missingrevive/gosec/errorlint/exhaustive) (§5). - Agent guidance entrypoints missing in
hubspot-cliandsalesforce-cli: both haveCLAUDE.mdbut lackAGENTS.mdanddocs/development.md(§2). CONTRIBUTING.md/CHANGELOG.mdgaps ingoogle-readonly,salesforce-cli,hubspot-cli, andatlassian-cliroot (§2).- Go versions:
hubspot-clideclaresgo 1.23.0ingo.modbut its CI pins1.22(an internal mismatch — sync both);salesforce-cliis on1.24; all others on1.26(§3). - Required-check names:
newrelic-cliandcodereview-clibranch protection requires onlytest+lint(nobuild, nopr-title, noidentity-check). Composite-based CI (ci.md§7) keeps these bare, so migration just adds the missing checks — no renaming of the existing ones (§6). - Makefile target coverage varies;
google-readonlyalone hastest-cover-check.cli-commonis intentionally minimal (library exemption, §4).