Skip to content

Commit 61b3c54

Browse files
gtrrz-victorclaude
andcommitted
Merge origin/main: compose help groups with experimental command gate
Union of both sides in root.go: commands gated by experimental.Register keep its build-dependent group; everything else keeps the new journey groups. Grouping test skips experimental.GroupID commands since their visibility is build-dependent, and the ungrouped-allowlist comment now notes completion never renders (hidden via HiddenDefaultCmd). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Entire-Checkpoint: 01KXJDWJ53X19E3NF0DQ2WMNQ2
2 parents 2311c87 + 85d9ec5 commit 61b3c54

25 files changed

Lines changed: 1624 additions & 189 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: 26 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
@@ -604,6 +622,7 @@ The phase state machine, metadata directory layout, sharded checkpoint format, m
604622

605623
- [Sessions and Checkpoints](docs/architecture/sessions-and-checkpoints.md) - domain model, storage layout, checkpoint ID linking, commit trailers, package structure
606624
- [Checkpoint Scenarios](docs/architecture/checkpoint-scenarios.md) - phase state machine and worked condensation scenarios
625+
- [Ref-Based Checkpoint Backend](docs/architecture/ref-checkpoint-backend.md) - git-refs backend: primary/mirror taxonomy, ref layout + sharding, push-discovery queue, read routing, config + rollout
607626

608627
#### When Modifying the Strategy
609628

cmd/entire/cli/checkpoint/remote/git.go

Lines changed: 99 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,18 @@ import (
1111
"strconv"
1212
"strings"
1313
"sync"
14+
"time"
1415

1516
"github.com/entireio/cli/cmd/entire/cli/logging"
1617
"github.com/entireio/cli/cmd/entire/cli/settings"
1718
)
1819

20+
// stampConfigTimeout bounds the local git-config reads/writes that mark a newly
21+
// created checkpoint remote as skipped. They run detached from the fetch's
22+
// context (see stampNewlyCreatedRemote), so a bound guards against a stuck
23+
// config lock hanging the caller.
24+
const stampConfigTimeout = 10 * time.Second
25+
1926
// CheckpointTokenEnvVar is the environment variable for providing an access token
2027
// used to authenticate git push/fetch operations for checkpoint branches.
2128
// The token is injected as an HTTP Basic Authorization header per RFC 7617:
@@ -85,80 +92,120 @@ func Fetch(ctx context.Context, opts FetchOptions) ([]byte, error) {
8592
args = append(args, opts.Remote)
8693
args = append(args, opts.RefSpecs...)
8794

95+
// A filtered fetch from a URL makes git record a URL-keyed remote section
96+
// (remote.<url>.*) so it can lazy-fetch filtered-out objects later. That
97+
// section also turns the URL into a phantom remote that `git fetch --all`
98+
// and `git remote update` keep dialing. When this fetch is the one creating
99+
// the section, stamp skipFetchAll so bulk fetches skip our adhoc remote.
100+
// Remotes that already existed are left untouched so we never rewrite the
101+
// user's config.
102+
var stampURL string
103+
var stampCandidate, existedBefore bool
104+
if filtered && IsURL(opts.Remote) {
105+
stampCandidate = true
106+
stampURL = opts.Remote
107+
if token := strings.TrimSpace(os.Getenv(CheckpointTokenEnvVar)); token != "" && isValidToken(token) {
108+
// With a checkpoint token, newCommand rewrites SSH targets to HTTPS
109+
// and git records the section under the rewritten URL.
110+
stampURL, _ = resolveTargetForTokenAuth(ctx, stampURL)
111+
}
112+
existedBefore = gitRemoteSectionExists(ctx, opts.Dir, stampURL)
113+
}
114+
88115
cmd := newCommand(ctx, args...)
89116
if opts.Dir != "" {
90117
cmd.Dir = opts.Dir
91118
}
92119
disableTerminalPrompt(cmd)
93120
out, err := cmd.CombinedOutput()
121+
122+
if stampCandidate && !existedBefore {
123+
stampNewlyCreatedRemote(ctx, opts.Dir, stampURL)
124+
}
125+
94126
if err != nil {
95127
return out, fmt.Errorf("git fetch: %w", err)
96128
}
97-
if filtered && IsURL(opts.Remote) {
98-
// Stamp the URL git actually fetched from: with a checkpoint token set,
99-
// newCommand rewrites SSH targets to HTTPS, and git records the
100-
// promisor entry under the rewritten URL.
101-
target := opts.Remote
102-
if token := strings.TrimSpace(os.Getenv(CheckpointTokenEnvVar)); token != "" && isValidToken(token) {
103-
target, _ = resolveTargetForTokenAuth(ctx, target)
104-
}
105-
markPromisorEntrySkipped(ctx, opts.Dir, target)
106-
}
107129
return out, nil
108130
}
109131

