Merge origin/main: compose help groups with experimental command gate · Entire
Log in
Merge origin/main: compose help groups with experimental command gate
61b3c54→main·
gtrrz-victor·2d ago·25 files·+1,629 added/-194 removed
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
Sessions
01KXJDWJ53X19E3NF0DQ2WMNQ2View transcript
?\ Organize CLI Commands into User Journey GroupsClaude Code·Fable 5·3 steps ?\ Cobra CLI Command Grouping and GatingClaude Code·4 steps
Changes
25
M.goreleaser.yaml+5
MCLAUDE.md+26/-7
cmd/entire/cli
checkpoint/remote
Mgit.go+103/-56
Mgit_test.go+184/-19
Mcheckpoint_group.go+2/-1
experimental
Aexperimental.go+50
Aexperimental_test.go+100
Aexperimental_wiring_test.go+117
Mexperts_test.go+7/-3
integration_test
Mtestenv.go+3/-4
interactive
Minteractive.go+11
Minvestigate_bridge_test.go+10/-4
Mlabs_test.go+53/-11
review
Mcmd.go+51/-16
Mcmd_test.go+70/-10
Mconfigure_test.go+120
Mfix.go+1/-2
Mprofile.go+31/-5
Mtui_sink.go+130/-31
Mtui_sink_test.go+233
Mroot.go+13/-10
Mroot_test.go+21/-8
docs/architecture
Mcheckpoint-scenarios.md+51
Aref-checkpoint-backend.md+216
Msessions-and-checkpoints.md+21/-7
25 unmodified lines
26
27
28
29
30
31
32
33
34
35
36
25 unmodified lines
- -X github.com/entireio/cli/cmd/entire/cli/versioninfo.Commit={{.ShortCommit}}
- -X github.com/entireio/cli/cmd/entire/cli/telemetry.PostHogAPIKey={{.Env.POSTHOG_API_KEY}}
- -X github.com/entireio/cli/cmd/entire/cli/telemetry.PostHogEndpoint={{.Env.POSTHOG_ENDPOINT}}
# Experimental-command visibility: hide in stable releases, keep visible
# in nightly (prerelease) builds. .Prerelease is empty for a stable tag
# (vX.Y.Z) and non-empty for a nightly tag (vX.Y.Z-nightly.*). Local
# builds carry no stamp and use the package default ("true" = visible).
- -X github.com/entireio/cli/cmd/entire/cli/experimental.Visible={{ if .Prerelease }}true{{ else }}false{{ end }}
# git-remote-entire is the git remote helper for entire:// URLs (see
# cmd/git-remote-entire). A small, dedicated binary shipped alongside
M.goreleaser.yaml+5
25 unmodified lines
26
27
28
29
30
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
32 unmodified lines
76
77
78
69
70
71
79
80
81
82
83
84
85
86
87
16 unmodified lines
104
105
106
94
107
108
109
110
111
112
113
9 unmodified lines
123
124
125
110
126
127
128
129
130
131
490 unmodified lines
622
623
624
625
626
627
628
25 unmodified lines
top-level verbs. The groups are the canonical home for each verb; legacy
top-level shortcuts remain functional but hidden, and emit a deprecation hint
pointing at the canonical group form. Newer experimental command families are
discoverable through `entire labs` and may remain hidden from root help while
their canonical paths are still runnable.
discoverable through `entire labs` and their canonical paths are always
runnable.
Experimental commands are gated by a build-time visibility flag (the
`cmd/entire/cli/experimental` package): they are shown — grouped under an
"Experimental commands:" help section — in developer and nightly builds, and
hidden in stable release builds. Visibility is toggled by `experimental.Visible`
(default `"true"`), which GoReleaser stamps `"false"` only on stable tags
(`.Prerelease` empty); nightly (`vX.Y.Z-nightly.*`) and local builds leave it at
the default. Register a command as experimental with `experimental.Register(parent,
child)` instead of `parent.AddCommand(child)`. Gating only controls visibility —
the commands are always runnable in every build.
- `session` (alias: `sessions`): `list`, `info`, `tokens`, `stop`, `attach`, `adopt`, `resume`, `current`.
`resume` with a branch arg switches to it and resumes its session; with no arg
32 unmodified lines
- `grant`: manage access grants and org membership — `org`, `project`, and `repo`
each support `add` / `list` / `remove`
Experimental command families advertised through `entire labs`:
- `tokens`: `profile` (hidden from root help while token diagnostics mature)
Experimental commands (gated by the build-time visibility flag above — visible
and grouped under "Experimental commands:" in developer/nightly builds, hidden
in stable releases, always runnable): `tokens`, `import`, `review`,
`investigate`, `blame`, `why`, the top-level `search` shortcut, `experts`,
`runner`, and `checkpoint policy`. `tokens` is also advertised through `entire
labs`. The canonical `checkpoint search` is not gated and stays visible.
Top-level lifecycle and standalone commands: `enable`, `disable`, `status`,
`login`, `logout`, `clean`, `version`, `dispatch`, `activity`, `help`,
16 unmodified lines
source of truth the first-turn context injection and the `--agent-help-skill`
skill point agents at, instead of enumerating a surface that goes stale.
Hidden commands opt into being advertised here by setting
`Annotations[agentHelpAnnotation] = "true"` (e.g. `trail`).
`Annotations[agentHelpAnnotation] = "true"` (e.g. `trail`). Because `agent-help`
renders live and lists non-hidden commands, the experimental commands appear in
`agent-help` in developer/nightly builds and are absent in stable releases — the
advertised surface is build-dependent, matching what `entire help` shows.
No-channel agents (Cursor, Copilot CLI, Factory Droid, MCP hosts — no
context-injection channel and no agent-help skill template) reach it without an
active push. All of them can discover it passively: it is visible in `entire
9 unmodified lines
`resume` → `session resume`, `attach` → `session attach`, `explain` →
`checkpoint explain`, `trace` → `doctor trace`.
Cobra-native aliases (no hint): `sessions` → `session`, `cp`/`checkpoints` →
`checkpoint`. The `search` top-level remains hidden without a hint.
`checkpoint`. The `search` top-level is experimental (see the visibility gate
above), so it follows the build-dependent visibility rather than being
unconditionally hidden.
Deprecated top-level commands (functional, print a cobra deprecation message):
`reset` → `clean`, and `rewind` (no replacement, announces removal — same
490 unmodified lines
- [Sessions and Checkpoints](docs/architecture/sessions-and-checkpoints.md) - domain model, storage layout, checkpoint ID linking, commit trailers, package structure
- [Checkpoint Scenarios](docs/architecture/checkpoint-scenarios.md) - phase state machine and worked condensation scenarios
- [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
#### When Modifying the Strategy
MCLAUDE.md+26/-7
10 unmodified lines
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
63 unmodified lines
92
93
94
88
89
90
91
92
93
94
95
96
95
96
97
98
99
100
101
102
103
104
98
99
100
101
105
106
107
103
108
109
110
111
105
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
110
111
112
113
114
115
116
117
118
119
120
121
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
150
151
152
153
177
178
179
180
181
182
183
184
185
1 unmodified line
187
188
189
161
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
10 unmodified lines
"strconv"
"strings"
"sync"
"time"
"github.com/entireio/cli/cmd/entire/cli/logging"
"github.com/entireio/cli/cmd/entire/cli/settings"
)
// stampConfigTimeout bounds the local git-config reads/writes that mark a newly
// created checkpoint remote as skipped. They run detached from the fetch's
// context (see stampNewlyCreatedRemote), so a bound guards against a stuck
// config lock hanging the caller.
const stampConfigTimeout = 10 * time.Second
// CheckpointTokenEnvVar is the environment variable for providing an access token
// used to authenticate git push/fetch operations for checkpoint branches.
// The token is injected as an HTTP Basic Authorization header per RFC 7617:
63 unmodified lines
args = append(args, opts.Remote)
args = append(args, opts.RefSpecs...)
cmd := newCommand(ctx, args...)
if opts.Dir != "" {
cmd.Dir = opts.Dir
}
disableTerminalPrompt(cmd)
out, err := cmd.CombinedOutput()
if err != nil {
return out, fmt.Errorf("git fetch: %w", err)
}
// A filtered fetch from a URL makes git record a URL-keyed remote section
// (remote.<url>.*) so it can lazy-fetch filtered-out objects later. That
// section also turns the URL into a phantom remote that `git fetch --all`
// and `git remote update` keep dialing. When this fetch is the one creating
// the section, stamp skipFetchAll so bulk fetches skip our adhoc remote.
// Remotes that already existed are left untouched so we never rewrite the
// user's config.
var stampURL string
var stampCandidate, existedBefore bool
if filtered && IsURL(opts.Remote) {
// Stamp the URL git actually fetched from: with a checkpoint token set,
// newCommand rewrites SSH targets to HTTPS, and git records the
// promisor entry under the rewritten URL.
target := opts.Remote
stampCandidate = true
stampURL = opts.Remote
if token := strings.TrimSpace(os.Getenv(CheckpointTokenEnvVar)); token != "" && isValidToken(token) {
target, _ = resolveTargetForTokenAuth(ctx, target)
// With a checkpoint token, newCommand rewrites SSH targets to HTTPS
// and git records the section under the rewritten URL.
stampURL, _ = resolveTargetForTokenAuth(ctx, stampURL)
}
markPromisorEntrySkipped(ctx, opts.Dir, target)
existedBefore = gitRemoteSectionExists(ctx, opts.Dir, stampURL)
}
cmd := newCommand(ctx, args...)
if opts.Dir != "" {
cmd.Dir = opts.Dir
}
disableTerminalPrompt(cmd)
out, err := cmd.CombinedOutput()
if stampCandidate && !existedBefore {
stampNewlyCreatedRemote(ctx, opts.Dir, stampURL)
}
if err != nil {
return out, fmt.Errorf("git fetch: %w", err)
}
return out, nil
}
// markPromisorEntrySkipped excludes the URL-keyed config section that git
// creates for a filtered URL fetch (remote.<url>.promisor=true) from
// `git fetch --all` and `git remote update`. Git needs the promisor entry to
// lazy-fetch filtered-out objects later, but the entry also makes the URL show
// up as a fetchable remote, so without this every checkpoint URL ever fetched
// from lingers as a phantom remote that bulk fetches keep dialing.
// Best-effort: the fetch already succeeded, so failures only log.
func markPromisorEntrySkipped(ctx context.Context, dir, url string) {
if !gitConfigBool(ctx, dir, "remote."+url+".promisor") {
// Git didn't record a promisor entry for this URL; don't invent a
// config section that wouldn't otherwise exist.
return
// stampNewlyCreatedRemote stamps a URL-keyed remote section that this fetch just
// created. Git writes remote.<url>.promisor eagerly during connection setup, so
// a filtered fetch that later fails still leaves the phantom remote behind;
// stamping here — rather than only on fetch success — keeps it from lingering
// unstamped forever (the section then exists on the next attempt, so it never
// looks "new" again). Re-checking existence keeps us from inventing a section
// when the fetch died before git wrote anything.
//
// The git-config commands run on a context detached from the fetch's deadline:
// a filtered fetch that timed out leaves ctx already past its deadline, and
// inheriting it would make these local commands fail immediately and leave the
// phantom unstamped — the very miss this stamping exists to prevent.
func stampNewlyCreatedRemote(ctx context.Context, dir, url string) {
ctx, cancel := context.WithTimeout(context.WithoutCancel(ctx), stampConfigTimeout)
defer cancel()
if gitRemoteSectionExists(ctx, dir, url) {
markRemoteSkipped(ctx, dir, url)
}
for _, key := range []string{"skipFetchAll", "skipDefaultUpdate"} {
fullKey := "remote." + url + "." + key
if gitConfigBool(ctx, dir, fullKey) {
// Checked per key so a partially-stamped entry (e.g. an earlier
// run failing between the two writes) still gets completed.
continue
}
cmd := exec.CommandContext(ctx, "git", "config", "--local", fullKey, "true")
if dir != "" {
cmd.Dir = dir
}
if out, cfgErr := cmd.CombinedOutput(); cfgErr != nil {
redactedURL := RedactURL(url)
// The output can echo the key, which embeds the URL — and a URL
// can carry credentials. Redact before logging.
msg := strings.TrimSpace(strings.ReplaceAll(string(out), url, redactedURL))
logging.Warn(ctx, "failed to mark promisor config entry as skipped for bulk fetches",
slog.String("url", redactedURL),
slog.String("key", key),
slog.String("output", msg),
slog.String("error", cfgErr.Error()),
)
return
}
}
// markRemoteSkipped stamps skipFetchAll on a URL-keyed remote section so
// `git fetch --all` and `git remote update` skip it. Called only for remotes
// this fetch just created, so an adhoc checkpoint URL never lingers as a phantom
// remote that bulk fetches keep dialing.
// Best-effort: the git config write is not worth failing the fetch over, so
// failures only log.
func markRemoteSkipped(ctx context.Context, dir, url string) {
fullKey := "remote." + url + ".skipFetchAll"
cmd := exec.CommandContext(ctx, "git", "config", "--local", fullKey, "true")
if dir != "" {
cmd.Dir = dir
}
if out, cfgErr := cmd.CombinedOutput(); cfgErr != nil {
redactedURL := RedactURL(url)
// The output can echo the key, which embeds the URL — and a URL can
// carry credentials. Redact before logging.
msg := strings.TrimSpace(strings.ReplaceAll(string(out), url, redactedURL))
logging.Warn(ctx, "failed to mark remote config entry as skipped for bulk fetches",
slog.String("url", redactedURL),
slog.String("output", msg),
slog.String("error", cfgErr.Error()),
)
}
}
// gitConfigBool reads a local git config key and reports whether it is set to
// a true value. Missing keys and read errors report false.
func gitConfigBool(ctx context.Context, dir, key string) bool {
cmd := exec.CommandContext(ctx, "git", "config", "--local", "--get", "--type=bool", key)
// gitRemoteSectionExists reports whether a remote.<url>.* config section already
// exists in the local git config. Used to tell whether a filtered URL fetch is
// about to create a new URL-keyed remote, so we only stamp remotes we create and
// never rewrite ones the user already has.
func gitRemoteSectionExists(ctx context.Context, dir, url string) bool {
cmd := exec.CommandContext(ctx, "git", "config", "--local", "--list", "--name-only")
if dir != "" {
cmd.Dir = dir
}
1 unmodified line
if err != nil {
return false
}
return strings.TrimSpace(string(out)) == "true"
// Each name is "remote.<url>.<key>". Git config keys carry no dots, so the
// final dotted component is the key and everything between "remote." and it
// is the subsection (the URL, whose case git preserves). Compare the
// subsection exactly so a longer URL that shares a prefix (e.g. a
// ".../repo.git" section vs a ".../repo" fetch) is not a false match.
for line := range strings.SplitSeq(string(out), "\n") {
rest, ok := strings.CutPrefix(line, "remote.")
if !ok {
continue
}
lastDot := strings.LastIndexByte(rest, '.')
if lastDot < 0 {
continue
}
if rest[:lastDot] == url {
return true
}
}
return false
}
// FetchBlobs fetches specific objects (typically blobs) by hash from a remote.
Mcmd/entire/cli/checkpoint/remote/git.go+103/-56
791 unmodified lines
792
793
794
795
796
797
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
800
814
815
816
817
46 unmodified lines
864
865
866
853
854
867
868
869
2 unmodified lines
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
864
865
926
927
928
929
930
32 unmodified lines
963
964
965
904
966
967
907
908
909
910
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1 unmodified line
1027
1028
1029
918
919
920
921
1030
1031
1032
924
925
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
791 unmodified lines
return m
}
// TestFetch_FilteredURLFetchMarksPromisorSkipped verifies that after a
// filtered fetch from a URL, the URL-keyed promisor config section git creates
// is excluded from `git fetch --all` / `git remote update` — otherwise every
// gitConfigBool reads a local git config key and reports whether it is set to a
// true value. Missing keys and read errors report false.
func gitConfigBool(ctx context.Context, dir, key string) bool {
cmd := exec.CommandContext(ctx, "git", "config", "--local", "--get", "--type=bool", key)
if dir != "" {
cmd.Dir = dir
}
out, err := cmd.Output()
if err != nil {
return false
}
return strings.TrimSpace(string(out)) == "true"
}
// TestFetch_FilteredURLFetchMarksNewRemoteSkipped verifies that when a filtered
// fetch from a URL creates a new URL-keyed remote section, that section is
// excluded from `git fetch --all` / `git remote update` — otherwise every
// checkpoint URL ever fetched from lingers as a phantom remote that bulk
// fetches keep dialing.
func TestFetch_FilteredURLFetchMarksPromisorSkipped(t *testing.T) {
func TestFetch_FilteredURLFetchMarksNewRemoteSkipped(t *testing.T) {
ctx := context.Background()
tmpDir := t.TempDir()
46 unmodified lines
assert.True(t, gitConfigBool(ctx, cloneDir, "remote."+fetchURL+".skipFetchAll"),
"URL-keyed promisor entry should be excluded from git fetch --all")
assert.True(t, gitConfigBool(ctx, cloneDir, "remote."+fetchURL+".skipDefaultUpdate"),
"URL-keyed promisor entry should be excluded from git remote update")
// git fetch --all must no longer dial the phantom entry: with the
// checkpoint repo gone, --all only succeeds if the URL-keyed entry is
2 unmodified lines
runIsolatedGit(ctx, t, cloneDir, "fetch", "--all", "--no-auto-gc")
}
// TestFetch_FailedFilteredFetchStillStampsNewRemote guards the resume
// regression: git writes remote.<url>.promisor eagerly during connection
// setup, so a filtered fetch that then fails (e.g. a missing ref) still leaves
// the phantom remote behind. The stamp must land anyway — otherwise the section
// exists on the next attempt, never looks new again, and lingers unstamped.
func TestFetch_FailedFilteredFetchStillStampsNewRemote(t *testing.T) {
ctx := context.Background()
tmpDir := t.TempDir()
originBare := filepath.Join(tmpDir, "origin.git")
checkpointBare := filepath.Join(tmpDir, "checkpoints.git")
seedDir := filepath.Join(tmpDir, "seed")
cloneDir := filepath.Join(tmpDir, "clone")
testutil.InitRepo(t, seedDir)
testutil.WriteFile(t, seedDir, "f.txt", "init")
testutil.GitAdd(t, seedDir, "f.txt")
testutil.GitCommit(t, seedDir, "init")
runIsolatedGit(ctx, t, "", "init", "--bare", originBare)
runIsolatedGit(ctx, t, "", "init", "--bare", checkpointBare)
runIsolatedGit(ctx, t, checkpointBare, "config", "uploadpack.allowFilter", "true")
runIsolatedGit(ctx, t, seedDir, "push", originBare, "HEAD:refs/heads/main")
runIsolatedGit(ctx, t, "", "clone", "--branch", "main", "file://"+originBare, cloneDir)
testutil.WriteFile(
t,
cloneDir,
".entire/settings.json",
`{"enabled": true, "strategy_options": {"filtered_fetches": true}}`,
)
t.Chdir(cloneDir)
fetchURL := "file://" + checkpointBare
// Fetch a ref that does not exist on the checkpoint remote: the command
// fails, but git has already recorded the URL-keyed promisor section.
_, err := Fetch(ctx, FetchOptions{
Remote: fetchURL,
RefSpecs: []string{"+refs/heads/does-not-exist:refs/entire-fetch-tmp/x"},
NoTags: true,
Dir: cloneDir,
})
require.Error(t, err, "fetch of a missing ref should fail")
require.True(t, gitConfigBool(ctx, cloneDir, "remote."+fetchURL+".promisor"),
"git records the promisor section even when the fetch fails")
assert.True(t, gitConfigBool(ctx, cloneDir, "remote."+fetchURL+".skipFetchAll"),
"a phantom remote left by a failed fetch must still be stamped")
}
// TestFetch_UnfilteredFetchDoesNotCreateConfigSection verifies the stamp is
// gated on git having created a promisor entry: a plain (unfiltered) URL fetch
// must not invent a remote.<url> config section.
// gated on a filtered fetch: a plain (unfiltered) URL fetch records no
// URL-keyed section, so we must not invent a remote.<url> config section.
func TestFetch_UnfilteredFetchDoesNotCreateConfigSection(t *testing.T) {
ctx := context.Background()
32 unmodified lines
assert.False(t, gitConfigBool(ctx, cloneDir, "remote."+fetchURL+".promisor"))
assert.False(t, gitConfigBool(ctx, cloneDir, "remote."+fetchURL+".skipFetchAll"))
assert.False(t, gitConfigBool(ctx, cloneDir, "remote."+fetchURL+".skipDefaultUpdate"))
}
// TestMarkPromisorEntrySkipped_CompletesPartialStamp verifies the keys are
// checked independently: an entry with skipFetchAll already set (e.g. an
// earlier run failing between the two writes) still gets skipDefaultUpdate.
func TestMarkPromisorEntrySkipped_CompletesPartialStamp(t *testing.T) {
// TestFetch_ExistingURLRemoteNotReStamped verifies we only stamp remotes we
// create: a filtered fetch from a URL that already has a remote.<url> section
// must leave that section as-is rather than rewriting the user's git config.
func TestFetch_ExistingURLRemoteNotReStamped(t *testing.T) {
ctx := context.Background()
testutil.InitRepo(t, seedDir)
testutil.WriteFile(t, seedDir, "f.txt", "init")
testutil.GitAdd(t, seedDir, "f.txt")
testutil.GitCommit(t, seedDir, "init")
testutil.WriteFile(t, seedDir, "f.txt", "init\nnext\n")
testutil.GitAdd(t, seedDir, "f.txt")
testutil.GitCommit(t, seedDir, "next")
runIsolatedGit(ctx, t, seedDir, "push", checkpointBare, "HEAD:refs/heads/main")
testutil.WriteFile(
t,
cloneDir,
".entire/settings.json",
`{"enabled": true, "strategy_options": {"filtered_fetches": true}}`,
)
t.Chdir(cloneDir)
fetchURL := "file://" + checkpointBare
// Simulate a pre-existing URL-keyed remote (e.g. a phantom left by an older
// CLI). Its presence means the section already exists before our fetch.
runIsolatedGit(ctx, t, cloneDir, "config", "--local", "remote."+fetchURL+".promisor", "true")
out, err := Fetch(ctx, FetchOptions{
Remote: fetchURL,
RefSpecs: []string{"+refs/heads/main:refs/entire-fetch-tmp/main"},
NoTags: true,
Dir: cloneDir,
})
require.NoError(t, err, "fetch output: %s", out)
assert.False(t, gitConfigBool(ctx, cloneDir, "remote."+fetchURL+".skipFetchAll"),
"a remote that already existed must not be stamped")
}
// TestMarkRemoteSkipped_SetsSkipFetchAll verifies the helper stamps skipFetchAll.
func TestMarkRemoteSkipped_SetsSkipFetchAll(t *testing.T) {
t.Parallel()
ctx := context.Background()
1 unmodified line
testutil.InitRepo(t, repoDir)
const url = "https://example.com/org/checkpoints.git"
runIsolatedGit(ctx, t, repoDir, "config", "--local", "remote."+url+".promisor", "true")
runIsolatedGit(ctx, t, repoDir, "config", "--local", "remote."+url+".skipFetchAll", "true")
markPromisorEntrySkipped(ctx, repoDir, url)
markRemoteSkipped(ctx, repoDir, url)
assert.True(t, gitConfigBool(ctx, repoDir, "remote."+url+".skipFetchAll"))
assert.True(t, gitConfigBool(ctx, repoDir, "remote."+url+".skipDefaultUpdate"),
"partially-stamped entry should be completed")
}
// TestGitRemoteSectionExists reports true only once a remote.<url>.* key is set.
func TestGitRemoteSectionExists(t *testing.T) {
t.Parallel()
ctx := context.Background()
repoDir := t.TempDir()
testutil.InitRepo(t, repoDir)
const url = "https://example.com/org/checkpoints.git"
assert.False(t, gitRemoteSectionExists(ctx, repoDir, url))
runIsolatedGit(ctx, t, repoDir, "config", "--local", "remote."+url+".promisor", "true")
assert.True(t, gitRemoteSectionExists(ctx, repoDir, url))
}
// TestGitRemoteSectionExists_ExactSubsectionMatch verifies the check compares
// the whole URL subsection, not a prefix: a longer URL that shares a prefix
// (e.g. ".../repo.git") must not make a shorter one (".../repo") look present.
func TestGitRemoteSectionExists_ExactSubsectionMatch(t *testing.T) {
t.Parallel()
ctx := context.Background()
repoDir := t.TempDir()
testutil.InitRepo(t, repoDir)
const longURL = "https://example.com/org/repo.git"
const shortURL = "https://example.com/org/repo"
runIsolatedGit(ctx, t, repoDir, "config", "--local", "remote."+longURL+".promisor", "true")
assert.True(t, gitRemoteSectionExists(ctx, repoDir, longURL),
"the exact URL section is present")
assert.False(t, gitRemoteSectionExists(ctx, repoDir, shortURL),
"a prefix of an existing URL section must not count as present")
}
// TestStampNewlyCreatedRemote_StampsUnderCancelledContext guards the timed-out
// fetch case: git records the promisor section before the fetch times out, so
// the stamp must still land even though the fetch context is already cancelled.
func TestStampNewlyCreatedRemote_StampsUnderCancelledContext(t *testing.T) {
t.Parallel()
repoDir := t.TempDir()
testutil.InitRepo(t, repoDir)
const url = "https://example.com/org/checkpoints.git"
// Simulate git having recorded the promisor section during a fetch that
// then timed out.
runIsolatedGit(context.Background(), t, repoDir, "config", "--local", "remote."+url+".promisor", "true")
ctx, cancel := context.WithCancel(context.Background())
cancel() // parent context already done, as after a timed-out fetch
stampNewlyCreatedRemote(ctx, repoDir, url)
assert.True(t, gitConfigBool(context.Background(), repoDir, "remote."+url+".skipFetchAll"),
"stamp must land even though the parent context is cancelled")
}
Mcmd/entire/cli/checkpoint/remote/git_test.go+184/-19
2 unmodified lines
3
4
5
6
7
8
9
30 unmodified lines
40
41
42
42
43
44
45
46
2 unmodified lines
import (
"errors"
"github.com/entireio/cli/cmd/entire/cli/experimental"
"github.com/entireio/cli/cmd/entire/cli/paths"
"github.com/spf13/cobra"
)
30 unmodified lines
cmd.AddCommand(newCheckpointResumeCmd())
cmd.AddCommand(newExplainCmd())
cmd.AddCommand(newCheckpointTokensCmd())
cmd.AddCommand(newCheckpointPolicyCmd())
experimental.Register(cmd, newCheckpointPolicyCmd()) // 'checkpoint policy' (experimental)
cmd.AddCommand(newRewindCmd())
cmd.AddCommand(newCheckpointSearchCmd())
Mcmd/entire/cli/checkpoint_group.go+2/-1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// Package experimental gates the visibility of experimental CLI commands.
//
// Experimental commands stay fully runnable in every build; this package only
// controls whether they appear in `entire help`. Developer builds (go build,
// go run, mise) show them, grouped under an "Experimental commands:" help
// section. Release builds (GoReleaser) hide them.
package experimental
import "github.com/spf13/cobra"
// Visible controls whether experimental commands are shown in help. It is
// stamped by GoReleaser via ldflags
// (-X github.com/entireio/cli/cmd/entire/cli/experimental.Visible=false)
// to hide them in shipped binaries. It defaults to "true", so every
// non-release build (go build, go run, mise) shows them. The commands remain
// experimental and fully runnable regardless of this flag — it only toggles
// visibility.
var Visible = "true"
// IsVisible reports whether experimental commands are shown in help.
func IsVisible() bool { return Visible != "false" }
// GroupID is the cobra group experimental commands are filed under.
const GroupID = "experimental"
const groupTitle = "Experimental commands:"
// Register adds child under parent as an experimental command.
//
// When experimental commands are visible, child is filed under parent's
// "Experimental commands:" help group (registering the group on parent once).
// When hidden, child is marked Hidden and left ungrouped — so release help
// never carries an empty group header, and cobra never references a group ID
// that was not registered.
//
// Register overrides any Hidden value the child's constructor set, so callers
// do not need to touch the constructors (including ones in other packages).
func Register(parent, child *cobra.Command) {
if IsVisible() {
if !parent.ContainsGroup(GroupID) {
parent.AddGroup(&cobra.Group{ID: GroupID, Title: groupTitle})
}
child.Hidden = false
child.GroupID = GroupID
} else {
child.Hidden = true
child.GroupID = ""
}
parent.AddCommand(child)
}
Acmd/entire/cli/experimental/experimental.go+50
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
package experimental
import (
"testing"
"github.com/spf13/cobra"
)
// setVisible sets the package-global Visible for the duration of the test and
// restores it afterward. Mutating a global means these tests cannot run in
// parallel.
func setVisible(t *testing.T, v string) {
t.Helper()
prev := Visible
Visible = v
t.Cleanup(func() { Visible = prev })
}
func TestIsVisible(t *testing.T) {
tests := []struct {
value string
want bool
}{
{"true", true},
{"false", false},
{"", true}, // only the literal "false" hides
{"anything", true}, // any non-"false" stamp is treated as visible
}
for _, tt := range tests {
t.Run(tt.value, func(t *testing.T) {
setVisible(t, tt.value)
if got := IsVisible(); got != tt.want {
t.Fatalf("IsVisible() with Visible=%q = %v, want %v", tt.value, got, tt.want)
}
})
}
}
func TestRegister_Visible(t *testing.T) {
setVisible(t, "true")
parent := &cobra.Command{Use: "parent"}
child := &cobra.Command{Use: "child", Hidden: true} // constructor-set Hidden must be overridden
Register(parent, child)
if child.Hidden {
t.Error("child should be visible when experimental commands are visible")
}
if child.GroupID != GroupID {
t.Errorf("child.GroupID = %q, want %q", child.GroupID, GroupID)
}
if !parent.ContainsGroup(GroupID) {
t.Error("parent should have the experimental group registered")
}
if len(parent.Commands()) != 1 || parent.Commands()[0] != child {
t.Error("child should be added to parent")
}
}
func TestRegister_Hidden(t *testing.T) {
setVisible(t, "false")
parent := &cobra.Command{Use: "parent"}
child := &cobra.Command{Use: "child"}
Register(parent, child)
if !child.Hidden {
t.Error("child should be hidden when experimental commands are hidden")
}
if child.GroupID != "" {
t.Errorf("child.GroupID = %q, want empty (no group referenced in release)", child.GroupID)
}
if parent.ContainsGroup(GroupID) {
t.Error("parent should not register the experimental group in release builds")
}
if len(parent.Commands()) != 1 || parent.Commands()[0] != child {
t.Error("child should still be added to parent")
}
}
// TestRegister_MultipleShareOneGroup verifies the group is registered once even
// when several experimental commands are registered under the same parent.
func TestRegister_MultipleShareOneGroup(t *testing.T) {
setVisible(t, "true")
parent := &cobra.Command{Use: "parent"}
Register(parent, &cobra.Command{Use: "a"})
Register(parent, &cobra.Command{Use: "b"})
groups := parent.Groups()
count := 0
for _, g := range groups {
if g.ID == GroupID {
count++
}
}
if count != 1 {
t.Errorf("experimental group registered %d times, want 1", count)
}
}
Acmd/entire/cli/experimental/experimental_test.go+100
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
package cli
import (
"testing"
"github.com/entireio/cli/cmd/entire/cli/experimental"
"github.com/spf13/cobra"
)
// experimentalRootCommands are the top-level commands gated behind the
// experimental visibility flag. Names match cobra's Command.Name() (the first
// token of Use).
var experimentalRootCommands = []string{
"tokens", "import", "review", "investigate",
"blame", "why", "search", "experts", "runner",
}
// withVisible sets the experimental visibility flag for the test and restores
// it afterward. Because it mutates a package global, callers must not run in
// parallel.
func withVisible(t *testing.T, v string) {
t.Helper()
prev := experimental.Visible
experimental.Visible = v
t.Cleanup(func() { experimental.Visible = prev })
}
func findCommand(parent *cobra.Command, name string) *cobra.Command {
for _, c := range parent.Commands() {
if c.Name() == name {
return c
}
}
return nil
}
// checkpointPolicy returns the `checkpoint policy` command.
func checkpointPolicy(t *testing.T, root *cobra.Command) *cobra.Command {
t.Helper()
cp := findCommand(root, "checkpoint")
if cp == nil {
t.Fatal("checkpoint command not found on root")
}
return findCommand(cp, "policy")
}
// TestExperimental_VisibleInDevBuild verifies that, in a developer build
// (Visible defaults to "true"), the experimental commands are shown and filed
// under the experimental group. Cannot use t.Parallel — mutates a global.
func TestExperimental_VisibleInDevBuild(t *testing.T) {
withVisible(t, "true")
root := NewRootCmd()
if !root.ContainsGroup(experimental.GroupID) {
t.Fatal("root should register the experimental group in a dev build")
}
for _, name := range experimentalRootCommands {
cmd := findCommand(root, name)
if cmd == nil {
t.Errorf("%q not found on root", name)
continue
}
if cmd.Hidden {
t.Errorf("%q should be visible in a dev build", name)
}
if cmd.GroupID != experimental.GroupID {
t.Errorf("%q GroupID = %q, want %q", name, cmd.GroupID, experimental.GroupID)
}
}
policy := checkpointPolicy(t, root)
if policy == nil {
t.Fatal("checkpoint policy not found")
}
if policy.Hidden {
t.Error("checkpoint policy should be visible in a dev build")
}
if policy.GroupID != experimental.GroupID {
t.Errorf("checkpoint policy GroupID = %q, want %q", policy.GroupID, experimental.GroupID)
}
}
// TestExperimental_HiddenInReleaseBuild verifies that, when GoReleaser stamps
// Visible=false, the experimental commands are hidden, carry no group, and the
// empty experimental group is never registered (so release help is unchanged).
// Cannot use t.Parallel — mutates a global.
func TestExperimental_HiddenInReleaseBuild(t *testing.T) {
withVisible(t, "false")
root := NewRootCmd()
if root.ContainsGroup(experimental.GroupID) {
t.Error("root should not register the experimental group in a release build")
}
for _, name := range experimentalRootCommands {
cmd := findCommand(root, name)
if cmd == nil {
t.Errorf("%q not found on root", name)
continue
}
if !cmd.Hidden {
t.Errorf("%q should be hidden in a release build", name)
}
if cmd.GroupID != "" {
t.Errorf("%q GroupID = %q, want empty in a release build", name, cmd.GroupID)
}
}
policy := checkpointPolicy(t, root)
if policy == nil {
t.Fatal("checkpoint policy not found")
}
if !policy.Hidden {
t.Error("checkpoint policy should be hidden in a release build")
}
}
Acmd/entire/cli/experimental_wiring_test.go+117
11 unmodified lines
12
13
14
15
16
17
18
19
98 unmodified lines
118
119
120
119
121
122
123
124
2 unmodified lines
127
128
129
128
129
130
131
132
133
134
135
136
11 unmodified lines
"strings"
"testing"
"github.com/entireio/cli/cmd/entire/cli/experimental"
"charm.land/lipgloss/v2"
"github.com/entireio/cli/cmd/entire/cli/palette"
"github.com/entireio/cli/cmd/entire/cli/paths"
98 unmodified lines
}`
}
func TestExpertsCommandIsHiddenAndListedInLabs(t *testing.T) {
func TestExpertsCommandIsExperimentalAndListedInLabs(t *testing.T) {
root := NewRootCmd()
cmd, _, err := root.Find([]string{"experts"})
if err != nil {
2 unmodified lines
if cmd.Name() != "experts" {
t.Fatalf("found command %q, want experts", cmd.Name())
}
if !cmd.Hidden {
t.Fatal("experts command should be hidden while in labs")
// Gated as experimental: visible and grouped in developer builds
// (the default test build), hidden in shipped releases.
if cmd.GroupID != experimental.GroupID {
t.Fatalf("experts GroupID = %q, want %q (experimental)", cmd.GroupID, experimental.GroupID)
}
if !strings.Contains(labsOverview(), "entire experts") {
t.Fatalf("labs overview missing experts:\n%s", labsOverview())
Mcmd/entire/cli/experts_test.go+7/-3
312 unmodified lines
313
314
315
316
316
317
318
319
320
321
322
323
324
325
323
324
325
326
327
312 unmodified lines
var gitConfigGuardRepositoryFormatVersionRE = regexp.MustCompile(`(?m)^([ \t]*)repositoryformatversion = [01]$`)
var gitConfigGuardTransportPromisorRemoteRE = regexp.MustCompile(
`(?m)^\[remote "(?:(?:https?|ssh|file)://|/|[A-Za-z]:[\\/]|[^"\n]+@[^"\n]+:[^"\n]+).+"\]\n(?:[ \t]+(?:promisor = true|partialclonefilter = blob:none|skipFetchAll = true|skipDefaultUpdate = true)\n?){2,4}`,
`(?m)^\[remote "(?:(?:https?|ssh|file)://|/|[A-Za-z]:[\\/]|[^"\n]+@[^"\n]+:[^"\n]+).+"\]\n(?:[ \t]+(?:promisor = true|partialclonefilter = blob:none|skipFetchAll = true)\n?){2,3}`,
)
func normalizeGitConfigForGuard(content string) string {
content = gitConfigGuardRepositoryFormatVersionRE.ReplaceAllString(content, `${1}repositoryformatversion = <normalized>`)
// Deliberately ignore only the URL-keyed remote sections written during
// filtered fetches: git's promisor+partialclonefilter pair plus the
// skipFetchAll/skipDefaultUpdate stamp the CLI adds so bulk fetches skip
// the entry. A section without the full promisor pair (or with any other
// key) still fails loudly.
// skipFetchAll stamp the CLI adds so bulk fetches skip the entry. A section
// without the full promisor pair (or with any other key) still fails loudly.
content = gitConfigGuardTransportPromisorRemoteRE.ReplaceAllStringFunc(content, func(section string) string {
if strings.Contains(section, "promisor = true") && strings.Contains(section, "partialclonefilter = blob:none") {
return ""
Mcmd/entire/cli/integration_test/testenv.go+3/-4
79 unmodified lines
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
79 unmodified lines
os.Getenv("GIT_TERMINAL_PROMPT") == "0"
}
// IsTerminalReader reports whether r is an *os.File backed by a terminal.
// It is useful when an explicitly interactive command needs to distinguish a
// human at stdin from an agent process that merely inherited a controlling TTY.
func IsTerminalReader(r io.Reader) bool {
f, ok := r.(*os.File)
if !ok {
return false
}
return term.IsTerminal(int(f.Fd())) //nolint:gosec // G115: uintptr->int is safe for fd
}
// IsTerminalWriter reports whether w is an *os.File backed by a terminal.
// Use for deciding on color, pager, progress bars, or other writer-scoped
// TTY formatting. For "can I prompt the user?" use CanPromptInteractively.
Mcmd/entire/cli/interactive/interactive.go+11
3 unmodified lines
4
5
6
7
8
9
10
11
58 unmodified lines
70
71
72
71
72
73
74
75
76
77
78
79
80
81
8 unmodified lines
90
91
92
87
88
93
94
95
96
97
3 unmodified lines
"bytes"
"strings"
"testing"
"github.com/entireio/cli/cmd/entire/cli/experimental"
)
// TestBuildInvestigateDeps_HasRequiredFields asserts that the bridge
58 unmodified lines
}
// TestRootCommand_HasInvestigate confirms `entire investigate` is wired
// into the root command tree. It also checks that the command is
// Hidden (the experimental discovery happens via `entire labs`).
// into the root command tree as an experimental command. Experimental
// commands are gated by the build-time visibility flag (see the
// experimental package): shown and grouped in developer builds, hidden
// in shipped releases. This test runs with the default (developer)
// visibility, so it asserts the command is visible and filed under the
// experimental group.
func TestRootCommand_HasInvestigate(t *testing.T) {
t.Parallel()
8 unmodified lines
if cmd.Name() != "investigate" {
t.Fatalf("resolved command name = %q, want %q", cmd.Name(), "investigate")
}
if !cmd.Hidden {
t.Fatal("investigate should be Hidden during maturation")
if cmd.GroupID != experimental.GroupID {
t.Fatalf("investigate GroupID = %q, want %q (experimental)", cmd.GroupID, experimental.GroupID)
}
}
Mcmd/entire/cli/investigate_bridge_test.go+10/-4
4 unmodified lines
5
6
7
8
9
10
11
12
71 unmodified lines
84
85
86
85
86
87
87
88
89
90
91
92
93
94
93
95
96
97
97
98
99
100
101
102
103
104
105
106
107
108
109
110
101
102
103
104
105
106
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
4 unmodified lines
"strings"
"testing"
"unicode/utf8"
"github.com/entireio/cli/cmd/entire/cli/experimental"
)
func TestLabsCmd_PrintsExperimentalCommandList(t *testing.T) {
71 unmodified lines
}
}
func TestRootHelp_ShowsLabsButHidesReview(t *testing.T) {
t.Parallel()
// rootHelp renders `entire --help` and returns its stdout.
func rootHelp(t *testing.T) string {
t.Helper()
root := NewRootCmd()
var out bytes.Buffer
root.SetOut(&out)
root.SetErr(&bytes.Buffer{})
root.SetArgs([]string{"--help"})
if err := root.Execute(); err != nil {
t.Fatalf("entire --help failed: %v", err)
}
got := out.String()
return out.String()
}
// TestRootHelp_AlwaysShowsLabs confirms the labs command is present in root
// help regardless of the experimental visibility gate — labs is the always-on
// discovery entry point for experimental workflows.
func TestRootHelp_AlwaysShowsLabs(t *testing.T) {
t.Parallel()
got := rootHelp(t)
if !strings.Contains(got, "labs") || !strings.Contains(got, "Explore experimental Entire workflows") {
t.Fatalf("root help should include labs command, got:\n%s", got)
}
for _, hiddenExperimentalCommand := range []string{
"review",
"tokens Analyze token usage across sessions and checkpoints",
} {
if strings.Contains(got, hiddenExperimentalCommand) {
t.Fatalf("root help should not include %q while it is listed in labs, got:\n%s", hiddenExperimentalCommand, got)
}
// experimentalCommandMarkers are substrings that only appear in root help when
// experimental commands are visible.
var experimentalCommandMarkers = []string{
"Experimental commands:",
"review",
"tokens Analyze token usage across sessions and checkpoints",
}
// TestRootHelp_ReleaseHidesExperimental verifies a shipped build
// (experimental.Visible="false") omits experimental commands and the group
// header from root help. Mutates the global gate, so it cannot run in parallel.
func TestRootHelp_ReleaseHidesExperimental(t *testing.T) {
withVisible(t, "false")
got := rootHelp(t)
for _, marker := range experimentalCommandMarkers {
if strings.Contains(got, marker) {
t.Fatalf("release root help should not include %q, got:\n%s", marker, got)
}
}
}
// TestRootHelp_DevShowsExperimentalGroup verifies a developer build
// (experimental.Visible="true") shows experimental commands under the
// "Experimental commands:" group in root help. Mutates the global gate, so it
// cannot run in parallel.
func TestRootHelp_DevShowsExperimentalGroup(t *testing.T) {
withVisible(t, "true")
got := rootHelp(t)
if !strings.Contains(got, experimental.GroupID) && !strings.Contains(got, "Experimental commands:") {
t.Fatalf("dev root help should include the experimental group header, got:\n%s", got)
}
for _, marker := range experimentalCommandMarkers {
if !strings.Contains(got, marker) {
t.Fatalf("dev root help should include %q, got:\n%s", marker, got)
}
}
}
Mcmd/entire/cli/labs_test.go+53/-11
209 unmodified lines
210
211
212
213
213
214
215
216
53 unmodified lines
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
53 unmodified lines
364
365
366
332
367
368
369
370
419 unmodified lines
790
791
792
758
793
794
795
796
22 unmodified lines
819
820
821
787
822
823
824
825
151 unmodified lines
977
978
979
945
980
981
982
983
984
950
985
986
987
988
55 unmodified lines
1044
1045
1046
1012
1047
1048
1049
1050
1051
47 unmodified lines
1099
1100
1101
1066
1102
1103
1069
1104
1105
1106
1107
80 unmodified lines
1188
1189
1190
1156
1191
1192
1193
1194
1195
75 unmodified lines
1271
1272
1273
1238
1274
1275
1276
1277
67 unmodified lines
1345
1346
1347
1312
1313
1314
1315
1316
1348
1349
1350
1351
1352
1353
1354
209 unmodified lines
}, deps)
}
if edit {
if !interactive.IsTerminalWriter(cmd.OutOrStdout()) || !interactive.CanPromptInteractively() {
if !reviewCommandIsInteractive(cmd) {
err := errors.New("--edit requires an interactive terminal")
cmd.SilenceUsage = true
fmt.Fprintln(cmd.ErrOrStderr(), "--edit requires an interactive terminal.")
53 unmodified lines
Slots []string // reviewer slots as "agent[=model]" entries (--set-slot)
}
// reviewCommandIsInteractive requires the exact stdin consumed by huh and
// Bubble Tea, plus stdout, to be terminals. CanPromptInteractively adds the
// independent policy gate for tests, CI, and agent subprocess sentinels; a
// controlling /dev/tty alone is insufficient because stdin may still be piped.
func reviewCommandIsInteractive(cmd *cobra.Command) bool {
hardDisabled := reviewInteractivityHardDisabled(
os.Getenv(interactive.EnvTestTTY),
os.Getenv("CI"),
interactive.UnderTest(),
)
return reviewTTYIsInteractive(
interactive.IsTerminalReader(cmd.InOrStdin()),
interactive.IsTerminalWriter(cmd.OutOrStdout()),
interactive.CanPromptInteractively(),
hardDisabled,
)
}
func reviewInteractivityHardDisabled(testTTY, ci string, underTest bool) bool {
// Match CanPromptInteractively's precedence: ENTIRE_TEST_TTY=1 may opt an
// in-process test into interaction, while tests without that explicit
// override must never read from a developer's real terminal.
if testTTY != "" {
return testTTY != "1"
}
return underTest || (ci != "" && ci != "false")
}
func reviewTTYIsInteractive(stdinTTY, stdoutTTY, canPrompt, hardDisabled bool) bool {
// Real stdio terminals are necessary but not sufficient: agent shells can
// allocate a PTY while advertising that no human is available through the
// sentinels enforced by CanPromptInteractively.
return !hardDisabled && stdinTTY && stdoutTTY && canPrompt
}
func (o reviewConfigureOptions) scripted() bool {
// Local selects the destination only; by itself it must not force the
// non-interactive/scripted path. `entire review --configure --local` should
53 unmodified lines
// duplicate the catalog here. Pass the raw --profile value (empty when not
// given) so the guided setup runs the "what kind of review?" type picker
// instead of being silently defaulted to the general profile.
if interactive.IsTerminalWriter(out) && interactive.CanPromptInteractively() {
if reviewCommandIsInteractive(cmd) {
name, profile, setupErr := RunReviewGuidedSetup(ctx, out, installed, deps.ReviewerFor, strings.TrimSpace(profileOverride), false, s)
if setupErr != nil {
return handlePickerError(cmd, silentErr, setupErr)
419 unmodified lines
applyLegacyReviewProfileFallback(s)
profileOverride = strings.TrimSpace(profileOverride)
interactiveTTY := interactive.IsTerminalWriter(out) && interactive.CanPromptInteractively()
interactiveTTY := reviewCommandIsInteractive(cmd)
// Bare `entire review` never auto-runs a profile. Without a TTY we cannot
// prompt, so list the profiles (or point at setup) and require an explicit
22 unmodified lines
// Non-interactive first run writes the shared project settings; interactive
// setup asks the user where to save below.
saveScope := reviewScopeProject
guidedSetup := interactive.IsTerminalWriter(out) && interactive.CanPromptInteractively()
guidedSetup := interactiveTTY
if guidedSetup {
var setupErr error
profileForSetup, profile, setupErr = RunReviewGuidedSetup(ctx, out, installed, deps.ReviewerFor, profileForSetup, true, s)
151 unmodified lines
// (true, nil). In a non-interactive context it cannot prompt, so it proceeds
// (the user explicitly invoked `entire review`) after printing a note rather
// than blocking on a confirm form that would error out.
func confirmReReviewOrProceed(ctx context.Context, out io.Writer, deps Deps) (bool, error) {
func confirmReReviewOrProceed(ctx context.Context, out io.Writer, deps Deps, canPrompt bool) (bool, error) {
reviewed, meta := deps.HeadHasReviewCheckpoint(ctx)
if !reviewed {
return true, nil
}
if !interactive.CanPromptInteractively() {
if !canPrompt {
fmt.Fprintf(out, "Note: HEAD was already reviewed (%s); re-running.\n", meta)
return true, nil
}
55 unmodified lines
}
// 4. Re-run guard: check if HEAD's checkpoint already has a review.
if proceed, guardErr := confirmReReviewOrProceed(ctx, out, deps); guardErr != nil {
canPrompt := reviewCommandIsInteractive(cmd)
if proceed, guardErr := confirmReReviewOrProceed(ctx, out, deps, canPrompt); guardErr != nil {
fmt.Fprintln(out, "prompt cancelled")
return silentErr(guardErr)
} else if !proceed {
47 unmodified lines
defer cancelRun()
runCfg.EnrichSummary = reviewSummaryTokenEnricher(worktreeRoot, headSHA)
canPrompt := interactive.CanPromptInteractively()
sinks := composeSingleAgentSinks(singleAgentSinkInputs{
out: out,
isTTY: interactive.IsTerminalWriter(out) && canPrompt,
isTTY: canPrompt,
canPrompt: canPrompt,
agentName: displayName,
cancelRun: cancelRun,
80 unmodified lines
return fmt.Errorf("resolve HEAD: %w", shaErr)
}
if proceed, guardErr := confirmReReviewOrProceed(ctx, out, deps); guardErr != nil {
canPrompt := reviewCommandIsInteractive(cmd)
if proceed, guardErr := confirmReReviewOrProceed(ctx, out, deps, canPrompt); guardErr != nil {
fmt.Fprintln(out, "prompt cancelled")
return deps.NewSilentError(guardErr)
} else if !proceed {
75 unmodified lines
masterLabel := judgeLabel(judge)
sinks := composeMultiAgentSinks(multiAgentSinkInputs{
out: out,
isTTY: interactive.IsTerminalWriter(out) && interactive.CanPromptInteractively(),
isTTY: canPrompt,
agentNames: agentNames,
cancelRun: cancelRun,
runContext: runCtx,
67 unmodified lines
// instead of monkey-patching interactive helpers at run time.
//
// isTTY here means "the TUI sink is safe to compose" — production callers
// AND IsTerminalWriter(out) with CanPromptInteractively() before passing
// it in, since the TUI both writes ANSI to stdout AND reads keypresses
// from stdin. A terminal-stdout-but-non-interactive-stdin scenario (an
// agent host like Claude Code invoking `entire review`) must NOT use the
// TUI — its dismissal loop would block forever.
// use reviewCommandIsInteractive before passing it in, since the TUI both
// writes ANSI to stdout and reads keypresses from stdin. A terminal stdout
// with non-interactive stdin must not use the TUI; its dismissal loop would
// block forever.
type multiAgentSinkInputs struct {
out io.Writer
isTTY bool
Mcmd/entire/cli/review/cmd.go+51/-16
1237 unmodified lines
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1243
1244
1245
1246
1247
1305
1306
1307
1308
1309
1250
1251
1310
1311
1312
1313
1314
2 unmodified lines
1317
1318
1319
1260
1320
1321
1322
1323
37 unmodified lines
1361
1362
1363
1304
1364
1365
1366
1367
6 unmodified lines
1374
1375
1376
1317
1377
1378
1379
1380
1237 unmodified lines
}
}
// TestDispatchFork_LegacyGeneratedCodexSkillIsRepairedAndLaunched prevents
// guided setup's historical /review default from silently removing Codex from
// a multi-agent run. The compatibility repair must reach dispatch, not merely
// make the profile look valid in listing/configuration code.
func TestDispatchFork_LegacyGeneratedCodexSkillIsRepairedAndLaunched(t *testing.T) {
setupCmdTestRepo(t)
t.Setenv("HOME", t.TempDir())
if err := seedReviewConfig(context.Background(), map[string]settings.ReviewConfig{
testAgentName: {Skills: []string{"/review"}},
testCodexAgent: {
Skills: []string{"/review"},
},
}); err != nil {
t.Fatal(err)
}
claudeReviewer := &captureRunConfigReviewer{name: testAgentName}
codexReviewer := &captureRunConfigReviewer{name: testCodexAgent}
deps := review.Deps{
GetAgentsWithHooksInstalled: func(_ context.Context) []types.AgentName {
return []types.AgentName{testAgentName, testCodexAgent}
},
NewSilentError: func(err error) error { return err },
HeadHasReviewCheckpoint: func(_ context.Context) (bool, string) {
return false, ""
},
ReviewerFor: func(agentName string) reviewtypes.AgentReviewer {
switch agentName {
case testAgentName:
return claudeReviewer
case testCodexAgent:
return codexReviewer
default:
return nil
}
},
}
cmd := review.NewCommand(deps)
cmd.SetOut(&bytes.Buffer{})
errBuf := &bytes.Buffer{}
cmd.SetErr(errBuf)
cmd.SetArgs([]string{"general"})
if err := cmd.Execute(); err != nil {
t.Fatalf("run legacy generated profile: %v", err)
}
if !codexReviewer.called {
t.Fatalf("Codex was silently excluded; stderr:\n%s", errBuf.String())
}
if len(codexReviewer.got.Skills) != 0 {
t.Fatalf("Codex received obsolete generated skills %v, want none", codexReviewer.got.Skills)
}
if codexReviewer.got.AlwaysPrompt != "Review the change according to the profile task." {
t.Fatalf("Codex repaired prompt = %q", codexReviewer.got.AlwaysPrompt)
}
if strings.Contains(errBuf.String(), "skipping reviewer codex") {
t.Fatalf("Codex was reported as skipped:\n%s", errBuf.String())
}
}
// TestDispatchFork_InvalidSkillExcludesWorkerNotWholeCrew pins the blast
// radius of spawn-time skill validation in multi-agent runs: a worker whose
// configured skill no longer validates (e.g. codex's legacy auto-preselected
// "/review", orphaned when the curated builtin was removed) is excluded with
// a loud warning, and the remaining reviewers still run. Aborting the whole
// crew for one stale entry held every other agent hostage to a codex
// reconfigure.
// explicitly configured skill no longer validates is excluded with a loud
// warning, and the remaining reviewers still run. Aborting the whole crew for
// one stale entry would hold every other agent hostage to a reconfigure.
func TestDispatchFork_InvalidSkillExcludesWorkerNotWholeCrew(t *testing.T) {
setupCmdTestRepo(t)
// Controlled empty HOME: codex discovery finds nothing, so its "/review"
// (no longer a curated builtin) fails validation. Cannot t.Parallel —
// Controlled empty HOME: Codex discovery finds nothing, so the configured
// custom skill fails validation. Cannot t.Parallel —
// t.Setenv (setupCmdTestRepo already precludes it via t.Chdir).
t.Setenv("HOME", t.TempDir())
2 unmodified lines
Skills: []string{"/review"},
},
testCodexAgent: {
Skills: []string{"/review"}, // stale legacy entry
Skills: []string{"$missing-review"},
},
}); err != nil {
t.Fatal(err)
37 unmodified lines
t.Error("codex reviewer started despite failing skill validation")
}
stderr := errBuf.String()
if !strings.Contains(stderr, "/review") || !strings.Contains(stderr, "skipping") {
if !strings.Contains(stderr, "$missing-review") || !strings.Contains(stderr, "skipping") {
t.Errorf("stderr should warn about the excluded worker and its skill; got:\n%s", stderr)
}
}
6 unmodified lines
t.Setenv("HOME", t.TempDir())
if err := seedReviewConfig(context.Background(), map[string]settings.ReviewConfig{
testCodexAgent: {Skills: []string{"/review"}},
testCodexAgent: {Skills: []string{"$missing-review"}},
"gemini": {Skills: []string{"$also-missing"}},
}); err != nil {
t.Fatal(err)
Mcmd/entire/cli/review/cmd_test.go+70/-10
42 unmodified lines
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
42 unmodified lines
}
}
func TestDefaultReviewAgentConfig_CodexIsPromptOnly(t *testing.T) {
t.Parallel()
cfg := defaultReviewAgentConfig(DefaultProfileName, tAgentCodex)
if len(cfg.Skills) != 0 {
t.Fatalf("Codex default skills = %v, want none", cfg.Skills)
}
if cfg.Prompt != defaultAgentReviewPrompt {
t.Fatalf("Codex default prompt = %q, want %q", cfg.Prompt, defaultAgentReviewPrompt)
}
}
func TestApplyLegacyReviewProfileFallback_RepairsGeneratedCodexSkill(t *testing.T) {
t.Parallel()
s := &settings.EntireSettings{ReviewProfiles: map[string]settings.ReviewProfileConfig{
DefaultProfileName: {Agents: map[string]settings.ReviewConfig{
tAgentCodex: {Skills: []string{"/review"}},
"codex-opus": {
Agent: tAgentCodex,
Model: "o3",
Skills: []string{"/review"},
},
"codex-custom": {
Agent: tAgentCodex,
Skills: []string{"$security-audit"},
},
}},
}}
applyLegacyReviewProfileFallback(s)
got := s.ReviewProfiles[DefaultProfileName].Agents[tAgentCodex]
if len(got.Skills) != 0 || got.Prompt != defaultAgentReviewPrompt {
t.Fatalf("repaired Codex config = %+v, want prompt-only default", got)
}
alias := s.ReviewProfiles[DefaultProfileName].Agents["codex-opus"]
if len(alias.Skills) != 0 || alias.Prompt != defaultAgentReviewPrompt || alias.Model != "o3" {
t.Fatalf("repaired aliased Codex config = %+v", alias)
}
custom := s.ReviewProfiles[DefaultProfileName].Agents["codex-custom"]
if len(custom.Skills) != 1 || custom.Skills[0] != "$security-audit" {
t.Fatalf("custom Codex config changed: %+v", custom)
}
}
func TestConfirmReReviewOrProceed_NonInteractiveDoesNotPrompt(t *testing.T) {
t.Parallel()
out := &bytes.Buffer{}
proceed, err := confirmReReviewOrProceed(context.Background(), out, Deps{
HeadHasReviewCheckpoint: func(context.Context) (bool, string) {
return true, "existing review"
},
}, false)
if err != nil {
t.Fatalf("confirmReReviewOrProceed: %v", err)
}
if !proceed {
t.Fatal("non-interactive re-review should proceed")
}
if !strings.Contains(out.String(), "already reviewed") {
t.Fatalf("missing non-interactive re-review note: %q", out.String())
}
}
func TestReviewInteractivityHardDisabled(t *testing.T) {
t.Parallel()
tests := []struct {
name string
testTTY string
ci string
underTest bool
want bool
}{
{name: "go test defaults off", underTest: true, want: true},
{name: "test override enables", testTTY: "1", ci: "true", underTest: true, want: false},
{name: "test override disables", testTTY: "0", want: true},
{name: "CI disables", ci: "true", want: true},
{name: "CI false does not disable", ci: "false", want: false},
{name: "normal process", want: false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
if got := reviewInteractivityHardDisabled(tt.testTTY, tt.ci, tt.underTest); got != tt.want {
t.Fatalf("reviewInteractivityHardDisabled(%q, %q, %v) = %v, want %v", tt.testTTY, tt.ci, tt.underTest, got, tt.want)
}
})
}
}
func TestReviewTTYIsInteractive(t *testing.T) {
t.Parallel()
tests := []struct {
name string
stdinTTY bool
stdoutTTY bool
canPrompt bool
hardDisabled bool
want bool
}{
{name: "direct human terminal", stdinTTY: true, stdoutTTY: true, canPrompt: true, want: true},
{name: "agent sentinel overrides real PTY", stdinTTY: true, stdoutTTY: true, canPrompt: false, want: false},
{name: "controlling terminal does not override piped stdin", stdinTTY: false, stdoutTTY: true, canPrompt: true, want: false},
{name: "captured stdout", stdinTTY: true, stdoutTTY: false, canPrompt: true, want: false},
{name: "agent with piped stdin", stdinTTY: false, stdoutTTY: true, canPrompt: false, want: false},
{name: "explicitly forced non-interactive", stdinTTY: true, stdoutTTY: true, canPrompt: true, hardDisabled: true, want: false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
if got := reviewTTYIsInteractive(tt.stdinTTY, tt.stdoutTTY, tt.canPrompt, tt.hardDisabled); got != tt.want {
t.Fatalf("reviewTTYIsInteractive(%v, %v, %v, %v) = %v, want %v", tt.stdinTTY, tt.stdoutTTY, tt.canPrompt, tt.hardDisabled, got, tt.want)
}
})
}
}
func TestBuildConfiguredProfile_FromFlags(t *testing.T) {
t.Parallel()
deps := configureTestDeps("claude-code", "codex")
Mcmd/entire/cli/review/configure_test.go+120
11 unmodified lines
12
13
14
15
15
16
17
28 unmodified lines
46
47
48
50
49
50
51
52
11 unmodified lines
"charm.land/huh/v2"
"github.com/spf13/cobra"
"github.com/entireio/cli/cmd/entire/cli/interactive"
"github.com/entireio/cli/cmd/entire/cli/mdrender"
"github.com/entireio/cli/cmd/entire/cli/paths"
"github.com/entireio/cli/cmd/entire/cli/stringutil"
28 unmodified lines
fmt.Fprintln(cmd.OutOrStdout(), "No local review findings found.")
return nil
}
if interactive.IsTerminalWriter(cmd.OutOrStdout()) && interactive.CanPromptInteractively() {
if reviewCommandIsInteractive(cmd) {
manifest, pickErr := promptForReviewManifest(ctx, manifests)
if pickErr != nil {
return pickErr
Mcmd/entire/cli/review/fix.go+1/-2
115 unmodified lines
116
117
118
119
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
10 unmodified lines
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
200 unmodified lines
365
366
367
368
369
370
371
372
2 unmodified lines
375
376
377
350
351
352
353
378
379
380
381
382
115 unmodified lines
}
func applyLegacyReviewProfileFallback(s *settings.EntireSettings) {
if s == nil || len(nonZeroProfiles(s.ReviewProfiles)) > 0 {
if s == nil {
return
}
// Older guided setup wrote Codex reviewers with Claude's curated /review
// command. Codex has no such built-in, so spawn-time validation excludes
// those workers. Repair that generated shape in memory to a prompt-only
// Codex reviewer; explicitly configured Codex skills are left untouched.
normalizeLegacyCodexDefaultSkills(s.Review) //nolint:staticcheck // intentional compatibility repair for deprecated review config
for name, profile := range s.ReviewProfiles {
normalizeLegacyCodexDefaultSkills(profile.Agents)
s.ReviewProfiles[name] = profile
}
if len(nonZeroProfiles(s.ReviewProfiles)) > 0 {
return
}
legacyAgents := nonZeroAgentConfigs(s.Review) //nolint:staticcheck // intentional compatibility fallback for deprecated review config
10 unmodified lines
}
}
func normalizeLegacyCodexDefaultSkills(configs map[string]settings.ReviewConfig) {
for workerName, cfg := range configs {
if reviewAgentName(workerName, cfg) != string(agent.AgentNameCodex) ||
len(cfg.Skills) != 1 || strings.TrimSpace(cfg.Skills[0]) != "/review" {
continue
}
cfg.Skills = nil
if strings.TrimSpace(cfg.Prompt) == "" {
cfg.Prompt = defaultAgentReviewPrompt
}
configs[workerName] = cfg
}
}
func nonZeroProfiles(in map[string]settings.ReviewProfileConfig) map[string]settings.ReviewProfileConfig {
return nonZeroNamed(in)
}
200 unmodified lines
return profile, nil
}
const defaultAgentReviewPrompt = "Review the change according to the profile task."
func defaultReviewAgentConfig(profileName, agentName string) settings.ReviewConfig {
focus := defaultProfileFocus(profileName)
switch agentName {
2 unmodified lines
return settings.ReviewConfig{Skills: []string{"/security-review"}}
}
return settings.ReviewConfig{Skills: []string{"/review"}, Prompt: focus}
case string(agent.AgentNameCodex):
return settings.ReviewConfig{Skills: []string{"/review"}, Prompt: focus}
case string(agent.AgentNameGemini), string(agent.AgentNamePi):
prompt := "Review the change according to the profile task."
case string(agent.AgentNameCodex), string(agent.AgentNameGemini), string(agent.AgentNamePi):
prompt := defaultAgentReviewPrompt
if focus != "" {
prompt += " " + focus
}
Mcmd/entire/cli/review/profile.go+31/-5
8 unmodified lines
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
23
24
25
26
43
44
45
46
47
48
49
3 unmodified lines
53
54
55
36
56
57
58
59
60
61
62
42
63
64
65
66
67
68
27 unmodified lines
96
97
98
99
100
101
102
103
104
105
77
78
106
107
108
109
110
111
112
36 unmodified lines
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
123
124
172
173
174
175
176
177
178
179
180
181
182
1 unmodified line
184
185
186
132
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
135
136
137
138
139
140
203
204
205
206
207
208
209
1 unmodified line
211
212
213
148
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
10 unmodified lines
258
259
260
165
261
262
263
264
5 unmodified lines
270
271
272
177
273
274
275
276
8 unmodified lines
285
286
287
192
288
289
290
291
6 unmodified lines
298
299
300
205
206
207
208
209
210
211
212
301
302
303
304
305
306
307
308
216
217
309
310
311
3 unmodified lines
315
316
317
318
319
320
321
322
323
324
325
326
8 unmodified lines
import (
"context"
"io"
"log/slog"
"sync"
"time"
tea "charm.land/bubbletea/v2"
"golang.org/x/term"
"github.com/entireio/cli/cmd/entire/cli/logging"
reviewtypes "github.com/entireio/cli/cmd/entire/cli/review/types"
)
// teaRunner is the slice of *tea.Program the sink depends on, extracted so
// tests can substitute a program with a deterministically stalled event loop.
type teaRunner interface {
Run() (tea.Model, error)
Send(msg tea.Msg)
Kill()
}
// tuiSinkQueueCap bounds the sink's internal dispatch queue. Program.Send is
// an unbuffered BLOCKING send: if the Bubble Tea Update/render pipeline ever
// stalls, a direct Send from the orchestrator's dispatch goroutine parks
// forever — freezing sink dispatch, the fanIn drain loop, the parsers, and
// reviewer-timeout handling with it (observed live: the 2026-07-07 run-6
// wedge, where the TUI froze mid-run and a 20m --timeout never surfaced).
// The queue absorbs bursts; overflow beyond the cap is dropped and counted —
// a display that can lag must never backpressure the data plane.
const tuiSinkQueueCap = 4096
// TUISink is a Sink that renders a Bubble Tea dashboard. The orchestrator
// calls AgentEvent/RunFinished from a single goroutine (CU4 serial-dispatch
// contract); the sink translates each event into a tea.Msg and sends it via
// Program.Send. Bubble Tea's Send is thread-safe, but we never rely on that
// property — the serial-dispatch promise means Send is only called from the
// orchestrator's dispatch goroutine.
// contract); the sink translates each event into a tea.Msg, enqueues it on a
// bounded internal queue, and a pump goroutine forwards it via Program.Send —
// so only the pump can ever block on a stalled Bubble Tea loop, never the
// orchestrator.
//
// Cancellation: cancel is the same context.CancelFunc that controls the
// orchestrator's run context. The first KeyCtrlC in the dashboard fires this
3 unmodified lines
// root's context, which cancels the same function — no parallel signal.Notify
// goroutine is needed here.
type TUISink struct {
program *tea.Program
program teaRunner
mu sync.Mutex
started bool
finished bool
dropped int
done chan struct{} // closed when the tea.Program exits
msgs chan tea.Msg // bounded dispatch queue drained by the pump
done chan struct{} // closed when the tea.Program exits
pumpDone chan struct{} // closed when the pump goroutine exits
}
// Compile-time interface check.
27 unmodified lines
tea.WithInput(input),
tea.WithoutSignalHandler(), // SIGINT handled by cobra root; KeyCtrlC calls cancel directly
)
return newTUISinkWithProgram(prog)
}
// newTUISinkWithProgram wires a TUISink around any teaRunner; tests inject
// fakes with stalled or recording Send implementations.
func newTUISinkWithProgram(prog teaRunner) *TUISink {
return &TUISink{
program: prog,
done: make(chan struct{}),
program: prog,
msgs: make(chan tea.Msg, tuiSinkQueueCap),
done: make(chan struct{}),
pumpDone: make(chan struct{}),
}
}
36 unmodified lines
_ = err
}
}()
// Pump: the only goroutine allowed to block on Program.Send. When the
// program exits (done closes), a blocked Send unblocks via the program's
// context and the pump drains out. A Send that races program exit (done
// closes while a queued msg is in hand) is equally safe: Bubble Tea's
// Send is a context-guarded select and the msgs channel is never closed,
// so a post-exit Send is an immediate no-op — not a panic, not a block.
go func() {
defer close(s.pumpDone)
for {
select {
case <-s.done:
return
case msg := <-s.msgs:
s.program.Send(msg)
}
}
}()
}
// Wait blocks until the Bubble Tea program exits. Safe to call after Start.
// If Start was never called, Wait returns immediately.
// Wait blocks until the Bubble Tea program exits, with a bounded escalation
// so teardown can never hang: in the normal flow PostRunComplete has already
// quit the program and Wait returns immediately; otherwise (early-error
// return paths, or a wedged loop that survived Kill) Wait gives the program
// one grace period, Kills it, gives it one more, and then abandons the
// goroutine — a stuck display must not hold command exit hostage. Joins the
// pump goroutine whenever the program actually exited. Safe to call after
// Start; if Start was never called, returns immediately.
func (s *TUISink) Wait() {
s.mu.Lock()
started := s.started
1 unmodified line
if !started {
return
}
<-s.done
select {
case <-s.done:
<-s.pumpDone
return
case <-time.After(tuiPostRunCompleteGrace):
}
s.program.Kill()
select {
case <-s.done:
<-s.pumpDone
case <-time.After(tuiPostRunCompleteGrace):
// Bubble Tea never returned from Run despite Kill. Abandon the
// program and pump goroutines rather than hanging teardown.
}
}
// AgentEvent (Sink interface): translate ev into a tea.Msg and Send it to the
// Bubble Tea program. Implements the serial-dispatch contract: the orchestrator
// calls this from a single goroutine.
//
// Note: Send is safe to call from goroutines other than the TUI's update loop;
// Bubble Tea's implementation queues the message internally.
// AgentEvent (Sink interface): translate ev into a tea.Msg and enqueue it for
// the pump. NEVER blocks: display events beyond the queue cap are dropped and
// counted rather than backpressuring the orchestrator's dispatch goroutine —
// see tuiSinkQueueCap for the incident this guards against.
func (s *TUISink) AgentEvent(agent string, ev reviewtypes.Event) {
s.mu.Lock()
ok := s.started && !s.finished
1 unmodified line
if !ok {
return
}
s.program.Send(agentEventMsg{agent: agent, ev: ev})
select {
case s.msgs <- agentEventMsg{agent: agent, ev: ev}:
default:
s.mu.Lock()
s.dropped++
s.mu.Unlock()
}
}
// enqueueControl enqueues a rare, must-not-be-lost-lightly message (run
// summary, phase transitions, quit) with a bounded wait: worth briefly
// waiting out a transient jam, but a wedged TUI must not hold the run
// hostage — callers all have degradation paths (PostRunComplete falls back
// to Kill; a lost summary leaves the footer stale until quit).
func (s *TUISink) enqueueControl(msg tea.Msg) {
select {
case s.msgs <- msg:
case <-s.done:
case <-time.After(tuiPostRunCompleteGrace):
s.mu.Lock()
s.dropped++
s.mu.Unlock()
}
}
// droppedCount reports how many messages were discarded due to a jammed
// queue. Zero in any healthy run.
func (s *TUISink) droppedCount() int {
s.mu.Lock()
defer s.mu.Unlock()
return s.dropped
}
// RunFinished (Sink interface): mark reviewer execution complete and send the
10 unmodified lines
s.finished = true
s.mu.Unlock()
s.program.Send(runFinishedMsg{summary: summary})
s.enqueueControl(runFinishedMsg{summary: summary})
}
// FinalPhaseStarted updates the TUI with a visible post-run phase such as the
5 unmodified lines
if !ok {
return
}
s.program.Send(finalPhaseStartedMsg{name: name})
s.enqueueControl(finalPhaseStartedMsg{name: name})
}
// FinalPhaseFinished marks the visible post-run phase complete.
8 unmodified lines
if err != nil {
msg.err = err.Error()
}
s.program.Send(msg)
s.enqueueControl(msg)
}
// PostRunComplete exits the TUI and waits for the Bubble Tea program to finish.
6 unmodified lines
return
}
// Program.Send can block if Bubble Tea has not entered its event loop yet.
// Send from a goroutine and fall back to Kill so a lost post-run quit cannot
// leave the CLI stuck on "Finalizing output..." forever.
sent := make(chan struct{})
go func() {
s.program.Send(postRunCompleteMsg{})
close(sent)
}()
// enqueueControl is bounded, so this cannot park forever even when the
// Bubble Tea loop is stalled or never entered; the Kill fallback below
// guarantees a lost post-run quit cannot leave the CLI stuck on
// "Finalizing output..." forever.
s.enqueueControl(postRunCompleteMsg{})
select {
case <-s.done:
return
case <-sent:
case <-time.After(tuiPostRunCompleteGrace):
s.program.Kill()
}
3 unmodified lines
case <-time.After(tuiPostRunCompleteGrace):
s.program.Kill()
}
// Surface silent loss: a healthy run never drops. A non-zero count means
// the TUI loop stalled or lagged badly enough to jam the queue — exactly
// the diagnostic a future wedge investigation needs first.
if n := s.droppedCount(); n > 0 {
logging.Debug(context.Background(), "tui sink dropped messages under backpressure",
slog.Int("dropped", n))
}
}
Mcmd/entire/cli/review/tui_sink.go+130/-31
1 unmodified line
2
3
4
5
6
7
8
225 unmodified lines
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
1 unmodified line
import (
"bytes"
"sync"
"testing"
"time"
225 unmodified lines
t.Errorf("invalid fd should yield zero dims, got width=%d height=%d", width, height)
}
}
// --- Non-blocking dispatch (wedge hardening) ---
// wedgedProgram is a teaRunner whose event loop never consumes messages:
// Send blocks until Kill, modeling a Bubble Tea program whose Update/render
// pipeline has stalled (the 2026-07-07 run-6 incident shape). Run blocks
// until Kill so the sink's done channel behaves like a live program's.
type wedgedProgram struct {
killed chan struct{}
}
func newWedgedProgram() *wedgedProgram {
return &wedgedProgram{killed: make(chan struct{})}
}
func (w *wedgedProgram) Run() (tea.Model, error) {
<-w.killed
return nil, nil //nolint:nilnil // mirrors tea.Program.Run's exit shape; callers ignore both values
}
func (w *wedgedProgram) Send(tea.Msg) { <-w.killed }
func (w *wedgedProgram) Kill() {
select {
case <-w.killed:
default:
close(w.killed)
}
}
// recordingProgram is a teaRunner that records every message it receives.
type recordingProgram struct {
killed chan struct{}
mu sync.Mutex
msgs []tea.Msg
}
func newRecordingProgram() *recordingProgram {
return &recordingProgram{killed: make(chan struct{})}
}
func (r *recordingProgram) Run() (tea.Model, error) {
<-r.killed
return nil, nil //nolint:nilnil // mirrors tea.Program.Run's exit shape; callers ignore both values
}
func (r *recordingProgram) Send(msg tea.Msg) {
r.mu.Lock()
r.msgs = append(r.msgs, msg)
r.mu.Unlock()
}
func (r *recordingProgram) Kill() {
select {
case <-r.killed:
default:
close(r.killed)
}
}
func (r *recordingProgram) recorded() []tea.Msg {
r.mu.Lock()
defer r.mu.Unlock()
return append([]tea.Msg(nil), r.msgs...)
}
// TestTUISink_AgentEventNeverBlocksWhenProgramLoopIsWedged pins the wedge
// hardening: a stalled Bubble Tea loop must never backpressure the
// orchestrator. Before the fix, the first AgentEvent after the stall parked
// forever inside Program.Send, freezing sink dispatch, the fanIn drain loop,
// the parsers, and reviewer-timeout handling with them.
func TestTUISink_AgentEventNeverBlocksWhenProgramLoopIsWedged(t *testing.T) {
t.Parallel()
prog := newWedgedProgram()
sink := newTUISinkWithProgram(prog)
sink.Start()
defer func() {
prog.Kill()
sink.Wait()
}()
finished := make(chan struct{})
go func() {
for range 3 * tuiSinkQueueCap {
sink.AgentEvent("agent-a", reviewtypes.AssistantText{Text: "x"})
}
close(finished)
}()
select {
case <-finished:
case <-time.After(5 * time.Second):
t.Fatal("AgentEvent blocked on a wedged TUI loop — orchestrator freeze")
}
if got := sink.droppedCount(); got == 0 {
t.Error("expected overflow drops to be counted when the queue jams")
}
}
// TestTUISink_EventsReachProgramInOrder pins that the async pump preserves
// dispatch order for a healthy program.
func TestTUISink_EventsReachProgramInOrder(t *testing.T) {
t.Parallel()
prog := newRecordingProgram()
sink := newTUISinkWithProgram(prog)
sink.Start()
defer func() {
prog.Kill()
sink.Wait()
}()
for i := range 50 {
sink.AgentEvent("agent-a", reviewtypes.AssistantText{Text: string(rune('a' + i%26))})
}
sink.RunFinished(reviewtypes.RunSummary{})
deadline := time.After(5 * time.Second)
for {
msgs := prog.recorded()
if len(msgs) >= 51 {
for i := range 50 {
if _, ok := msgs[i].(agentEventMsg); !ok {
t.Fatalf("msgs[%d] = %T, want agentEventMsg", i, msgs[i])
}
}
if _, ok := msgs[50].(runFinishedMsg); !ok {
t.Fatalf("msgs[50] = %T, want runFinishedMsg (order violated)", msgs[50])
}
return
}
select {
case <-deadline:
t.Fatalf("only %d/51 messages reached the program", len(msgs))
case <-time.After(10 * time.Millisecond):
}
}
}
// TestTUISink_RunFinishedBoundedWhenWedged pins that control messages use a
// bounded wait rather than blocking forever when the queue is jammed.
func TestTUISink_RunFinishedBoundedWhenWedged(t *testing.T) {
t.Parallel()
prog := newWedgedProgram()
sink := newTUISinkWithProgram(prog)
sink.Start()
defer func() {
prog.Kill()
sink.Wait()
}()
// Jam the queue.
for range 2 * tuiSinkQueueCap {
sink.AgentEvent("agent-a", reviewtypes.AssistantText{Text: "x"})
}
finished := make(chan struct{})
go func() {
sink.RunFinished(reviewtypes.RunSummary{})
close(finished)
}()
select {
case <-finished:
case <-time.After(tuiPostRunCompleteGrace + 3*time.Second):
t.Fatal("RunFinished blocked past its bounded wait on a wedged TUI")
}
}
// stubbornProgram is a teaRunner whose Run NEVER returns, even after Kill —
// modeling a Bubble Tea teardown stuck restoring a blocked terminal. Send
// unblocks on Kill so the pump can drain, but done never closes.
type stubbornProgram struct {
killed chan struct{}
block chan struct{}
}
func newStubbornProgram() *stubbornProgram {
return &stubbornProgram{killed: make(chan struct{}), block: make(chan struct{})}
}
func (p *stubbornProgram) Run() (tea.Model, error) {
<-p.block // never closed — Run never returns
return nil, nil //nolint:nilnil // unreachable; mirrors tea.Program.Run's shape
}
func (p *stubbornProgram) Send(tea.Msg) { <-p.killed }
func (p *stubbornProgram) Kill() {
select {
case <-p.killed:
default:
close(p.killed)
}
}
// TestTUISink_WaitIsBoundedWhenProgramNeverExits pins the teardown guarantee:
// `defer tuiSink.Wait()` must not hang the command forever when the Bubble
// Tea program never returns from Run, even after Kill. Wait escalates
// (grace → Kill → grace) and then abandons the goroutine.
func TestTUISink_WaitIsBoundedWhenProgramNeverExits(t *testing.T) {
t.Parallel()
prog := newStubbornProgram()
sink := newTUISinkWithProgram(prog)
sink.Start()
finished := make(chan struct{})
go func() {
sink.Wait()
close(finished)
}()
select {
case <-finished:
case <-time.After(2*tuiPostRunCompleteGrace + 3*time.Second):
t.Fatal("Wait hung on a program that never exits — teardown wedge")
}
}
// TestTUISink_WaitJoinsPump pins that a normal Wait joins the pump goroutine
// (no leak between done closing and the pump observing it).
func TestTUISink_WaitJoinsPump(t *testing.T) {
t.Parallel()
prog := newRecordingProgram()
sink := newTUISinkWithProgram(prog)
sink.Start()
prog.Kill()
sink.Wait()
select {
case <-sink.pumpDone:
case <-time.After(2 * time.Second):
t.Fatal("Wait returned before the pump goroutine exited")
}
}
Mcmd/entire/cli/review/tui_sink_test.go+233
3 unmodified lines
4
5
6
7
8
9
10
102 unmodified lines
113
114
115
115
116
117
118
119
120
121
121
122
123
124
125
126
127
128
128
129
129
130
131
132
133
134
135
135
136
136
137
138
139
140
8 unmodified lines
149
150
151
151
152
153
153
154
154
155
156
157
158
4 unmodified lines
163
164
165
165
166
167
168
169
170
171
172
173
174
3 unmodified lines
"fmt"
"runtime"
"github.com/entireio/cli/cmd/entire/cli/experimental"
"github.com/entireio/cli/cmd/entire/cli/investigate"
"github.com/entireio/cli/cmd/entire/cli/paths"
cliReview "github.com/entireio/cli/cmd/entire/cli/review"
102 unmodified lines
// Noun groups (canonical homes for subcommands).
cmd.AddCommand(inGroup(newSessionsCmd(), groupSessions)) // 'session' (with 'sessions' as Cobra alias)
cmd.AddCommand(inGroup(newCheckpointGroupCmd(), groupSessions)) // 'checkpoint' / 'cp' / 'checkpoints'
cmd.AddCommand(newTokensGroupCmd()) // 'tokens'
experimental.Register(cmd, newTokensGroupCmd()) // 'tokens' (experimental)
cmd.AddCommand(inGroup(newAgentGroupCmd(), groupSetup)) // 'agent'
cmd.AddCommand(inGroup(newAuthCmd(), groupAccount)) // 'auth'
cmd.AddCommand(inGroup(newDoctorCmd(), groupSetup)) // 'doctor' (group: trace/logs/bundle)
cmd.AddCommand(newLabsCmd()) // 'labs' (experimental workflow discovery)
cmd.AddCommand(inGroup(newPluginGroupCmd(), groupSetup)) // 'plugin' (managed install/list/remove)
cmd.AddCommand(newImportCmd()) // 'import' (hidden; import pre-existing agent history)
experimental.Register(cmd, newImportCmd()) // 'import' (experimental; import pre-existing agent history)
cmd.AddCommand(inGroup(newOrgCmd(), groupControlPlane)) // 'org' — control-plane org management
cmd.AddCommand(inGroup(newProjectCmd(), groupControlPlane)) // 'project' — control-plane project management
cmd.AddCommand(inGroup(newRepoCmd(), groupControlPlane)) // 'repo' — control-plane repo lifecycle
cmd.AddCommand(inGroup(newGrantCmd(), groupControlPlane)) // 'grant' — control-plane access grants
// Top-level lifecycle and standalone commands.
cmd.AddCommand(cliReview.NewCommand(buildReviewDeps())) // `review`; hidden during maturation
cmd.AddCommand(investigate.NewCommand(buildInvestigateDeps())) // hidden during maturation; runs a multi-agent investigation
experimental.Register(cmd, cliReview.NewCommand(buildReviewDeps())) // `review` (experimental)
experimental.Register(cmd, investigate.NewCommand(buildInvestigateDeps())) // `investigate` (experimental); multi-agent investigation
cmd.AddCommand(inGroup(newCleanCmd(), groupSetup))
cmd.AddCommand(inGroup(newSetupCmd(), groupSetup)) // 'configure' — non-agent settings; agent CRUD lives under 'agent'
cmd.AddCommand(inGroup(newEnableCmd(), groupSetup))
cmd.AddCommand(inGroup(newDisableCmd(), groupSetup))
cmd.AddCommand(inGroup(newStatusCmd(), groupSetup))
cmd.AddCommand(newBlameCmd())
cmd.AddCommand(newWhyCmd())
experimental.Register(cmd, newBlameCmd()) // 'blame' (experimental)
experimental.Register(cmd, newWhyCmd()) // 'why' (experimental)
cmd.AddCommand(inGroup(newLoginCmd(), groupAccount))
cmd.AddCommand(inGroup(newLogoutCmd(), groupAccount))
cmd.AddCommand(newVersionCmd())
8 unmodified lines
cmd.AddCommand(hideAsAlias(newAttachCmd(), "entire session attach"))
cmd.AddCommand(hideAsAlias(newExplainCmd(), "entire checkpoint explain"))
cmd.AddCommand(hideAsAlias(newTraceCmd(), "entire doctor trace"))
cmd.AddCommand(newSearchCmd()) // 'entire search' = 'checkpoint search' (hidden, no hint)
experimental.Register(cmd, newSearchCmd()) // 'entire search' = 'checkpoint search' (experimental)
// Hidden labs commands (listed via `entire labs`; not deprecation shortcuts).
cmd.AddCommand(newExpertsCmd()) // agent/workflow provenance
// Experimental labs commands (listed via `entire labs`; not deprecation shortcuts).
experimental.Register(cmd, newExpertsCmd()) // 'experts' (experimental); agent/workflow provenance
// Deprecated top-level commands (functional; the constructors mark them
// Deprecated, which also excludes them from help and completion).
4 unmodified lines
cmd.AddCommand(newMCPCmd(cmd)) // MCP stdio server for MCP-host agents
cmd.AddCommand(newHooksCmd())
cmd.AddCommand(newTrailCmd())
cmd.AddCommand(newRunnerCmd()) // 'runner' (setup/tune runners); hidden during maturation
cmd.AddCommand(newSendAnalyticsCmd())
cmd.AddCommand(newCurlBashPostInstallCmd())
// Experimental command (developer-only visibility; setup/tune runners).
experimental.Register(cmd, newRunnerCmd()) // 'runner' (experimental)
cmd.SetVersionTemplate(versionString())
// Replace default help command with custom one that supports -t flag
Mcmd/entire/cli/root.go+13/-10
5 unmodified lines
6
7
8
9
10
11
12
200 unmodified lines
213
214
215
215
216
217
218
219
6 unmodified lines
226
227
228
229
230
231
232
233
234
235
232
233
236
237
238
239
240
237
241
242
243
244
5 unmodified lines
250
251
252
249
250
253
254
255
256
257
258
259
5 unmodified lines
265
266
267
262
263
268
269
270
271
272
273
274
37 unmodified lines
312
313
314
315
316
317
318
319
320
321
322
5 unmodified lines
"strings"
"testing"
"github.com/entireio/cli/cmd/entire/cli/experimental"
"github.com/entireio/cli/cmd/entire/cli/versioninfo"
"github.com/spf13/cobra"
)
200 unmodified lines
}
}
func TestCheckpointSearchIsVisibleButTopLevelSearchIsHidden(t *testing.T) {
func TestCheckpointSearchIsVisibleButTopLevelSearchIsExperimental(t *testing.T) {
t.Parallel()
root := NewRootCmd()
6 unmodified lines
t.Fatal("checkpoint search should be visible in checkpoint help")
}
// The top-level `entire search` shortcut is gated as experimental:
// visible and grouped in developer builds (the default test build),
// hidden in shipped releases.
topLevelSearch, _, err := root.Find([]string{"search"})
if err != nil {
t.Fatalf("find top-level search: %v", err)
}
if !topLevelSearch.Hidden {
t.Fatal("top-level search should remain hidden as a compatibility alias")
if topLevelSearch.GroupID != experimental.GroupID {
t.Fatalf("top-level search GroupID = %q, want %q (experimental)", topLevelSearch.GroupID, experimental.GroupID)
}
}
func TestCheckpointPolicyCommandIsHiddenDuringDevelopment(t *testing.T) {
func TestCheckpointPolicyCommandIsExperimental(t *testing.T) {
t.Parallel()
root := NewRootCmd()
5 unmodified lines
if len(remaining) != 0 || checkpointPolicy.Use != "policy" {
t.Fatalf("checkpoint policy resolved to %q with remaining args %v", checkpointPolicy.Use, remaining)
}
if !checkpointPolicy.Hidden {
t.Fatal("checkpoint policy should be hidden while it is in active development")
// Gated as experimental: visible and grouped in developer builds
// (the default test build), hidden in shipped releases.
if checkpointPolicy.GroupID != experimental.GroupID {
t.Fatalf("checkpoint policy GroupID = %q, want %q (experimental)", checkpointPolicy.GroupID, experimental.GroupID)
}
topLevelPolicy, remaining, err := root.Find([]string{"policy"})
5 unmodified lines
func TestRoot_VisibleCommandsAreGrouped(t *testing.T) {
t.Parallel()
// Commands intentionally left out of any group; cobra renders them
// under "Additional Commands".
// Commands intentionally left out of any group. version, labs, agent-help,
// and help render under cobra's "Additional Commands"; completion is
// allowlisted for completeness but never renders (hidden via
// CompletionOptions.HiddenDefaultCmd in NewRootCmd).
ungrouped := map[string]bool{
"version": true,
"labs": true,
37 unmodified lines
if c.Hidden || c.Deprecated != "" {
continue
}
// Experimental commands are grouped by experimental.Register (visible
// only in developer/nightly builds) — not part of this table.
if c.GroupID == experimental.GroupID {
continue
}
name := c.Name()
if ungrouped[name] {
if c.GroupID != "" {
Mcmd/entire/cli/root_test.go+21/-8
500 unmodified lines
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
5 unmodified lines
562
563
564
565
566
567
568
500 unmodified lines
---
## Scenario 8: git-refs Backend — Condensation and Push
All scenarios above describe the default **git-branch** backend, which condenses to the single `entire/checkpoints/v1` branch. When the primary backend is **git-refs**, the session/timing/overlap logic is **identical** — the only differences are *where* condensation writes and *how* the result is pushed. Everything about when a checkpoint is created, what it contains, and content-aware carry-forward is unchanged.
Two differences:
1. **Condensation target.** Instead of splicing the checkpoint subtree under `<id[:2]>/<id[2:]>/` on the `v1` branch, git-refs commits that same subtree as the tree root of a per-checkpoint ref, `refs/entire/checkpoints/<shard>/<id>` (orphan commit on first write, parented on later backfills). The ref is then recorded in a **push-discovery queue** rather than advancing a shared branch tip.
2. **Push mechanism.** Pre-push drains the queue and pushes exactly the changed refs, fast-forward-only, instead of pushing one branch.
```mermaid
sequenceDiagram
participant U as User
participant G as Git Hooks
participant SB as Shadow Branch
participant R as refs/entire/checkpoints/*
participant PQ as Push Queue
participant Rem as Remote
U->>G: git commit -a
Note over G: PrepareCommitMsg (adds Entire-Checkpoint trailer)
Note over G: PostCommit hook
G->>SB: Read accumulated shadow state
G->>R: Commit checkpoint subtree at refs/.../<shard>/<id>
G->>PQ: Enqueue the ref (best-effort)
G->>SB: Delete shadow branch
Note over U: Later...
U->>G: git push
Note over G: PrePush hook (PrimaryIsRefs → refs path)
G->>PQ: Drain queued refs
G->>Rem: Batch-push refs (fast-forward-only)
alt push accepted
G->>PQ: Remove pushed refs
else non-fast-forward (diverged)
G->>Rem: Fetch ref + replay local commits, retry (still non-force)
G->>PQ: Remove only refs that landed
end
```
### Key Points
- Condensation writes one commit per checkpoint under `refs/entire/checkpoints/<shard>/<id>`; there is no shared branch tip to serialize on.
- Enqueue is best-effort — a checkpoint that lands locally but fails to enqueue is still correct locally and re-enqueues on its next write.
- Pushes are never forced; a diverged ref is recovered by fetch + replay so the remote commit is preserved as an ancestor.
- Failed or interrupted pushes leave refs queued for the next pre-push — the queue degrades toward "will retry", never toward silent loss.
- Reads route by ID kind across both backends, so a repo mid-migration reads hex (branch) and ULID (refs) checkpoints transparently.
See [Ref-Based Checkpoint Backend](ref-checkpoint-backend.md) for the full backend design (sharding, read routing, configuration, and rollout).
---
## Summary Table
| Scenario | When Checkpoint Created | Checkpoint Contains | Key Mechanism |
5 unmodified lines
| 5. Partial commit + stash + new prompt + commit new | PostCommit (IDLE) | Full transcript (both prompts) | FilesTouched accumulation, stashed files "fall out" |
| 6. Stash + new prompt + unstash + commit all | PostCommit (IDLE) | All files + full transcript | Shadow branch accumulation |
| 7. Partial staging with `git add -p` | Each PostCommit (IDLE) | Full transcript per checkpoint | Content-aware carry-forward (hash comparison) |
| 8. git-refs backend | Same timing as 1–7 (backend-orthogonal) | Same as 1–7 | Condense to `refs/entire/checkpoints/<shard>/<id>` + push-queue drain at pre-push |
---
Mdocs/architecture/checkpoint-scenarios.md+51
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
# Ref-Based Checkpoint Backend (git-refs)
This document explains the **git-refs** checkpoint backend as a system: how it stores checkpoints, how it pushes and fetches them, how it coexists with the legacy **git-branch** backend, and how it is selected through configuration.
It is the companion to [Sessions and Checkpoints](sessions-and-checkpoints.md), which covers the domain model (sessions, checkpoints, IDs) shared by both backends. Read that first for the checkpoint tree layout, checkpoint-ID linking, and the compact-transcript format; this doc focuses on what is specific to the ref-based store.
## Why a second backend
The original backend stores every committed checkpoint as a subtree of a single long-lived branch, `entire/checkpoints/v1` (the **git-branch** backend). That branch is a serialization point: every condensation rewrites its tip, every push races on one ref, and the whole history travels together.
The **git-refs** backend instead keeps **one git ref per checkpoint**:
```
refs/entire/checkpoints/<shard>/<id>
```
Each ref points at a commit whose **tree root is that checkpoint's contents** (`metadata.json`, `0/`, `1/`, `tasks/…`) — the same subtree the git-branch backend splices *under* `<id[:2]>/<id[2:]>/` in the v1 tree. Independent refs mean checkpoints are written, pushed, and fetched independently: no shared tip to contend on, and a reader can fetch exactly the one checkpoint it needs instead of the whole branch.
Both backends are **git-backed** — they store the committed record in the repo's own object store — and never touch the working branch's history.
## Backend taxonomy: primary and mirrors
Checkpoint storage is pluggable. The topology is a single **primary** plus zero or more **mirrors**:
- **Primary** — the source of truth. It serves all reads and writes, and the full checkpoint lifecycle (resume bootstrap, `doctor` reconcile, `explain` tree reads, push, cleanup, pre-push OPF) drives *its* record.
- **Mirror** — an independent backend that receives best-effort **write fan-out** only. Reads never come from a mirror.
Backends register in `checkpoint/registry.go`. Each carries a `gitBacked` capability:
| Capability | Meaning | Can be primary? | Can be mirror? |
|------------|---------|-----------------|----------------|
| `gitBacked: true` | Stores the committed record in this repo's git object store | Yes | Yes |
| `gitBacked: false` | Stores elsewhere (e.g. a filesystem store) | No — mirror-only | Yes |
Only a git-backed backend can be the primary, because the lifecycle paths above operate through the repo and its refs; a non-git-backed backend has no such ref to drive them. The two built-in backends — `git-branch` and `git-refs` — are **both** git-backed and are registered directly in the built-in registry map. The `Register()` entry point is for non-git-backed (mirror-only) backends and is used in practice only by test-only backends, so a production binary can never select an unregistered one.
A **one-of-each-type** rule permits two distinct git-backed backends in the same topology. Note, though, that the branch→refs migration deliberately does **not** run `git-branch` as a mirror of `git-refs`. Cross-format compatibility comes from read routing plus the version policy — every reader (CLI, entire.io, entire-api) reads refs first and falls back to the branch — not from dual-writing the same checkpoint into both backends (see [Migration and coexistence](#migration-and-coexistence)). Mirroring stays available as a general mechanism, primarily for non-git-backed targets (e.g. a filesystem store).
## Ref layout and sharding
```
refs/entire/checkpoints/<shard>/<id>
```
- `<id>` is the full checkpoint ID (12-hex or ULID) and is always the leaf, so the ref round-trips: `RefName(id)` builds it and `ParseRef(name)` recovers the ID (`checkpoint/refs_naming.go`).
- `<shard>` is `id.ShardFor()` — the **last two characters** of the ID, for **both** formats.
A single positional rule (independent of ID kind) keeps ref naming impossible to compute inconsistently between callers, and the suffix distributes checkpoints evenly for either format:
- A **legacy hex** ID is random throughout, so its last two chars are as good as any.
- A **ULID**'s leading chars encode a millisecond timestamp (barely varying between nearby checkpoints) while its trailing chars are random — so sharding on the suffix keeps buckets even *and* keeps the ID itself lexicographically time-sortable.
`ParseRef` validates that the shard in a ref name matches the ID's own `ShardFor()` and that the tail is exactly `<shard>/<id>` (no extra path segments), so a malformed or foreign ref is rejected rather than resolved to the wrong bucket. `RefName` errors on an empty or unrecognized ID rather than emitting `refs/entire/checkpoints//`.
> **Note:** this is the git-refs namespace only. The git-branch backend keeps its own independent **first-two-chars** tree layout (`<id[:2]>/<id[2:]>/`) inside the v1 branch. The two sharding schemes are deliberately different and do not interact.
## ID formats
Checkpoint IDs come in two shapes; the store determines which is minted:
- **git-branch primary** → 12-char hex IDs.
- **git-refs primary** → 26-char ULIDs (Crockford base32, lexicographically time-sortable).
IDs are minted by `checkpoint.GenerateCheckpointID`, which picks the format from the configured primary. Never call `id.Generate()` / `id.GenerateULID()` directly from a write path. The full rationale for the two formats, `DisplayShort`, and `id.MaxIDLength` lives in [Sessions and Checkpoints → Checkpoint ID Linking](sessions-and-checkpoints.md#checkpoint-id-linking).
## Write path
The git-refs store (`gitRefsStore`, `checkpoint/refs_store.go`) shares the checkpoint-subtree machinery with the git-branch store via an embedded `*treeWriter`. Both build the exact same checkpoint subtree; they differ only in the **base path** they write it at and in **where the result is committed**. The git-branch store writes each checkpoint under its shard prefix `<id[:2]>/<id[2:]>/` inside the single `v1` tree, so many checkpoints share one tree. The git-refs store writes with **no prefix** (an empty base path), so the checkpoint subtree *is* the root of that checkpoint's own commit tree, and the commit is the tip of a per-checkpoint ref rather than a subtree of the v1 branch.
Every persistent write (`WriteSession`, and the `Backfill*` operations for transcript / summary / attribution) follows the same shape:
1. **Resolve the ref's current tip** (`refBase`). A missing ref → `(ZeroHash, nil)`, so the first write to a checkpoint becomes an **orphan commit**. A real lookup failure (IO/corruption) is surfaced, never silently treated as "new checkpoint".
2. **Build the updated checkpoint subtree** from the existing tree plus the new content (shared `treeWriter` logic).
3. **Create a commit** with the current tip as parent (orphan on first write, parented thereafter), so each checkpoint accretes its **own per-checkpoint history**.
4. **Point the ref at the new commit** (`setRef`) and **enqueue it for push**.
Enqueue is best-effort: a write that lands locally but fails to enqueue must not fail condensation. The ref is still local and correct; only its remote sync is deferred until the next write to the same checkpoint re-enqueues it (see the push queue below).
## Push and fetch
Because reads can *fetch* refs on demand and there is no single branch tip to push, the git-refs backend cannot simply "push everything" at pre-push time. Deleting local refs after pushing would also hurt local workflows. Instead it tracks exactly which checkpoints changed, in a **push-discovery queue**, and pushes those.
### Push-discovery queue
`checkpoint/pushqueue.go` — a flock-protected JSONL file in the git **common dir** (so every worktree sharing the object store enqueues into one queue):
- `entire-checkpoint-push-queue.jsonl` — one `{"ref": …}` record per queued ref.
- `entire-checkpoint-push-queue.lock` — the flock.
Semantics:
- **Enqueue** appends under the lock; enqueuing an already-present or already-pushed ref is safe (drain de-dups, the push is idempotent).
- **Drain** returns the de-duplicated queued refs *without* removing them, and compacts the file in place when it held redundant lines (so a long-lived session that keeps re-enqueuing the same ref cannot grow the file unboundedly).
- **Remove** deletes refs only **after a confirmed push**, preserving any entry appended during the push. An interrupted or failed push therefore leaves its refs queued for the next pre-push — the queue degrades toward "will retry", never toward silent loss.
Rewrites are atomic (temp file + rename under the lock) so a concurrent reader never sees a half-written queue.
### Pre-push flow
`ManualCommitStrategy.PrePush` (`strategy/manual_commit_push.go`) branches on `checkpoint.PrimaryIsRefs(cfg)`. When the primary is git-refs it:
1. **Drains** the queue.
2. **Partitions** the drained refs into those that still exist locally and stale ones (dropped from the queue).
3. **Batch-pushes** the existing refs in one network round-trip (`batchPushRefs`, `strategy/push_common.go`).
4. On success, **removes** the pushed refs from the queue and runs shadow-branch cleanup.
5. On a batch failure (typically a non-fast-forward rejection), **falls back to per-ref recovery** (`pushCheckpointRefWithRecovery`) and removes from the queue only the refs that land.
### Non-force, fast-forward-only
All checkpoint-ref pushes are **fast-forward-only — never a force push.** There is no server-side ref protection, so a force push risks silently clobbering a checkpoint written elsewhere. Per-checkpoint refs normally advance by fast-forward (append-only per-checkpoint history), so this is the common case.
When a push *is* rejected as non-fast-forward — genuine divergence, e.g. the same checkpoint was written on two machines — recovery **fetches the remote ref and replays the local-only commits on top** (`fetchAndRebaseRefCommon`), then retries. After the replay the local ref is a fast-forward over the remote, so the retry is *still* non-force and the remote commit is preserved as an ancestor rather than overwritten. A genuine cherry-pick conflict (both sides rewrote the same file, e.g. root `metadata.json`) leaves the ref queued — degrading to the safe state, never forcing.
### On-demand fetch for reads
A checkpoint written on another machine has no local ref. When a read misses locally and a **ref fetcher** is configured, `resolveRefMaybeFetch` fetches that one ref from the remote and retries once. It carefully distinguishes:
- **genuinely absent** (remote has no such checkpoint) → maps to `ErrCheckpointNotFound`;
- **a real failure** (IO, network, context cancellation) → returned as-is, never swallowed as "not found".
`List` at the storage level is **local-refs-only** — it enumerates local refs and reads each root summary. There is no remote enumeration.
## Read routing and coexistence
`checkpoint.Open` returns a `kindRoutingStore` (`checkpoint/routing_store.go`) that resolves id-keyed reads across **both** git backends by the checkpoint's ID kind, so a repo running git-refs and git-branch side by side (or mid-migration) reads either format without reconfiguring:
| ID kind | Read from | Rationale |
|---------|-----------|-----------|
| **ULID** | git-refs only, never the branch | ULIDs are only ever minted under git-refs |
| **hex**, git-branch primary | branch only | branch is authoritative for hex |
| **hex**, git-refs primary | refs first, then git-branch fallback | a hex checkpoint may still sit on the pre-migration v1 branch, or have been migrated into refs |
- `List` **unions both** backends and de-dups by ID (the same checkpoint can appear in both during coexistence), keeping the most recent.
- The `firstResolved` helper tries stores in priority order; a non-final store that reports absent *or* errors falls through to the next, so a transient git-refs fetch error cannot hide a checkpoint that resolves on the branch. The final store's result (hit, absent, or error) is returned verbatim.
- The optional `AuthorReader` capability (`explain` relies on it) is preserved and routed by the same rules when both read stores provide it.
- **Writes are not kind-routed.** They target the configured primary (+ mirrors); the minted ID already matches the primary's format.
All general read paths — resume, explain, attribution, blame, tokens, attach — inherit this routing for free through `checkpoint.Open`; there is no per-command config knob.
## Configuration and rollout
Backend selection lives in the `checkpoints` block of settings (`settings/checkpoints.go`):
```json
{
"checkpoints": {
"primary": { "type": "git-refs" }
}
}
```
- `primary.type` is required. When the whole block is absent, the layer defaults to the **git-branch** backend with no mirrors — so existing repos are unchanged.
- `settings.local.json`'s `checkpoints` block **replaces** the one in `settings.json` wholesale (this is a selection config, not a deep-merged document).
- Config loading is **fail-soft**: a missing file, a whole-file JSON syntax error, or unrelated invalid fields all resolve to "no config" → default git-branch. It errors *only* when a present `checkpoints` block is itself invalid.
- Unknown fields are rejected (`DisallowUnknownFields`) to surface typos. The trade-off: adding a `checkpoints` field is a coordinated rollout — ship the reader before any writer emits the field.
### Environment override
`ENTIRE_CHECKPOINTS_PRIMARY` (and the optional comma-separated `ENTIRE_CHECKPOINTS_MIRRORS`) **fully replace** any settings block — env wins over file, matching other `ENTIRE_*` overrides. This is how e2e/CI and rollout drive a specific backend without editing settings; the CI test-canary job runs a matrix over `[git-branch, git-refs]` via this variable. The env override is selection-only (no per-backend config blocks).
### Rollout states
The switch is a **primary flip**, not a dual-write phase. There is no "run both backends in parallel" step — see [Migration and coexistence](#migration-and-coexistence) for why read routing makes it unnecessary.
| State | `primary` | Behavior |
|-------|-----------|----------|
| **Default** (today) | `git-branch` | Hex checkpoints on the `v1` branch; unchanged legacy behavior |
| **Refs-only** | `git-refs` | New checkpoints are ULIDs written as per-checkpoint refs; pre-existing hex/`v1` checkpoints stay readable via the read-routing fallback |
## Checkpoint version and policy
Checkpoint formats are named `<family>-v<major>` and validated in `checkpointpolicy/format.go`:
| Format | Family | Written by |
|--------|--------|------------|
| `branch-v1` | `branch` | git-branch backend |
| `refs-v1` | `refs` | git-refs backend |
Both are in the CLI's read **and** write sets. The repo-wide checkpoint policy (`refs/entire/policies/checkpoint`, `checkpoint_version` / `checkpoint_min_version`) gates which formats a client may write and nudges upgrades; see [Sessions and Checkpoints → Checkpoint Policy](sessions-and-checkpoints.md#checkpoint-policy).
## Migration and coexistence
The read-routing rules above are what make a hex-on-branch repo and a ULID-in-refs repo the same repo: nothing needs to move for both formats to be readable, so the branch→refs switch is a primary flip with **no dual-write step**.
Concretely, flipping the primary to git-refs means new checkpoints are ULIDs stored as per-checkpoint refs, while every checkpoint already written to the `v1` branch stays exactly where it is and keeps resolving through the branch fallback. This works because **every reader routes the same way — refs first (for both ID formats), branch fallback for the legacy format** — not just the CLI but also entire.io and entire-api. So a repo can move to refs-only on the remote without keeping the `v1` branch alive for any reader's benefit.
A mixed fleet is fine and needs no special handling:
- A **modern** CLI (or the server) on git-refs primary reads everything: ULID/refs checkpoints directly, and older hex/`v1` checkpoints via the fallback.
- An **old** CLI keeps writing hex checkpoints to the `v1` branch, and everyone modern still reads those. It simply **cannot read** newer ULID/refs checkpoints — which is the intended behavior: it fails closed, and the [version policy](#checkpoint-version-and-policy) (`checkpoint_min_version`) turns that into an explicit "upgrade" nudge rather than a silent half-working state.
This is why running `git-branch` as a *mirror* of git-refs is **not** part of the migration: it would dual-write every checkpoint into both backends to keep `v1` populated, but no reader needs that — read routing already covers both formats, and the "old client can't read the new format" case is a feature, not something to paper over.
When checkpoints *are* actively migrated from the branch into refs (a path that is tooling-only today, not an official flow), they are written under `RefName(hexID)` — i.e. **hex-named refs** — which is why a hex ID under a git-refs primary is looked up in refs first and only then falls back to the branch.
## Key files
| File | Responsibility |
|------|----------------|
| `checkpoint/registry.go` | Backend registry, `gitBacked` capability, built-in `git-branch`/`git-refs` |
| `checkpoint/open.go` | `Open` topology resolution, `PrimaryIsRefs`, `kindRoutingStore` wiring |
| `checkpoint/refs_naming.go` | `RefName` / `ParseRef`, `CheckpointRefPrefix` |
| `checkpoint/refs_store.go` | `gitRefsStore` — per-checkpoint write/read, on-demand fetch |
| `checkpoint/pushqueue.go` | Flock JSONL push-discovery queue |
| `checkpoint/routing_store.go` | `kindRoutingStore` — id-kind read routing across both backends |
| `checkpoint/id/id.go` | `ShardFor`, `Kind`/`KindOf`, ID generation |
| `checkpointpolicy/format.go` | `branch-v1` / `refs-v1` format families and read/write sets |
| `settings/checkpoints.go` | `checkpoints` block parsing + env override |
| `strategy/manual_commit_push.go` | Pre-push: drain queue, batch push, per-ref recovery |
| `strategy/push_common.go` | `batchPushRefs`, `pushCheckpointRefWithRecovery`, fetch+replay |
## Known limitations and deferred work
- **Storage-level `List` is local-only** — no remote enumeration of checkpoint refs. `List` at the routing layer still unions the two local stores.
- **OPF (OpenAI Privacy Filter) at pre-push is git-branch-only for now.** The per-ref push does not run OPF re-redaction; that is deferred until after the store lands. See `strategy/manual_commit_opf_rewrite.go` and [security-and-privacy.md](../security-and-privacy.md).
- **The "ULIDs never land on the branch" invariant is not yet enforced at write time.** A config flip or a missing `ENTIRE_CHECKPOINTS_PRIMARY` in an amending environment could, in principle, condense a ULID checkpoint onto the `v1` branch, which readers (routing ULIDs to refs only) would then fail to find. Because git-branch is *not* a mirror of git-refs (see [Migration and coexistence](#migration-and-coexistence)), a ULID reaching the git-branch write path is unambiguously a bug — so enforcing this is a straightforward reject at that write path, not a topology-role-aware check.
Adocs/architecture/ref-checkpoint-backend.md+216
3 unmodified lines
4
5
6
7
8
9
10
11
134 unmodified lines
146
147
148
147
149
150
151
152
153
154
155
323 unmodified lines
479
480
481
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
485
499
500
501
502
3 unmodified lines
Entire CLI creates checkpoints for AI coding sessions. The system is agent-agnostic - it works with Claude Code, Codex, Gemini CLI, OpenCode, Cursor, Factory AI Droid, Copilot CLI, or any tool that triggers Entire hooks.
This document covers the domain model shared by both checkpoint storage backends. For how the **git-refs** backend stores checkpoints as one ref per checkpoint — its layout, push/fetch model, read routing, and configuration — see [Ref-Based Checkpoint Backend](ref-checkpoint-backend.md).
## Domain Model
### Session
134 unmodified lines
|------|----------|----------|
| Session State | `.git/entire-sessions/<id>.json` | Active session tracking |
| Ephemeral | `entire/<commit[:7]>-<worktreeHash[:6]>` branch | Full state (code + metadata) |
| Persistent | `entire/checkpoints/v1` branch (sharded) | Metadata + commit reference |
| Persistent (git-branch) | `entire/checkpoints/v1` branch, sharded `<id[:2]>/<id[2:]>/` | Metadata + commit reference |
| Persistent (git-refs) | `refs/entire/checkpoints/<shard>/<id>`, one ref per checkpoint | Metadata + commit reference |
The persistent store is pluggable: `git-branch` (the default) stores every committed checkpoint as a subtree of a single `entire/checkpoints/v1` branch, while `git-refs` stores one ref per checkpoint. Both are git-backed and share the same checkpoint tree layout; they differ only in where that tree is committed. This document describes the git-branch layout; for the ref-based backend — its ref naming, sharding, push/fetch model, read routing, and configuration — see [Ref-Based Checkpoint Backend](ref-checkpoint-backend.md).
### Session State
323 unmodified lines
├── phase.go # Session phase state machine (ACTIVE, IDLE, ENDED, etc.)
checkpoint/
├── checkpoint.go # checkpoint.Type, checkpoint.Store interface, CheckpointSummary, etc.
├── store.go # GitStore implementation
├── temporary.go # Shadow branch storage
├── committed.go # Metadata branch storage
├── id/ # CheckpointID type and generation
├── checkpoint.go # checkpoint.Type, store interfaces, CheckpointSummary, etc.
├── open.go # Open() facade: resolves topology, wires stores + fetchers
├── registry.go # Backend registry + gitBacked capability (git-branch, git-refs)
├── routing_store.go # kindRoutingStore: id-kind read routing across both backends
├── fanout.go # Mirror write fan-out (primary + best-effort mirrors)
├── generate.go # GenerateCheckpointID (format follows the configured primary)
├── persistent.go # git-branch persistent store (entire/checkpoints/v1)
├── persistent_write.go # git-branch write path (treeWriter, subtree splicing)
├── refs_store.go # git-refs persistent store (one ref per checkpoint)
├── refs_naming.go # RefName / ParseRef, CheckpointRefPrefix, sharding
├── pushqueue.go # git-refs push-discovery queue (flock JSONL)
├── ephemeral.go # Shadow-branch (ephemeral) store
├── fsstore/ # Filesystem mirror backend (non-git-backed, mirror-only)
├── id/ # CheckpointID type, Kind/KindOf, ShardFor, generation
│ └── id.go
```
Strategies use `checkpoint.Store` primitives - storage details are encapsulated.
Strategies use the `checkpoint.Open` facade and store primitives - backend and storage details are encapsulated.
## Strategy Role
Mdocs/architecture/sessions-and-checkpoints.md+21/-7