Skip to content

Commit 279520a

Browse files
authored
Merge pull request #1703 from entireio/experimental-command-gating
feat: gate experimental commands behind build-time visibility flag
2 parents a26e53c + e9ccb98 commit 279520a

11 files changed

Lines changed: 405 additions & 53 deletions

.goreleaser.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@ builds:
2626
- -X github.com/entireio/cli/cmd/entire/cli/versioninfo.Commit={{.ShortCommit}}
2727
- -X github.com/entireio/cli/cmd/entire/cli/telemetry.PostHogAPIKey={{.Env.POSTHOG_API_KEY}}
2828
- -X github.com/entireio/cli/cmd/entire/cli/telemetry.PostHogEndpoint={{.Env.POSTHOG_ENDPOINT}}
29+
# Experimental-command visibility: hide in stable releases, keep visible
30+
# in nightly (prerelease) builds. .Prerelease is empty for a stable tag
31+
# (vX.Y.Z) and non-empty for a nightly tag (vX.Y.Z-nightly.*). Local
32+
# builds carry no stamp and use the package default ("true" = visible).
33+
- -X github.com/entireio/cli/cmd/entire/cli/experimental.Visible={{ if .Prerelease }}true{{ else }}false{{ end }}
2934

3035
# git-remote-entire is the git remote helper for entire:// URLs (see
3136
# cmd/git-remote-entire). A small, dedicated binary shipped alongside

CLAUDE.md

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,18 @@ The visible CLI is organized around a set of noun groups plus a small set of
2626
top-level verbs. The groups are the canonical home for each verb; legacy
2727
top-level shortcuts remain functional but hidden, and emit a deprecation hint
2828
pointing at the canonical group form. Newer experimental command families are
29-
discoverable through `entire labs` and may remain hidden from root help while
30-
their canonical paths are still runnable.
29+
discoverable through `entire labs` and their canonical paths are always
30+
runnable.
31+
32+
Experimental commands are gated by a build-time visibility flag (the
33+
`cmd/entire/cli/experimental` package): they are shown — grouped under an
34+
"Experimental commands:" help section — in developer and nightly builds, and
35+
hidden in stable release builds. Visibility is toggled by `experimental.Visible`
36+
(default `"true"`), which GoReleaser stamps `"false"` only on stable tags
37+
(`.Prerelease` empty); nightly (`vX.Y.Z-nightly.*`) and local builds leave it at
38+
the default. Register a command as experimental with `experimental.Register(parent,
39+
child)` instead of `parent.AddCommand(child)`. Gating only controls visibility —
40+
the commands are always runnable in every build.
3141

3242
- `session` (alias: `sessions`): `list`, `info`, `tokens`, `stop`, `attach`, `adopt`, `resume`, `current`.
3343
`resume` with a branch arg switches to it and resumes its session; with no arg
@@ -66,9 +76,12 @@ their canonical paths are still runnable.
6676
- `grant`: manage access grants and org membership — `org`, `project`, and `repo`
6777
each support `add` / `list` / `remove`
6878

69-
Experimental command families advertised through `entire labs`:
70-
71-
- `tokens`: `profile` (hidden from root help while token diagnostics mature)
79+
Experimental commands (gated by the build-time visibility flag above — visible
80+
and grouped under "Experimental commands:" in developer/nightly builds, hidden
81+
in stable releases, always runnable): `tokens`, `import`, `review`,
82+
`investigate`, `blame`, `why`, the top-level `search` shortcut, `experts`,
83+
`runner`, and `checkpoint policy`. `tokens` is also advertised through `entire
84+
labs`. The canonical `checkpoint search` is not gated and stays visible.
7285