110-
// markPromisorEntrySkipped excludes the URL-keyed config section that git
111-
// creates for a filtered URL fetch (remote.<url>.promisor=true) from
112-
// `git fetch --all` and `git remote update`. Git needs the promisor entry to
113-
// lazy-fetch filtered-out objects later, but the entry also makes the URL show
114-
// up as a fetchable remote, so without this every checkpoint URL ever fetched
115-
// from lingers as a phantom remote that bulk fetches keep dialing.
116-
// Best-effort: the fetch already succeeded, so failures only log.
117-
func markPromisorEntrySkipped(ctx context.Context, dir, url string) {
118-
if !gitConfigBool(ctx, dir, "remote."+url+".promisor") {
119-
// Git didn't record a promisor entry for this URL; don't invent a
120-
// config section that wouldn't otherwise exist.
121-
return
122-
}
123-
for _, key := range []string{"skipFetchAll", "skipDefaultUpdate"} {
124-
fullKey := "remote." + url + "." + key
125-
if gitConfigBool(ctx, dir, fullKey) {
126-
// Checked per key so a partially-stamped entry (e.g. an earlier
127-
// run failing between the two writes) still gets completed.
128-
continue
129-
}
130-
cmd := exec.CommandContext(ctx, "git", "config", "--local", fullKey, "true")
131-
if dir != "" {
132-
cmd.Dir = dir
133-
}
134-
if out, cfgErr := cmd.CombinedOutput(); cfgErr != nil {
135-
redactedURL := RedactURL(url)
136-
// The output can echo the key, which embeds the URL — and a URL
137-
// can carry credentials. Redact before logging.
138-
msg := strings.TrimSpace(strings.ReplaceAll(string(out), url, redactedURL))
139-
logging.Warn(ctx, "failed to mark promisor config entry as skipped for bulk fetches",
140-
slog.String("url", redactedURL),
141-
slog.String("key", key),
142-
slog.String("output", msg),
143-
slog.String("error", cfgErr.Error()),
144-
)
145-
return
146-
}
132+
// stampNewlyCreatedRemote stamps a URL-keyed remote section that this fetch just
133+
// created. Git writes remote.<url>.promisor eagerly during connection setup, so
134+
// a filtered fetch that later fails still leaves the phantom remote behind;
135+
// stamping here — rather than only on fetch success — keeps it from lingering
136+
// unstamped forever (the section then exists on the next attempt, so it never
137+
// looks "new" again). Re-checking existence keeps us from inventing a section
138+
// when the fetch died before git wrote anything.
139+
//
140+
// The git-config commands run on a context detached from the fetch's deadline:
141+
// a filtered fetch that timed out leaves ctx already past its deadline, and
142+
// inheriting it would make these local commands fail immediately and leave the
143+
// phantom unstamped — the very miss this stamping exists to prevent.
144+
func stampNewlyCreatedRemote(ctx context.Context, dir, url string) {
145+
ctx, cancel := context.WithTimeout(context.WithoutCancel(ctx), stampConfigTimeout)
146+
defer cancel()
147+
if gitRemoteSectionExists(ctx, dir, url) {
148+
markRemoteSkipped(ctx, dir, url)
149+
}
150+
}
151+
152+
// markRemoteSkipped stamps skipFetchAll on a URL-keyed remote section so
153+
// `git fetch --all` and `git remote update` skip it. Called only for remotes
154+
// this fetch just created, so an adhoc checkpoint URL never lingers as a phantom
155+
// remote that bulk fetches keep dialing.
156+
// Best-effort: the git config write is not worth failing the fetch over, so
157+
// failures only log.
158+
func markRemoteSkipped(ctx context.Context, dir, url string) {
159+
fullKey := "remote." + url + ".skipFetchAll"
160+
cmd := exec.CommandContext(ctx, "git", "config", "--local", fullKey, "true")
161+
if dir != "" {
162+
cmd.Dir = dir
163+
}
164+
if out, cfgErr := cmd.CombinedOutput(); cfgErr != nil {
165+
redactedURL := RedactURL(url)
166+
// The output can echo the key, which embeds the URL — and a URL can
167+
// carry credentials. Redact before logging.
168+
msg := strings.TrimSpace(strings.ReplaceAll(string(out), url, redactedURL))
169+
logging.Warn(ctx, "failed to mark remote config entry as skipped for bulk fetches",
170+
slog.String("url", redactedURL),
171+
slog.String("output", msg),
172+
slog.String("error", cfgErr.Error()),
173+
)
147174
}
148175
}
149176

150-
// gitConfigBool reads a local git config key and reports whether it is set to
151-
// a true value. Missing keys and read errors report false.
152-
func gitConfigBool(ctx context.Context, dir, key string) bool {
153-
cmd := exec.CommandContext(ctx, "git", "config", "--local", "--get", "--type=bool", key)
177+
// gitRemoteSectionExists reports whether a remote.<url>.* config section already
178+
// exists in the local git config. Used to tell whether a filtered URL fetch is
179+
// about to create a new URL-keyed remote, so we only stamp remotes we create and
180+
// never rewrite ones the user already has.
181+
func gitRemoteSectionExists(ctx context.Context, dir, url string) bool {
182+
cmd := exec.CommandContext(ctx, "git", "config", "--local", "--list", "--name-only")
154183
if dir != "" {
155184
cmd.Dir = dir
156185
}
157186
out, err := cmd.Output()
158187
if err != nil {
159188
return false
160189
}
161-
return strings.TrimSpace(string(out)) == "true"
190+
// Each name is "remote.<url>.<key>". Git config keys carry no dots, so the
191+
// final dotted component is the key and everything between "remote." and it
192+
// is the subsection (the URL, whose case git preserves). Compare the
193+
// subsection exactly so a longer URL that shares a prefix (e.g. a
194+
// ".../repo.git" section vs a ".../repo" fetch) is not a false match.
195+
for line := range strings.SplitSeq(string(out), "\n") {
196+
rest, ok := strings.CutPrefix(line, "remote.")
197+
if !ok {
198+
continue
199+
}
200+
lastDot := strings.LastIndexByte(rest, '.')
201+
if lastDot < 0 {
202+
continue
203+
}
204+
if rest[:lastDot] == url {
205+
return true
206+
}
207+
}
208+
return false
162209
}
163210

164211
// FetchBlobs fetches specific objects (typically blobs) by hash from a remote.

0 commit comments

Comments
 (0)