diff --git a/CHANGELOG.md b/CHANGELOG.md index 611db93..450a751 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,20 @@ and the project adheres to [Semantic Versioning 2.0.0](https://semver.org/spec/v ## [Unreleased] +### Added +- **`commitbrief upgrade` — in-tool updates across every install method (ADR-0034).** + Detects whether the running binary came from Homebrew, Scoop, `go install` + or a GitHub Releases tarball. Package-managed installs are delegated to + their own manager (`brew upgrade` / `scoop update` / `go install …@latest`) + because overwriting a manager-owned binary desynchronizes its metadata; + only a manual install is replaced in place, after its SHA-256 is verified + against the release `checksums.txt`. An unwritable target aborts *before* + anything is downloaded and prints the exact command to run — CommitBrief + never invokes `sudo` itself. `--check` reports without installing and + always exits 0; `--json` implies `--check`. + The version check runs **only** when you invoke the command: there is no + automatic update check and no telemetry. + ## [1.14.0] - 2026-07-25 ### Added diff --git a/README.md b/README.md index d0ab021..a1f3fba 100644 --- a/README.md +++ b/README.md @@ -104,6 +104,24 @@ Pre-built binaries for Linux, macOS, and Windows on amd64 and arm64 are attached to each tagged release at [github.com/CommitBrief/commitbrief/releases](https://github.com/CommitBrief/commitbrief/releases). +### Upgrade + +```sh +commitbrief upgrade # check, confirm, install +commitbrief upgrade --check # report only; install nothing +``` + +`upgrade` detects how the binary was installed and does the right thing for +it: Homebrew, Scoop and `go install` are handed to their own package +manager, while a manually installed binary is downloaded from GitHub +Releases, SHA-256 verified against the release checksums, and swapped in +place. If the binary's directory is not writable, nothing is downloaded and +the exact command you need is printed — CommitBrief never runs `sudo` +itself. Only the binary is replaced; bundled man pages are not installed. + +This is the only network request CommitBrief makes on its own behalf, and +only when you run this command. There is no automatic update check. + ## Stability The v1.0.0 line is an **API freeze**. CLI flag surface, the JSON @@ -222,6 +240,7 @@ commitbrief init [--force] # write COMMITBRIEF.md + OUTPUT.md commitbrief compress [--level=balanced] [--dry-run] # shrink COMMITBRIEF.md (preview first if you want) commitbrief doctor # health-check the pipeline commitbrief install-hook [--hook=...] # install a git hook that runs commitbrief +commitbrief upgrade [--check] # check GitHub Releases and install a newer CommitBrief commitbrief dry-run # pipeline preview; no API call commitbrief list # command reference commitbrief mcp # run an MCP server over stdio (agent review gate; see "MCP server") diff --git a/internal/cli/cli_test.go b/internal/cli/cli_test.go index a7bfae3..4d14871 100644 --- a/internal/cli/cli_test.go +++ b/internal/cli/cli_test.go @@ -11,7 +11,7 @@ import ( func TestRootCommandHasSubcommands(t *testing.T) { root := newRootCmd() - want := []string{"cache", "commit", "compress", "config", "diff", "doctor", "dry-run", "guard", "init", "install-hook", "list", "mcp", "providers", "remote", "setup", "summary"} + want := []string{"cache", "commit", "compress", "config", "diff", "doctor", "dry-run", "guard", "init", "install-hook", "list", "mcp", "providers", "remote", "setup", "summary", "upgrade"} got := []string{} for _, c := range root.Commands() { // cobra adds `help` and `completion` automatically; filter to ours. diff --git a/internal/cli/root.go b/internal/cli/root.go index f11445f..4c20b15 100644 --- a/internal/cli/root.go +++ b/internal/cli/root.go @@ -169,6 +169,7 @@ func newRootCmd() *cobra.Command { newSummaryCmd(), newMCPCmd(), newGuardCmd(), + newUpgradeCmd(), ) return cmd } diff --git a/internal/cli/upgrade.go b/internal/cli/upgrade.go new file mode 100644 index 0000000..34037c1 --- /dev/null +++ b/internal/cli/upgrade.go @@ -0,0 +1,306 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package cli + +import ( + "encoding/json" + "errors" + "fmt" + "io" + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" + + "github.com/spf13/cobra" + + "github.com/CommitBrief/commitbrief/internal/ui" + "github.com/CommitBrief/commitbrief/internal/upgrade" + "github.com/CommitBrief/commitbrief/internal/version" +) + +// upgradeReport is the --json payload. It is intentionally NOT review +// schema v1 — it describes an installation, not a review, and nothing +// in the findings contract changes because of it. +type upgradeReport struct { + Current string `json:"current"` + Latest string `json:"latest"` + Method string `json:"method"` + UpdateAvailable bool `json:"update_available"` + Action string `json:"action"` +} + +func writeUpgradeJSON(w io.Writer, rep upgradeReport) error { + enc := json.NewEncoder(w) + enc.SetIndent("", " ") + return enc.Encode(rep) +} + +func newUpgradeCmd() *cobra.Command { + var checkOnly bool + cmd := &cobra.Command{ + Use: "upgrade", + Short: "Check for a newer CommitBrief and install it", + Long: `Checks GitHub Releases for a newer CommitBrief and installs it. + +How the binary was installed decides what happens. A Homebrew, Scoop or +'go install' install is upgraded by its own package manager, because +overwriting a manager-owned binary desynchronizes its metadata. Only a +manually installed binary (a GitHub Releases tarball) is downloaded, +SHA-256 verified against the release checksums, and replaced in place. + +Nothing is installed without confirmation, and nothing is downloaded if +the target cannot be written — CommitBrief never invokes sudo itself. + +--check reports what would happen and installs nothing. --json implies +--check: it prints a single report object and exits. + +This is the only network request CommitBrief makes on its own behalf, +and only when you run this command. There is no automatic update check +and no telemetry.`, + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + return runUpgrade(cmd, checkOnly || global.json) + }, + } + cmd.Flags().BoolVar(&checkOnly, "check", false, "only report whether a newer version exists; install nothing") + return cmd +} + +func runUpgrade(cmd *cobra.Command, checkOnly bool) error { + app, err := resolveContext(false) + if err != nil { + return err + } + cat := app.Catalog + out := cmd.OutOrStdout() + msg := cmd.ErrOrStderr() // human chatter goes to stderr so --json stdout stays clean + + exe, err := upgrade.ResolveExe() + if err != nil { + return err + } + // Sweep the moved-aside binary a previous Windows upgrade could not + // delete while it was still executing. + upgrade.CleanupStale(exe) + + method := upgrade.Detect(upgrade.CurrentEnv(exe)) + + current, ok := upgrade.ParseVersion(version.Version) + if !ok { + // --json promises a report object on every non-error run. A dev + // build must therefore still emit one: returning nil with empty + // stdout would hand a parsing script silence and exit 0, with no + // way to tell "no update" from "the command did nothing". + if global.json { + return writeUpgradeJSON(out, upgradeReport{ + Current: version.Version, + Method: string(method), + UpdateAvailable: false, + }) + } + if checkOnly { + _, _ = fmt.Fprintln(msg, cat.T("upgrade.dev_build", version.Version)) + return nil + } + return errors.New(cat.T("upgrade.dev_build", version.Version)) + } + + client := upgrade.NewClient(version.Version) + rel, err := client.Latest(cmd.Context()) + if err != nil { + switch { + case errors.Is(err, upgrade.ErrRateLimited): + return errors.New(cat.T("upgrade.err_rate_limited")) + case errors.Is(err, upgrade.ErrNoRelease): + return errors.New(cat.T("upgrade.err_no_release")) + case errors.Is(err, upgrade.ErrBadResponse): + // The server was reached and answered; the payload just + // didn't parse. That is distinct from a network failure and + // must not be reported as one. + return errors.New(cat.T("upgrade.err_bad_response", err)) + default: + return errors.New(cat.T("upgrade.err_network", err)) + } + } + + latest, ok := upgrade.ParseVersion(rel.TagName) + if !ok { + return errors.New(cat.T("upgrade.err_no_release")) + } + + argv := upgrade.Command(method) + action := strings.Join(argv, " ") + if method == upgrade.MethodManual { + action = upgrade.AssetName(rel.TagName, runtime.GOOS, runtime.GOARCH) + } + + rep := upgradeReport{ + Current: current.String(), + Latest: latest.String(), + Method: string(method), + UpdateAvailable: current.Compare(latest) < 0, + Action: action, + } + + if !rep.UpdateAvailable { + if global.json { + rep.Action = "" + return writeUpgradeJSON(out, rep) + } + _, _ = fmt.Fprintln(msg, cat.T("upgrade.up_to_date", current.String())) + return nil + } + + if global.json { + return writeUpgradeJSON(out, rep) + } + + _, _ = fmt.Fprintln(msg, cat.T("upgrade.available", current.String(), latest.String())) + _, _ = fmt.Fprintln(msg, cat.T("upgrade.method", string(method))) + if method == upgrade.MethodManual { + _, _ = fmt.Fprintln(msg, cat.T("upgrade.action_manual", action)) + } else { + _, _ = fmt.Fprintln(msg, cat.T("upgrade.action_managed", action)) + } + + if checkOnly { + // Surface the same permission gate a real run would hit, so a + // root-owned or read-only manual install is reported honestly + // instead of promising an install that would actually be + // refused. Never reached when --json implied checkOnly: that + // path already returned above, and --json prints only the + // report object. A warning only — --check always exits 0. + if method == upgrade.MethodManual { + if err := upgrade.PreflightWritable(exe); err != nil { + _, _ = fmt.Fprintln(msg, cat.T("upgrade.err_not_writable", exe)) + _, _ = fmt.Fprintln(msg, cat.T("upgrade.hint_manual")) + _, _ = fmt.Fprintf(msg, " sudo %s upgrade\n", exe) + _, _ = fmt.Fprintf(msg, " %s\n", upgrade.ReleasesPage) + } + } + return nil + } + + // NonInteractive mirrors internal/cli/cache.go: without a TTY and + // without --yes the answer is a deterministic "no", so an unattended + // run aborts instead of hanging or half-consuming stdin. + confirmed, err := ui.Confirm(cmd.InOrStdin(), msg, cat.T("upgrade.confirm"), ui.AskOptions{ + AssumeYes: global.yes, + Interactive: ui.IsStdinTTY(os.Stdin), + NonInteractive: !ui.IsStdinTTY(os.Stdin), + Catalog: app.Catalog, + }) + if err != nil { + return err + } + if !confirmed { + _, _ = fmt.Fprintln(msg, cat.T("upgrade.cancelled")) + return nil + } + + if method == upgrade.MethodManual { + if err := upgrade.PreflightWritable(exe); err != nil { + _, _ = fmt.Fprintln(msg, cat.T("upgrade.hint_manual")) + _, _ = fmt.Fprintf(msg, " sudo %s upgrade\n", exe) + _, _ = fmt.Fprintf(msg, " %s\n", upgrade.ReleasesPage) + return errors.New(cat.T("upgrade.err_not_writable", exe)) + } + if err := upgrade.InstallManual(cmd.Context(), upgrade.ManualOptions{ + Client: client, + Release: rel, + Target: exe, + GOOS: runtime.GOOS, + GOARCH: runtime.GOARCH, + }); err != nil { + switch { + case errors.Is(err, upgrade.ErrChecksumMismatch): + return errors.New(cat.T("upgrade.err_checksum", action)) + case errors.Is(err, upgrade.ErrAssetMissing): + return errors.New(cat.T("upgrade.err_asset_missing", runtime.GOOS, runtime.GOARCH, upgrade.ReleasesPage)) + case errors.Is(err, upgrade.ErrNotWritable): + return errors.New(cat.T("upgrade.err_not_writable", exe)) + default: + return err + } + } + verifyReplacement(cmd, cat, msg, exe, latest) + } else { + if err := upgrade.Run(cmd.Context(), argv, msg, cmd.ErrOrStderr()); err != nil { + if errors.Is(err, upgrade.ErrToolMissing) { + return errors.New(cat.T("upgrade.err_tool_missing", string(method), argv[0])) + } + return err + } + } + + _, _ = fmt.Fprintln(msg, cat.T("upgrade.success", latest.String())) + return nil +} + +// verifyReplacement re-runs the binary we just swapped and checks two +// independent things, each with its own warning: whether it reports the +// expected version, and whether some other "commitbrief" resolves +// earlier on PATH than the file that was just upgraded. Neither implies +// the other — a version mismatch after execing the resolved `exe` +// directly cannot be explained by PATH shadowing, since exec bypasses +// PATH entirely; the shadow check exists to catch the separate, real +// problem that the command the user types next may still resolve to +// the old binary even though this exact file was upgraded correctly. +// +// Never fatal — the upgrade already happened, and neither a failure to +// re-exec (a sandbox, a hardened mount) nor a shadowing PATH entry is a +// reason to report failure. Applies to the manual path only; a package +// manager may relocate its binary, so `exe` is not necessarily the new +// file after delegation. +func verifyReplacement(cmd *cobra.Command, cat catalog, msg io.Writer, exe string, latest upgrade.Version) { + out, err := exec.CommandContext(cmd.Context(), exe, "--version").Output() + if err != nil { + _, _ = fmt.Fprintln(msg, cat.T("upgrade.verify_failed", err)) + } else if reported := strings.TrimSpace(string(out)); !reportsVersion(reported, latest) { + _, _ = fmt.Fprintln(msg, cat.T("upgrade.verify_mismatch", reported, latest.String())) + } + + if shadow, ok := shadowingPath(exe); ok { + _, _ = fmt.Fprintln(msg, cat.T("upgrade.verify_shadowed", exe, shadow)) + } +} + +// shadowingPath reports whatever "commitbrief" the user's shell would +// actually resolve on PATH, if it differs from exe (the binary just +// upgraded). ok is false when PATH has no commitbrief, or it resolves +// to exe itself — neither is worth reporting, and a LookPath failure is +// not an error in its own right, just the common case of a manual +// install that was never put on PATH. +// +// Compares with upgrade.SamePath rather than == : on Windows, exe (from +// os.Executable + EvalSymlinks) and the PATH lookup can differ in +// letter case or `\`-vs-`/` without naming a different file — EvalSymlinks +// normalizes neither — and a plain == would warn about a shadow that +// does not exist. +func shadowingPath(exe string) (shadow string, ok bool) { + found, err := exec.LookPath("commitbrief") + if err != nil { + return "", false + } + resolved, err := filepath.EvalSymlinks(found) + if err != nil { + // A broken or unreadable link is not fatal to the check — + // compare what LookPath found, unresolved. + resolved = found + } + if upgrade.SamePath(resolved, exe, runtime.GOOS) { + return "", false + } + return resolved, true +} + +// reportsVersion checks whether --version output names the expected +// release. The leading "v" is trimmed because goreleaser injects the tag +// without it (`-X …version.Version={{.Version}}`), so a released binary +// prints "commitbrief 1.15.0 (…)" while the tag reads "v1.15.0". +// Comparing them verbatim would warn on every successful upgrade. +func reportsVersion(output string, v upgrade.Version) bool { + return strings.Contains(output, strings.TrimPrefix(v.String(), "v")) +} diff --git a/internal/cli/upgrade_test.go b/internal/cli/upgrade_test.go new file mode 100644 index 0000000..ca96c4f --- /dev/null +++ b/internal/cli/upgrade_test.go @@ -0,0 +1,161 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package cli + +import ( + "os" + "path/filepath" + "runtime" + "strings" + "testing" + + "github.com/spf13/cobra" + + "github.com/CommitBrief/commitbrief/internal/upgrade" +) + +func findSubcommand(root *cobra.Command, name string) *cobra.Command { + for _, c := range root.Commands() { + if c.Name() == name { + return c + } + } + return nil +} + +func TestUpgradeCommandRegistered(t *testing.T) { + root := newRootCmd() + cmd := findSubcommand(root, "upgrade") + if cmd == nil { + t.Fatal("upgrade command is not registered on root") + return + } + if cmd.Flags().Lookup("check") == nil { + t.Fatal("--check flag is missing") + } + if cmd.Args == nil { + t.Fatal("upgrade should reject positional arguments") + } +} + +func TestUpgradeCommandRejectsArgs(t *testing.T) { + cmd := newUpgradeCmd() + if err := cmd.Args(cmd, []string{"v1.2.3"}); err == nil { + t.Fatal("Args() error = nil, want a rejection of positional arguments") + } +} + +func TestUpgradeReportJSON(t *testing.T) { + rep := upgradeReport{ + Current: "v1.14.0", + Latest: "v1.15.0", + Method: "homebrew", + UpdateAvailable: true, + Action: "brew upgrade commitbrief", + } + var sb strings.Builder + if err := writeUpgradeJSON(&sb, rep); err != nil { + t.Fatalf("writeUpgradeJSON() error = %v", err) + } + out := sb.String() + for _, want := range []string{`"current": "v1.14.0"`, `"latest": "v1.15.0"`, `"method": "homebrew"`, `"update_available": true`} { + if !strings.Contains(out, want) { + t.Fatalf("JSON output missing %s:\n%s", want, out) + } + } +} + +// TestReportsVersion pins the tag-vs-binary version comparison. The +// goreleaser-shaped case is the one that matters: the binary prints its +// version WITHOUT a leading "v", so a verbatim comparison against the tag +// would warn on every successful upgrade. +func TestReportsVersion(t *testing.T) { + v, ok := upgrade.ParseVersion("v1.15.0") + if !ok { + t.Fatal("ParseVersion failed") + } + cases := []struct { + output string + want bool + }{ + {"commitbrief 1.15.0 (commit abc1234, built 2026-07-26)", true}, + {"commitbrief v1.15.0 (commit abc1234, built 2026-07-26)", true}, + {"commitbrief 1.14.0 (commit abc1234, built 2026-07-04)", false}, + {"commitbrief dev (commit none, built unknown)", false}, + {"", false}, + } + for _, c := range cases { + if got := reportsVersion(c.output, v); got != c.want { + t.Fatalf("reportsVersion(%q) = %v, want %v", c.output, got, c.want) + } + } +} + +// TestShadowingPath pins the three outcomes verifyReplacement's shadow +// warning depends on: nothing on PATH, PATH resolving to a different +// binary than the one just upgraded, and PATH resolving to that same +// binary (the common, unremarkable case — no warning). +func TestShadowingPath(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("PATH/executable-bit resolution differs on windows") + } + dir := t.TempDir() + bin := filepath.Join(dir, "commitbrief") + if err := os.WriteFile(bin, []byte("#!/bin/sh\necho ok\n"), 0o755); err != nil { + t.Fatal(err) + } + resolved, err := filepath.EvalSymlinks(bin) + if err != nil { + t.Fatal(err) + } + + t.Run("nothing on PATH", func(t *testing.T) { + t.Setenv("PATH", t.TempDir()) + if _, ok := shadowingPath("/wherever/commitbrief"); ok { + t.Fatal("shadowingPath() ok = true, want false when PATH has no commitbrief") + } + }) + + t.Run("resolves to a different binary", func(t *testing.T) { + t.Setenv("PATH", dir) + shadow, ok := shadowingPath("/somewhere/else/commitbrief") + if !ok { + t.Fatal("shadowingPath() ok = false, want true") + } + if shadow != resolved { + t.Fatalf("shadowingPath() = %q, want %q", shadow, resolved) + } + }) + + t.Run("resolves to the same binary", func(t *testing.T) { + t.Setenv("PATH", dir) + if _, ok := shadowingPath(resolved); ok { + t.Fatal("shadowingPath() ok = true, want false when PATH resolves to exe itself") + } + }) +} + +// TestUpgradeReportJSONDevBuild pins the shape emitted for a build whose +// version cannot be parsed. --json must still produce a report there: +// empty stdout with exit 0 gives a parsing script no way to tell "no +// update available" from "the command did nothing". +func TestUpgradeReportJSONDevBuild(t *testing.T) { + rep := upgradeReport{ + Current: "dev", + Method: "manual", + UpdateAvailable: false, + } + var sb strings.Builder + if err := writeUpgradeJSON(&sb, rep); err != nil { + t.Fatalf("writeUpgradeJSON() error = %v", err) + } + out := sb.String() + if strings.TrimSpace(out) == "" { + t.Fatal("dev-build report produced empty output") + } + for _, want := range []string{`"current": "dev"`, `"method": "manual"`, `"update_available": false`} { + if !strings.Contains(out, want) { + t.Fatalf("JSON output missing %s:\n%s", want, out) + } + } +} diff --git a/internal/i18n/messages.en.yml b/internal/i18n/messages.en.yml index 368ba73..b9fe3af 100644 --- a/internal/i18n/messages.en.yml +++ b/internal/i18n/messages.en.yml @@ -265,3 +265,24 @@ flaky.sandbox.transient: "Sandbox rerun did not reproduce the flake: the test pa flaky.sandbox.command_invalid: "Invalid review.sandbox_command: %s" flaky.sandbox.running: "Sandbox rerun: running `%s` up to %d times per flagged test." flaky.sandbox.no_test_name: "Sandbox rerun skipped for %s:%d — could not resolve the enclosing test name." +upgrade.up_to_date: "You are already on the latest version (%s)." +upgrade.available: "A newer version is available: %s → %s" +upgrade.method: "Install method: %s" +upgrade.action_managed: "Will run: %s" +upgrade.action_manual: "Will download and install: %s" +upgrade.confirm: "Upgrade now?" +upgrade.cancelled: "Upgrade cancelled." +upgrade.success: "Upgraded to %s." +upgrade.dev_build: "This is a development build (%s); upgrade does not apply." +upgrade.err_network: "could not reach the GitHub release API: %v" +upgrade.err_bad_response: "the GitHub release API returned a response that could not be understood: %v" +upgrade.err_rate_limited: "GitHub API rate limit reached; try again in an hour" +upgrade.err_no_release: "no published release found" +upgrade.err_asset_missing: "no release asset published for %s/%s; download it manually from %s" +upgrade.err_checksum: "checksum mismatch for %s — nothing was installed" +upgrade.err_not_writable: "cannot write to %s" +upgrade.hint_manual: "Run it with elevated privileges, or install manually:" +upgrade.err_tool_missing: "%s install detected, but %q was not found on PATH" +upgrade.verify_failed: "warning: could not re-run the upgraded binary to verify it: %v" +upgrade.verify_mismatch: "warning: the upgraded binary reports %q, not the expected %q" +upgrade.verify_shadowed: "warning: %q was upgraded, but %q comes first on PATH and will run instead" diff --git a/internal/i18n/messages.tr.yml b/internal/i18n/messages.tr.yml index 0ac1d82..0daccbb 100644 --- a/internal/i18n/messages.tr.yml +++ b/internal/i18n/messages.tr.yml @@ -264,3 +264,24 @@ flaky.sandbox.transient: "Sandbox yeniden çalıştırma kararsızlığı yenide flaky.sandbox.command_invalid: "Geçersiz review.sandbox_command: %s" flaky.sandbox.running: "Sandbox yeniden çalıştırma: `%s` her işaretlenen test için en fazla %d kez çalıştırılıyor." flaky.sandbox.no_test_name: "%s:%d için sandbox yeniden çalıştırma atlandı — kapsayan test adı çözülemedi." +upgrade.up_to_date: "Zaten en güncel sürümdesin (%s)." +upgrade.available: "Yeni bir sürüm var: %s → %s" +upgrade.method: "Kurulum yöntemi: %s" +upgrade.action_managed: "Çalıştırılacak: %s" +upgrade.action_manual: "İndirilip kurulacak: %s" +upgrade.confirm: "Şimdi güncellensin mi?" +upgrade.cancelled: "Güncelleme iptal edildi." +upgrade.success: "%s sürümüne güncellendi." +upgrade.dev_build: "Bu bir geliştirme derlemesi (%s); upgrade uygulanmaz." +upgrade.err_network: "GitHub release API'sine erişilemedi: %v" +upgrade.err_bad_response: "GitHub release API'si anlaşılamayan bir yanıt döndürdü: %v" +upgrade.err_rate_limited: "GitHub API istek limiti doldu; bir saat sonra tekrar dene" +upgrade.err_no_release: "yayınlanmış release bulunamadı" +upgrade.err_asset_missing: "%s/%s için yayınlanmış release dosyası yok; %s adresinden elle indir" +upgrade.err_checksum: "%s için checksum uyuşmadı — hiçbir şey kurulmadı" +upgrade.err_not_writable: "%s yazılamıyor" +upgrade.hint_manual: "Yükseltilmiş yetkiyle çalıştır ya da elle kur:" +upgrade.err_tool_missing: "%s kurulumu tespit edildi ama %q PATH'te bulunamadı" +upgrade.verify_failed: "uyarı: güncellenen binary doğrulama için çalıştırılamadı: %v" +upgrade.verify_mismatch: "uyarı: güncellenen binary %q bildiriyor, beklenen %q değil" +upgrade.verify_shadowed: "uyarı: %q güncellendi, ama PATH'te önce %q geliyor ve onun yerine o çalışacak" diff --git a/internal/upgrade/asset.go b/internal/upgrade/asset.go new file mode 100644 index 0000000..8ab0d1a --- /dev/null +++ b/internal/upgrade/asset.go @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package upgrade + +import ( + "fmt" + "strings" +) + +// ChecksumsFile is the name goreleaser gives the checksum manifest +// attached to every release (.goreleaser.yaml → checksum.name_template). +const ChecksumsFile = "checksums.txt" + +// AssetName mirrors the archive name_template in .goreleaser.yaml: +// +// {{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ arch }} +// +// where .Version carries no leading "v", amd64 renders as x86_64, 386 as +// i386, and Windows archives are .zip while everything else is .tar.gz. +// If that template ever changes, this function changes with it. +func AssetName(version, goos, goarch string) string { + v := strings.TrimPrefix(strings.TrimSpace(version), "v") + arch := goarch + switch goarch { + case "amd64": + arch = "x86_64" + case "386": + arch = "i386" + } + ext := ".tar.gz" + if goos == "windows" { + ext = ".zip" + } + return fmt.Sprintf("commitbrief_%s_%s_%s%s", v, goos, arch, ext) +} + +// BinaryEntryName is the archive entry holding the executable itself. +// goreleaser places it at the archive root next to LICENSE/README. +func BinaryEntryName(goos string) string { + if goos == "windows" { + return "commitbrief.exe" + } + return "commitbrief" +} + +// ParseChecksums reads a " " manifest into a +// filename → hex-sum map. A leading '*' on the filename marks binary +// mode in the sha256sum format and is not part of the name. +func ParseChecksums(data []byte) map[string]string { + sums := make(map[string]string) + for _, line := range strings.Split(string(data), "\n") { + fields := strings.Fields(strings.TrimSpace(line)) + if len(fields) != 2 { + continue + } + sums[strings.TrimPrefix(fields[1], "*")] = fields[0] + } + return sums +} diff --git a/internal/upgrade/asset_test.go b/internal/upgrade/asset_test.go new file mode 100644 index 0000000..6d3bb80 --- /dev/null +++ b/internal/upgrade/asset_test.go @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package upgrade + +import "testing" + +func TestAssetName(t *testing.T) { + cases := []struct { + version string + goos string + goarch string + want string + }{ + {"v1.15.0", "darwin", "arm64", "commitbrief_1.15.0_darwin_arm64.tar.gz"}, + {"1.15.0", "darwin", "amd64", "commitbrief_1.15.0_darwin_x86_64.tar.gz"}, + {"v1.15.0", "linux", "amd64", "commitbrief_1.15.0_linux_x86_64.tar.gz"}, + {"v1.15.0", "linux", "arm64", "commitbrief_1.15.0_linux_arm64.tar.gz"}, + {"v1.15.0", "windows", "amd64", "commitbrief_1.15.0_windows_x86_64.zip"}, + {"v2.0.0-rc.1", "linux", "arm64", "commitbrief_2.0.0-rc.1_linux_arm64.tar.gz"}, + } + for _, c := range cases { + if got := AssetName(c.version, c.goos, c.goarch); got != c.want { + t.Fatalf("AssetName(%q,%q,%q) = %q, want %q", c.version, c.goos, c.goarch, got, c.want) + } + } +} + +func TestBinaryEntryName(t *testing.T) { + if got := BinaryEntryName("linux"); got != "commitbrief" { + t.Fatalf("BinaryEntryName(linux) = %q", got) + } + if got := BinaryEntryName("windows"); got != "commitbrief.exe" { + t.Fatalf("BinaryEntryName(windows) = %q", got) + } +} + +func TestParseChecksums(t *testing.T) { + data := []byte( + "abc123 commitbrief_1.15.0_darwin_arm64.tar.gz\n" + + "def456 commitbrief_1.15.0_linux_x86_64.tar.gz\n" + + "\n" + + "789fed *commitbrief_1.15.0_windows_x86_64.zip\n") + sums := ParseChecksums(data) + if got := sums["commitbrief_1.15.0_darwin_arm64.tar.gz"]; got != "abc123" { + t.Fatalf("darwin sum = %q, want abc123", got) + } + if got := sums["commitbrief_1.15.0_linux_x86_64.tar.gz"]; got != "def456" { + t.Fatalf("linux sum = %q, want def456", got) + } + // goreleaser writes binary-mode entries with a leading '*'; the + // star belongs to the format, not to the file name. + if got := sums["commitbrief_1.15.0_windows_x86_64.zip"]; got != "789fed" { + t.Fatalf("windows sum = %q, want 789fed", got) + } + if len(sums) != 3 { + t.Fatalf("len(sums) = %d, want 3", len(sums)) + } +} diff --git a/internal/upgrade/cleanup.go b/internal/upgrade/cleanup.go new file mode 100644 index 0000000..33836c1 --- /dev/null +++ b/internal/upgrade/cleanup.go @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package upgrade + +import ( + "os" + "path/filepath" +) + +// CleanupStale removes scratch files a previous, interrupted upgrade +// could not clean up itself. A normal InstallManual run's defers remove +// its own ".commitbrief-dl-*" (downloaded archive) and +// ".commitbrief-bin-*" (extracted binary) temp files, but Ctrl-C kills +// the process before any defer runs, so an interrupted upgrade leaves +// one of them — up to ~12 MiB — sitting next to the target binary +// forever, until the next `upgrade` sweeps it here. +// +// A concurrent upgrade's in-flight temp file could in principle be +// swept out from under it by this same glob. That upgrade simply fails +// on its next read or write of the now-missing file; it never gets far +// enough to touch the installed binary, so the loser of that race fails +// safely rather than corrupting anything. +// +// Also runs cleanupOld, the platform-specific half of this sweep: the +// moved-aside ".old" binary a previous Windows upgrade could not +// delete while it was still executing (a no-op on Unix, where the +// rename leaves nothing behind). +func CleanupStale(target string) { + dir := filepath.Dir(target) + for _, pattern := range []string{".commitbrief-dl-*", ".commitbrief-bin-*"} { + matches, _ := filepath.Glob(filepath.Join(dir, pattern)) + for _, m := range matches { + _ = os.Remove(m) + } + } + cleanupOld(target) +} diff --git a/internal/upgrade/detect.go b/internal/upgrade/detect.go new file mode 100644 index 0000000..b1c02a0 --- /dev/null +++ b/internal/upgrade/detect.go @@ -0,0 +1,165 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package upgrade + +import ( + "os" + "path/filepath" + "runtime" + "strings" +) + +// Method is how the running binary was installed. It decides whether +// `upgrade` delegates to a package manager or replaces the file itself +// (ADR-0034 §D1). +type Method string + +const ( + MethodHomebrew Method = "homebrew" + MethodScoop Method = "scoop" + MethodGoInstall Method = "go-install" + MethodManual Method = "manual" +) + +// Env is every piece of ambient state Detect reads. It is passed in +// rather than read from os.Getenv/runtime inside Detect so a macOS host +// can exercise the Windows and Scoop branches in a unit test. +type Env struct { + ExePath string // resolved (symlinks evaluated) path of the running binary + GOOS string + Scoop string // $SCOOP + UserProfile string // %USERPROFILE% + GOBIN string + GOPATH string + Home string +} + +// ResolveExe returns the running binary's path with symlinks resolved. +// Resolution is mandatory, not cosmetic: Homebrew installs the binary +// into the Cellar and links it from /bin, so an unresolved path +// hides the one marker that identifies a brew install. +func ResolveExe() (string, error) { + exe, err := os.Executable() + if err != nil { + return "", err + } + resolved, err := filepath.EvalSymlinks(exe) + if err != nil { + // A broken or unreadable link is not fatal — fall back to the + // unresolved path and let detection do what it can. + return exe, nil + } + return resolved, nil +} + +// CurrentEnv builds an Env from the live process environment. +func CurrentEnv(exePath string) Env { + home, _ := os.UserHomeDir() + return Env{ + ExePath: exePath, + GOOS: runtime.GOOS, + Scoop: os.Getenv("SCOOP"), + UserProfile: os.Getenv("USERPROFILE"), + GOBIN: os.Getenv("GOBIN"), + GOPATH: os.Getenv("GOPATH"), + Home: home, + } +} + +// Detect classifies the installation, most specific marker first. +// Anything unrecognized is MethodManual — including distro packages we +// do not publish (nix, AUR, apt). That is safe by construction: those +// live in read-only or root-owned locations, so the write-permission +// gate aborts before a single byte is downloaded. +func Detect(env Env) Method { + p := normalizePath(env.ExePath, env.GOOS) + + if strings.Contains(p, "/cellar/commitbrief/") { + return MethodHomebrew + } + + if scoop := normalizePath(env.Scoop, env.GOOS); scoop != "" && + strings.HasPrefix(p, strings.TrimSuffix(scoop, "/")+"/apps/commitbrief/") { + return MethodScoop + } + if strings.Contains(p, "/scoop/apps/commitbrief/") { + return MethodScoop + } + + dir := pathDir(p) + for _, bin := range goBinDirs(env) { + if b := normalizePath(bin, env.GOOS); b != "" && strings.TrimSuffix(b, "/") == dir { + return MethodGoInstall + } + } + + return MethodManual +} + +// goBinDirs lists the directories `go install` could have written to, +// in the same precedence order the go command uses. +func goBinDirs(env Env) []string { + var dirs []string + if env.GOBIN != "" { + dirs = append(dirs, env.GOBIN) + } + if env.GOPATH != "" { + // GOPATH may be a list; only the first entry receives binaries. + first := strings.Split(env.GOPATH, string(os.PathListSeparator))[0] + if first != "" { + dirs = append(dirs, filepath.Join(first, "bin")) + } + } + if env.Home != "" { + dirs = append(dirs, filepath.Join(env.Home, "go", "bin")) + } + return dirs +} + +// SamePath reports whether a and b name the same filesystem location, +// under the platform rules normalizePath already applies for the +// marker comparisons above: case-insensitive and separator-normalized +// on Windows (whose filesystem is not case-sensitive), exact bytes +// everywhere else. goos is a parameter rather than read from runtime +// for the same reason Env.GOOS is: it lets a non-Windows host exercise +// the Windows comparison rules in a test. +// +// Exported for internal/cli's shadowingPath, which compares a +// PATH-resolved binary against the one just upgraded. Without this, it +// would warn about a shadowing commitbrief on Windows purely from a +// letter-case or `\`-vs-`/` difference that filepath.EvalSymlinks does +// not normalize away — a false positive, not a real shadow. +func SamePath(a, b, goos string) bool { + return normalizePath(a, goos) == normalizePath(b, goos) +} + +// normalizePath lowercases on Windows (its paths are case-insensitive) +// and converts separators to forward slashes so the marker checks above +// can be written once instead of per-OS. +func normalizePath(p, goos string) string { + if p == "" { + return "" + } + // On Windows, backslash is the path separator; convert to forward slash. + // filepath.ToSlash doesn't work for testing Windows paths on Unix hosts. + if goos == "windows" { + p = strings.ReplaceAll(p, "\\", "/") + p = strings.ToLower(p) + } else { + p = filepath.ToSlash(p) + // Marker comparisons are lowercase; on case-sensitive systems + // only the fixed markers are folded, never the user's path. + p = strings.Replace(p, "/Cellar/", "/cellar/", 1) + } + return p +} + +// pathDir returns the parent directory of an already-normalized +// (forward-slash) path, without a trailing slash. +func pathDir(p string) string { + i := strings.LastIndex(p, "/") + if i <= 0 { + return p + } + return p[:i] +} diff --git a/internal/upgrade/detect_test.go b/internal/upgrade/detect_test.go new file mode 100644 index 0000000..9c1b08e --- /dev/null +++ b/internal/upgrade/detect_test.go @@ -0,0 +1,144 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package upgrade + +import "testing" + +func TestDetect(t *testing.T) { + cases := []struct { + name string + env Env + want Method + }{ + { + name: "homebrew macos arm", + env: Env{ExePath: "/opt/homebrew/Cellar/commitbrief/1.13.0/bin/commitbrief", GOOS: "darwin"}, + want: MethodHomebrew, + }, + { + name: "homebrew intel macos", + env: Env{ExePath: "/usr/local/Cellar/commitbrief/1.13.0/bin/commitbrief", GOOS: "darwin"}, + want: MethodHomebrew, + }, + { + name: "linuxbrew", + env: Env{ExePath: "/home/linuxbrew/.linuxbrew/Cellar/commitbrief/1.13.0/bin/commitbrief", GOOS: "linux"}, + want: MethodHomebrew, + }, + { + name: "scoop default location", + env: Env{ExePath: `C:\Users\ada\scoop\apps\commitbrief\current\commitbrief.exe`, GOOS: "windows", UserProfile: `C:\Users\ada`}, + want: MethodScoop, + }, + { + name: "scoop custom SCOOP dir", + env: Env{ExePath: `D:\tools\sc\apps\commitbrief\1.13.0\commitbrief.exe`, GOOS: "windows", Scoop: `D:\tools\sc`}, + want: MethodScoop, + }, + { + name: "go install via GOBIN", + env: Env{ExePath: "/home/ada/dev/bin/commitbrief", GOOS: "linux", GOBIN: "/home/ada/dev/bin"}, + want: MethodGoInstall, + }, + { + name: "go install via GOPATH", + env: Env{ExePath: "/home/ada/go/bin/commitbrief", GOOS: "linux", GOPATH: "/home/ada/go"}, + want: MethodGoInstall, + }, + { + name: "go install via default GOPATH under home", + env: Env{ExePath: "/home/ada/go/bin/commitbrief", GOOS: "linux", Home: "/home/ada"}, + want: MethodGoInstall, + }, + { + name: "manual tarball in usr local bin", + env: Env{ExePath: "/usr/local/bin/commitbrief", GOOS: "linux"}, + want: MethodManual, + }, + { + name: "manual tarball in home bin", + env: Env{ExePath: "/home/ada/bin/commitbrief", GOOS: "linux", Home: "/home/ada"}, + want: MethodManual, + }, + { + // A distro package is classified manual; the write-permission + // gate in Task 6 is what actually protects it. + name: "distro package looks manual", + env: Env{ExePath: "/usr/bin/commitbrief", GOOS: "linux"}, + want: MethodManual, + }, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + if got := Detect(c.env); got != c.want { + t.Fatalf("Detect() = %q, want %q", got, c.want) + } + }) + } +} + +func TestDetectWindowsIsCaseInsensitive(t *testing.T) { + env := Env{ExePath: `C:\Users\Ada\Scoop\Apps\CommitBrief\current\commitbrief.exe`, GOOS: "windows", UserProfile: `C:\Users\Ada`} + if got := Detect(env); got != MethodScoop { + t.Fatalf("Detect() = %q, want %q", got, MethodScoop) + } +} + +// TestSamePath pins the comparison internal/cli's shadowingPath relies +// on: exact-byte on any non-Windows GOOS (unchanged from a plain ==), +// but case- and separator-insensitive on Windows, since neither +// os.Executable nor exec.LookPath is guaranteed to return byte-identical +// casing/separators for the same file, and EvalSymlinks normalizes +// neither. goos is passed explicitly so this runs the Windows rules on +// any host, the same way TestDetect exercises them. +func TestSamePath(t *testing.T) { + cases := []struct { + name string + a, b string + goos string + want bool + }{ + { + name: "windows case difference is the same path", + a: `C:\Users\ada\bin\commitbrief.exe`, + b: `C:\USERS\ada\BIN\CommitBrief.EXE`, + goos: "windows", + want: true, + }, + { + name: "windows separator difference is the same path", + a: `C:\Users\ada\bin\commitbrief.exe`, + b: `C:/Users/ada/bin/commitbrief.exe`, + goos: "windows", + want: true, + }, + { + name: "windows genuinely different paths do not match", + a: `C:\Users\ada\bin\commitbrief.exe`, + b: `C:\Program Files\CommitBrief\commitbrief.exe`, + goos: "windows", + want: false, + }, + { + name: "unix comparison stays case-sensitive", + a: "/usr/local/bin/commitbrief", + b: "/usr/local/bin/CommitBrief", + goos: "linux", + want: false, + }, + { + name: "unix identical paths match", + a: "/usr/local/bin/commitbrief", + b: "/usr/local/bin/commitbrief", + goos: "darwin", + want: true, + }, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + if got := SamePath(c.a, c.b, c.goos); got != c.want { + t.Fatalf("SamePath(%q, %q, %q) = %v, want %v", c.a, c.b, c.goos, got, c.want) + } + }) + } +} diff --git a/internal/upgrade/managed.go b/internal/upgrade/managed.go new file mode 100644 index 0000000..63b8e3d --- /dev/null +++ b/internal/upgrade/managed.go @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package upgrade + +import ( + "context" + "errors" + "fmt" + "io" + "os/exec" +) + +// ModulePath is the `go install` target for CommitBrief. +const ModulePath = "github.com/CommitBrief/commitbrief/cmd/commitbrief" + +// ErrToolMissing means the package manager that owns this install is +// not on PATH, so the upgrade cannot be delegated. +var ErrToolMissing = errors.New("package manager not found on PATH") + +// Command returns the argv that upgrades a package-managed install. +// It returns nil for MethodManual, which is handled by InstallManual. +// +// Delegating rather than overwriting the file is the core decision of +// ADR-0034 §D1: replacing a brew- or scoop-owned binary desynchronizes +// the manager's metadata, and its next upgrade either conflicts or +// silently reverts the change. +func Command(m Method) []string { + switch m { + case MethodHomebrew: + return []string{"brew", "upgrade", "commitbrief"} + case MethodScoop: + return []string{"scoop", "update", "commitbrief"} + case MethodGoInstall: + return []string{"go", "install", ModulePath + "@latest"} + default: + return nil + } +} + +// Run executes argv with its output streamed straight through to the +// user. The delegated tool's output is never parsed, so an upstream +// format change cannot break CommitBrief. +// +// argv is passed to exec directly — no shell is involved, per the +// argv-not-shell rule in the engineering standards. +func Run(ctx context.Context, argv []string, out, errOut io.Writer) error { + if len(argv) == 0 { + return errors.New("upgrade: empty command") + } + if _, err := exec.LookPath(argv[0]); err != nil { + return fmt.Errorf("%w: %s", ErrToolMissing, argv[0]) + } + cmd := exec.CommandContext(ctx, argv[0], argv[1:]...) + cmd.Stdout = out + cmd.Stderr = errOut + return cmd.Run() +} diff --git a/internal/upgrade/managed_test.go b/internal/upgrade/managed_test.go new file mode 100644 index 0000000..8fe5ef1 --- /dev/null +++ b/internal/upgrade/managed_test.go @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package upgrade + +import ( + "bytes" + "context" + "errors" + "reflect" + "testing" +) + +func TestCommandPerMethod(t *testing.T) { + cases := []struct { + m Method + want []string + }{ + {MethodHomebrew, []string{"brew", "upgrade", "commitbrief"}}, + {MethodScoop, []string{"scoop", "update", "commitbrief"}}, + {MethodGoInstall, []string{"go", "install", "github.com/CommitBrief/commitbrief/cmd/commitbrief@latest"}}, + {MethodManual, nil}, + } + for _, c := range cases { + if got := Command(c.m); !reflect.DeepEqual(got, c.want) { + t.Fatalf("Command(%q) = %v, want %v", c.m, got, c.want) + } + } +} + +func TestRunReportsMissingTool(t *testing.T) { + var out, errOut bytes.Buffer + err := Run(context.Background(), []string{"commitbrief-no-such-tool", "upgrade"}, &out, &errOut) + if !errors.Is(err, ErrToolMissing) { + t.Fatalf("error = %v, want ErrToolMissing", err) + } +} + +func TestRunStreamsOutput(t *testing.T) { + var out, errOut bytes.Buffer + // `go version` exists wherever the test suite itself can run. + if err := Run(context.Background(), []string{"go", "version"}, &out, &errOut); err != nil { + t.Fatalf("Run() error = %v", err) + } + if out.Len() == 0 { + t.Fatal("expected the delegated command's stdout to be streamed") + } +} + +func TestRunPropagatesFailure(t *testing.T) { + var out, errOut bytes.Buffer + err := Run(context.Background(), []string{"go", "this-is-not-a-go-subcommand"}, &out, &errOut) + if err == nil { + t.Fatal("Run() error = nil, want the delegated command's failure") + } + if errors.Is(err, ErrToolMissing) { + t.Fatalf("error = %v, want an exit failure, not ErrToolMissing", err) + } +} + +func TestRunRejectsEmptyArgv(t *testing.T) { + var out, errOut bytes.Buffer + if err := Run(context.Background(), nil, &out, &errOut); err == nil { + t.Fatal("Run(nil) error = nil, want a rejection") + } +} diff --git a/internal/upgrade/manual.go b/internal/upgrade/manual.go new file mode 100644 index 0000000..7e4a330 --- /dev/null +++ b/internal/upgrade/manual.go @@ -0,0 +1,221 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package upgrade + +import ( + "archive/tar" + "archive/zip" + "bytes" + "compress/gzip" + "context" + "crypto/sha256" + "encoding/hex" + "errors" + "fmt" + "io" + "os" + "path" + "path/filepath" + "strings" +) + +var ( + // ErrNotWritable means the directory holding the binary cannot be + // written by this user. Reported before any download happens. + ErrNotWritable = errors.New("target directory is not writable") + // ErrAssetMissing means the release has no archive for this platform. + ErrAssetMissing = errors.New("no release asset for this platform") + // ErrChecksumMismatch means the downloaded bytes did not match + // checksums.txt. Nothing is installed. + ErrChecksumMismatch = errors.New("checksum mismatch") +) + +// ManualOptions carries everything InstallManual needs. Target is the +// resolved (symlink-free) path of the running binary. +type ManualOptions struct { + Client *Client + Release *Release + Target string + GOOS string + GOARCH string +} + +// PreflightWritable reports whether the binary can be swapped. It +// probes the *directory*, not the file: replacement is a rename, and +// rename permission comes from the parent directory. This is what +// stops the manual path from touching a root-owned /usr/bin or a +// read-only /nix/store — a distro-packaged install that Detect could +// only classify as "manual". +func PreflightWritable(target string) error { + dir := filepath.Dir(target) + f, err := os.CreateTemp(dir, ".commitbrief-probe-*") + if err != nil { + return fmt.Errorf("%w: %s", ErrNotWritable, dir) + } + name := f.Name() + _ = f.Close() + _ = os.Remove(name) + return nil +} + +// InstallManual downloads the release archive for this platform, +// verifies its SHA-256 against checksums.txt, extracts just the binary, +// and swaps it into place. +// +// Trust model: the anchor is TLS to github.com. checksums.txt is not +// signed, so this detects a truncated or corrupted download, not a +// compromised release (ADR-0034 §D7). +func InstallManual(ctx context.Context, o ManualOptions) error { + assetName := AssetName(o.Release.TagName, o.GOOS, o.GOARCH) + asset, ok := o.Release.AssetByName(assetName) + if !ok { + return fmt.Errorf("%w: %s", ErrAssetMissing, assetName) + } + sumsAsset, ok := o.Release.AssetByName(ChecksumsFile) + if !ok { + return fmt.Errorf("%w: %s", ErrAssetMissing, ChecksumsFile) + } + + var sumsBuf bytes.Buffer + if err := o.Client.Download(ctx, sumsAsset.BrowserDownloadURL, &sumsBuf); err != nil { + return err + } + want := ParseChecksums(sumsBuf.Bytes())[assetName] + if want == "" { + return fmt.Errorf("%w: %s has no entry for %s", ErrChecksumMismatch, ChecksumsFile, assetName) + } + + dir := filepath.Dir(o.Target) + + // The archive lands next to the binary so the later rename stays on + // one filesystem and therefore stays atomic. + archiveFile, err := os.CreateTemp(dir, ".commitbrief-dl-*") + if err != nil { + return fmt.Errorf("%w: %s", ErrNotWritable, dir) + } + archivePath := archiveFile.Name() + defer func() { + _ = archiveFile.Close() + _ = os.Remove(archivePath) + }() + + hasher := sha256.New() + if err := o.Client.Download(ctx, asset.BrowserDownloadURL, io.MultiWriter(archiveFile, hasher)); err != nil { + return err + } + if got := hex.EncodeToString(hasher.Sum(nil)); got != want { + return fmt.Errorf("%w: %s (want %s, got %s)", ErrChecksumMismatch, assetName, want, got) + } + if _, err := archiveFile.Seek(0, io.SeekStart); err != nil { + return err + } + + binFile, err := os.CreateTemp(dir, ".commitbrief-bin-*") + if err != nil { + return fmt.Errorf("%w: %s", ErrNotWritable, dir) + } + binPath := binFile.Name() + // Removed unconditionally: on success the rename has already taken + // the file away, and Remove on a missing path is harmless. + defer func() { + _ = binFile.Close() + _ = os.Remove(binPath) + }() + + entry := BinaryEntryName(o.GOOS) + if o.GOOS == "windows" { + info, statErr := archiveFile.Stat() + if statErr != nil { + return statErr + } + err = extractZip(archiveFile, info.Size(), entry, binFile) + } else { + err = extractTarGz(archiveFile, entry, binFile) + } + if err != nil { + return err + } + if err := binFile.Close(); err != nil { + return err + } + + // Carry over the existing binary's mode instead of forcing 0755 — + // a deliberately locked-down install stays locked down. + mode := os.FileMode(0o755) + if info, statErr := os.Stat(o.Target); statErr == nil { + mode = info.Mode().Perm() + } + if err := os.Chmod(binPath, mode); err != nil { + return err + } + + return replaceBinary(binPath, o.Target) +} + +// extractTarGz writes the single entry named want into w. +func extractTarGz(r io.Reader, want string, w io.Writer) error { + gz, err := gzip.NewReader(r) + if err != nil { + return err + } + defer func() { _ = gz.Close() }() + + tr := tar.NewReader(gz) + for { + hdr, err := tr.Next() + if errors.Is(err, io.EOF) { + return fmt.Errorf("archive has no %q entry", want) + } + if err != nil { + return err + } + if err := checkEntryName(hdr.Name); err != nil { + return err + } + if hdr.Name != want { + continue + } + if _, err := io.Copy(w, io.LimitReader(tr, maxDownloadBytes)); err != nil { + return err + } + return nil + } +} + +// extractZip writes the single entry named want into w. +func extractZip(r io.ReaderAt, size int64, want string, w io.Writer) error { + zr, err := zip.NewReader(r, size) + if err != nil { + return err + } + for _, f := range zr.File { + if err := checkEntryName(f.Name); err != nil { + return err + } + if f.Name != want { + continue + } + rc, err := f.Open() + if err != nil { + return err + } + defer func() { _ = rc.Close() }() + if _, err := io.Copy(w, io.LimitReader(rc, maxDownloadBytes)); err != nil { + return err + } + return nil + } + return fmt.Errorf("archive has no %q entry", want) +} + +// checkEntryName rejects absolute paths and parent-directory escapes. +// Only one known-named entry is ever extracted, so this cannot trigger +// on a well-formed release — it is a standing guard against a malformed +// or hostile archive (tar-slip / zip-slip). +func checkEntryName(name string) error { + clean := path.Clean(filepath.ToSlash(name)) + if path.IsAbs(clean) || strings.HasPrefix(clean, "../") || clean == ".." { + return fmt.Errorf("refusing unsafe archive entry %q", name) + } + return nil +} diff --git a/internal/upgrade/manual_test.go b/internal/upgrade/manual_test.go new file mode 100644 index 0000000..30798b4 --- /dev/null +++ b/internal/upgrade/manual_test.go @@ -0,0 +1,326 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package upgrade + +import ( + "archive/tar" + "archive/zip" + "bytes" + "compress/gzip" + "context" + "crypto/sha256" + "encoding/hex" + "errors" + "fmt" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "runtime" + "testing" +) + +// buildTarGz returns a gzip'd tar holding one root-level entry per map +// key — the same shape goreleaser produces (binary at the root). +func buildTarGz(t *testing.T, entries map[string]string) []byte { + t.Helper() + var buf bytes.Buffer + gz := gzip.NewWriter(&buf) + tw := tar.NewWriter(gz) + for name, content := range entries { + hdr := &tar.Header{Name: name, Mode: 0o755, Size: int64(len(content)), Typeflag: tar.TypeReg} + if err := tw.WriteHeader(hdr); err != nil { + t.Fatal(err) + } + if _, err := tw.Write([]byte(content)); err != nil { + t.Fatal(err) + } + } + if err := tw.Close(); err != nil { + t.Fatal(err) + } + if err := gz.Close(); err != nil { + t.Fatal(err) + } + return buf.Bytes() +} + +// buildZip returns a zip archive holding one root-level entry per map key +// — the shape goreleaser produces for the Windows asset. +func buildZip(t *testing.T, entries map[string]string) []byte { + t.Helper() + var buf bytes.Buffer + zw := zip.NewWriter(&buf) + for name, content := range entries { + w, err := zw.Create(name) + if err != nil { + t.Fatal(err) + } + if _, err := w.Write([]byte(content)); err != nil { + t.Fatal(err) + } + } + if err := zw.Close(); err != nil { + t.Fatal(err) + } + return buf.Bytes() +} + +func sha256Hex(b []byte) string { + sum := sha256.Sum256(b) + return hex.EncodeToString(sum[:]) +} + +// serveRelease starts a server that hands out the archive and its +// checksum manifest, and returns a Release pointing at it. +func serveRelease(t *testing.T, tag string, archive []byte, sum string) (*Client, *Release) { + t.Helper() + assetName := AssetName(tag, runtime.GOOS, runtime.GOARCH) + mux := http.NewServeMux() + mux.HandleFunc("/"+assetName, func(w http.ResponseWriter, r *http.Request) { + _, _ = w.Write(archive) + }) + mux.HandleFunc("/"+ChecksumsFile, func(w http.ResponseWriter, r *http.Request) { + _, _ = fmt.Fprintf(w, "%s %s\n", sum, assetName) + }) + srv := httptest.NewServer(mux) + t.Cleanup(srv.Close) + + rel := &Release{ + TagName: tag, + HTMLURL: srv.URL, + Assets: []Asset{ + {Name: assetName, BrowserDownloadURL: srv.URL + "/" + assetName}, + {Name: ChecksumsFile, BrowserDownloadURL: srv.URL + "/" + ChecksumsFile}, + }, + } + return NewClient("v1.14.0"), rel +} + +func TestInstallManualReplacesBinary(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("this fixture is a tar.gz; the Windows asset is a zip, covered by TestExtractZipWritesEntry") + } + dir := t.TempDir() + target := filepath.Join(dir, "commitbrief") + // A non-default mode: manual.go falls back to a hardcoded 0755 when + // it cannot stat the existing target, so asserting 0755 here would + // pass even if the actual preservation logic were deleted. 0700 + // makes the assertion mean something. + if err := os.WriteFile(target, []byte("OLD BINARY"), 0o700); err != nil { + t.Fatal(err) + } + + archive := buildTarGz(t, map[string]string{ + BinaryEntryName(runtime.GOOS): "NEW BINARY", + "LICENSE": "GPL", + }) + client, rel := serveRelease(t, "v1.15.0", archive, sha256Hex(archive)) + + err := InstallManual(context.Background(), ManualOptions{ + Client: client, Release: rel, Target: target, + GOOS: runtime.GOOS, GOARCH: runtime.GOARCH, + }) + if err != nil { + t.Fatalf("InstallManual() error = %v", err) + } + + got, err := os.ReadFile(target) + if err != nil { + t.Fatal(err) + } + if string(got) != "NEW BINARY" { + t.Fatalf("target content = %q, want NEW BINARY", got) + } + info, err := os.Stat(target) + if err != nil { + t.Fatal(err) + } + if info.Mode().Perm() != 0o700 { + t.Fatalf("mode = %v, want 0700 preserved from the old binary", info.Mode().Perm()) + } + // No .commitbrief-* scratch files may survive a successful run. + leftovers, _ := filepath.Glob(filepath.Join(dir, ".commitbrief-*")) + if len(leftovers) != 0 { + t.Fatalf("scratch files left behind: %v", leftovers) + } +} + +func TestInstallManualRejectsChecksumMismatch(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("tar.gz fixture") + } + dir := t.TempDir() + target := filepath.Join(dir, "commitbrief") + if err := os.WriteFile(target, []byte("OLD BINARY"), 0o755); err != nil { + t.Fatal(err) + } + + archive := buildTarGz(t, map[string]string{BinaryEntryName(runtime.GOOS): "NEW BINARY"}) + // Advertise a sum for different bytes. + client, rel := serveRelease(t, "v1.15.0", archive, sha256Hex([]byte("something else"))) + + err := InstallManual(context.Background(), ManualOptions{ + Client: client, Release: rel, Target: target, + GOOS: runtime.GOOS, GOARCH: runtime.GOARCH, + }) + if !errors.Is(err, ErrChecksumMismatch) { + t.Fatalf("error = %v, want ErrChecksumMismatch", err) + } + got, _ := os.ReadFile(target) + if string(got) != "OLD BINARY" { + t.Fatalf("target was modified despite the mismatch: %q", got) + } + leftovers, _ := filepath.Glob(filepath.Join(dir, ".commitbrief-*")) + if len(leftovers) != 0 { + t.Fatalf("scratch files left behind: %v", leftovers) + } +} + +func TestInstallManualMissingAsset(t *testing.T) { + dir := t.TempDir() + target := filepath.Join(dir, "commitbrief") + if err := os.WriteFile(target, []byte("OLD"), 0o755); err != nil { + t.Fatal(err) + } + rel := &Release{TagName: "v1.15.0", Assets: []Asset{{Name: "unrelated.txt"}}} + + err := InstallManual(context.Background(), ManualOptions{ + Client: NewClient("v1.14.0"), Release: rel, Target: target, + GOOS: runtime.GOOS, GOARCH: runtime.GOARCH, + }) + if !errors.Is(err, ErrAssetMissing) { + t.Fatalf("error = %v, want ErrAssetMissing", err) + } +} + +func TestPreflightWritable(t *testing.T) { + dir := t.TempDir() + target := filepath.Join(dir, "commitbrief") + if err := os.WriteFile(target, []byte("BIN"), 0o755); err != nil { + t.Fatal(err) + } + if err := PreflightWritable(target); err != nil { + t.Fatalf("PreflightWritable() error = %v, want nil", err) + } +} + +func TestPreflightWritableFailsOnReadOnlyDir(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("unix permission bits do not model windows ACLs") + } + if os.Geteuid() == 0 { + t.Skip("root ignores the permission bits this test relies on") + } + dir := t.TempDir() + target := filepath.Join(dir, "commitbrief") + if err := os.WriteFile(target, []byte("BIN"), 0o755); err != nil { + t.Fatal(err) + } + if err := os.Chmod(dir, 0o500); err != nil { + t.Fatal(err) + } + t.Cleanup(func() { _ = os.Chmod(dir, 0o700) }) + + if err := PreflightWritable(target); !errors.Is(err, ErrNotWritable) { + t.Fatalf("error = %v, want ErrNotWritable", err) + } +} + +// TestInstallManualMissingChecksumEntry covers the case where +// checksums.txt downloads fine but carries no line for our asset. That +// must fail closed — an absent entry is not a passing verification. +func TestInstallManualMissingChecksumEntry(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("tar.gz fixture; the Windows extraction path is covered by TestExtractZipWritesEntry") + } + dir := t.TempDir() + target := filepath.Join(dir, "commitbrief") + if err := os.WriteFile(target, []byte("OLD BINARY"), 0o755); err != nil { + t.Fatal(err) + } + + archive := buildTarGz(t, map[string]string{BinaryEntryName(runtime.GOOS): "NEW BINARY"}) + assetName := AssetName("v1.15.0", runtime.GOOS, runtime.GOARCH) + + mux := http.NewServeMux() + mux.HandleFunc("/"+assetName, func(w http.ResponseWriter, r *http.Request) { + _, _ = w.Write(archive) + }) + mux.HandleFunc("/"+ChecksumsFile, func(w http.ResponseWriter, r *http.Request) { + // A valid manifest that simply does not mention our asset. + _, _ = fmt.Fprintf(w, "%s some_other_file.tar.gz\n", sha256Hex(archive)) + }) + srv := httptest.NewServer(mux) + t.Cleanup(srv.Close) + + rel := &Release{ + TagName: "v1.15.0", + Assets: []Asset{ + {Name: assetName, BrowserDownloadURL: srv.URL + "/" + assetName}, + {Name: ChecksumsFile, BrowserDownloadURL: srv.URL + "/" + ChecksumsFile}, + }, + } + + err := InstallManual(context.Background(), ManualOptions{ + Client: NewClient("v1.14.0"), Release: rel, Target: target, + GOOS: runtime.GOOS, GOARCH: runtime.GOARCH, + }) + if !errors.Is(err, ErrChecksumMismatch) { + t.Fatalf("error = %v, want ErrChecksumMismatch", err) + } + got, _ := os.ReadFile(target) + if string(got) != "OLD BINARY" { + t.Fatalf("target was modified despite the missing checksum entry: %q", got) + } +} + +// TestExtractZipWritesEntry covers the Windows extraction path. Without +// it, the only code a real Windows manual install exercises would ship +// with no test at all. +func TestExtractZipWritesEntry(t *testing.T) { + archive := buildZip(t, map[string]string{ + "commitbrief.exe": "NEW BINARY", + "LICENSE": "GPL", + }) + var out bytes.Buffer + if err := extractZip(bytes.NewReader(archive), int64(len(archive)), "commitbrief.exe", &out); err != nil { + t.Fatalf("extractZip() error = %v", err) + } + if out.String() != "NEW BINARY" { + t.Fatalf("extracted content = %q, want NEW BINARY", out.String()) + } +} + +func TestExtractZipRejectsTraversal(t *testing.T) { + archive := buildZip(t, map[string]string{"../escape": "EVIL"}) + var out bytes.Buffer + if err := extractZip(bytes.NewReader(archive), int64(len(archive)), "../escape", &out); err == nil { + t.Fatal("extractZip() error = nil, want a rejection") + } +} + +func TestExtractZipEntryNotFound(t *testing.T) { + archive := buildZip(t, map[string]string{"LICENSE": "GPL"}) + var out bytes.Buffer + if err := extractZip(bytes.NewReader(archive), int64(len(archive)), "commitbrief.exe", &out); err == nil { + t.Fatal("extractZip() error = nil, want entry-not-found") + } +} + +func TestExtractTarGzRejectsTraversal(t *testing.T) { + archive := buildTarGz(t, map[string]string{"../escape": "EVIL"}) + var out bytes.Buffer + err := extractTarGz(bytes.NewReader(archive), "../escape", &out) + if err == nil { + t.Fatal("extractTarGz() error = nil, want a rejection") + } +} + +func TestExtractTarGzEntryNotFound(t *testing.T) { + archive := buildTarGz(t, map[string]string{"LICENSE": "GPL"}) + var out bytes.Buffer + if err := extractTarGz(bytes.NewReader(archive), "commitbrief", &out); err == nil { + t.Fatal("extractTarGz() error = nil, want entry-not-found") + } +} diff --git a/internal/upgrade/release.go b/internal/upgrade/release.go new file mode 100644 index 0000000..34ba0e5 --- /dev/null +++ b/internal/upgrade/release.go @@ -0,0 +1,172 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package upgrade + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "time" +) + +// DefaultAPIURL is the only endpoint CommitBrief ever contacts on its +// own behalf, and only when the user runs `commitbrief upgrade` +// (ADR-0034 §D3 — there is no automatic update check). "latest" +// excludes prereleases, so -rc tags are never offered. +const DefaultAPIURL = "https://api.github.com/repos/CommitBrief/commitbrief/releases/latest" + +// ReleasesPage is shown to the user when an automated path is not +// available (no asset for their platform, unwritable target). +const ReleasesPage = "https://github.com/CommitBrief/commitbrief/releases" + +// maxDownloadBytes caps any single response body. A release archive is +// a few megabytes; the cap only exists so a malformed or hostile +// response cannot fill the disk. +const maxDownloadBytes = 200 << 20 // 200 MiB + +var ( + // ErrRateLimited is the unauthenticated GitHub API hourly cap. + ErrRateLimited = errors.New("github api rate limit exceeded") + // ErrNoRelease means the repository has no published release. + ErrNoRelease = errors.New("no published release found") + // ErrBadResponse means the GitHub API answered — the server was + // reached, and returned a 200 — but the body did not decode as the + // expected JSON shape. Distinct from a network failure: the request + // itself succeeded, only the payload was unusable. + ErrBadResponse = errors.New("could not parse the github release response") +) + +// Asset is one file attached to a release. +type Asset struct { + Name string `json:"name"` + BrowserDownloadURL string `json:"browser_download_url"` +} + +// Release is the subset of the GitHub release payload we use. +type Release struct { + TagName string `json:"tag_name"` + HTMLURL string `json:"html_url"` + Assets []Asset `json:"assets"` +} + +// AssetByName finds an attached file by its exact name. +func (r *Release) AssetByName(name string) (Asset, bool) { + for _, a := range r.Assets { + if a.Name == name { + return a, true + } + } + return Asset{}, false +} + +// Client talks to the GitHub Releases API. APIURL is a field so tests +// can point it at an httptest server — no test ever reaches github.com. +type Client struct { + HTTP *http.Client + Assets *http.Client // used by Download; falls back to HTTP when nil + APIURL string + UserAgent string +} + +// NewClient returns a client that identifies itself with the running +// CommitBrief version. HTTP (the API client) gives up after 10 seconds — +// a release-metadata response is a few KB and fast. +// +// Assets (used by Download) deliberately has no whole-request Timeout. +// http.Client.Timeout covers the entire round trip, including reading +// the response body, and a release archive can be several megabytes — +// a fixed deadline there fails a slow or throttled connection outright, +// permanently, no matter how much of the file already arrived. Instead +// it is bounded by ResponseHeaderTimeout (a stalled server still gives +// up after 30s) and by the context passed to Download for cancellation. +// +// Assets' Transport is a *clone of http.DefaultTransport*, not a bare +// &http.Transport{} — a zero-valued Transport is a materially different, +// worse thing than "DefaultTransport with one field changed". A bare +// Transport has Proxy == nil, so it silently ignores HTTPS_PROXY/ +// HTTP_PROXY on a proxied network — while Latest's client (nil +// Transport ⇒ http.DefaultTransport) still honors it, so a user behind +// a corporate proxy would see `upgrade` correctly detect an update and +// then fail to download it. A bare Transport also has no DialContext +// and TLSHandshakeTimeout == 0; since ResponseHeaderTimeout only starts +// counting after connect + TLS finish, a blackholed endpoint would fall +// back to the OS TCP timeout (commonly 75s+) with an unbounded TLS +// handshake on top of that — nothing else in this codebase bounds it, +// since the context passed in has no deadline of its own. +func NewClient(version string) *Client { + assetsTransport := http.DefaultTransport.(*http.Transport).Clone() + assetsTransport.ResponseHeaderTimeout = 30 * time.Second + return &Client{ + HTTP: &http.Client{Timeout: 10 * time.Second}, + Assets: &http.Client{Transport: assetsTransport}, + APIURL: DefaultAPIURL, + UserAgent: "commitbrief/" + version, + } +} + +// Latest fetches the newest published (non-prerelease) release. +func (c *Client) Latest(ctx context.Context) (*Release, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.APIURL, nil) + if err != nil { + return nil, err + } + req.Header.Set("Accept", "application/vnd.github+json") + req.Header.Set("User-Agent", c.UserAgent) + + resp, err := c.HTTP.Do(req) + if err != nil { + return nil, err + } + defer func() { _ = resp.Body.Close() }() + + switch { + case resp.StatusCode == http.StatusForbidden && resp.Header.Get("X-RateLimit-Remaining") == "0": + return nil, ErrRateLimited + case resp.StatusCode == http.StatusNotFound: + return nil, ErrNoRelease + case resp.StatusCode != http.StatusOK: + return nil, fmt.Errorf("github api: unexpected status %s", resp.Status) + } + + var rel Release + // The raw body is deliberately not echoed on a parse failure: an + // error page can be arbitrarily long and is never actionable. + if err := json.NewDecoder(io.LimitReader(resp.Body, maxDownloadBytes)).Decode(&rel); err != nil { + return nil, fmt.Errorf("%w: %v", ErrBadResponse, err) + } + if rel.TagName == "" { + return nil, ErrNoRelease + } + return &rel, nil +} + +// Download streams url into w. Redirects are followed (GitHub sends +// release downloads to objects.githubusercontent.com). Uses c.Assets — +// the client with no whole-request timeout — falling back to c.HTTP so +// a hand-constructed Client (as in tests) still works. +func (c *Client) Download(ctx context.Context, url string, w io.Writer) error { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return err + } + req.Header.Set("User-Agent", c.UserAgent) + + client := c.Assets + if client == nil { + client = c.HTTP + } + resp, err := client.Do(req) + if err != nil { + return err + } + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("download %s: unexpected status %s", url, resp.Status) + } + _, err = io.Copy(w, io.LimitReader(resp.Body, maxDownloadBytes)) + return err +} diff --git a/internal/upgrade/release_test.go b/internal/upgrade/release_test.go new file mode 100644 index 0000000..512030c --- /dev/null +++ b/internal/upgrade/release_test.go @@ -0,0 +1,201 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package upgrade + +import ( + "bytes" + "context" + "errors" + "net/http" + "net/http/httptest" + "testing" + "time" +) + +func newTestClient(t *testing.T, h http.Handler) (*Client, *httptest.Server) { + t.Helper() + srv := httptest.NewServer(h) + t.Cleanup(srv.Close) + c := NewClient("v1.14.0") + c.APIURL = srv.URL + return c, srv +} + +func TestLatestParsesRelease(t *testing.T) { + body := `{ + "tag_name": "v1.15.0", + "html_url": "https://github.com/CommitBrief/commitbrief/releases/tag/v1.15.0", + "assets": [ + {"name": "commitbrief_1.15.0_linux_x86_64.tar.gz", "browser_download_url": "https://example.test/a.tar.gz"}, + {"name": "checksums.txt", "browser_download_url": "https://example.test/checksums.txt"} + ] + }` + c, _ := newTestClient(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if got := r.Header.Get("User-Agent"); got != "commitbrief/v1.14.0" { + t.Errorf("User-Agent = %q", got) + } + if got := r.Header.Get("Accept"); got != "application/vnd.github+json" { + t.Errorf("Accept = %q", got) + } + _, _ = w.Write([]byte(body)) + })) + + rel, err := c.Latest(context.Background()) + if err != nil { + t.Fatalf("Latest() error = %v", err) + } + if rel.TagName != "v1.15.0" { + t.Fatalf("TagName = %q", rel.TagName) + } + a, ok := rel.AssetByName("checksums.txt") + if !ok || a.BrowserDownloadURL != "https://example.test/checksums.txt" { + t.Fatalf("AssetByName(checksums.txt) = %+v, ok=%v", a, ok) + } + if _, ok := rel.AssetByName("nope"); ok { + t.Fatal("AssetByName(nope) should not be found") + } +} + +func TestLatestRateLimited(t *testing.T) { + c, _ := newTestClient(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("X-RateLimit-Remaining", "0") + w.WriteHeader(http.StatusForbidden) + })) + _, err := c.Latest(context.Background()) + if !errors.Is(err, ErrRateLimited) { + t.Fatalf("error = %v, want ErrRateLimited", err) + } +} + +func TestLatestNoRelease(t *testing.T) { + c, _ := newTestClient(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotFound) + })) + _, err := c.Latest(context.Background()) + if !errors.Is(err, ErrNoRelease) { + t.Fatalf("error = %v, want ErrNoRelease", err) + } +} + +// TestLatestMalformedJSON pins that a decode failure is reported as +// ErrBadResponse — the server was reached and answered, so this must +// not collapse into the same message as a network failure. +func TestLatestMalformedJSON(t *testing.T) { + c, _ := newTestClient(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, _ = w.Write([]byte("not json")) + })) + _, err := c.Latest(context.Background()) + if !errors.Is(err, ErrBadResponse) { + t.Fatalf("error = %v, want ErrBadResponse", err) + } + if errors.Is(err, ErrRateLimited) || errors.Is(err, ErrNoRelease) { + t.Fatalf("error = %v, want a plain parse error", err) + } +} + +func TestLatestServerError(t *testing.T) { + c, _ := newTestClient(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + })) + if _, err := c.Latest(context.Background()); err == nil { + t.Fatal("Latest() error = nil, want a status error") + } +} + +func TestDownloadWritesBody(t *testing.T) { + c, srv := newTestClient(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, _ = w.Write([]byte("payload")) + })) + var buf bytes.Buffer + if err := c.Download(context.Background(), srv.URL, &buf); err != nil { + t.Fatalf("Download() error = %v", err) + } + if buf.String() != "payload" { + t.Fatalf("body = %q", buf.String()) + } +} + +func TestDownloadRejectsNon200(t *testing.T) { + c, srv := newTestClient(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotFound) + })) + var buf bytes.Buffer + if err := c.Download(context.Background(), srv.URL, &buf); err == nil { + t.Fatal("Download() error = nil, want a status error") + } +} + +// TestNewClientTransportConfiguration pins the exact fields NewClient +// produces for Assets (the client Download uses) versus HTTP (the API +// client) — a structural check, not a timing-based one, because a +// timing-based test cannot reliably distinguish "no whole-request +// deadline" from "a deadline long enough not to fire in this test run" +// without either being slow or being flaky. A previous version of this +// test used a hand-built client with a short delay and passed +// regardless of what NewClient actually configured (proven by setting +// Assets.Timeout to 1ms and re-running: still green) — this test reads +// the fields straight off NewClient's return value instead, so it fails +// immediately if either regresses: +// - HTTP keeps a whole-request Timeout (release metadata is small). +// - Assets has Timeout == 0 (no whole-request deadline on a +// multi-megabyte download). +// - Assets' Transport is asserted as *http.Transport with a non-nil +// Proxy (so HTTPS_PROXY/HTTP_PROXY isn't silently dropped for +// downloads only) and a non-zero TLSHandshakeTimeout (so a stalled +// handshake — which ResponseHeaderTimeout does not bound, since it +// only starts counting after connect+TLS finish — cannot hang +// forever), confirming Assets was built from a clone of +// http.DefaultTransport rather than a bare &http.Transport{}. +// - Assets' Transport.ResponseHeaderTimeout is exactly 30s. +func TestNewClientTransportConfiguration(t *testing.T) { + c := NewClient("v1.14.0") + + if c.HTTP.Timeout != 10*time.Second { + t.Fatalf("HTTP.Timeout = %v, want 10s", c.HTTP.Timeout) + } + + if c.Assets.Timeout != 0 { + t.Fatalf("Assets.Timeout = %v, want 0 (no whole-request deadline)", c.Assets.Timeout) + } + tr, ok := c.Assets.Transport.(*http.Transport) + if !ok { + t.Fatalf("Assets.Transport = %T, want *http.Transport", c.Assets.Transport) + } + if tr.Proxy == nil { + t.Fatal("Assets.Transport.Proxy is nil — HTTPS_PROXY/HTTP_PROXY would be ignored for asset downloads") + } + if tr.TLSHandshakeTimeout == 0 { + t.Fatal("Assets.Transport.TLSHandshakeTimeout is 0 — a stalled TLS handshake would never time out") + } + if tr.ResponseHeaderTimeout != 30*time.Second { + t.Fatalf("Assets.Transport.ResponseHeaderTimeout = %v, want 30s", tr.ResponseHeaderTimeout) + } +} + +// TestDownloadSurvivesSlowBody exercises Download against the actual +// client NewClient produces (not a test-only override): headers arrive +// immediately, then the body trickles in after a short delay. This +// alone cannot prove there is no whole-request deadline — that boundary +// is pinned structurally, at the field level, by +// TestNewClientTransportConfiguration above — but it does catch a +// regression that reintroduces a deadline through some other path (a +// context timeout, a per-request deadline) without necessarily changing +// the Assets.Timeout field that test inspects. +func TestDownloadSurvivesSlowBody(t *testing.T) { + c, srv := newTestClient(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + if f, ok := w.(http.Flusher); ok { + f.Flush() + } + time.Sleep(100 * time.Millisecond) + _, _ = w.Write([]byte("payload")) + })) + + var buf bytes.Buffer + if err := c.Download(context.Background(), srv.URL, &buf); err != nil { + t.Fatalf("Download() error = %v, want nil", err) + } + if buf.String() != "payload" { + t.Fatalf("body = %q, want %q", buf.String(), "payload") + } +} diff --git a/internal/upgrade/replace_test.go b/internal/upgrade/replace_test.go new file mode 100644 index 0000000..0259de1 --- /dev/null +++ b/internal/upgrade/replace_test.go @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package upgrade + +import ( + "os" + "path/filepath" + "testing" +) + +func TestReplaceBinarySwapsContent(t *testing.T) { + dir := t.TempDir() + target := filepath.Join(dir, "commitbrief") + tmp := filepath.Join(dir, ".commitbrief-new") + + if err := os.WriteFile(target, []byte("OLD"), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(tmp, []byte("NEW"), 0o755); err != nil { + t.Fatal(err) + } + + if err := replaceBinary(tmp, target); err != nil { + t.Fatalf("replaceBinary() error = %v", err) + } + + got, err := os.ReadFile(target) + if err != nil { + t.Fatal(err) + } + if string(got) != "NEW" { + t.Fatalf("target content = %q, want NEW", got) + } + if _, err := os.Stat(tmp); !os.IsNotExist(err) { + t.Fatalf("temp file still present: %v", err) + } +} + +func TestCleanupStaleIsSafeWhenNothingToClean(t *testing.T) { + dir := t.TempDir() + target := filepath.Join(dir, "commitbrief") + if err := os.WriteFile(target, []byte("BIN"), 0o755); err != nil { + t.Fatal(err) + } + // Must not panic and must not touch the live binary. + CleanupStale(target) + if _, err := os.Stat(target); err != nil { + t.Fatalf("target disappeared: %v", err) + } +} + +// TestCleanupStaleRemovesTempFiles covers the orphan an interrupted +// InstallManual (Ctrl-C, before its defers run) leaves behind: a +// ".commitbrief-dl-*" or ".commitbrief-bin-*" scratch file next to the +// target, never cleaned up until the next upgrade sweeps it. +func TestCleanupStaleRemovesTempFiles(t *testing.T) { + dir := t.TempDir() + target := filepath.Join(dir, "commitbrief") + if err := os.WriteFile(target, []byte("BIN"), 0o755); err != nil { + t.Fatal(err) + } + stale := []string{ + filepath.Join(dir, ".commitbrief-dl-abc123"), + filepath.Join(dir, ".commitbrief-bin-abc123"), + } + for _, f := range stale { + if err := os.WriteFile(f, []byte("orphan"), 0o600); err != nil { + t.Fatal(err) + } + } + + CleanupStale(target) + + for _, f := range stale { + if _, err := os.Stat(f); !os.IsNotExist(err) { + t.Fatalf("stale temp file still present: %s", f) + } + } + if _, err := os.Stat(target); err != nil { + t.Fatalf("target disappeared: %v", err) + } +} diff --git a/internal/upgrade/replace_unix.go b/internal/upgrade/replace_unix.go new file mode 100644 index 0000000..8a1f826 --- /dev/null +++ b/internal/upgrade/replace_unix.go @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +//go:build !windows + +package upgrade + +import "os" + +// replaceBinary swaps target with the prepared file at tmp. On Unix a +// rename is a single atomic operation and is legal while the target is +// executing: the running process keeps its own inode, so the swap is +// invisible to it. On failure nothing has changed, so there is no +// rollback to perform. +func replaceBinary(tmp, target string) error { + return os.Rename(tmp, target) +} + +// cleanupOld is a no-op on Unix — the rename leaves nothing +// platform-specific behind. Temp-file sweeping is shared across +// platforms; see CleanupStale in cleanup.go. +func cleanupOld(target string) {} diff --git a/internal/upgrade/replace_windows.go b/internal/upgrade/replace_windows.go new file mode 100644 index 0000000..38022a9 --- /dev/null +++ b/internal/upgrade/replace_windows.go @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +//go:build windows + +package upgrade + +import ( + "fmt" + "os" +) + +// osRename indirects os.Rename so the rollback path — the one branch +// that decides whether a failed upgrade leaves a working binary behind — +// can be forced in a test. Production code never reassigns it. +var osRename = os.Rename + +// replaceBinary swaps target with the prepared file at tmp. Windows +// refuses to overwrite a running .exe but does allow renaming it, so +// the live binary is moved aside first. If the second rename fails the +// original is put back. +// +// If that rollback ALSO fails, the target is left with no binary at +// all, so the returned error names both failures and points at the +// moved-aside copy — without that, a user in this state has a missing +// command and no clue that a recoverable backup is sitting next to it. +// +// Removing the moved-aside file fails while the process is still +// running; that is expected, and CleanupStale sweeps it on the next +// upgrade. +func replaceBinary(tmp, target string) error { + old := target + ".old" + _ = os.Remove(old) + + if err := osRename(target, old); err != nil { + return err + } + if err := osRename(tmp, target); err != nil { + if rollbackErr := osRename(old, target); rollbackErr != nil { + return fmt.Errorf( + "upgrade failed (%v) and the rollback also failed (%v); "+ + "your previous binary is still at %s — rename it back to %s to recover", + err, rollbackErr, old, target) + } + return err + } + _ = os.Remove(old) + return nil +} + +// cleanupOld removes the moved-aside binary a previous upgrade could +// not delete because it was still executing. Best effort by design: a +// failure here is never worth interrupting an upgrade over. Temp-file +// sweeping is shared across platforms; see CleanupStale in cleanup.go. +func cleanupOld(target string) { + _ = os.Remove(target + ".old") +} diff --git a/internal/upgrade/replace_windows_test.go b/internal/upgrade/replace_windows_test.go new file mode 100644 index 0000000..22b9458 --- /dev/null +++ b/internal/upgrade/replace_windows_test.go @@ -0,0 +1,109 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +//go:build windows + +package upgrade + +import ( + "errors" + "os" + "path/filepath" + "strings" + "testing" +) + +// TestReplaceBinaryRollsBackWhenSecondRenameFails covers the branch that +// decides whether a failed upgrade leaves a working install behind. It is +// forced through osRename rather than left to chance, because the natural +// trigger (two consecutive renames failing in one directory) cannot be +// reproduced reliably. +func TestReplaceBinaryRollsBackWhenSecondRenameFails(t *testing.T) { + dir := t.TempDir() + target := filepath.Join(dir, "commitbrief.exe") + tmp := filepath.Join(dir, ".commitbrief-new") + if err := os.WriteFile(target, []byte("OLD"), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(tmp, []byte("NEW"), 0o755); err != nil { + t.Fatal(err) + } + + original := osRename + calls := 0 + osRename = func(from, to string) error { + calls++ + if calls == 2 { // the tmp → target rename + return errors.New("induced failure") + } + return original(from, to) + } + t.Cleanup(func() { osRename = original }) + + if err := replaceBinary(tmp, target); err == nil { + t.Fatal("replaceBinary() error = nil, want the induced failure") + } + + got, err := os.ReadFile(target) + if err != nil { + t.Fatalf("target missing after rollback — the install was left broken: %v", err) + } + if string(got) != "OLD" { + t.Fatalf("target content = %q, want the original OLD restored", got) + } +} + +// TestReplaceBinaryReportsFailedRollback asserts the worst case is at +// least diagnosable: when the rollback fails too, the error must name the +// backup path so the user can recover by hand. +func TestReplaceBinaryReportsFailedRollback(t *testing.T) { + dir := t.TempDir() + target := filepath.Join(dir, "commitbrief.exe") + tmp := filepath.Join(dir, ".commitbrief-new") + if err := os.WriteFile(target, []byte("OLD"), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(tmp, []byte("NEW"), 0o755); err != nil { + t.Fatal(err) + } + + original := osRename + calls := 0 + osRename = func(from, to string) error { + calls++ + if calls >= 2 { // both the swap and the rollback fail + return errors.New("induced failure") + } + return original(from, to) + } + t.Cleanup(func() { osRename = original }) + + err := replaceBinary(tmp, target) + if err == nil { + t.Fatal("replaceBinary() error = nil, want a failed-rollback error") + } + if !strings.Contains(err.Error(), target+".old") { + t.Fatalf("error must name the recovery path %q, got: %v", target+".old", err) + } +} + +func TestCleanupStaleRemovesOldFile(t *testing.T) { + dir := t.TempDir() + target := filepath.Join(dir, "commitbrief.exe") + old := target + ".old" + + if err := os.WriteFile(target, []byte("BIN"), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(old, []byte("PREVIOUS"), 0o755); err != nil { + t.Fatal(err) + } + + CleanupStale(target) + + if _, err := os.Stat(old); !os.IsNotExist(err) { + t.Fatalf(".old file still present: %v", err) + } + if _, err := os.Stat(target); err != nil { + t.Fatalf("target disappeared: %v", err) + } +} diff --git a/internal/upgrade/semver.go b/internal/upgrade/semver.go new file mode 100644 index 0000000..6bc8587 --- /dev/null +++ b/internal/upgrade/semver.go @@ -0,0 +1,127 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +// Package upgrade implements `commitbrief upgrade`: detecting how the +// running binary was installed, asking GitHub Releases whether a newer +// version exists, and either delegating to the owning package manager +// or replacing a manually installed binary in place. See ADR-0034. +package upgrade + +import ( + "fmt" + "strconv" + "strings" +) + +// Version is a parsed semantic version. Build metadata (+meta) is not +// modelled: CommitBrief never tags with it, and semver says it is +// ignored for precedence anyway. +type Version struct { + Major int + Minor int + Patch int + Pre string // "" for a release; "rc.1" for v1.0.0-rc.1 +} + +// ParseVersion accepts "v1.2.3", "1.2.3" and "v1.2.3-rc.1". It returns +// ok=false for anything else — most importantly the "dev" placeholder a +// locally built binary carries, which the caller turns into a "this is a +// development build" message rather than a bogus comparison. +func ParseVersion(s string) (Version, bool) { + s = strings.TrimSpace(s) + s = strings.TrimPrefix(s, "v") + if s == "" { + return Version{}, false + } + core := s + pre := "" + if i := strings.IndexByte(s, '-'); i >= 0 { + core, pre = s[:i], s[i+1:] + if pre == "" { + return Version{}, false + } + } + parts := strings.Split(core, ".") + if len(parts) != 3 { + return Version{}, false + } + nums := make([]int, 3) + for i, p := range parts { + n, err := strconv.Atoi(p) + if err != nil || n < 0 { + return Version{}, false + } + nums[i] = n + } + return Version{Major: nums[0], Minor: nums[1], Patch: nums[2], Pre: pre}, true +} + +// String renders the version back in tag form (always v-prefixed). +func (v Version) String() string { + if v.Pre == "" { + return fmt.Sprintf("v%d.%d.%d", v.Major, v.Minor, v.Patch) + } + return fmt.Sprintf("v%d.%d.%d-%s", v.Major, v.Minor, v.Patch, v.Pre) +} + +// Compare returns -1, 0 or +1 as v sorts before, equal to, or after o, +// following semver precedence: numeric core first, then a release +// outranking any prerelease of the same core. +func (v Version) Compare(o Version) int { + if c := cmpInt(v.Major, o.Major); c != 0 { + return c + } + if c := cmpInt(v.Minor, o.Minor); c != 0 { + return c + } + if c := cmpInt(v.Patch, o.Patch); c != 0 { + return c + } + switch { + case v.Pre == "" && o.Pre == "": + return 0 + case v.Pre == "": + return 1 + case o.Pre == "": + return -1 + } + return comparePre(v.Pre, o.Pre) +} + +// comparePre orders two dot-separated prerelease strings. Per semver: +// identifiers are compared left to right; a purely numeric identifier +// sorts below an alphanumeric one and compares numerically; a shorter +// identifier list sorts below a longer one when the shared prefix is +// equal (so rc < rc.1). +func comparePre(a, b string) int { + as, bs := strings.Split(a, "."), strings.Split(b, ".") + for i := 0; i < len(as) && i < len(bs); i++ { + an, aErr := strconv.Atoi(as[i]) + bn, bErr := strconv.Atoi(bs[i]) + switch { + case aErr == nil && bErr == nil: // both numeric + if c := cmpInt(an, bn); c != 0 { + return c + } + case aErr == nil: // numeric sorts below alphanumeric + return -1 + case bErr == nil: + return 1 + default: + if c := strings.Compare(as[i], bs[i]); c != 0 { + return c + } + } + } + return cmpInt(len(as), len(bs)) +} + +func cmpInt(a, b int) int { + switch { + case a < b: + return -1 + case a > b: + return 1 + default: + return 0 + } +} diff --git a/internal/upgrade/semver_test.go b/internal/upgrade/semver_test.go new file mode 100644 index 0000000..8d96921 --- /dev/null +++ b/internal/upgrade/semver_test.go @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package upgrade + +import "testing" + +func TestParseVersion(t *testing.T) { + cases := []struct { + in string + ok bool + major int + minor int + patch int + pre string + }{ + {"v1.13.0", true, 1, 13, 0, ""}, + {"1.13.0", true, 1, 13, 0, ""}, + {"v1.0.0-rc.1", true, 1, 0, 0, "rc.1"}, + {"v2.0.0-beta.10", true, 2, 0, 0, "beta.10"}, + {"dev", false, 0, 0, 0, ""}, + {"", false, 0, 0, 0, ""}, + {"v1.2", false, 0, 0, 0, ""}, + {"v1.2.x", false, 0, 0, 0, ""}, + } + for _, c := range cases { + got, ok := ParseVersion(c.in) + if ok != c.ok { + t.Fatalf("ParseVersion(%q) ok = %v, want %v", c.in, ok, c.ok) + } + if !ok { + continue + } + if got.Major != c.major || got.Minor != c.minor || got.Patch != c.patch || got.Pre != c.pre { + t.Fatalf("ParseVersion(%q) = %+v, want %d.%d.%d-%q", c.in, got, c.major, c.minor, c.patch, c.pre) + } + } +} + +func TestVersionCompare(t *testing.T) { + cases := []struct { + a string + b string + want int + }{ + {"v1.13.0", "v1.13.0", 0}, + {"v1.13.0", "v1.14.0", -1}, + {"v1.14.0", "v1.13.0", 1}, + {"v1.13.0", "v2.0.0", -1}, + {"v1.13.0", "v1.13.1", -1}, + // a release outranks its own prerelease + {"v1.14.0-rc.1", "v1.14.0", -1}, + {"v1.14.0", "v1.14.0-rc.1", 1}, + // numeric prerelease identifiers compare numerically, not lexically + {"v1.14.0-rc.2", "v1.14.0-rc.10", -1}, + {"v1.14.0-rc.1", "v1.14.0-rc.1", 0}, + // fewer identifiers sort lower when the prefix matches + {"v1.14.0-rc", "v1.14.0-rc.1", -1}, + // numeric identifiers sort below alphanumeric ones + {"v1.14.0-1", "v1.14.0-alpha", -1}, + } + for _, c := range cases { + va, ok := ParseVersion(c.a) + if !ok { + t.Fatalf("ParseVersion(%q) failed", c.a) + } + vb, ok := ParseVersion(c.b) + if !ok { + t.Fatalf("ParseVersion(%q) failed", c.b) + } + if got := va.Compare(vb); got != c.want { + t.Fatalf("Compare(%q, %q) = %d, want %d", c.a, c.b, got, c.want) + } + } +} + +func TestVersionString(t *testing.T) { + v, _ := ParseVersion("1.2.3-rc.4") + if got := v.String(); got != "v1.2.3-rc.4" { + t.Fatalf("String() = %q, want %q", got, "v1.2.3-rc.4") + } +} diff --git a/man/commitbrief-cache-clear.1 b/man/commitbrief-cache-clear.1 index a109a99..69bf549 100644 --- a/man/commitbrief-cache-clear.1 +++ b/man/commitbrief-cache-clear.1 @@ -1,5 +1,5 @@ .nh -.TH "COMMITBRIEF" "1" "Jun 2026" "Auto generated by spf13/cobra" "" +.TH "COMMITBRIEF" "1" "Jul 2026" "Auto generated by spf13/cobra" "" .SH NAME commitbrief-cache-clear - Remove cached LLM responses for this repo @@ -40,7 +40,7 @@ Remove cached LLM responses for this repo .PP \fB-d\fP, \fB--dir\fP=[] - review only files under these directories (repeatable); combines with the active scope flag + review only files under these directories or matching dir globs (e.g. \fBinternal/**\fR; repeatable, one pattern per flag); combines with the active scope flag .PP \fB--fail-on\fP="" @@ -48,7 +48,7 @@ Remove cached LLM responses for this repo .PP \fB-f\fP, \fB--file\fP=[] - review only these files (repeatable); combines with the active scope flag + review only these files or globs (e.g. \fB*.go\fR, \fBinternal/**/*.ts\fR; repeatable, one pattern per flag — patterns can't be comma-joined); combines with the active scope flag .PP \fB--json\fP[=false] @@ -70,6 +70,14 @@ Remove cached LLM responses for this repo \fB--model\fP="" override configured model +.PP +\fB--no-architecture\fP[=false] + skip architecture-aware review (do not read architecture.json into the prompt) for this run (ADR-0030) + +.PP +\fB--no-baseline\fP[=false] + ignore the signal-control baseline for this run (show everything, even baselined findings) + .PP \fB--no-cache\fP[=false] bypass cache (read and write) @@ -94,6 +102,10 @@ Remove cached LLM responses for this repo \fB-q\fP, \fB--quiet\fP[=false] suppress info messages on stderr +.PP +\fB--sandbox-rerun\fP[=0] + confirm flagged flaky tests by re-running each in isolation N times (ADR-0022); mixed pass+fail = confirmed flaky, all-fail = real failure, all-pass = transient. 0 = off; bare --sandbox-rerun uses N=5. Requires a bound rerun executor + .PP \fB--show-prompt\fP[=false] print the exact system + user prompt that would be sent, then exit (no provider call, no cost) @@ -102,6 +114,10 @@ Remove cached LLM responses for this repo \fB--suggest-commit\fP[=false] after the review, suggest a Conventional Commit message for the staged diff (requires --staged; prints to stdout; not with --json/--markdown/--output) +.PP +\fB--update-baseline\fP[=false] + rewrite .commitbrief/baseline.json from the current findings (accepts them all) instead of filtering this run; user-private, gitignored (ADR-0027) + .PP \fB-v\fP, \fB--verbose\fP[=false] show token/cost/latency footer @@ -120,4 +136,4 @@ Remove cached LLM responses for this repo .SH HISTORY -19-Jun-2026 Auto generated by spf13/cobra +26-Jul-2026 Auto generated by spf13/cobra diff --git a/man/commitbrief-cache-inspect.1 b/man/commitbrief-cache-inspect.1 index 280dedc..7ba0e25 100644 --- a/man/commitbrief-cache-inspect.1 +++ b/man/commitbrief-cache-inspect.1 @@ -1,5 +1,5 @@ .nh -.TH "COMMITBRIEF" "1" "Jun 2026" "Auto generated by spf13/cobra" "" +.TH "COMMITBRIEF" "1" "Jul 2026" "Auto generated by spf13/cobra" "" .SH NAME commitbrief-cache-inspect - Show metadata for a single cache entry by key @@ -44,7 +44,7 @@ Dumps one cached entry's metadata (provider, model, language, timestamps, freshn .PP \fB-d\fP, \fB--dir\fP=[] - review only files under these directories (repeatable); combines with the active scope flag + review only files under these directories or matching dir globs (e.g. \fBinternal/**\fR; repeatable, one pattern per flag); combines with the active scope flag .PP \fB--fail-on\fP="" @@ -52,7 +52,7 @@ Dumps one cached entry's metadata (provider, model, language, timestamps, freshn .PP \fB-f\fP, \fB--file\fP=[] - review only these files (repeatable); combines with the active scope flag + review only these files or globs (e.g. \fB*.go\fR, \fBinternal/**/*.ts\fR; repeatable, one pattern per flag — patterns can't be comma-joined); combines with the active scope flag .PP \fB--json\fP[=false] @@ -74,6 +74,14 @@ Dumps one cached entry's metadata (provider, model, language, timestamps, freshn \fB--model\fP="" override configured model +.PP +\fB--no-architecture\fP[=false] + skip architecture-aware review (do not read architecture.json into the prompt) for this run (ADR-0030) + +.PP +\fB--no-baseline\fP[=false] + ignore the signal-control baseline for this run (show everything, even baselined findings) + .PP \fB--no-cache\fP[=false] bypass cache (read and write) @@ -98,6 +106,10 @@ Dumps one cached entry's metadata (provider, model, language, timestamps, freshn \fB-q\fP, \fB--quiet\fP[=false] suppress info messages on stderr +.PP +\fB--sandbox-rerun\fP[=0] + confirm flagged flaky tests by re-running each in isolation N times (ADR-0022); mixed pass+fail = confirmed flaky, all-fail = real failure, all-pass = transient. 0 = off; bare --sandbox-rerun uses N=5. Requires a bound rerun executor + .PP \fB--show-prompt\fP[=false] print the exact system + user prompt that would be sent, then exit (no provider call, no cost) @@ -106,6 +118,10 @@ Dumps one cached entry's metadata (provider, model, language, timestamps, freshn \fB--suggest-commit\fP[=false] after the review, suggest a Conventional Commit message for the staged diff (requires --staged; prints to stdout; not with --json/--markdown/--output) +.PP +\fB--update-baseline\fP[=false] + rewrite .commitbrief/baseline.json from the current findings (accepts them all) instead of filtering this run; user-private, gitignored (ADR-0027) + .PP \fB-v\fP, \fB--verbose\fP[=false] show token/cost/latency footer @@ -124,4 +140,4 @@ Dumps one cached entry's metadata (provider, model, language, timestamps, freshn .SH HISTORY -19-Jun-2026 Auto generated by spf13/cobra +26-Jul-2026 Auto generated by spf13/cobra diff --git a/man/commitbrief-cache-prune.1 b/man/commitbrief-cache-prune.1 index ceb9dca..6c4174d 100644 --- a/man/commitbrief-cache-prune.1 +++ b/man/commitbrief-cache-prune.1 @@ -1,5 +1,5 @@ .nh -.TH "COMMITBRIEF" "1" "Jun 2026" "Auto generated by spf13/cobra" "" +.TH "COMMITBRIEF" "1" "Jul 2026" "Auto generated by spf13/cobra" "" .SH NAME commitbrief-cache-prune - Drop old/excess cache entries (keep newest N + entries within age window) @@ -56,7 +56,7 @@ Without flags, defaults to \fB--keep-last 500 --older-than 7d\fR\&. Entries surv .PP \fB-d\fP, \fB--dir\fP=[] - review only files under these directories (repeatable); combines with the active scope flag + review only files under these directories or matching dir globs (e.g. \fBinternal/**\fR; repeatable, one pattern per flag); combines with the active scope flag .PP \fB--fail-on\fP="" @@ -64,7 +64,7 @@ Without flags, defaults to \fB--keep-last 500 --older-than 7d\fR\&. Entries surv .PP \fB-f\fP, \fB--file\fP=[] - review only these files (repeatable); combines with the active scope flag + review only these files or globs (e.g. \fB*.go\fR, \fBinternal/**/*.ts\fR; repeatable, one pattern per flag — patterns can't be comma-joined); combines with the active scope flag .PP \fB--json\fP[=false] @@ -82,6 +82,14 @@ Without flags, defaults to \fB--keep-last 500 --older-than 7d\fR\&. Entries surv \fB--min-severity\fP="" hide findings below this severity in the rendered output (critical|high|medium|low|info); --json and --fail-on still see the full set +.PP +\fB--no-architecture\fP[=false] + skip architecture-aware review (do not read architecture.json into the prompt) for this run (ADR-0030) + +.PP +\fB--no-baseline\fP[=false] + ignore the signal-control baseline for this run (show everything, even baselined findings) + .PP \fB--no-cache\fP[=false] bypass cache (read and write) @@ -102,6 +110,10 @@ Without flags, defaults to \fB--keep-last 500 --older-than 7d\fR\&. Entries surv \fB-q\fP, \fB--quiet\fP[=false] suppress info messages on stderr +.PP +\fB--sandbox-rerun\fP[=0] + confirm flagged flaky tests by re-running each in isolation N times (ADR-0022); mixed pass+fail = confirmed flaky, all-fail = real failure, all-pass = transient. 0 = off; bare --sandbox-rerun uses N=5. Requires a bound rerun executor + .PP \fB--show-prompt\fP[=false] print the exact system + user prompt that would be sent, then exit (no provider call, no cost) @@ -110,6 +122,10 @@ Without flags, defaults to \fB--keep-last 500 --older-than 7d\fR\&. Entries surv \fB--suggest-commit\fP[=false] after the review, suggest a Conventional Commit message for the staged diff (requires --staged; prints to stdout; not with --json/--markdown/--output) +.PP +\fB--update-baseline\fP[=false] + rewrite .commitbrief/baseline.json from the current findings (accepts them all) instead of filtering this run; user-private, gitignored (ADR-0027) + .PP \fB-v\fP, \fB--verbose\fP[=false] show token/cost/latency footer @@ -128,4 +144,4 @@ Without flags, defaults to \fB--keep-last 500 --older-than 7d\fR\&. Entries surv .SH HISTORY -19-Jun-2026 Auto generated by spf13/cobra +26-Jul-2026 Auto generated by spf13/cobra diff --git a/man/commitbrief-cache-stats.1 b/man/commitbrief-cache-stats.1 index a035143..34162ce 100644 --- a/man/commitbrief-cache-stats.1 +++ b/man/commitbrief-cache-stats.1 @@ -1,5 +1,5 @@ .nh -.TH "COMMITBRIEF" "1" "Jun 2026" "Auto generated by spf13/cobra" "" +.TH "COMMITBRIEF" "1" "Jul 2026" "Auto generated by spf13/cobra" "" .SH NAME commitbrief-cache-stats - Show cache entry count, size, age range, and per-provider breakdown @@ -40,7 +40,7 @@ Summarizes the repo-local response cache at /.commitbrief/cache/: total entries .PP \fB-d\fP, \fB--dir\fP=[] - review only files under these directories (repeatable); combines with the active scope flag + review only files under these directories or matching dir globs (e.g. \fBinternal/**\fR; repeatable, one pattern per flag); combines with the active scope flag .PP \fB--fail-on\fP="" @@ -48,7 +48,7 @@ Summarizes the repo-local response cache at /.commitbrief/cache/: total entries .PP \fB-f\fP, \fB--file\fP=[] - review only these files (repeatable); combines with the active scope flag + review only these files or globs (e.g. \fB*.go\fR, \fBinternal/**/*.ts\fR; repeatable, one pattern per flag — patterns can't be comma-joined); combines with the active scope flag .PP \fB--json\fP[=false] @@ -70,6 +70,14 @@ Summarizes the repo-local response cache at /.commitbrief/cache/: total entries \fB--model\fP="" override configured model +.PP +\fB--no-architecture\fP[=false] + skip architecture-aware review (do not read architecture.json into the prompt) for this run (ADR-0030) + +.PP +\fB--no-baseline\fP[=false] + ignore the signal-control baseline for this run (show everything, even baselined findings) + .PP \fB--no-cache\fP[=false] bypass cache (read and write) @@ -94,6 +102,10 @@ Summarizes the repo-local response cache at /.commitbrief/cache/: total entries \fB-q\fP, \fB--quiet\fP[=false] suppress info messages on stderr +.PP +\fB--sandbox-rerun\fP[=0] + confirm flagged flaky tests by re-running each in isolation N times (ADR-0022); mixed pass+fail = confirmed flaky, all-fail = real failure, all-pass = transient. 0 = off; bare --sandbox-rerun uses N=5. Requires a bound rerun executor + .PP \fB--show-prompt\fP[=false] print the exact system + user prompt that would be sent, then exit (no provider call, no cost) @@ -102,6 +114,10 @@ Summarizes the repo-local response cache at /.commitbrief/cache/: total entries \fB--suggest-commit\fP[=false] after the review, suggest a Conventional Commit message for the staged diff (requires --staged; prints to stdout; not with --json/--markdown/--output) +.PP +\fB--update-baseline\fP[=false] + rewrite .commitbrief/baseline.json from the current findings (accepts them all) instead of filtering this run; user-private, gitignored (ADR-0027) + .PP \fB-v\fP, \fB--verbose\fP[=false] show token/cost/latency footer @@ -120,4 +136,4 @@ Summarizes the repo-local response cache at /.commitbrief/cache/: total entries .SH HISTORY -19-Jun-2026 Auto generated by spf13/cobra +26-Jul-2026 Auto generated by spf13/cobra diff --git a/man/commitbrief-cache.1 b/man/commitbrief-cache.1 index 6c8237e..7591fa8 100644 --- a/man/commitbrief-cache.1 +++ b/man/commitbrief-cache.1 @@ -1,5 +1,5 @@ .nh -.TH "COMMITBRIEF" "1" "Jun 2026" "Auto generated by spf13/cobra" "" +.TH "COMMITBRIEF" "1" "Jul 2026" "Auto generated by spf13/cobra" "" .SH NAME commitbrief-cache - Inspect and manage the local response cache @@ -40,7 +40,7 @@ Inspect and manage the local response cache .PP \fB-d\fP, \fB--dir\fP=[] - review only files under these directories (repeatable); combines with the active scope flag + review only files under these directories or matching dir globs (e.g. \fBinternal/**\fR; repeatable, one pattern per flag); combines with the active scope flag .PP \fB--fail-on\fP="" @@ -48,7 +48,7 @@ Inspect and manage the local response cache .PP \fB-f\fP, \fB--file\fP=[] - review only these files (repeatable); combines with the active scope flag + review only these files or globs (e.g. \fB*.go\fR, \fBinternal/**/*.ts\fR; repeatable, one pattern per flag — patterns can't be comma-joined); combines with the active scope flag .PP \fB--json\fP[=false] @@ -70,6 +70,14 @@ Inspect and manage the local response cache \fB--model\fP="" override configured model +.PP +\fB--no-architecture\fP[=false] + skip architecture-aware review (do not read architecture.json into the prompt) for this run (ADR-0030) + +.PP +\fB--no-baseline\fP[=false] + ignore the signal-control baseline for this run (show everything, even baselined findings) + .PP \fB--no-cache\fP[=false] bypass cache (read and write) @@ -94,6 +102,10 @@ Inspect and manage the local response cache \fB-q\fP, \fB--quiet\fP[=false] suppress info messages on stderr +.PP +\fB--sandbox-rerun\fP[=0] + confirm flagged flaky tests by re-running each in isolation N times (ADR-0022); mixed pass+fail = confirmed flaky, all-fail = real failure, all-pass = transient. 0 = off; bare --sandbox-rerun uses N=5. Requires a bound rerun executor + .PP \fB--show-prompt\fP[=false] print the exact system + user prompt that would be sent, then exit (no provider call, no cost) @@ -102,6 +114,10 @@ Inspect and manage the local response cache \fB--suggest-commit\fP[=false] after the review, suggest a Conventional Commit message for the staged diff (requires --staged; prints to stdout; not with --json/--markdown/--output) +.PP +\fB--update-baseline\fP[=false] + rewrite .commitbrief/baseline.json from the current findings (accepts them all) instead of filtering this run; user-private, gitignored (ADR-0027) + .PP \fB-v\fP, \fB--verbose\fP[=false] show token/cost/latency footer @@ -120,4 +136,4 @@ Inspect and manage the local response cache .SH HISTORY -19-Jun-2026 Auto generated by spf13/cobra +26-Jul-2026 Auto generated by spf13/cobra diff --git a/man/commitbrief-commit.1 b/man/commitbrief-commit.1 index 006c314..159b9ac 100644 --- a/man/commitbrief-commit.1 +++ b/man/commitbrief-commit.1 @@ -1,5 +1,5 @@ .nh -.TH "COMMITBRIEF" "1" "Jun 2026" "Auto generated by spf13/cobra" "" +.TH "COMMITBRIEF" "1" "Jul 2026" "Auto generated by spf13/cobra" "" .SH NAME commitbrief-commit - Generate a commit message from staged changes and commit @@ -54,7 +54,7 @@ Needs an interactive terminal to confirm (or to pick from --generate alternative .PP \fB-d\fP, \fB--dir\fP=[] - review only files under these directories (repeatable); combines with the active scope flag + review only files under these directories or matching dir globs (e.g. \fBinternal/**\fR; repeatable, one pattern per flag); combines with the active scope flag .PP \fB--fail-on\fP="" @@ -62,7 +62,7 @@ Needs an interactive terminal to confirm (or to pick from --generate alternative .PP \fB-f\fP, \fB--file\fP=[] - review only these files (repeatable); combines with the active scope flag + review only these files or globs (e.g. \fB*.go\fR, \fBinternal/**/*.ts\fR; repeatable, one pattern per flag — patterns can't be comma-joined); combines with the active scope flag .PP \fB--json\fP[=false] @@ -84,6 +84,14 @@ Needs an interactive terminal to confirm (or to pick from --generate alternative \fB--model\fP="" override configured model +.PP +\fB--no-architecture\fP[=false] + skip architecture-aware review (do not read architecture.json into the prompt) for this run (ADR-0030) + +.PP +\fB--no-baseline\fP[=false] + ignore the signal-control baseline for this run (show everything, even baselined findings) + .PP \fB--no-cache\fP[=false] bypass cache (read and write) @@ -108,6 +116,10 @@ Needs an interactive terminal to confirm (or to pick from --generate alternative \fB-q\fP, \fB--quiet\fP[=false] suppress info messages on stderr +.PP +\fB--sandbox-rerun\fP[=0] + confirm flagged flaky tests by re-running each in isolation N times (ADR-0022); mixed pass+fail = confirmed flaky, all-fail = real failure, all-pass = transient. 0 = off; bare --sandbox-rerun uses N=5. Requires a bound rerun executor + .PP \fB--show-prompt\fP[=false] print the exact system + user prompt that would be sent, then exit (no provider call, no cost) @@ -116,6 +128,10 @@ Needs an interactive terminal to confirm (or to pick from --generate alternative \fB--suggest-commit\fP[=false] after the review, suggest a Conventional Commit message for the staged diff (requires --staged; prints to stdout; not with --json/--markdown/--output) +.PP +\fB--update-baseline\fP[=false] + rewrite .commitbrief/baseline.json from the current findings (accepts them all) instead of filtering this run; user-private, gitignored (ADR-0027) + .PP \fB-v\fP, \fB--verbose\fP[=false] show token/cost/latency footer @@ -134,4 +150,4 @@ Needs an interactive terminal to confirm (or to pick from --generate alternative .SH HISTORY -19-Jun-2026 Auto generated by spf13/cobra +26-Jul-2026 Auto generated by spf13/cobra diff --git a/man/commitbrief-completion-bash.1 b/man/commitbrief-completion-bash.1 index 45b9df5..b4b4d50 100644 --- a/man/commitbrief-completion-bash.1 +++ b/man/commitbrief-completion-bash.1 @@ -1,5 +1,5 @@ .nh -.TH "COMMITBRIEF" "1" "Jun 2026" "Auto generated by spf13/cobra" "" +.TH "COMMITBRIEF" "1" "Jul 2026" "Auto generated by spf13/cobra" "" .SH NAME commitbrief-completion-bash - Generate the autocompletion script for bash @@ -71,7 +71,7 @@ You will need to start a new shell for this setup to take effect. .PP \fB-d\fP, \fB--dir\fP=[] - review only files under these directories (repeatable); combines with the active scope flag + review only files under these directories or matching dir globs (e.g. \fBinternal/**\fR; repeatable, one pattern per flag); combines with the active scope flag .PP \fB--fail-on\fP="" @@ -79,7 +79,7 @@ You will need to start a new shell for this setup to take effect. .PP \fB-f\fP, \fB--file\fP=[] - review only these files (repeatable); combines with the active scope flag + review only these files or globs (e.g. \fB*.go\fR, \fBinternal/**/*.ts\fR; repeatable, one pattern per flag — patterns can't be comma-joined); combines with the active scope flag .PP \fB--json\fP[=false] @@ -101,6 +101,14 @@ You will need to start a new shell for this setup to take effect. \fB--model\fP="" override configured model +.PP +\fB--no-architecture\fP[=false] + skip architecture-aware review (do not read architecture.json into the prompt) for this run (ADR-0030) + +.PP +\fB--no-baseline\fP[=false] + ignore the signal-control baseline for this run (show everything, even baselined findings) + .PP \fB--no-cache\fP[=false] bypass cache (read and write) @@ -125,6 +133,10 @@ You will need to start a new shell for this setup to take effect. \fB-q\fP, \fB--quiet\fP[=false] suppress info messages on stderr +.PP +\fB--sandbox-rerun\fP[=0] + confirm flagged flaky tests by re-running each in isolation N times (ADR-0022); mixed pass+fail = confirmed flaky, all-fail = real failure, all-pass = transient. 0 = off; bare --sandbox-rerun uses N=5. Requires a bound rerun executor + .PP \fB--show-prompt\fP[=false] print the exact system + user prompt that would be sent, then exit (no provider call, no cost) @@ -133,6 +145,10 @@ You will need to start a new shell for this setup to take effect. \fB--suggest-commit\fP[=false] after the review, suggest a Conventional Commit message for the staged diff (requires --staged; prints to stdout; not with --json/--markdown/--output) +.PP +\fB--update-baseline\fP[=false] + rewrite .commitbrief/baseline.json from the current findings (accepts them all) instead of filtering this run; user-private, gitignored (ADR-0027) + .PP \fB-v\fP, \fB--verbose\fP[=false] show token/cost/latency footer @@ -151,4 +167,4 @@ You will need to start a new shell for this setup to take effect. .SH HISTORY -19-Jun-2026 Auto generated by spf13/cobra +26-Jul-2026 Auto generated by spf13/cobra diff --git a/man/commitbrief-completion-fish.1 b/man/commitbrief-completion-fish.1 index 8e1767c..79a7763 100644 --- a/man/commitbrief-completion-fish.1 +++ b/man/commitbrief-completion-fish.1 @@ -1,5 +1,5 @@ .nh -.TH "COMMITBRIEF" "1" "Jun 2026" "Auto generated by spf13/cobra" "" +.TH "COMMITBRIEF" "1" "Jul 2026" "Auto generated by spf13/cobra" "" .SH NAME commitbrief-completion-fish - Generate the autocompletion script for fish @@ -61,7 +61,7 @@ You will need to start a new shell for this setup to take effect. .PP \fB-d\fP, \fB--dir\fP=[] - review only files under these directories (repeatable); combines with the active scope flag + review only files under these directories or matching dir globs (e.g. \fBinternal/**\fR; repeatable, one pattern per flag); combines with the active scope flag .PP \fB--fail-on\fP="" @@ -69,7 +69,7 @@ You will need to start a new shell for this setup to take effect. .PP \fB-f\fP, \fB--file\fP=[] - review only these files (repeatable); combines with the active scope flag + review only these files or globs (e.g. \fB*.go\fR, \fBinternal/**/*.ts\fR; repeatable, one pattern per flag — patterns can't be comma-joined); combines with the active scope flag .PP \fB--json\fP[=false] @@ -91,6 +91,14 @@ You will need to start a new shell for this setup to take effect. \fB--model\fP="" override configured model +.PP +\fB--no-architecture\fP[=false] + skip architecture-aware review (do not read architecture.json into the prompt) for this run (ADR-0030) + +.PP +\fB--no-baseline\fP[=false] + ignore the signal-control baseline for this run (show everything, even baselined findings) + .PP \fB--no-cache\fP[=false] bypass cache (read and write) @@ -115,6 +123,10 @@ You will need to start a new shell for this setup to take effect. \fB-q\fP, \fB--quiet\fP[=false] suppress info messages on stderr +.PP +\fB--sandbox-rerun\fP[=0] + confirm flagged flaky tests by re-running each in isolation N times (ADR-0022); mixed pass+fail = confirmed flaky, all-fail = real failure, all-pass = transient. 0 = off; bare --sandbox-rerun uses N=5. Requires a bound rerun executor + .PP \fB--show-prompt\fP[=false] print the exact system + user prompt that would be sent, then exit (no provider call, no cost) @@ -123,6 +135,10 @@ You will need to start a new shell for this setup to take effect. \fB--suggest-commit\fP[=false] after the review, suggest a Conventional Commit message for the staged diff (requires --staged; prints to stdout; not with --json/--markdown/--output) +.PP +\fB--update-baseline\fP[=false] + rewrite .commitbrief/baseline.json from the current findings (accepts them all) instead of filtering this run; user-private, gitignored (ADR-0027) + .PP \fB-v\fP, \fB--verbose\fP[=false] show token/cost/latency footer @@ -141,4 +157,4 @@ You will need to start a new shell for this setup to take effect. .SH HISTORY -19-Jun-2026 Auto generated by spf13/cobra +26-Jul-2026 Auto generated by spf13/cobra diff --git a/man/commitbrief-completion-powershell.1 b/man/commitbrief-completion-powershell.1 index be0f13a..e781921 100644 --- a/man/commitbrief-completion-powershell.1 +++ b/man/commitbrief-completion-powershell.1 @@ -1,5 +1,5 @@ .nh -.TH "COMMITBRIEF" "1" "Jun 2026" "Auto generated by spf13/cobra" "" +.TH "COMMITBRIEF" "1" "Jul 2026" "Auto generated by spf13/cobra" "" .SH NAME commitbrief-completion-powershell - Generate the autocompletion script for powershell @@ -55,7 +55,7 @@ to your powershell profile. .PP \fB-d\fP, \fB--dir\fP=[] - review only files under these directories (repeatable); combines with the active scope flag + review only files under these directories or matching dir globs (e.g. \fBinternal/**\fR; repeatable, one pattern per flag); combines with the active scope flag .PP \fB--fail-on\fP="" @@ -63,7 +63,7 @@ to your powershell profile. .PP \fB-f\fP, \fB--file\fP=[] - review only these files (repeatable); combines with the active scope flag + review only these files or globs (e.g. \fB*.go\fR, \fBinternal/**/*.ts\fR; repeatable, one pattern per flag — patterns can't be comma-joined); combines with the active scope flag .PP \fB--json\fP[=false] @@ -85,6 +85,14 @@ to your powershell profile. \fB--model\fP="" override configured model +.PP +\fB--no-architecture\fP[=false] + skip architecture-aware review (do not read architecture.json into the prompt) for this run (ADR-0030) + +.PP +\fB--no-baseline\fP[=false] + ignore the signal-control baseline for this run (show everything, even baselined findings) + .PP \fB--no-cache\fP[=false] bypass cache (read and write) @@ -109,6 +117,10 @@ to your powershell profile. \fB-q\fP, \fB--quiet\fP[=false] suppress info messages on stderr +.PP +\fB--sandbox-rerun\fP[=0] + confirm flagged flaky tests by re-running each in isolation N times (ADR-0022); mixed pass+fail = confirmed flaky, all-fail = real failure, all-pass = transient. 0 = off; bare --sandbox-rerun uses N=5. Requires a bound rerun executor + .PP \fB--show-prompt\fP[=false] print the exact system + user prompt that would be sent, then exit (no provider call, no cost) @@ -117,6 +129,10 @@ to your powershell profile. \fB--suggest-commit\fP[=false] after the review, suggest a Conventional Commit message for the staged diff (requires --staged; prints to stdout; not with --json/--markdown/--output) +.PP +\fB--update-baseline\fP[=false] + rewrite .commitbrief/baseline.json from the current findings (accepts them all) instead of filtering this run; user-private, gitignored (ADR-0027) + .PP \fB-v\fP, \fB--verbose\fP[=false] show token/cost/latency footer @@ -135,4 +151,4 @@ to your powershell profile. .SH HISTORY -19-Jun-2026 Auto generated by spf13/cobra +26-Jul-2026 Auto generated by spf13/cobra diff --git a/man/commitbrief-completion-zsh.1 b/man/commitbrief-completion-zsh.1 index d68d866..a2c9672 100644 --- a/man/commitbrief-completion-zsh.1 +++ b/man/commitbrief-completion-zsh.1 @@ -1,5 +1,5 @@ .nh -.TH "COMMITBRIEF" "1" "Jun 2026" "Auto generated by spf13/cobra" "" +.TH "COMMITBRIEF" "1" "Jul 2026" "Auto generated by spf13/cobra" "" .SH NAME commitbrief-completion-zsh - Generate the autocompletion script for zsh @@ -75,7 +75,7 @@ You will need to start a new shell for this setup to take effect. .PP \fB-d\fP, \fB--dir\fP=[] - review only files under these directories (repeatable); combines with the active scope flag + review only files under these directories or matching dir globs (e.g. \fBinternal/**\fR; repeatable, one pattern per flag); combines with the active scope flag .PP \fB--fail-on\fP="" @@ -83,7 +83,7 @@ You will need to start a new shell for this setup to take effect. .PP \fB-f\fP, \fB--file\fP=[] - review only these files (repeatable); combines with the active scope flag + review only these files or globs (e.g. \fB*.go\fR, \fBinternal/**/*.ts\fR; repeatable, one pattern per flag — patterns can't be comma-joined); combines with the active scope flag .PP \fB--json\fP[=false] @@ -105,6 +105,14 @@ You will need to start a new shell for this setup to take effect. \fB--model\fP="" override configured model +.PP +\fB--no-architecture\fP[=false] + skip architecture-aware review (do not read architecture.json into the prompt) for this run (ADR-0030) + +.PP +\fB--no-baseline\fP[=false] + ignore the signal-control baseline for this run (show everything, even baselined findings) + .PP \fB--no-cache\fP[=false] bypass cache (read and write) @@ -129,6 +137,10 @@ You will need to start a new shell for this setup to take effect. \fB-q\fP, \fB--quiet\fP[=false] suppress info messages on stderr +.PP +\fB--sandbox-rerun\fP[=0] + confirm flagged flaky tests by re-running each in isolation N times (ADR-0022); mixed pass+fail = confirmed flaky, all-fail = real failure, all-pass = transient. 0 = off; bare --sandbox-rerun uses N=5. Requires a bound rerun executor + .PP \fB--show-prompt\fP[=false] print the exact system + user prompt that would be sent, then exit (no provider call, no cost) @@ -137,6 +149,10 @@ You will need to start a new shell for this setup to take effect. \fB--suggest-commit\fP[=false] after the review, suggest a Conventional Commit message for the staged diff (requires --staged; prints to stdout; not with --json/--markdown/--output) +.PP +\fB--update-baseline\fP[=false] + rewrite .commitbrief/baseline.json from the current findings (accepts them all) instead of filtering this run; user-private, gitignored (ADR-0027) + .PP \fB-v\fP, \fB--verbose\fP[=false] show token/cost/latency footer @@ -155,4 +171,4 @@ You will need to start a new shell for this setup to take effect. .SH HISTORY -19-Jun-2026 Auto generated by spf13/cobra +26-Jul-2026 Auto generated by spf13/cobra diff --git a/man/commitbrief-completion.1 b/man/commitbrief-completion.1 index c82047f..dd6551f 100644 --- a/man/commitbrief-completion.1 +++ b/man/commitbrief-completion.1 @@ -1,5 +1,5 @@ .nh -.TH "COMMITBRIEF" "1" "Jun 2026" "Auto generated by spf13/cobra" "" +.TH "COMMITBRIEF" "1" "Jul 2026" "Auto generated by spf13/cobra" "" .SH NAME commitbrief-completion - Generate the autocompletion script for the specified shell @@ -41,7 +41,7 @@ See each sub-command's help for details on how to use the generated script. .PP \fB-d\fP, \fB--dir\fP=[] - review only files under these directories (repeatable); combines with the active scope flag + review only files under these directories or matching dir globs (e.g. \fBinternal/**\fR; repeatable, one pattern per flag); combines with the active scope flag .PP \fB--fail-on\fP="" @@ -49,7 +49,7 @@ See each sub-command's help for details on how to use the generated script. .PP \fB-f\fP, \fB--file\fP=[] - review only these files (repeatable); combines with the active scope flag + review only these files or globs (e.g. \fB*.go\fR, \fBinternal/**/*.ts\fR; repeatable, one pattern per flag — patterns can't be comma-joined); combines with the active scope flag .PP \fB--json\fP[=false] @@ -71,6 +71,14 @@ See each sub-command's help for details on how to use the generated script. \fB--model\fP="" override configured model +.PP +\fB--no-architecture\fP[=false] + skip architecture-aware review (do not read architecture.json into the prompt) for this run (ADR-0030) + +.PP +\fB--no-baseline\fP[=false] + ignore the signal-control baseline for this run (show everything, even baselined findings) + .PP \fB--no-cache\fP[=false] bypass cache (read and write) @@ -95,6 +103,10 @@ See each sub-command's help for details on how to use the generated script. \fB-q\fP, \fB--quiet\fP[=false] suppress info messages on stderr +.PP +\fB--sandbox-rerun\fP[=0] + confirm flagged flaky tests by re-running each in isolation N times (ADR-0022); mixed pass+fail = confirmed flaky, all-fail = real failure, all-pass = transient. 0 = off; bare --sandbox-rerun uses N=5. Requires a bound rerun executor + .PP \fB--show-prompt\fP[=false] print the exact system + user prompt that would be sent, then exit (no provider call, no cost) @@ -103,6 +115,10 @@ See each sub-command's help for details on how to use the generated script. \fB--suggest-commit\fP[=false] after the review, suggest a Conventional Commit message for the staged diff (requires --staged; prints to stdout; not with --json/--markdown/--output) +.PP +\fB--update-baseline\fP[=false] + rewrite .commitbrief/baseline.json from the current findings (accepts them all) instead of filtering this run; user-private, gitignored (ADR-0027) + .PP \fB-v\fP, \fB--verbose\fP[=false] show token/cost/latency footer @@ -121,4 +137,4 @@ See each sub-command's help for details on how to use the generated script. .SH HISTORY -19-Jun-2026 Auto generated by spf13/cobra +26-Jul-2026 Auto generated by spf13/cobra diff --git a/man/commitbrief-compress.1 b/man/commitbrief-compress.1 index 67c83a2..88e2692 100644 --- a/man/commitbrief-compress.1 +++ b/man/commitbrief-compress.1 @@ -1,5 +1,5 @@ .nh -.TH "COMMITBRIEF" "1" "Jun 2026" "Auto generated by spf13/cobra" "" +.TH "COMMITBRIEF" "1" "Jul 2026" "Auto generated by spf13/cobra" "" .SH NAME commitbrief-compress - Shrink COMMITBRIEF.md losslessly via the configured provider @@ -57,7 +57,7 @@ an ISO timestamp before the file is replaced. .PP \fB-d\fP, \fB--dir\fP=[] - review only files under these directories (repeatable); combines with the active scope flag + review only files under these directories or matching dir globs (e.g. \fBinternal/**\fR; repeatable, one pattern per flag); combines with the active scope flag .PP \fB--fail-on\fP="" @@ -65,7 +65,7 @@ an ISO timestamp before the file is replaced. .PP \fB-f\fP, \fB--file\fP=[] - review only these files (repeatable); combines with the active scope flag + review only these files or globs (e.g. \fB*.go\fR, \fBinternal/**/*.ts\fR; repeatable, one pattern per flag — patterns can't be comma-joined); combines with the active scope flag .PP \fB--json\fP[=false] @@ -87,6 +87,14 @@ an ISO timestamp before the file is replaced. \fB--model\fP="" override configured model +.PP +\fB--no-architecture\fP[=false] + skip architecture-aware review (do not read architecture.json into the prompt) for this run (ADR-0030) + +.PP +\fB--no-baseline\fP[=false] + ignore the signal-control baseline for this run (show everything, even baselined findings) + .PP \fB--no-cache\fP[=false] bypass cache (read and write) @@ -111,6 +119,10 @@ an ISO timestamp before the file is replaced. \fB-q\fP, \fB--quiet\fP[=false] suppress info messages on stderr +.PP +\fB--sandbox-rerun\fP[=0] + confirm flagged flaky tests by re-running each in isolation N times (ADR-0022); mixed pass+fail = confirmed flaky, all-fail = real failure, all-pass = transient. 0 = off; bare --sandbox-rerun uses N=5. Requires a bound rerun executor + .PP \fB--show-prompt\fP[=false] print the exact system + user prompt that would be sent, then exit (no provider call, no cost) @@ -119,6 +131,10 @@ an ISO timestamp before the file is replaced. \fB--suggest-commit\fP[=false] after the review, suggest a Conventional Commit message for the staged diff (requires --staged; prints to stdout; not with --json/--markdown/--output) +.PP +\fB--update-baseline\fP[=false] + rewrite .commitbrief/baseline.json from the current findings (accepts them all) instead of filtering this run; user-private, gitignored (ADR-0027) + .PP \fB-v\fP, \fB--verbose\fP[=false] show token/cost/latency footer @@ -137,4 +153,4 @@ an ISO timestamp before the file is replaced. .SH HISTORY -19-Jun-2026 Auto generated by spf13/cobra +26-Jul-2026 Auto generated by spf13/cobra diff --git a/man/commitbrief-config-get.1 b/man/commitbrief-config-get.1 index 4f0bf55..8c58c5f 100644 --- a/man/commitbrief-config-get.1 +++ b/man/commitbrief-config-get.1 @@ -1,5 +1,5 @@ .nh -.TH "COMMITBRIEF" "1" "Jun 2026" "Auto generated by spf13/cobra" "" +.TH "COMMITBRIEF" "1" "Jul 2026" "Auto generated by spf13/cobra" "" .SH NAME commitbrief-config-get - Print a single configuration value by dotted path @@ -47,7 +47,7 @@ Examples: .PP \fB-d\fP, \fB--dir\fP=[] - review only files under these directories (repeatable); combines with the active scope flag + review only files under these directories or matching dir globs (e.g. \fBinternal/**\fR; repeatable, one pattern per flag); combines with the active scope flag .PP \fB--fail-on\fP="" @@ -55,7 +55,7 @@ Examples: .PP \fB-f\fP, \fB--file\fP=[] - review only these files (repeatable); combines with the active scope flag + review only these files or globs (e.g. \fB*.go\fR, \fBinternal/**/*.ts\fR; repeatable, one pattern per flag — patterns can't be comma-joined); combines with the active scope flag .PP \fB--json\fP[=false] @@ -77,6 +77,14 @@ Examples: \fB--model\fP="" override configured model +.PP +\fB--no-architecture\fP[=false] + skip architecture-aware review (do not read architecture.json into the prompt) for this run (ADR-0030) + +.PP +\fB--no-baseline\fP[=false] + ignore the signal-control baseline for this run (show everything, even baselined findings) + .PP \fB--no-cache\fP[=false] bypass cache (read and write) @@ -101,6 +109,10 @@ Examples: \fB-q\fP, \fB--quiet\fP[=false] suppress info messages on stderr +.PP +\fB--sandbox-rerun\fP[=0] + confirm flagged flaky tests by re-running each in isolation N times (ADR-0022); mixed pass+fail = confirmed flaky, all-fail = real failure, all-pass = transient. 0 = off; bare --sandbox-rerun uses N=5. Requires a bound rerun executor + .PP \fB--show-prompt\fP[=false] print the exact system + user prompt that would be sent, then exit (no provider call, no cost) @@ -109,6 +121,10 @@ Examples: \fB--suggest-commit\fP[=false] after the review, suggest a Conventional Commit message for the staged diff (requires --staged; prints to stdout; not with --json/--markdown/--output) +.PP +\fB--update-baseline\fP[=false] + rewrite .commitbrief/baseline.json from the current findings (accepts them all) instead of filtering this run; user-private, gitignored (ADR-0027) + .PP \fB-v\fP, \fB--verbose\fP[=false] show token/cost/latency footer @@ -127,4 +143,4 @@ Examples: .SH HISTORY -19-Jun-2026 Auto generated by spf13/cobra +26-Jul-2026 Auto generated by spf13/cobra diff --git a/man/commitbrief-config-set.1 b/man/commitbrief-config-set.1 index 7378207..dc084e3 100644 --- a/man/commitbrief-config-set.1 +++ b/man/commitbrief-config-set.1 @@ -1,5 +1,5 @@ .nh -.TH "COMMITBRIEF" "1" "Jun 2026" "Auto generated by spf13/cobra" "" +.TH "COMMITBRIEF" "1" "Jul 2026" "Auto generated by spf13/cobra" "" .SH NAME commitbrief-config-set - Write a single configuration value by dotted path @@ -55,7 +55,7 @@ By default writes to ~/.commitbrief/config.yml; --local writes to the repo. .PP \fB-d\fP, \fB--dir\fP=[] - review only files under these directories (repeatable); combines with the active scope flag + review only files under these directories or matching dir globs (e.g. \fBinternal/**\fR; repeatable, one pattern per flag); combines with the active scope flag .PP \fB--fail-on\fP="" @@ -63,7 +63,7 @@ By default writes to ~/.commitbrief/config.yml; --local writes to the repo. .PP \fB-f\fP, \fB--file\fP=[] - review only these files (repeatable); combines with the active scope flag + review only these files or globs (e.g. \fB*.go\fR, \fBinternal/**/*.ts\fR; repeatable, one pattern per flag — patterns can't be comma-joined); combines with the active scope flag .PP \fB--json\fP[=false] @@ -85,6 +85,14 @@ By default writes to ~/.commitbrief/config.yml; --local writes to the repo. \fB--model\fP="" override configured model +.PP +\fB--no-architecture\fP[=false] + skip architecture-aware review (do not read architecture.json into the prompt) for this run (ADR-0030) + +.PP +\fB--no-baseline\fP[=false] + ignore the signal-control baseline for this run (show everything, even baselined findings) + .PP \fB--no-cache\fP[=false] bypass cache (read and write) @@ -109,6 +117,10 @@ By default writes to ~/.commitbrief/config.yml; --local writes to the repo. \fB-q\fP, \fB--quiet\fP[=false] suppress info messages on stderr +.PP +\fB--sandbox-rerun\fP[=0] + confirm flagged flaky tests by re-running each in isolation N times (ADR-0022); mixed pass+fail = confirmed flaky, all-fail = real failure, all-pass = transient. 0 = off; bare --sandbox-rerun uses N=5. Requires a bound rerun executor + .PP \fB--show-prompt\fP[=false] print the exact system + user prompt that would be sent, then exit (no provider call, no cost) @@ -117,6 +129,10 @@ By default writes to ~/.commitbrief/config.yml; --local writes to the repo. \fB--suggest-commit\fP[=false] after the review, suggest a Conventional Commit message for the staged diff (requires --staged; prints to stdout; not with --json/--markdown/--output) +.PP +\fB--update-baseline\fP[=false] + rewrite .commitbrief/baseline.json from the current findings (accepts them all) instead of filtering this run; user-private, gitignored (ADR-0027) + .PP \fB-v\fP, \fB--verbose\fP[=false] show token/cost/latency footer @@ -135,4 +151,4 @@ By default writes to ~/.commitbrief/config.yml; --local writes to the repo. .SH HISTORY -19-Jun-2026 Auto generated by spf13/cobra +26-Jul-2026 Auto generated by spf13/cobra diff --git a/man/commitbrief-config-show.1 b/man/commitbrief-config-show.1 index b780e93..5b276f3 100644 --- a/man/commitbrief-config-show.1 +++ b/man/commitbrief-config-show.1 @@ -1,5 +1,5 @@ .nh -.TH "COMMITBRIEF" "1" "Jun 2026" "Auto generated by spf13/cobra" "" +.TH "COMMITBRIEF" "1" "Jul 2026" "Auto generated by spf13/cobra" "" .SH NAME commitbrief-config-show - Print the merged configuration (API keys masked) @@ -40,7 +40,7 @@ Print the merged configuration (API keys masked) .PP \fB-d\fP, \fB--dir\fP=[] - review only files under these directories (repeatable); combines with the active scope flag + review only files under these directories or matching dir globs (e.g. \fBinternal/**\fR; repeatable, one pattern per flag); combines with the active scope flag .PP \fB--fail-on\fP="" @@ -48,7 +48,7 @@ Print the merged configuration (API keys masked) .PP \fB-f\fP, \fB--file\fP=[] - review only these files (repeatable); combines with the active scope flag + review only these files or globs (e.g. \fB*.go\fR, \fBinternal/**/*.ts\fR; repeatable, one pattern per flag — patterns can't be comma-joined); combines with the active scope flag .PP \fB--json\fP[=false] @@ -70,6 +70,14 @@ Print the merged configuration (API keys masked) \fB--model\fP="" override configured model +.PP +\fB--no-architecture\fP[=false] + skip architecture-aware review (do not read architecture.json into the prompt) for this run (ADR-0030) + +.PP +\fB--no-baseline\fP[=false] + ignore the signal-control baseline for this run (show everything, even baselined findings) + .PP \fB--no-cache\fP[=false] bypass cache (read and write) @@ -94,6 +102,10 @@ Print the merged configuration (API keys masked) \fB-q\fP, \fB--quiet\fP[=false] suppress info messages on stderr +.PP +\fB--sandbox-rerun\fP[=0] + confirm flagged flaky tests by re-running each in isolation N times (ADR-0022); mixed pass+fail = confirmed flaky, all-fail = real failure, all-pass = transient. 0 = off; bare --sandbox-rerun uses N=5. Requires a bound rerun executor + .PP \fB--show-prompt\fP[=false] print the exact system + user prompt that would be sent, then exit (no provider call, no cost) @@ -102,6 +114,10 @@ Print the merged configuration (API keys masked) \fB--suggest-commit\fP[=false] after the review, suggest a Conventional Commit message for the staged diff (requires --staged; prints to stdout; not with --json/--markdown/--output) +.PP +\fB--update-baseline\fP[=false] + rewrite .commitbrief/baseline.json from the current findings (accepts them all) instead of filtering this run; user-private, gitignored (ADR-0027) + .PP \fB-v\fP, \fB--verbose\fP[=false] show token/cost/latency footer @@ -120,4 +136,4 @@ Print the merged configuration (API keys masked) .SH HISTORY -19-Jun-2026 Auto generated by spf13/cobra +26-Jul-2026 Auto generated by spf13/cobra diff --git a/man/commitbrief-config.1 b/man/commitbrief-config.1 index 5eed80c..60be57c 100644 --- a/man/commitbrief-config.1 +++ b/man/commitbrief-config.1 @@ -1,5 +1,5 @@ .nh -.TH "COMMITBRIEF" "1" "Jun 2026" "Auto generated by spf13/cobra" "" +.TH "COMMITBRIEF" "1" "Jul 2026" "Auto generated by spf13/cobra" "" .SH NAME commitbrief-config - Show, get, or set individual configuration values @@ -40,7 +40,7 @@ Show, get, or set individual configuration values .PP \fB-d\fP, \fB--dir\fP=[] - review only files under these directories (repeatable); combines with the active scope flag + review only files under these directories or matching dir globs (e.g. \fBinternal/**\fR; repeatable, one pattern per flag); combines with the active scope flag .PP \fB--fail-on\fP="" @@ -48,7 +48,7 @@ Show, get, or set individual configuration values .PP \fB-f\fP, \fB--file\fP=[] - review only these files (repeatable); combines with the active scope flag + review only these files or globs (e.g. \fB*.go\fR, \fBinternal/**/*.ts\fR; repeatable, one pattern per flag — patterns can't be comma-joined); combines with the active scope flag .PP \fB--json\fP[=false] @@ -70,6 +70,14 @@ Show, get, or set individual configuration values \fB--model\fP="" override configured model +.PP +\fB--no-architecture\fP[=false] + skip architecture-aware review (do not read architecture.json into the prompt) for this run (ADR-0030) + +.PP +\fB--no-baseline\fP[=false] + ignore the signal-control baseline for this run (show everything, even baselined findings) + .PP \fB--no-cache\fP[=false] bypass cache (read and write) @@ -94,6 +102,10 @@ Show, get, or set individual configuration values \fB-q\fP, \fB--quiet\fP[=false] suppress info messages on stderr +.PP +\fB--sandbox-rerun\fP[=0] + confirm flagged flaky tests by re-running each in isolation N times (ADR-0022); mixed pass+fail = confirmed flaky, all-fail = real failure, all-pass = transient. 0 = off; bare --sandbox-rerun uses N=5. Requires a bound rerun executor + .PP \fB--show-prompt\fP[=false] print the exact system + user prompt that would be sent, then exit (no provider call, no cost) @@ -102,6 +114,10 @@ Show, get, or set individual configuration values \fB--suggest-commit\fP[=false] after the review, suggest a Conventional Commit message for the staged diff (requires --staged; prints to stdout; not with --json/--markdown/--output) +.PP +\fB--update-baseline\fP[=false] + rewrite .commitbrief/baseline.json from the current findings (accepts them all) instead of filtering this run; user-private, gitignored (ADR-0027) + .PP \fB-v\fP, \fB--verbose\fP[=false] show token/cost/latency footer @@ -120,4 +136,4 @@ Show, get, or set individual configuration values .SH HISTORY -19-Jun-2026 Auto generated by spf13/cobra +26-Jul-2026 Auto generated by spf13/cobra diff --git a/man/commitbrief-diff.1 b/man/commitbrief-diff.1 index a0552cd..8d9a7a1 100644 --- a/man/commitbrief-diff.1 +++ b/man/commitbrief-diff.1 @@ -1,5 +1,5 @@ .nh -.TH "COMMITBRIEF" "1" "Jun 2026" "Auto generated by spf13/cobra" "" +.TH "COMMITBRIEF" "1" "Jul 2026" "Auto generated by spf13/cobra" "" .SH NAME commitbrief-diff - Run a review against an arbitrary git diff (passthrough) @@ -40,7 +40,7 @@ Review the output of \fBgit diff \fR\&. Arguments are forwarded verbatim t .PP \fB-d\fP, \fB--dir\fP=[] - review only files under these directories (repeatable); combines with the active scope flag + review only files under these directories or matching dir globs (e.g. \fBinternal/**\fR; repeatable, one pattern per flag); combines with the active scope flag .PP \fB--fail-on\fP="" @@ -48,7 +48,7 @@ Review the output of \fBgit diff \fR\&. Arguments are forwarded verbatim t .PP \fB-f\fP, \fB--file\fP=[] - review only these files (repeatable); combines with the active scope flag + review only these files or globs (e.g. \fB*.go\fR, \fBinternal/**/*.ts\fR; repeatable, one pattern per flag — patterns can't be comma-joined); combines with the active scope flag .PP \fB--json\fP[=false] @@ -70,6 +70,14 @@ Review the output of \fBgit diff \fR\&. Arguments are forwarded verbatim t \fB--model\fP="" override configured model +.PP +\fB--no-architecture\fP[=false] + skip architecture-aware review (do not read architecture.json into the prompt) for this run (ADR-0030) + +.PP +\fB--no-baseline\fP[=false] + ignore the signal-control baseline for this run (show everything, even baselined findings) + .PP \fB--no-cache\fP[=false] bypass cache (read and write) @@ -94,6 +102,10 @@ Review the output of \fBgit diff \fR\&. Arguments are forwarded verbatim t \fB-q\fP, \fB--quiet\fP[=false] suppress info messages on stderr +.PP +\fB--sandbox-rerun\fP[=0] + confirm flagged flaky tests by re-running each in isolation N times (ADR-0022); mixed pass+fail = confirmed flaky, all-fail = real failure, all-pass = transient. 0 = off; bare --sandbox-rerun uses N=5. Requires a bound rerun executor + .PP \fB--show-prompt\fP[=false] print the exact system + user prompt that would be sent, then exit (no provider call, no cost) @@ -102,6 +114,10 @@ Review the output of \fBgit diff \fR\&. Arguments are forwarded verbatim t \fB--suggest-commit\fP[=false] after the review, suggest a Conventional Commit message for the staged diff (requires --staged; prints to stdout; not with --json/--markdown/--output) +.PP +\fB--update-baseline\fP[=false] + rewrite .commitbrief/baseline.json from the current findings (accepts them all) instead of filtering this run; user-private, gitignored (ADR-0027) + .PP \fB-v\fP, \fB--verbose\fP[=false] show token/cost/latency footer @@ -120,4 +136,4 @@ Review the output of \fBgit diff \fR\&. Arguments are forwarded verbatim t .SH HISTORY -19-Jun-2026 Auto generated by spf13/cobra +26-Jul-2026 Auto generated by spf13/cobra diff --git a/man/commitbrief-doctor.1 b/man/commitbrief-doctor.1 index 0857288..512578b 100644 --- a/man/commitbrief-doctor.1 +++ b/man/commitbrief-doctor.1 @@ -1,5 +1,5 @@ .nh -.TH "COMMITBRIEF" "1" "Jun 2026" "Auto generated by spf13/cobra" "" +.TH "COMMITBRIEF" "1" "Jul 2026" "Auto generated by spf13/cobra" "" .SH NAME commitbrief-doctor - Run a health check across the configured pipeline @@ -52,7 +52,7 @@ run produces no output. .PP \fB-d\fP, \fB--dir\fP=[] - review only files under these directories (repeatable); combines with the active scope flag + review only files under these directories or matching dir globs (e.g. \fBinternal/**\fR; repeatable, one pattern per flag); combines with the active scope flag .PP \fB--fail-on\fP="" @@ -60,7 +60,7 @@ run produces no output. .PP \fB-f\fP, \fB--file\fP=[] - review only these files (repeatable); combines with the active scope flag + review only these files or globs (e.g. \fB*.go\fR, \fBinternal/**/*.ts\fR; repeatable, one pattern per flag — patterns can't be comma-joined); combines with the active scope flag .PP \fB--json\fP[=false] @@ -82,6 +82,14 @@ run produces no output. \fB--model\fP="" override configured model +.PP +\fB--no-architecture\fP[=false] + skip architecture-aware review (do not read architecture.json into the prompt) for this run (ADR-0030) + +.PP +\fB--no-baseline\fP[=false] + ignore the signal-control baseline for this run (show everything, even baselined findings) + .PP \fB--no-cache\fP[=false] bypass cache (read and write) @@ -102,6 +110,10 @@ run produces no output. \fB--provider\fP="" override configured provider +.PP +\fB--sandbox-rerun\fP[=0] + confirm flagged flaky tests by re-running each in isolation N times (ADR-0022); mixed pass+fail = confirmed flaky, all-fail = real failure, all-pass = transient. 0 = off; bare --sandbox-rerun uses N=5. Requires a bound rerun executor + .PP \fB--show-prompt\fP[=false] print the exact system + user prompt that would be sent, then exit (no provider call, no cost) @@ -110,6 +122,10 @@ run produces no output. \fB--suggest-commit\fP[=false] after the review, suggest a Conventional Commit message for the staged diff (requires --staged; prints to stdout; not with --json/--markdown/--output) +.PP +\fB--update-baseline\fP[=false] + rewrite .commitbrief/baseline.json from the current findings (accepts them all) instead of filtering this run; user-private, gitignored (ADR-0027) + .PP \fB-v\fP, \fB--verbose\fP[=false] show token/cost/latency footer @@ -128,4 +144,4 @@ run produces no output. .SH HISTORY -19-Jun-2026 Auto generated by spf13/cobra +26-Jul-2026 Auto generated by spf13/cobra diff --git a/man/commitbrief-dry-run.1 b/man/commitbrief-dry-run.1 index 27d350e..01a2fcc 100644 --- a/man/commitbrief-dry-run.1 +++ b/man/commitbrief-dry-run.1 @@ -1,5 +1,5 @@ .nh -.TH "COMMITBRIEF" "1" "Jun 2026" "Auto generated by spf13/cobra" "" +.TH "COMMITBRIEF" "1" "Jul 2026" "Auto generated by spf13/cobra" "" .SH NAME commitbrief-dry-run - Build prompt and report what would be sent; no API call @@ -48,7 +48,7 @@ Build prompt and report what would be sent; no API call .PP \fB-d\fP, \fB--dir\fP=[] - review only files under these directories (repeatable); combines with the active scope flag + review only files under these directories or matching dir globs (e.g. \fBinternal/**\fR; repeatable, one pattern per flag); combines with the active scope flag .PP \fB--fail-on\fP="" @@ -56,7 +56,7 @@ Build prompt and report what would be sent; no API call .PP \fB-f\fP, \fB--file\fP=[] - review only these files (repeatable); combines with the active scope flag + review only these files or globs (e.g. \fB*.go\fR, \fBinternal/**/*.ts\fR; repeatable, one pattern per flag — patterns can't be comma-joined); combines with the active scope flag .PP \fB--json\fP[=false] @@ -78,6 +78,14 @@ Build prompt and report what would be sent; no API call \fB--model\fP="" override configured model +.PP +\fB--no-architecture\fP[=false] + skip architecture-aware review (do not read architecture.json into the prompt) for this run (ADR-0030) + +.PP +\fB--no-baseline\fP[=false] + ignore the signal-control baseline for this run (show everything, even baselined findings) + .PP \fB--no-cache\fP[=false] bypass cache (read and write) @@ -102,6 +110,10 @@ Build prompt and report what would be sent; no API call \fB-q\fP, \fB--quiet\fP[=false] suppress info messages on stderr +.PP +\fB--sandbox-rerun\fP[=0] + confirm flagged flaky tests by re-running each in isolation N times (ADR-0022); mixed pass+fail = confirmed flaky, all-fail = real failure, all-pass = transient. 0 = off; bare --sandbox-rerun uses N=5. Requires a bound rerun executor + .PP \fB--show-prompt\fP[=false] print the exact system + user prompt that would be sent, then exit (no provider call, no cost) @@ -110,6 +122,10 @@ Build prompt and report what would be sent; no API call \fB--suggest-commit\fP[=false] after the review, suggest a Conventional Commit message for the staged diff (requires --staged; prints to stdout; not with --json/--markdown/--output) +.PP +\fB--update-baseline\fP[=false] + rewrite .commitbrief/baseline.json from the current findings (accepts them all) instead of filtering this run; user-private, gitignored (ADR-0027) + .PP \fB-v\fP, \fB--verbose\fP[=false] show token/cost/latency footer @@ -128,4 +144,4 @@ Build prompt and report what would be sent; no API call .SH HISTORY -19-Jun-2026 Auto generated by spf13/cobra +26-Jul-2026 Auto generated by spf13/cobra diff --git a/man/commitbrief-guard.1 b/man/commitbrief-guard.1 new file mode 100644 index 0000000..0461d21 --- /dev/null +++ b/man/commitbrief-guard.1 @@ -0,0 +1,158 @@ +.nh +.TH "COMMITBRIEF" "1" "Jul 2026" "Auto generated by spf13/cobra" "" + +.SH NAME +commitbrief-guard - Gate a review against a declarative policy (.commitbrief/policy.yml) + + +.SH SYNOPSIS +\fBcommitbrief guard [flags]\fP + + +.SH DESCRIPTION +Evaluate a review's actionable findings against a declarative policy and exit non-zero when it is breached — a richer, opt-in alternative to --fail-on for gating (often AI-authored) pull requests. + +.PP +The policy (.commitbrief/policy.yml) caps how many findings of each severity a change may carry (plus an optional total cap). Unlike --fail-on (a single threshold), guard enforces a per-severity budget and can consume a prior review's JSON via --from-json without re-running the provider. It evaluates the set that survives baseline + suppression. + + +.SH OPTIONS +\fB--diff\fP=[] + run-mode: review an arbitrary \fBgit diff\fR range (e.g. main...HEAD); repeatable + +.PP +\fB--from-json\fP="" + evaluate a prior schema-v1 review JSON (a file path, or - for stdin) instead of running a review + +.PP +\fB-h\fP, \fB--help\fP[=false] + help for guard + +.PP +\fB--policy\fP=".commitbrief/policy.yml" + path to the policy file + +.PP +\fB--unstaged\fP[=false] + run-mode: review the unstaged working tree instead of the staged index + + +.SH OPTIONS INHERITED FROM PARENT COMMANDS +\fB--allow-secrets\fP[=false] + bypass the pre-send secret scanner (use with care) + +.PP +\fB--cli\fP="" + use a locally-installed CLI tool (claude|gemini|codex) as the review backend; shorthand for --provider -cli + +.PP +\fB--color\fP="auto" + color output: auto, always, never + +.PP +\fB--compact\fP[=false] + one-line per finding (dense review output) + +.PP +\fB--copy\fP[=false] + copy findings (severity, path, title, description) to the system clipboard via OSC 52 + native tool + +.PP +\fB-d\fP, \fB--dir\fP=[] + review only files under these directories or matching dir globs (e.g. \fBinternal/**\fR; repeatable, one pattern per flag); combines with the active scope flag + +.PP +\fB--fail-on\fP="" + exit 1 if any finding meets/exceeds severity (critical|high|medium|low|info|any|none) + +.PP +\fB-f\fP, \fB--file\fP=[] + review only these files or globs (e.g. \fB*.go\fR, \fBinternal/**/*.ts\fR; repeatable, one pattern per flag — patterns can't be comma-joined); combines with the active scope flag + +.PP +\fB--json\fP[=false] + emit machine-readable JSON output + +.PP +\fB--lang\fP="" + AI output language (e.g. tr, fr); the CLI interface localizes for en/tr only, output for any recognized language. Resolution: --lang → repo config → user config → English + +.PP +\fB--markdown\fP[=false] + emit plain markdown (no ANSI) + +.PP +\fB--min-severity\fP="" + hide findings below this severity in the rendered output (critical|high|medium|low|info); --json and --fail-on still see the full set + +.PP +\fB--model\fP="" + override configured model + +.PP +\fB--no-architecture\fP[=false] + skip architecture-aware review (do not read architecture.json into the prompt) for this run (ADR-0030) + +.PP +\fB--no-baseline\fP[=false] + ignore the signal-control baseline for this run (show everything, even baselined findings) + +.PP +\fB--no-cache\fP[=false] + bypass cache (read and write) + +.PP +\fB--no-cost-check\fP[=false] + skip the pre-send cost estimate prompt + +.PP +\fB--no-flaky\fP[=false] + skip the deterministic flaky-test detector (ADR-0022) + +.PP +\fB-o\fP, \fB--output\fP="" + write output to file instead of stdout + +.PP +\fB--provider\fP="" + override configured provider + +.PP +\fB-q\fP, \fB--quiet\fP[=false] + suppress info messages on stderr + +.PP +\fB--sandbox-rerun\fP[=0] + confirm flagged flaky tests by re-running each in isolation N times (ADR-0022); mixed pass+fail = confirmed flaky, all-fail = real failure, all-pass = transient. 0 = off; bare --sandbox-rerun uses N=5. Requires a bound rerun executor + +.PP +\fB--show-prompt\fP[=false] + print the exact system + user prompt that would be sent, then exit (no provider call, no cost) + +.PP +\fB--suggest-commit\fP[=false] + after the review, suggest a Conventional Commit message for the staged diff (requires --staged; prints to stdout; not with --json/--markdown/--output) + +.PP +\fB--update-baseline\fP[=false] + rewrite .commitbrief/baseline.json from the current findings (accepts them all) instead of filtering this run; user-private, gitignored (ADR-0027) + +.PP +\fB-v\fP, \fB--verbose\fP[=false] + show token/cost/latency footer + +.PP +\fB--with-context\fP[=false] + let the CLI provider read project files beyond the diff to ground the review (CLI providers only; the host CLI's agent reads your repo — see --help) + +.PP +\fB-y\fP, \fB--yes\fP[=false] + auto-confirm prompts (pre-send guard, init overwrite) + + +.SH SEE ALSO +\fBcommitbrief(1)\fP + + +.SH HISTORY +26-Jul-2026 Auto generated by spf13/cobra diff --git a/man/commitbrief-init.1 b/man/commitbrief-init.1 index db5a6c5..132c71e 100644 --- a/man/commitbrief-init.1 +++ b/man/commitbrief-init.1 @@ -1,5 +1,5 @@ .nh -.TH "COMMITBRIEF" "1" "Jun 2026" "Auto generated by spf13/cobra" "" +.TH "COMMITBRIEF" "1" "Jul 2026" "Auto generated by spf13/cobra" "" .SH NAME commitbrief-init - Write COMMITBRIEF.md and a per-user OUTPUT.md template @@ -53,7 +53,7 @@ to overwrite the existing file(s) too. .PP \fB-d\fP, \fB--dir\fP=[] - review only files under these directories (repeatable); combines with the active scope flag + review only files under these directories or matching dir globs (e.g. \fBinternal/**\fR; repeatable, one pattern per flag); combines with the active scope flag .PP \fB--fail-on\fP="" @@ -61,7 +61,7 @@ to overwrite the existing file(s) too. .PP \fB-f\fP, \fB--file\fP=[] - review only these files (repeatable); combines with the active scope flag + review only these files or globs (e.g. \fB*.go\fR, \fBinternal/**/*.ts\fR; repeatable, one pattern per flag — patterns can't be comma-joined); combines with the active scope flag .PP \fB--json\fP[=false] @@ -83,6 +83,14 @@ to overwrite the existing file(s) too. \fB--model\fP="" override configured model +.PP +\fB--no-architecture\fP[=false] + skip architecture-aware review (do not read architecture.json into the prompt) for this run (ADR-0030) + +.PP +\fB--no-baseline\fP[=false] + ignore the signal-control baseline for this run (show everything, even baselined findings) + .PP \fB--no-cache\fP[=false] bypass cache (read and write) @@ -107,6 +115,10 @@ to overwrite the existing file(s) too. \fB-q\fP, \fB--quiet\fP[=false] suppress info messages on stderr +.PP +\fB--sandbox-rerun\fP[=0] + confirm flagged flaky tests by re-running each in isolation N times (ADR-0022); mixed pass+fail = confirmed flaky, all-fail = real failure, all-pass = transient. 0 = off; bare --sandbox-rerun uses N=5. Requires a bound rerun executor + .PP \fB--show-prompt\fP[=false] print the exact system + user prompt that would be sent, then exit (no provider call, no cost) @@ -115,6 +127,10 @@ to overwrite the existing file(s) too. \fB--suggest-commit\fP[=false] after the review, suggest a Conventional Commit message for the staged diff (requires --staged; prints to stdout; not with --json/--markdown/--output) +.PP +\fB--update-baseline\fP[=false] + rewrite .commitbrief/baseline.json from the current findings (accepts them all) instead of filtering this run; user-private, gitignored (ADR-0027) + .PP \fB-v\fP, \fB--verbose\fP[=false] show token/cost/latency footer @@ -133,4 +149,4 @@ to overwrite the existing file(s) too. .SH HISTORY -19-Jun-2026 Auto generated by spf13/cobra +26-Jul-2026 Auto generated by spf13/cobra diff --git a/man/commitbrief-install-hook.1 b/man/commitbrief-install-hook.1 index 31777ef..6de5d9d 100644 --- a/man/commitbrief-install-hook.1 +++ b/man/commitbrief-install-hook.1 @@ -1,5 +1,5 @@ .nh -.TH "COMMITBRIEF" "1" "Jun 2026" "Auto generated by spf13/cobra" "" +.TH "COMMITBRIEF" "1" "Jul 2026" "Auto generated by spf13/cobra" "" .SH NAME commitbrief-install-hook - Install (or uninstall) a git hook that runs commitbrief on commit @@ -72,7 +72,7 @@ comment). Refuses to touch a hook that doesn't carry our marker. .PP \fB-d\fP, \fB--dir\fP=[] - review only files under these directories (repeatable); combines with the active scope flag + review only files under these directories or matching dir globs (e.g. \fBinternal/**\fR; repeatable, one pattern per flag); combines with the active scope flag .PP \fB--fail-on\fP="" @@ -80,7 +80,7 @@ comment). Refuses to touch a hook that doesn't carry our marker. .PP \fB-f\fP, \fB--file\fP=[] - review only these files (repeatable); combines with the active scope flag + review only these files or globs (e.g. \fB*.go\fR, \fBinternal/**/*.ts\fR; repeatable, one pattern per flag — patterns can't be comma-joined); combines with the active scope flag .PP \fB--json\fP[=false] @@ -102,6 +102,14 @@ comment). Refuses to touch a hook that doesn't carry our marker. \fB--model\fP="" override configured model +.PP +\fB--no-architecture\fP[=false] + skip architecture-aware review (do not read architecture.json into the prompt) for this run (ADR-0030) + +.PP +\fB--no-baseline\fP[=false] + ignore the signal-control baseline for this run (show everything, even baselined findings) + .PP \fB--no-cache\fP[=false] bypass cache (read and write) @@ -126,6 +134,10 @@ comment). Refuses to touch a hook that doesn't carry our marker. \fB-q\fP, \fB--quiet\fP[=false] suppress info messages on stderr +.PP +\fB--sandbox-rerun\fP[=0] + confirm flagged flaky tests by re-running each in isolation N times (ADR-0022); mixed pass+fail = confirmed flaky, all-fail = real failure, all-pass = transient. 0 = off; bare --sandbox-rerun uses N=5. Requires a bound rerun executor + .PP \fB--show-prompt\fP[=false] print the exact system + user prompt that would be sent, then exit (no provider call, no cost) @@ -134,6 +146,10 @@ comment). Refuses to touch a hook that doesn't carry our marker. \fB--suggest-commit\fP[=false] after the review, suggest a Conventional Commit message for the staged diff (requires --staged; prints to stdout; not with --json/--markdown/--output) +.PP +\fB--update-baseline\fP[=false] + rewrite .commitbrief/baseline.json from the current findings (accepts them all) instead of filtering this run; user-private, gitignored (ADR-0027) + .PP \fB-v\fP, \fB--verbose\fP[=false] show token/cost/latency footer @@ -152,4 +168,4 @@ comment). Refuses to touch a hook that doesn't carry our marker. .SH HISTORY -19-Jun-2026 Auto generated by spf13/cobra +26-Jul-2026 Auto generated by spf13/cobra diff --git a/man/commitbrief-list.1 b/man/commitbrief-list.1 index 1413ceb..366a798 100644 --- a/man/commitbrief-list.1 +++ b/man/commitbrief-list.1 @@ -1,5 +1,5 @@ .nh -.TH "COMMITBRIEF" "1" "Jun 2026" "Auto generated by spf13/cobra" "" +.TH "COMMITBRIEF" "1" "Jul 2026" "Auto generated by spf13/cobra" "" .SH NAME commitbrief-list - Print the command reference @@ -40,7 +40,7 @@ Print the command reference .PP \fB-d\fP, \fB--dir\fP=[] - review only files under these directories (repeatable); combines with the active scope flag + review only files under these directories or matching dir globs (e.g. \fBinternal/**\fR; repeatable, one pattern per flag); combines with the active scope flag .PP \fB--fail-on\fP="" @@ -48,7 +48,7 @@ Print the command reference .PP \fB-f\fP, \fB--file\fP=[] - review only these files (repeatable); combines with the active scope flag + review only these files or globs (e.g. \fB*.go\fR, \fBinternal/**/*.ts\fR; repeatable, one pattern per flag — patterns can't be comma-joined); combines with the active scope flag .PP \fB--json\fP[=false] @@ -70,6 +70,14 @@ Print the command reference \fB--model\fP="" override configured model +.PP +\fB--no-architecture\fP[=false] + skip architecture-aware review (do not read architecture.json into the prompt) for this run (ADR-0030) + +.PP +\fB--no-baseline\fP[=false] + ignore the signal-control baseline for this run (show everything, even baselined findings) + .PP \fB--no-cache\fP[=false] bypass cache (read and write) @@ -94,6 +102,10 @@ Print the command reference \fB-q\fP, \fB--quiet\fP[=false] suppress info messages on stderr +.PP +\fB--sandbox-rerun\fP[=0] + confirm flagged flaky tests by re-running each in isolation N times (ADR-0022); mixed pass+fail = confirmed flaky, all-fail = real failure, all-pass = transient. 0 = off; bare --sandbox-rerun uses N=5. Requires a bound rerun executor + .PP \fB--show-prompt\fP[=false] print the exact system + user prompt that would be sent, then exit (no provider call, no cost) @@ -102,6 +114,10 @@ Print the command reference \fB--suggest-commit\fP[=false] after the review, suggest a Conventional Commit message for the staged diff (requires --staged; prints to stdout; not with --json/--markdown/--output) +.PP +\fB--update-baseline\fP[=false] + rewrite .commitbrief/baseline.json from the current findings (accepts them all) instead of filtering this run; user-private, gitignored (ADR-0027) + .PP \fB-v\fP, \fB--verbose\fP[=false] show token/cost/latency footer @@ -120,4 +136,4 @@ Print the command reference .SH HISTORY -19-Jun-2026 Auto generated by spf13/cobra +26-Jul-2026 Auto generated by spf13/cobra diff --git a/man/commitbrief-mcp.1 b/man/commitbrief-mcp.1 new file mode 100644 index 0000000..cc972f9 --- /dev/null +++ b/man/commitbrief-mcp.1 @@ -0,0 +1,142 @@ +.nh +.TH "COMMITBRIEF" "1" "Jul 2026" "Auto generated by spf13/cobra" "" + +.SH NAME +commitbrief-mcp - Run an MCP (Model Context Protocol) server over stdio + + +.SH SYNOPSIS +\fBcommitbrief mcp [flags]\fP + + +.SH DESCRIPTION +Expose CommitBrief to an AI agent/host as an MCP tool. The server speaks JSON-RPC 2.0 over stdio (the MCP stdio transport) and offers a \fBreview\fR tool that runs the standard review pipeline on the current repo's diff and returns the structured findings (JSON schema v1). + +.PP +Wire it into a host (e.g. Claude Desktop / an agent runtime) as a stdio MCP server invoking \fBcommitbrief mcp\fR\&. The host owns the lifecycle; this process reads requests on stdin and writes responses on stdout until the host closes the stream. Diagnostics are written to stderr. + + +.SH OPTIONS +\fB-h\fP, \fB--help\fP[=false] + help for mcp + + +.SH OPTIONS INHERITED FROM PARENT COMMANDS +\fB--allow-secrets\fP[=false] + bypass the pre-send secret scanner (use with care) + +.PP +\fB--cli\fP="" + use a locally-installed CLI tool (claude|gemini|codex) as the review backend; shorthand for --provider -cli + +.PP +\fB--color\fP="auto" + color output: auto, always, never + +.PP +\fB--compact\fP[=false] + one-line per finding (dense review output) + +.PP +\fB--copy\fP[=false] + copy findings (severity, path, title, description) to the system clipboard via OSC 52 + native tool + +.PP +\fB-d\fP, \fB--dir\fP=[] + review only files under these directories or matching dir globs (e.g. \fBinternal/**\fR; repeatable, one pattern per flag); combines with the active scope flag + +.PP +\fB--fail-on\fP="" + exit 1 if any finding meets/exceeds severity (critical|high|medium|low|info|any|none) + +.PP +\fB-f\fP, \fB--file\fP=[] + review only these files or globs (e.g. \fB*.go\fR, \fBinternal/**/*.ts\fR; repeatable, one pattern per flag — patterns can't be comma-joined); combines with the active scope flag + +.PP +\fB--json\fP[=false] + emit machine-readable JSON output + +.PP +\fB--lang\fP="" + AI output language (e.g. tr, fr); the CLI interface localizes for en/tr only, output for any recognized language. Resolution: --lang → repo config → user config → English + +.PP +\fB--markdown\fP[=false] + emit plain markdown (no ANSI) + +.PP +\fB--min-severity\fP="" + hide findings below this severity in the rendered output (critical|high|medium|low|info); --json and --fail-on still see the full set + +.PP +\fB--model\fP="" + override configured model + +.PP +\fB--no-architecture\fP[=false] + skip architecture-aware review (do not read architecture.json into the prompt) for this run (ADR-0030) + +.PP +\fB--no-baseline\fP[=false] + ignore the signal-control baseline for this run (show everything, even baselined findings) + +.PP +\fB--no-cache\fP[=false] + bypass cache (read and write) + +.PP +\fB--no-cost-check\fP[=false] + skip the pre-send cost estimate prompt + +.PP +\fB--no-flaky\fP[=false] + skip the deterministic flaky-test detector (ADR-0022) + +.PP +\fB-o\fP, \fB--output\fP="" + write output to file instead of stdout + +.PP +\fB--provider\fP="" + override configured provider + +.PP +\fB-q\fP, \fB--quiet\fP[=false] + suppress info messages on stderr + +.PP +\fB--sandbox-rerun\fP[=0] + confirm flagged flaky tests by re-running each in isolation N times (ADR-0022); mixed pass+fail = confirmed flaky, all-fail = real failure, all-pass = transient. 0 = off; bare --sandbox-rerun uses N=5. Requires a bound rerun executor + +.PP +\fB--show-prompt\fP[=false] + print the exact system + user prompt that would be sent, then exit (no provider call, no cost) + +.PP +\fB--suggest-commit\fP[=false] + after the review, suggest a Conventional Commit message for the staged diff (requires --staged; prints to stdout; not with --json/--markdown/--output) + +.PP +\fB--update-baseline\fP[=false] + rewrite .commitbrief/baseline.json from the current findings (accepts them all) instead of filtering this run; user-private, gitignored (ADR-0027) + +.PP +\fB-v\fP, \fB--verbose\fP[=false] + show token/cost/latency footer + +.PP +\fB--with-context\fP[=false] + let the CLI provider read project files beyond the diff to ground the review (CLI providers only; the host CLI's agent reads your repo — see --help) + +.PP +\fB-y\fP, \fB--yes\fP[=false] + auto-confirm prompts (pre-send guard, init overwrite) + + +.SH SEE ALSO +\fBcommitbrief(1)\fP + + +.SH HISTORY +26-Jul-2026 Auto generated by spf13/cobra diff --git a/man/commitbrief-providers-list.1 b/man/commitbrief-providers-list.1 index 65bca9a..a16a4f1 100644 --- a/man/commitbrief-providers-list.1 +++ b/man/commitbrief-providers-list.1 @@ -1,5 +1,5 @@ .nh -.TH "COMMITBRIEF" "1" "Jun 2026" "Auto generated by spf13/cobra" "" +.TH "COMMITBRIEF" "1" "Jul 2026" "Auto generated by spf13/cobra" "" .SH NAME commitbrief-providers-list - Show configured providers (active marker, model, API key status) @@ -40,7 +40,7 @@ Show configured providers (active marker, model, API key status) .PP \fB-d\fP, \fB--dir\fP=[] - review only files under these directories (repeatable); combines with the active scope flag + review only files under these directories or matching dir globs (e.g. \fBinternal/**\fR; repeatable, one pattern per flag); combines with the active scope flag .PP \fB--fail-on\fP="" @@ -48,7 +48,7 @@ Show configured providers (active marker, model, API key status) .PP \fB-f\fP, \fB--file\fP=[] - review only these files (repeatable); combines with the active scope flag + review only these files or globs (e.g. \fB*.go\fR, \fBinternal/**/*.ts\fR; repeatable, one pattern per flag — patterns can't be comma-joined); combines with the active scope flag .PP \fB--json\fP[=false] @@ -70,6 +70,14 @@ Show configured providers (active marker, model, API key status) \fB--model\fP="" override configured model +.PP +\fB--no-architecture\fP[=false] + skip architecture-aware review (do not read architecture.json into the prompt) for this run (ADR-0030) + +.PP +\fB--no-baseline\fP[=false] + ignore the signal-control baseline for this run (show everything, even baselined findings) + .PP \fB--no-cache\fP[=false] bypass cache (read and write) @@ -94,6 +102,10 @@ Show configured providers (active marker, model, API key status) \fB-q\fP, \fB--quiet\fP[=false] suppress info messages on stderr +.PP +\fB--sandbox-rerun\fP[=0] + confirm flagged flaky tests by re-running each in isolation N times (ADR-0022); mixed pass+fail = confirmed flaky, all-fail = real failure, all-pass = transient. 0 = off; bare --sandbox-rerun uses N=5. Requires a bound rerun executor + .PP \fB--show-prompt\fP[=false] print the exact system + user prompt that would be sent, then exit (no provider call, no cost) @@ -102,6 +114,10 @@ Show configured providers (active marker, model, API key status) \fB--suggest-commit\fP[=false] after the review, suggest a Conventional Commit message for the staged diff (requires --staged; prints to stdout; not with --json/--markdown/--output) +.PP +\fB--update-baseline\fP[=false] + rewrite .commitbrief/baseline.json from the current findings (accepts them all) instead of filtering this run; user-private, gitignored (ADR-0027) + .PP \fB-v\fP, \fB--verbose\fP[=false] show token/cost/latency footer @@ -120,4 +136,4 @@ Show configured providers (active marker, model, API key status) .SH HISTORY -19-Jun-2026 Auto generated by spf13/cobra +26-Jul-2026 Auto generated by spf13/cobra diff --git a/man/commitbrief-providers-test.1 b/man/commitbrief-providers-test.1 index 63d0c6b..6b9a9d3 100644 --- a/man/commitbrief-providers-test.1 +++ b/man/commitbrief-providers-test.1 @@ -1,5 +1,5 @@ .nh -.TH "COMMITBRIEF" "1" "Jun 2026" "Auto generated by spf13/cobra" "" +.TH "COMMITBRIEF" "1" "Jul 2026" "Auto generated by spf13/cobra" "" .SH NAME commitbrief-providers-test - Ping a configured provider to verify the API key and reachability @@ -40,7 +40,7 @@ Ping a configured provider to verify the API key and reachability .PP \fB-d\fP, \fB--dir\fP=[] - review only files under these directories (repeatable); combines with the active scope flag + review only files under these directories or matching dir globs (e.g. \fBinternal/**\fR; repeatable, one pattern per flag); combines with the active scope flag .PP \fB--fail-on\fP="" @@ -48,7 +48,7 @@ Ping a configured provider to verify the API key and reachability .PP \fB-f\fP, \fB--file\fP=[] - review only these files (repeatable); combines with the active scope flag + review only these files or globs (e.g. \fB*.go\fR, \fBinternal/**/*.ts\fR; repeatable, one pattern per flag — patterns can't be comma-joined); combines with the active scope flag .PP \fB--json\fP[=false] @@ -70,6 +70,14 @@ Ping a configured provider to verify the API key and reachability \fB--model\fP="" override configured model +.PP +\fB--no-architecture\fP[=false] + skip architecture-aware review (do not read architecture.json into the prompt) for this run (ADR-0030) + +.PP +\fB--no-baseline\fP[=false] + ignore the signal-control baseline for this run (show everything, even baselined findings) + .PP \fB--no-cache\fP[=false] bypass cache (read and write) @@ -94,6 +102,10 @@ Ping a configured provider to verify the API key and reachability \fB-q\fP, \fB--quiet\fP[=false] suppress info messages on stderr +.PP +\fB--sandbox-rerun\fP[=0] + confirm flagged flaky tests by re-running each in isolation N times (ADR-0022); mixed pass+fail = confirmed flaky, all-fail = real failure, all-pass = transient. 0 = off; bare --sandbox-rerun uses N=5. Requires a bound rerun executor + .PP \fB--show-prompt\fP[=false] print the exact system + user prompt that would be sent, then exit (no provider call, no cost) @@ -102,6 +114,10 @@ Ping a configured provider to verify the API key and reachability \fB--suggest-commit\fP[=false] after the review, suggest a Conventional Commit message for the staged diff (requires --staged; prints to stdout; not with --json/--markdown/--output) +.PP +\fB--update-baseline\fP[=false] + rewrite .commitbrief/baseline.json from the current findings (accepts them all) instead of filtering this run; user-private, gitignored (ADR-0027) + .PP \fB-v\fP, \fB--verbose\fP[=false] show token/cost/latency footer @@ -120,4 +136,4 @@ Ping a configured provider to verify the API key and reachability .SH HISTORY -19-Jun-2026 Auto generated by spf13/cobra +26-Jul-2026 Auto generated by spf13/cobra diff --git a/man/commitbrief-providers-use.1 b/man/commitbrief-providers-use.1 index df1eb16..aa9c59e 100644 --- a/man/commitbrief-providers-use.1 +++ b/man/commitbrief-providers-use.1 @@ -1,5 +1,5 @@ .nh -.TH "COMMITBRIEF" "1" "Jun 2026" "Auto generated by spf13/cobra" "" +.TH "COMMITBRIEF" "1" "Jul 2026" "Auto generated by spf13/cobra" "" .SH NAME commitbrief-providers-use - Switch the active default provider (no API keys changed) @@ -44,7 +44,7 @@ Switch the active default provider (no API keys changed) .PP \fB-d\fP, \fB--dir\fP=[] - review only files under these directories (repeatable); combines with the active scope flag + review only files under these directories or matching dir globs (e.g. \fBinternal/**\fR; repeatable, one pattern per flag); combines with the active scope flag .PP \fB--fail-on\fP="" @@ -52,7 +52,7 @@ Switch the active default provider (no API keys changed) .PP \fB-f\fP, \fB--file\fP=[] - review only these files (repeatable); combines with the active scope flag + review only these files or globs (e.g. \fB*.go\fR, \fBinternal/**/*.ts\fR; repeatable, one pattern per flag — patterns can't be comma-joined); combines with the active scope flag .PP \fB--json\fP[=false] @@ -74,6 +74,14 @@ Switch the active default provider (no API keys changed) \fB--model\fP="" override configured model +.PP +\fB--no-architecture\fP[=false] + skip architecture-aware review (do not read architecture.json into the prompt) for this run (ADR-0030) + +.PP +\fB--no-baseline\fP[=false] + ignore the signal-control baseline for this run (show everything, even baselined findings) + .PP \fB--no-cache\fP[=false] bypass cache (read and write) @@ -98,6 +106,10 @@ Switch the active default provider (no API keys changed) \fB-q\fP, \fB--quiet\fP[=false] suppress info messages on stderr +.PP +\fB--sandbox-rerun\fP[=0] + confirm flagged flaky tests by re-running each in isolation N times (ADR-0022); mixed pass+fail = confirmed flaky, all-fail = real failure, all-pass = transient. 0 = off; bare --sandbox-rerun uses N=5. Requires a bound rerun executor + .PP \fB--show-prompt\fP[=false] print the exact system + user prompt that would be sent, then exit (no provider call, no cost) @@ -106,6 +118,10 @@ Switch the active default provider (no API keys changed) \fB--suggest-commit\fP[=false] after the review, suggest a Conventional Commit message for the staged diff (requires --staged; prints to stdout; not with --json/--markdown/--output) +.PP +\fB--update-baseline\fP[=false] + rewrite .commitbrief/baseline.json from the current findings (accepts them all) instead of filtering this run; user-private, gitignored (ADR-0027) + .PP \fB-v\fP, \fB--verbose\fP[=false] show token/cost/latency footer @@ -124,4 +140,4 @@ Switch the active default provider (no API keys changed) .SH HISTORY -19-Jun-2026 Auto generated by spf13/cobra +26-Jul-2026 Auto generated by spf13/cobra diff --git a/man/commitbrief-providers.1 b/man/commitbrief-providers.1 index efe7137..3f6e801 100644 --- a/man/commitbrief-providers.1 +++ b/man/commitbrief-providers.1 @@ -1,5 +1,5 @@ .nh -.TH "COMMITBRIEF" "1" "Jun 2026" "Auto generated by spf13/cobra" "" +.TH "COMMITBRIEF" "1" "Jul 2026" "Auto generated by spf13/cobra" "" .SH NAME commitbrief-providers - List, switch, and test configured LLM providers @@ -40,7 +40,7 @@ List, switch, and test configured LLM providers .PP \fB-d\fP, \fB--dir\fP=[] - review only files under these directories (repeatable); combines with the active scope flag + review only files under these directories or matching dir globs (e.g. \fBinternal/**\fR; repeatable, one pattern per flag); combines with the active scope flag .PP \fB--fail-on\fP="" @@ -48,7 +48,7 @@ List, switch, and test configured LLM providers .PP \fB-f\fP, \fB--file\fP=[] - review only these files (repeatable); combines with the active scope flag + review only these files or globs (e.g. \fB*.go\fR, \fBinternal/**/*.ts\fR; repeatable, one pattern per flag — patterns can't be comma-joined); combines with the active scope flag .PP \fB--json\fP[=false] @@ -70,6 +70,14 @@ List, switch, and test configured LLM providers \fB--model\fP="" override configured model +.PP +\fB--no-architecture\fP[=false] + skip architecture-aware review (do not read architecture.json into the prompt) for this run (ADR-0030) + +.PP +\fB--no-baseline\fP[=false] + ignore the signal-control baseline for this run (show everything, even baselined findings) + .PP \fB--no-cache\fP[=false] bypass cache (read and write) @@ -94,6 +102,10 @@ List, switch, and test configured LLM providers \fB-q\fP, \fB--quiet\fP[=false] suppress info messages on stderr +.PP +\fB--sandbox-rerun\fP[=0] + confirm flagged flaky tests by re-running each in isolation N times (ADR-0022); mixed pass+fail = confirmed flaky, all-fail = real failure, all-pass = transient. 0 = off; bare --sandbox-rerun uses N=5. Requires a bound rerun executor + .PP \fB--show-prompt\fP[=false] print the exact system + user prompt that would be sent, then exit (no provider call, no cost) @@ -102,6 +114,10 @@ List, switch, and test configured LLM providers \fB--suggest-commit\fP[=false] after the review, suggest a Conventional Commit message for the staged diff (requires --staged; prints to stdout; not with --json/--markdown/--output) +.PP +\fB--update-baseline\fP[=false] + rewrite .commitbrief/baseline.json from the current findings (accepts them all) instead of filtering this run; user-private, gitignored (ADR-0027) + .PP \fB-v\fP, \fB--verbose\fP[=false] show token/cost/latency footer @@ -120,4 +136,4 @@ List, switch, and test configured LLM providers .SH HISTORY -19-Jun-2026 Auto generated by spf13/cobra +26-Jul-2026 Auto generated by spf13/cobra diff --git a/man/commitbrief-remote-pr.1 b/man/commitbrief-remote-pr.1 index 4203934..8e8c8b1 100644 --- a/man/commitbrief-remote-pr.1 +++ b/man/commitbrief-remote-pr.1 @@ -1,5 +1,5 @@ .nh -.TH "COMMITBRIEF" "1" "Jun 2026" "Auto generated by spf13/cobra" "" +.TH "COMMITBRIEF" "1" "Jul 2026" "Auto generated by spf13/cobra" "" .SH NAME commitbrief-remote-pr - Review a GitHub pull request and post findings as inline comments @@ -57,7 +57,7 @@ See ADR-0016. .PP \fB-d\fP, \fB--dir\fP=[] - review only files under these directories (repeatable); combines with the active scope flag + review only files under these directories or matching dir globs (e.g. \fBinternal/**\fR; repeatable, one pattern per flag); combines with the active scope flag .PP \fB--fail-on\fP="" @@ -65,7 +65,7 @@ See ADR-0016. .PP \fB-f\fP, \fB--file\fP=[] - review only these files (repeatable); combines with the active scope flag + review only these files or globs (e.g. \fB*.go\fR, \fBinternal/**/*.ts\fR; repeatable, one pattern per flag — patterns can't be comma-joined); combines with the active scope flag .PP \fB--json\fP[=false] @@ -87,6 +87,14 @@ See ADR-0016. \fB--model\fP="" override configured model +.PP +\fB--no-architecture\fP[=false] + skip architecture-aware review (do not read architecture.json into the prompt) for this run (ADR-0030) + +.PP +\fB--no-baseline\fP[=false] + ignore the signal-control baseline for this run (show everything, even baselined findings) + .PP \fB--no-cache\fP[=false] bypass cache (read and write) @@ -111,6 +119,10 @@ See ADR-0016. \fB-q\fP, \fB--quiet\fP[=false] suppress info messages on stderr +.PP +\fB--sandbox-rerun\fP[=0] + confirm flagged flaky tests by re-running each in isolation N times (ADR-0022); mixed pass+fail = confirmed flaky, all-fail = real failure, all-pass = transient. 0 = off; bare --sandbox-rerun uses N=5. Requires a bound rerun executor + .PP \fB--show-prompt\fP[=false] print the exact system + user prompt that would be sent, then exit (no provider call, no cost) @@ -119,6 +131,10 @@ See ADR-0016. \fB--suggest-commit\fP[=false] after the review, suggest a Conventional Commit message for the staged diff (requires --staged; prints to stdout; not with --json/--markdown/--output) +.PP +\fB--update-baseline\fP[=false] + rewrite .commitbrief/baseline.json from the current findings (accepts them all) instead of filtering this run; user-private, gitignored (ADR-0027) + .PP \fB-v\fP, \fB--verbose\fP[=false] show token/cost/latency footer @@ -137,4 +153,4 @@ See ADR-0016. .SH HISTORY -19-Jun-2026 Auto generated by spf13/cobra +26-Jul-2026 Auto generated by spf13/cobra diff --git a/man/commitbrief-remote.1 b/man/commitbrief-remote.1 index 82d8b13..72e1e5b 100644 --- a/man/commitbrief-remote.1 +++ b/man/commitbrief-remote.1 @@ -1,5 +1,5 @@ .nh -.TH "COMMITBRIEF" "1" "Jun 2026" "Auto generated by spf13/cobra" "" +.TH "COMMITBRIEF" "1" "Jul 2026" "Auto generated by spf13/cobra" "" .SH NAME commitbrief-remote - Drive GitHub operations (PR review) through the gh CLI @@ -44,7 +44,7 @@ they don't produce structured findings). .PP \fB-d\fP, \fB--dir\fP=[] - review only files under these directories (repeatable); combines with the active scope flag + review only files under these directories or matching dir globs (e.g. \fBinternal/**\fR; repeatable, one pattern per flag); combines with the active scope flag .PP \fB--fail-on\fP="" @@ -52,7 +52,7 @@ they don't produce structured findings). .PP \fB-f\fP, \fB--file\fP=[] - review only these files (repeatable); combines with the active scope flag + review only these files or globs (e.g. \fB*.go\fR, \fBinternal/**/*.ts\fR; repeatable, one pattern per flag — patterns can't be comma-joined); combines with the active scope flag .PP \fB--json\fP[=false] @@ -74,6 +74,14 @@ they don't produce structured findings). \fB--model\fP="" override configured model +.PP +\fB--no-architecture\fP[=false] + skip architecture-aware review (do not read architecture.json into the prompt) for this run (ADR-0030) + +.PP +\fB--no-baseline\fP[=false] + ignore the signal-control baseline for this run (show everything, even baselined findings) + .PP \fB--no-cache\fP[=false] bypass cache (read and write) @@ -98,6 +106,10 @@ they don't produce structured findings). \fB-q\fP, \fB--quiet\fP[=false] suppress info messages on stderr +.PP +\fB--sandbox-rerun\fP[=0] + confirm flagged flaky tests by re-running each in isolation N times (ADR-0022); mixed pass+fail = confirmed flaky, all-fail = real failure, all-pass = transient. 0 = off; bare --sandbox-rerun uses N=5. Requires a bound rerun executor + .PP \fB--show-prompt\fP[=false] print the exact system + user prompt that would be sent, then exit (no provider call, no cost) @@ -106,6 +118,10 @@ they don't produce structured findings). \fB--suggest-commit\fP[=false] after the review, suggest a Conventional Commit message for the staged diff (requires --staged; prints to stdout; not with --json/--markdown/--output) +.PP +\fB--update-baseline\fP[=false] + rewrite .commitbrief/baseline.json from the current findings (accepts them all) instead of filtering this run; user-private, gitignored (ADR-0027) + .PP \fB-v\fP, \fB--verbose\fP[=false] show token/cost/latency footer @@ -124,4 +140,4 @@ they don't produce structured findings). .SH HISTORY -19-Jun-2026 Auto generated by spf13/cobra +26-Jul-2026 Auto generated by spf13/cobra diff --git a/man/commitbrief-setup.1 b/man/commitbrief-setup.1 index d8bee8f..2f64a00 100644 --- a/man/commitbrief-setup.1 +++ b/man/commitbrief-setup.1 @@ -1,5 +1,5 @@ .nh -.TH "COMMITBRIEF" "1" "Jun 2026" "Auto generated by spf13/cobra" "" +.TH "COMMITBRIEF" "1" "Jul 2026" "Auto generated by spf13/cobra" "" .SH NAME commitbrief-setup - Interactive provider + API key wizard @@ -62,7 +62,7 @@ the chosen name already shadows a command on your PATH you are warned first. .PP \fB-d\fP, \fB--dir\fP=[] - review only files under these directories (repeatable); combines with the active scope flag + review only files under these directories or matching dir globs (e.g. \fBinternal/**\fR; repeatable, one pattern per flag); combines with the active scope flag .PP \fB--fail-on\fP="" @@ -70,7 +70,7 @@ the chosen name already shadows a command on your PATH you are warned first. .PP \fB-f\fP, \fB--file\fP=[] - review only these files (repeatable); combines with the active scope flag + review only these files or globs (e.g. \fB*.go\fR, \fBinternal/**/*.ts\fR; repeatable, one pattern per flag — patterns can't be comma-joined); combines with the active scope flag .PP \fB--json\fP[=false] @@ -92,6 +92,14 @@ the chosen name already shadows a command on your PATH you are warned first. \fB--model\fP="" override configured model +.PP +\fB--no-architecture\fP[=false] + skip architecture-aware review (do not read architecture.json into the prompt) for this run (ADR-0030) + +.PP +\fB--no-baseline\fP[=false] + ignore the signal-control baseline for this run (show everything, even baselined findings) + .PP \fB--no-cache\fP[=false] bypass cache (read and write) @@ -116,6 +124,10 @@ the chosen name already shadows a command on your PATH you are warned first. \fB-q\fP, \fB--quiet\fP[=false] suppress info messages on stderr +.PP +\fB--sandbox-rerun\fP[=0] + confirm flagged flaky tests by re-running each in isolation N times (ADR-0022); mixed pass+fail = confirmed flaky, all-fail = real failure, all-pass = transient. 0 = off; bare --sandbox-rerun uses N=5. Requires a bound rerun executor + .PP \fB--show-prompt\fP[=false] print the exact system + user prompt that would be sent, then exit (no provider call, no cost) @@ -124,6 +136,10 @@ the chosen name already shadows a command on your PATH you are warned first. \fB--suggest-commit\fP[=false] after the review, suggest a Conventional Commit message for the staged diff (requires --staged; prints to stdout; not with --json/--markdown/--output) +.PP +\fB--update-baseline\fP[=false] + rewrite .commitbrief/baseline.json from the current findings (accepts them all) instead of filtering this run; user-private, gitignored (ADR-0027) + .PP \fB-v\fP, \fB--verbose\fP[=false] show token/cost/latency footer @@ -142,4 +158,4 @@ the chosen name already shadows a command on your PATH you are warned first. .SH HISTORY -19-Jun-2026 Auto generated by spf13/cobra +26-Jul-2026 Auto generated by spf13/cobra diff --git a/man/commitbrief-summary.1 b/man/commitbrief-summary.1 index 0957cb1..fecdd7c 100644 --- a/man/commitbrief-summary.1 +++ b/man/commitbrief-summary.1 @@ -1,5 +1,5 @@ .nh -.TH "COMMITBRIEF" "1" "Jun 2026" "Auto generated by spf13/cobra" "" +.TH "COMMITBRIEF" "1" "Jul 2026" "Auto generated by spf13/cobra" "" .SH NAME commitbrief-summary - Summarize a set of changes in plain language (read-only) @@ -54,7 +54,7 @@ Output is plain text; use -o/--output to write it to a file. The pre-send guard, .PP \fB-d\fP, \fB--dir\fP=[] - review only files under these directories (repeatable); combines with the active scope flag + review only files under these directories or matching dir globs (e.g. \fBinternal/**\fR; repeatable, one pattern per flag); combines with the active scope flag .PP \fB--fail-on\fP="" @@ -62,7 +62,7 @@ Output is plain text; use -o/--output to write it to a file. The pre-send guard, .PP \fB-f\fP, \fB--file\fP=[] - review only these files (repeatable); combines with the active scope flag + review only these files or globs (e.g. \fB*.go\fR, \fBinternal/**/*.ts\fR; repeatable, one pattern per flag — patterns can't be comma-joined); combines with the active scope flag .PP \fB--json\fP[=false] @@ -84,6 +84,14 @@ Output is plain text; use -o/--output to write it to a file. The pre-send guard, \fB--model\fP="" override configured model +.PP +\fB--no-architecture\fP[=false] + skip architecture-aware review (do not read architecture.json into the prompt) for this run (ADR-0030) + +.PP +\fB--no-baseline\fP[=false] + ignore the signal-control baseline for this run (show everything, even baselined findings) + .PP \fB--no-cache\fP[=false] bypass cache (read and write) @@ -108,6 +116,10 @@ Output is plain text; use -o/--output to write it to a file. The pre-send guard, \fB-q\fP, \fB--quiet\fP[=false] suppress info messages on stderr +.PP +\fB--sandbox-rerun\fP[=0] + confirm flagged flaky tests by re-running each in isolation N times (ADR-0022); mixed pass+fail = confirmed flaky, all-fail = real failure, all-pass = transient. 0 = off; bare --sandbox-rerun uses N=5. Requires a bound rerun executor + .PP \fB--show-prompt\fP[=false] print the exact system + user prompt that would be sent, then exit (no provider call, no cost) @@ -116,6 +128,10 @@ Output is plain text; use -o/--output to write it to a file. The pre-send guard, \fB--suggest-commit\fP[=false] after the review, suggest a Conventional Commit message for the staged diff (requires --staged; prints to stdout; not with --json/--markdown/--output) +.PP +\fB--update-baseline\fP[=false] + rewrite .commitbrief/baseline.json from the current findings (accepts them all) instead of filtering this run; user-private, gitignored (ADR-0027) + .PP \fB-v\fP, \fB--verbose\fP[=false] show token/cost/latency footer @@ -134,4 +150,4 @@ Output is plain text; use -o/--output to write it to a file. The pre-send guard, .SH HISTORY -19-Jun-2026 Auto generated by spf13/cobra +26-Jul-2026 Auto generated by spf13/cobra diff --git a/man/commitbrief-upgrade.1 b/man/commitbrief-upgrade.1 new file mode 100644 index 0000000..f46e290 --- /dev/null +++ b/man/commitbrief-upgrade.1 @@ -0,0 +1,163 @@ +.nh +.TH "COMMITBRIEF" "1" "Jul 2026" "Auto generated by spf13/cobra" "" + +.SH NAME +commitbrief-upgrade - Check for a newer CommitBrief and install it + + +.SH SYNOPSIS +\fBcommitbrief upgrade [flags]\fP + + +.SH DESCRIPTION +Checks GitHub Releases for a newer CommitBrief and installs it. + +.PP +How the binary was installed decides what happens. A Homebrew, Scoop or +\&'go install' install is upgraded by its own package manager, because +overwriting a manager-owned binary desynchronizes its metadata. Only a +manually installed binary (a GitHub Releases tarball) is downloaded, +SHA-256 verified against the release checksums, and replaced in place. + +.PP +Nothing is installed without confirmation, and nothing is downloaded if +the target cannot be written — CommitBrief never invokes sudo itself. + +.PP +--check reports what would happen and installs nothing. --json implies +--check: it prints a single report object and exits. + +.PP +This is the only network request CommitBrief makes on its own behalf, +and only when you run this command. There is no automatic update check +and no telemetry. + + +.SH OPTIONS +\fB--check\fP[=false] + only report whether a newer version exists; install nothing + +.PP +\fB-h\fP, \fB--help\fP[=false] + help for upgrade + + +.SH OPTIONS INHERITED FROM PARENT COMMANDS +\fB--allow-secrets\fP[=false] + bypass the pre-send secret scanner (use with care) + +.PP +\fB--cli\fP="" + use a locally-installed CLI tool (claude|gemini|codex) as the review backend; shorthand for --provider -cli + +.PP +\fB--color\fP="auto" + color output: auto, always, never + +.PP +\fB--compact\fP[=false] + one-line per finding (dense review output) + +.PP +\fB--copy\fP[=false] + copy findings (severity, path, title, description) to the system clipboard via OSC 52 + native tool + +.PP +\fB-d\fP, \fB--dir\fP=[] + review only files under these directories or matching dir globs (e.g. \fBinternal/**\fR; repeatable, one pattern per flag); combines with the active scope flag + +.PP +\fB--fail-on\fP="" + exit 1 if any finding meets/exceeds severity (critical|high|medium|low|info|any|none) + +.PP +\fB-f\fP, \fB--file\fP=[] + review only these files or globs (e.g. \fB*.go\fR, \fBinternal/**/*.ts\fR; repeatable, one pattern per flag — patterns can't be comma-joined); combines with the active scope flag + +.PP +\fB--json\fP[=false] + emit machine-readable JSON output + +.PP +\fB--lang\fP="" + AI output language (e.g. tr, fr); the CLI interface localizes for en/tr only, output for any recognized language. Resolution: --lang → repo config → user config → English + +.PP +\fB--markdown\fP[=false] + emit plain markdown (no ANSI) + +.PP +\fB--min-severity\fP="" + hide findings below this severity in the rendered output (critical|high|medium|low|info); --json and --fail-on still see the full set + +.PP +\fB--model\fP="" + override configured model + +.PP +\fB--no-architecture\fP[=false] + skip architecture-aware review (do not read architecture.json into the prompt) for this run (ADR-0030) + +.PP +\fB--no-baseline\fP[=false] + ignore the signal-control baseline for this run (show everything, even baselined findings) + +.PP +\fB--no-cache\fP[=false] + bypass cache (read and write) + +.PP +\fB--no-cost-check\fP[=false] + skip the pre-send cost estimate prompt + +.PP +\fB--no-flaky\fP[=false] + skip the deterministic flaky-test detector (ADR-0022) + +.PP +\fB-o\fP, \fB--output\fP="" + write output to file instead of stdout + +.PP +\fB--provider\fP="" + override configured provider + +.PP +\fB-q\fP, \fB--quiet\fP[=false] + suppress info messages on stderr + +.PP +\fB--sandbox-rerun\fP[=0] + confirm flagged flaky tests by re-running each in isolation N times (ADR-0022); mixed pass+fail = confirmed flaky, all-fail = real failure, all-pass = transient. 0 = off; bare --sandbox-rerun uses N=5. Requires a bound rerun executor + +.PP +\fB--show-prompt\fP[=false] + print the exact system + user prompt that would be sent, then exit (no provider call, no cost) + +.PP +\fB--suggest-commit\fP[=false] + after the review, suggest a Conventional Commit message for the staged diff (requires --staged; prints to stdout; not with --json/--markdown/--output) + +.PP +\fB--update-baseline\fP[=false] + rewrite .commitbrief/baseline.json from the current findings (accepts them all) instead of filtering this run; user-private, gitignored (ADR-0027) + +.PP +\fB-v\fP, \fB--verbose\fP[=false] + show token/cost/latency footer + +.PP +\fB--with-context\fP[=false] + let the CLI provider read project files beyond the diff to ground the review (CLI providers only; the host CLI's agent reads your repo — see --help) + +.PP +\fB-y\fP, \fB--yes\fP[=false] + auto-confirm prompts (pre-send guard, init overwrite) + + +.SH SEE ALSO +\fBcommitbrief(1)\fP + + +.SH HISTORY +26-Jul-2026 Auto generated by spf13/cobra diff --git a/man/commitbrief.1 b/man/commitbrief.1 index 43255eb..57b6f47 100644 --- a/man/commitbrief.1 +++ b/man/commitbrief.1 @@ -1,5 +1,5 @@ .nh -.TH "COMMITBRIEF" "1" "Jun 2026" "Auto generated by spf13/cobra" "" +.TH "COMMITBRIEF" "1" "Jul 2026" "Auto generated by spf13/cobra" "" .SH NAME commitbrief - Local LLM-powered code review of git diffs @@ -35,7 +35,7 @@ Local LLM-powered code review of git diffs .PP \fB-d\fP, \fB--dir\fP=[] - review only files under these directories (repeatable); combines with the active scope flag + review only files under these directories or matching dir globs (e.g. \fBinternal/**\fR; repeatable, one pattern per flag); combines with the active scope flag .PP \fB--fail-on\fP="" @@ -43,7 +43,7 @@ Local LLM-powered code review of git diffs .PP \fB-f\fP, \fB--file\fP=[] - review only these files (repeatable); combines with the active scope flag + review only these files or globs (e.g. \fB*.go\fR, \fBinternal/**/*.ts\fR; repeatable, one pattern per flag — patterns can't be comma-joined); combines with the active scope flag .PP \fB-h\fP, \fB--help\fP[=false] @@ -69,6 +69,14 @@ Local LLM-powered code review of git diffs \fB--model\fP="" override configured model +.PP +\fB--no-architecture\fP[=false] + skip architecture-aware review (do not read architecture.json into the prompt) for this run (ADR-0030) + +.PP +\fB--no-baseline\fP[=false] + ignore the signal-control baseline for this run (show everything, even baselined findings) + .PP \fB--no-cache\fP[=false] bypass cache (read and write) @@ -93,6 +101,10 @@ Local LLM-powered code review of git diffs \fB-q\fP, \fB--quiet\fP[=false] suppress info messages on stderr +.PP +\fB--sandbox-rerun\fP[=0] + confirm flagged flaky tests by re-running each in isolation N times (ADR-0022); mixed pass+fail = confirmed flaky, all-fail = real failure, all-pass = transient. 0 = off; bare --sandbox-rerun uses N=5. Requires a bound rerun executor + .PP \fB--show-prompt\fP[=false] print the exact system + user prompt that would be sent, then exit (no provider call, no cost) @@ -109,6 +121,10 @@ Local LLM-powered code review of git diffs \fB-u\fP, \fB--unstaged\fP[=false] review unstaged changes +.PP +\fB--update-baseline\fP[=false] + rewrite .commitbrief/baseline.json from the current findings (accepts them all) instead of filtering this run; user-private, gitignored (ADR-0027) + .PP \fB-v\fP, \fB--verbose\fP[=false] show token/cost/latency footer @@ -127,8 +143,8 @@ Local LLM-powered code review of git diffs .SH SEE ALSO -\fBcommitbrief-cache(1)\fP, \fBcommitbrief-commit(1)\fP, \fBcommitbrief-completion(1)\fP, \fBcommitbrief-compress(1)\fP, \fBcommitbrief-config(1)\fP, \fBcommitbrief-diff(1)\fP, \fBcommitbrief-doctor(1)\fP, \fBcommitbrief-dry-run(1)\fP, \fBcommitbrief-init(1)\fP, \fBcommitbrief-install-hook(1)\fP, \fBcommitbrief-list(1)\fP, \fBcommitbrief-providers(1)\fP, \fBcommitbrief-remote(1)\fP, \fBcommitbrief-setup(1)\fP, \fBcommitbrief-summary(1)\fP +\fBcommitbrief-cache(1)\fP, \fBcommitbrief-commit(1)\fP, \fBcommitbrief-completion(1)\fP, \fBcommitbrief-compress(1)\fP, \fBcommitbrief-config(1)\fP, \fBcommitbrief-diff(1)\fP, \fBcommitbrief-doctor(1)\fP, \fBcommitbrief-dry-run(1)\fP, \fBcommitbrief-guard(1)\fP, \fBcommitbrief-init(1)\fP, \fBcommitbrief-install-hook(1)\fP, \fBcommitbrief-list(1)\fP, \fBcommitbrief-mcp(1)\fP, \fBcommitbrief-providers(1)\fP, \fBcommitbrief-remote(1)\fP, \fBcommitbrief-setup(1)\fP, \fBcommitbrief-summary(1)\fP, \fBcommitbrief-upgrade(1)\fP .SH HISTORY -19-Jun-2026 Auto generated by spf13/cobra +26-Jul-2026 Auto generated by spf13/cobra