7386
Top-level lifecycle and standalone commands: `enable`, `disable`, `status`,
7487
`login`, `logout`, `clean`, `version`, `dispatch`, `activity`, `help`,
@@ -91,7 +104,10 @@ one command's current flags; `--json` emits structured output. It is the single
91104
source of truth the first-turn context injection and the `--agent-help-skill`
92105
skill point agents at, instead of enumerating a surface that goes stale.
93106
Hidden commands opt into being advertised here by setting
94-
`Annotations[agentHelpAnnotation] = "true"` (e.g. `trail`).
107+
`Annotations[agentHelpAnnotation] = "true"` (e.g. `trail`). Because `agent-help`
108+
renders live and lists non-hidden commands, the experimental commands appear in
109+
`agent-help` in developer/nightly builds and are absent in stable releases — the
110+
advertised surface is build-dependent, matching what `entire help` shows.
95111
No-channel agents (Cursor, Copilot CLI, Factory Droid, MCP hosts — no
96112
context-injection channel and no agent-help skill template) reach it without an
97113
active push. All of them can discover it passively: it is visible in `entire
@@ -107,7 +123,9 @@ Hidden top-level shortcuts (functional, emit a one-line deprecation hint):
107123
`resume``session resume`, `attach``session attach`, `explain`
108124
`checkpoint explain`, `trace``doctor trace`.
109125
Cobra-native aliases (no hint): `sessions``session`, `cp`/`checkpoints`
110-
`checkpoint`. The `search` top-level remains hidden without a hint.
126+
`checkpoint`. The `search` top-level is experimental (see the visibility gate
127+
above), so it follows the build-dependent visibility rather than being
128+
unconditionally hidden.
111129

112130
Deprecated top-level commands (functional, print a cobra deprecation message):
113131
`reset``clean`, and `rewind` (no replacement, announces removal — same

cmd/entire/cli/checkpoint_group.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package cli
33
import (
44
"errors"
55

6+
"github.com/entireio/cli/cmd/entire/cli/experimental"
67
"github.com/entireio/cli/cmd/entire/cli/paths"
78
"github.com/spf13/cobra"
89
)
@@ -39,7 +40,7 @@ Examples:
3940
cmd.AddCommand(newCheckpointResumeCmd())
4041
cmd.AddCommand(newExplainCmd())
4142
cmd.AddCommand(newCheckpointTokensCmd())
42-
cmd.AddCommand(newCheckpointPolicyCmd())
43+
experimental.Register(cmd, newCheckpointPolicyCmd()) // 'checkpoint policy' (experimental)
4344
cmd.AddCommand(newRewindCmd())
4445
cmd.AddCommand(newCheckpointSearchCmd())
4546

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// Package experimental gates the visibility of experimental CLI commands.
2+
//
3+
// Experimental commands stay fully runnable in every build; this package only
4+
// controls whether they appear in `entire help`. Developer builds (go build,
5+
// go run, mise) show them, grouped under an "Experimental commands:" help
6+
// section. Release builds (GoReleaser) hide them.
7+
package experimental
8+
9+
import "github.com/spf13/cobra"
10+
11+
// Visible controls whether experimental commands are shown in help. It is
12+
// stamped by GoReleaser via ldflags
13+
// (-X github.com/entireio/cli/cmd/entire/cli/experimental.Visible=false)
14+
// to hide them in shipped binaries. It defaults to "true", so every
15+
// non-release build (go build, go run, mise) shows them. The commands remain
16+
// experimental and fully runnable regardless of this flag — it only toggles
17+
// visibility.
18+
var Visible = "true"
19+
20+
// IsVisible reports whether experimental commands are shown in help.
21+
func IsVisible() bool { return Visible != "false" }
22+
23+
// GroupID is the cobra group experimental commands are filed under.
24+
const GroupID = "experimental"
25+
26+
const groupTitle = "Experimental commands:"
27+
28+
// Register adds child under parent as an experimental command.
29+
//
30+
// When experimental commands are visible, child is filed under parent's
31+
// "Experimental commands:" help group (registering the group on parent once).
32+
// When hidden, child is marked Hidden and left ungrouped — so release help
33+
// never carries an empty group header, and cobra never references a group ID
34+
// that was not registered.
35+
//
36+
// Register overrides any Hidden value the child's constructor set, so callers
37+
// do not need to touch the constructors (including ones in other packages).
38+
func Register(parent, child *cobra.Command) {
39+
if IsVisible() {
40+
if !parent.ContainsGroup(GroupID) {
41+
parent.AddGroup(&cobra.Group{ID: GroupID, Title: groupTitle})
42+
}
43+
child.Hidden = false
44+
child.GroupID = GroupID
45+
} else {
46+
child.Hidden = true
47+
child.GroupID = ""
48+
}
49+
parent.AddCommand(child)
50+
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
package experimental
2+
3+
import (
4+
"testing"
5+
6+
"github.com/spf13/cobra"
7+
)
8+
9+
// setVisible sets the package-global Visible for the duration of the test and
10+
// restores it afterward. Mutating a global means these tests cannot run in
11+
// parallel.
12+
func setVisible(t *testing.T, v string) {
13+
t.Helper()
14+
prev := Visible
15+
Visible = v
16+
t.Cleanup(func() { Visible = prev })
17+
}
18+
19+
func TestIsVisible(t *testing.T) {
20+
tests := []struct {
21+
value string
22+
want bool
23+
}{
24+
{"true", true},
25+
{"false", false},
26+
{"", true}, // only the literal "false" hides
27+
{"anything", true}, // any non-"false" stamp is treated as visible
28+
}
29+
for _, tt := range tests {
30+
t.Run(tt.value, func(t *testing.T) {
31+
setVisible(t, tt.value)
32+
if got := IsVisible(); got != tt.want {
33+
t.Fatalf("IsVisible() with Visible=%q = %v, want %v", tt.value, got, tt.want)
34+
}
35+
})
36+
}
37+
}
38+
39+
func TestRegister_Visible(t *testing.T) {
40+
setVisible(t, "true")
41+
42+
parent := &cobra.Command{Use: "parent"}
43+
child := &cobra.Command{Use: "child", Hidden: true} // constructor-set Hidden must be overridden
44+
Register(parent, child)
45+
46+
if child.Hidden {
47+
t.Error("child should be visible when experimental commands are visible")
48+
}
49+
if child.GroupID != GroupID {
50+
t.Errorf("child.GroupID = %q, want %q", child.GroupID, GroupID)
51+
}
52+
if !parent.ContainsGroup(GroupID) {
53+
t.Error("parent should have the experimental group registered")
54+
}
55+
if len(parent.Commands()) != 1 || parent.Commands()[0] != child {
56+
t.Error("child should be added to parent")
57+
}
58+
}
59+
60+
func TestRegister_Hidden(t *testing.T) {
61+
setVisible(t, "false")
62+
63+
parent := &cobra.Command{Use: "parent"}
64+
child := &cobra.Command{Use: "child"}
65+
Register(parent, child)
66+
67+
if !child.Hidden {
68+
t.Error("child should be hidden when experimental commands are hidden")
69+
}
70+
if child.GroupID != "" {
71+
t.Errorf("child.GroupID = %q, want empty (no group referenced in release)", child.GroupID)
72+
}
73+
if parent.ContainsGroup(GroupID) {
74+
t.Error("parent should not register the experimental group in release builds")
75+
}
76+
if len(parent.Commands()) != 1 || parent.Commands()[0] != child {
77+
t.Error("child should still be added to parent")
78+
}
79+
}
80+
81+
// TestRegister_MultipleShareOneGroup verifies the group is registered once even
82+
// when several experimental commands are registered under the same parent.
83+
func TestRegister_MultipleShareOneGroup(t *testing.T) {
84+
setVisible(t, "true")
85+
86+
parent := &cobra.Command{Use: "parent"}
87+
Register(parent, &cobra.Command{Use: "a"})
88+
Register(parent, &cobra.Command{Use: "b"})
89+
90+
groups := parent.Groups()
91+
count := 0
92+
for _, g := range groups {
93+
if g.ID == GroupID {
94+
count++
95+
}
96+
}
97+
if count != 1 {
98+
t.Errorf("experimental group registered %d times, want 1", count)
99+
}
100+
}
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
package cli
2+
3+
import (
4+
"testing"
5+
6+
"github.com/entireio/cli/cmd/entire/cli/experimental"
7+
"github.com/spf13/cobra"
8+
)
9+
10+
// experimentalRootCommands are the top-level commands gated behind the
11+
// experimental visibility flag. Names match cobra's Command.Name() (the first
12+
// token of Use).
13+
var experimentalRootCommands = []string{
14+
"tokens", "import", "review", "investigate",
15+
"blame", "why", "search", "experts", "runner",
16+
}
17+
18+
// withVisible sets the experimental visibility flag for the test and restores
19+
// it afterward. Because it mutates a package global, callers must not run in
20+
// parallel.
21+
func withVisible(t *testing.T, v string) {
22+
t.Helper()
23+
prev := experimental.Visible
24+
experimental.Visible = v
25+
t.Cleanup(func() { experimental.Visible = prev })
26+
}
27+
28+
func findCommand(parent *cobra.Command, name string) *cobra.Command {
29+
for _, c := range parent.Commands() {
30+
if c.Name() == name {
31+
return c
32+
}
33+
}
34+
return nil
35+
}
36+
37+
// checkpointPolicy returns the `checkpoint policy` command.
38+
func checkpointPolicy(t *testing.T, root *cobra.Command) *cobra.Command {
39+
t.Helper()
40+
cp := findCommand(root, "checkpoint")
41+
if cp == nil {
42+
t.Fatal("checkpoint command not found on root")
43+
}
44+
return findCommand(cp, "policy")
45+
}
46+
47+
// TestExperimental_VisibleInDevBuild verifies that, in a developer build
48+
// (Visible defaults to "true"), the experimental commands are shown and filed
49+
// under the experimental group. Cannot use t.Parallel — mutates a global.
50+
func TestExperimental_VisibleInDevBuild(t *testing.T) {
51+
withVisible(t, "true")
52+
53+
root := NewRootCmd()
54+
55+
if !root.ContainsGroup(experimental.GroupID) {
56+
t.Fatal("root should register the experimental group in a dev build")
57+
}
58+
for _, name := range experimentalRootCommands {
59+
cmd := findCommand(root, name)
60+
if cmd == nil {
61+
t.Errorf("%q not found on root", name)
62+
continue
63+
}
64+
if cmd.Hidden {
65+
t.Errorf("%q should be visible in a dev build", name)
66+
}
67+
if cmd.GroupID != experimental.GroupID {
68+
t.Errorf("%q GroupID = %q, want %q", name, cmd.GroupID, experimental.GroupID)
69+
}
70+
}
71+
72+
policy := checkpointPolicy(t, root)
73+
if policy == nil {
74+
t.Fatal("checkpoint policy not found")
75+
}
76+
if policy.Hidden {
77+
t.Error("checkpoint policy should be visible in a dev build")
78+
}
79+
if policy.GroupID != experimental.GroupID {
80+
t.Errorf("checkpoint policy GroupID = %q, want %q", policy.GroupID, experimental.GroupID)
81+
}
82+
}
83+
84+
// TestExperimental_HiddenInReleaseBuild verifies that, when GoReleaser stamps
85+
// Visible=false, the experimental commands are hidden, carry no group, and the
86+
// empty experimental group is never registered (so release help is unchanged).
87+
// Cannot use t.Parallel — mutates a global.
88+
func TestExperimental_HiddenInReleaseBuild(t *testing.T) {
89+
withVisible(t, "false")
90+
91+
root := NewRootCmd()
92+
93+
if root.ContainsGroup(experimental.GroupID) {
94+
t.Error("root should not register the experimental group in a release build")
95+
}
96+
for _, name := range experimentalRootCommands {
97+
cmd := findCommand(root, name)
98+
if cmd == nil {
99+
t.Errorf("%q not found on root", name)
100+
continue
101+
}
102+
if !cmd.Hidden {
103+
t.Errorf("%q should be hidden in a release build", name)
104+
}
105+
if cmd.GroupID != "" {
106+
t.Errorf("%q GroupID = %q, want empty in a release build", name, cmd.GroupID)
107+
}
108+
}
109+
110+
policy := checkpointPolicy(t, root)
111+
if policy == nil {
112+
t.Fatal("checkpoint policy not found")
113+
}
114+
if !policy.Hidden {
115+
t.Error("checkpoint policy should be hidden in a release build")
116+
}
117+
}

cmd/entire/cli/experts_test.go

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import (
1212
"strings"
1313
"testing"
1414

15+
"github.com/entireio/cli/cmd/entire/cli/experimental"
16+
1517
"charm.land/lipgloss/v2"
1618
"github.com/entireio/cli/cmd/entire/cli/palette"
1719
"github.com/entireio/cli/cmd/entire/cli/paths"
@@ -116,7 +118,7 @@ func expertsSuccessBody() string {
116118
}`
117119
}
118120

119-
func TestExpertsCommandIsHiddenAndListedInLabs(t *testing.T) {
121+
func TestExpertsCommandIsExperimentalAndListedInLabs(t *testing.T) {
120122
root := NewRootCmd()
121123
cmd, _, err := root.Find([]string{"experts"})
122124
if err != nil {
@@ -125,8 +127,10 @@ func TestExpertsCommandIsHiddenAndListedInLabs(t *testing.T) {
125127
if cmd.Name() != "experts" {
126128
t.Fatalf("found command %q, want experts", cmd.Name())
127129
}
128-
if !cmd.Hidden {
129-
t.Fatal("experts command should be hidden while in labs")
130+
// Gated as experimental: visible and grouped in developer builds
131+
// (the default test build), hidden in shipped releases.
132+
if cmd.GroupID != experimental.GroupID {
133+
t.Fatalf("experts GroupID = %q, want %q (experimental)", cmd.GroupID, experimental.GroupID)
130134
}
131135
if !strings.Contains(labsOverview(), "entire experts") {
132136
t.Fatalf("labs overview missing experts:\n%s", labsOverview())

0 commit comments

Comments
 (0)