Merge branch 'main' into fix/329-subagent-extract · Entire

Home

Log in

Merge branch 'main' into fix/329-subagent-extract

9f16312→main·

peyton-alt·2d ago·83 files·+5,227 added/-465 removed

Changes

83

23 unmodified lines

24
25
26
27
27
28
29
30
31
32
33
33
34
35
36

23 unmodified lines

ref: ${{ github.event.pull_request.head.sha }}

- name: Initialize CodeQL
        uses: github/codeql-action/init@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3
        uses: github/codeql-action/init@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0
        with:
          languages: actions
          queries: security-extended,security-and-quality

- name: Perform CodeQL analysis
        uses: github/codeql-action/analyze@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3
        uses: github/codeql-action/analyze@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0
        with:
          category: /language:actions
          # Fork PRs receive a read-only GITHUB_TOKEN, so SARIF upload to the

M.github/workflows/codeql-actions.yml+2/-2

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
469 unmodified lines

601
602
603
586
604
605
606
607
14 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
469 unmodified lines

- **Shadow branch migration** - if user does stash/pull/rebase (HEAD changes without commit), shadow branch is automatically moved to new base commit
- **Orphaned branch cleanup** - if a shadow branch exists without a corresponding session state file, it is automatically reset when a new session starts
- PrePush hook can push `entire/checkpoints/v1` branch alongside user pushes
- **OPF (OpenAI Privacy Filter) runs at pre-push, not post-commit**: when `redaction.openai_privacy_filter.enabled` is true, the PrePush hook re-redacts unpushed `entire/checkpoints/v1` commits with the OPF 8th layer, builds new commits carrying an `Entire-OPF-Applied: true` trailer, and atomically updates the local v1 ref before pushing. Per-commit condensation stays on the fast 7-layer pipeline. See `strategy/manual_commit_opf_rewrite.go` and `docs/security-and-privacy.md` for the full flow, including divergence detection, bootstrap caps, and CAS-on-conflict semantics.
- **OPF (OpenAI Privacy Filter) runs at pre-push, not post-commit**: when `redaction.openai_privacy_filter.enabled` is true, the PrePush hook re-redacts unpushed `entire/checkpoints/v1` commits with the OPF 9th layer, builds new commits carrying an `Entire-OPF-Applied: true` trailer, and atomically updates the local v1 ref before pushing. Per-commit condensation stays on the fast 8-layer pipeline. See `strategy/manual_commit_opf_rewrite.go` and `docs/security-and-privacy.md` for the full flow, including divergence detection, bootstrap caps, and CAS-on-conflict semantics.
- Safe to use on main/master since it never modifies commit history

#### Key Files
14 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+27/-8

24 unmodified lines

25
26
27
28
29
30
31
175 unmodified lines

207
208
209
210
211
212
213
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
248
249
250

24 unmodified lines

- [Key Concepts](#key-concepts)
  - [How It Works](#how-it-works)
  - [Strategy](#strategy)
- [Headless & CI Authentication](#headless--ci-authentication)
- [Local Device Auth Testing](#local-device-auth-testing)
- [Commands Reference](#commands-reference)
- [Configuration](#configuration)
175 unmodified lines

Multiple AI sessions can run on the same commit. If you start a second session while another has uncommitted work, Entire warns you and tracks them separately. Both sessions' checkpoints are preserved and can be rewound independently.

## Headless & CI Authentication

By default `entire login` stores tokens in the OS keyring (macOS Keychain,
Linux Secret Service, Windows Credential Manager). Machines without a usable
keyring — headless servers, containers, minimal VMs, CI runners — have two
supported paths:

### Interactive login on a headless machine

Use the file-backed token store. The device-auth flow already works without a
local browser (the CLI prints an approval URL you can open on any machine);
only token storage needs the override:

```bash
ENTIRE_TOKEN_STORE=file entire login
```

Tokens are written with `0600` permissions to `tokens.json` in your Entire
config directory (`~/.config/entire` by default). Override the location with
`ENTIRE_TOKEN_STORE_PATH`. Set `ENTIRE_TOKEN_STORE=file` persistently (e.g. in
your shell profile) so later commands read from the same store.

### Non-interactive automation (CI, workload identity)

Skip login and storage entirely by injecting a token per invocation:

```bash
ENTIRE_TOKEN=<login-or-sa-session-JWT> entire ...
```

`ENTIRE_TOKEN` bypasses stored credentials; the CLI derives the control-plane
endpoint from the token itself. Nothing is written to disk. This is the right
path for CI pipelines and service accounts.

> **Seeing `save login` / `failed to unlock correct collection` errors from
> `entire login`?** That's the OS keyring being unavailable — use one of the
> two paths above.

## Local Device Auth Testing

If you're working on the CLI device auth flow against a local `entire.io` checkout:

MREADME.md+39

11 unmodified lines

12
13
14
15
16
17
18
119 unmodified lines

138
139
140
140
141
141
142
143
144
145
146
147
148

11 unmodified lines

"github.com/entireio/cli/cmd/entire/cli/agent"
    "github.com/entireio/cli/cmd/entire/cli/logging"
    "github.com/entireio/cli/cmd/entire/cli/textutil"
)

// Compile-time interface assertions for new interfaces.
119 unmodified lines

Type:       agent.TurnStart,
        SessionID:  raw.SessionID,
        SessionRef: raw.TranscriptPath,
        Prompt:     raw.Prompt,
        Timestamp:  time.Now(),
        // Strip IDE-injected context (e.g. <ide_opened_file> from the VS Code
        // extension) so the session/checkpoint title and prompt show what the
        // user actually typed, not the injected block.
        Prompt:    textutil.StripIDEContextTags(raw.Prompt),
        Timestamp: time.Now(),
    }, nil
}

Mcmd/entire/cli/agent/claudecode/lifecycle.go+6/-2

98 unmodified lines

99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121

98 unmodified lines

}
}

// The VS Code extension prepends an <ide_opened_file> context block to the
// prompt; it must be stripped so the session/checkpoint title and prompt show
// only what the user typed.
func TestParseHookEvent_TurnStart_StripsIDEContextTags(t *testing.T) {
    t.Parallel()

ag := &ClaudeCodeAgent{}
    input := `{"session_id":"s1","transcript_path":"/tmp/t.jsonl","prompt":"<ide_opened_file>The user opened /a/b.md in the IDE.</ide_opened_file>\n\nrewrite these docs as one plan"}`

event, err := ag.ParseHookEvent(context.Background(), HookNameUserPromptSubmit, strings.NewReader(input))
    require.NoError(t, err)
    require.NotNil(t, event)
    if event.Prompt != "rewrite these docs as one plan" {
        t.Errorf("IDE context tag not stripped; prompt = %q", event.Prompt)
    }
}

func TestParseHookEvent_TurnEnd(t *testing.T) {
    t.Parallel()

Mcmd/entire/cli/agent/claudecode/lifecycle_test.go+17

168 unmodified lines

169
170
171
172
172
173
174
175
174
176
177
176
178
179
180
181

168 unmodified lines

}

func (c *CopilotCLIAgent) readHookEnvelope(stdin io.Reader) (*hookEnvelope, error) {
    data, err := io.ReadAll(stdin)
    // Stream one JSON value rather than io.ReadAll so the hook never blocks
    // waiting for stdin EOF that some agents don't send on Windows (issue #1398).
    raw, err := agent.ReadHookInputRaw(stdin)
    if err != nil {
        return nil, fmt.Errorf("failed to read hook input: %w", err)
        return nil, fmt.Errorf("read hook input: %w", err)
    }
    return parseHookEnvelope(data)
    return parseHookEnvelope(raw)
}

// resolveTranscriptRef computes the transcript path from the session ID.

Mcmd/entire/cli/agent/copilotcli/lifecycle.go+5/-3

4 unmodified lines

5
6
7
8
9
10
11
12
13
14
145 unmodified lines

160
161
162
160
161
163
164
165
166
167
168
169
170
171
172
173
174
163
175
176
165
166
167
168
177
178
179
171
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
217
218
219
220
221
222
223
224
225
226
227
228

4 unmodified lines

"errors"
    "fmt"
    "io"
    "os"
    "time"

"golang.org/x/term"
)

// EventType represents a normalized lifecycle event from any agent.
145 unmodified lines

Metadata map[string]string
}

// ReadAndParseHookInput reads all bytes from stdin and unmarshals JSON into the given type.
// This is a shared helper for agent ParseHookEvent implementations.
// ReadAndParseHookInput decodes a single JSON hook payload from stdin into the
// given type. This is a shared helper for agent ParseHookEvent implementations.
//
// It deliberately does NOT use io.ReadAll, which waits for stdin to reach EOF.
// Agents drive hooks by piping a JSON payload to the hook process, but some
// keep the write end of that pipe open for the hook's lifetime rather than
// closing it after writing — notably on Windows/Git Bash, where a full payload
// arrives but EOF never does. io.ReadAll then blocked indefinitely and the hook
// (e.g. gemini session-start) hung forever (issue #1398). A streaming
// json.Decoder returns as soon as one complete JSON value has been read,
// independent of when — or whether — stdin is closed.
func ReadAndParseHookInput[T any](stdin io.Reader) (*T, error) {
    data, err := io.ReadAll(stdin)
    raw, err := ReadHookInputRaw(stdin)
    if err != nil {
        return nil, fmt.Errorf("failed to read hook input: %w", err)
    }
    if len(data) == 0 {
        return nil, errors.New("empty hook input")
        return nil, err
    }
    var result T
    if err := json.Unmarshal(data, &result); err != nil {
    if err := json.Unmarshal(raw, &result); err != nil {
        return nil, fmt.Errorf("failed to parse hook input: %w", err)
    }
    return &result, nil
}

// ReadHookInputRaw returns the raw bytes of a single JSON hook payload read from
// stdin, without waiting for EOF. It is the shared primitive behind every
// agent's hook-input read (issue #1398); callers that need custom parsing
// (e.g. key-name fallbacks, or forwarding the bytes to a subprocess) use this
// directly, while the common case uses ReadAndParseHookInput.
func ReadHookInputRaw(stdin io.Reader) (json.RawMessage, error) {
    return ReadHookInputRawLimited(stdin, -1)
}

// ReadHookInputRawLimited is ReadHookInputRaw with a ceiling of limit bytes on
// the JSON value (limit < 0 means unlimited). It is used at the external/plugin
// boundary to bound an untrusted payload — without reintroducing the EOF-wait
// hang, since the streaming decoder still returns on the first complete value.
func ReadHookInputRawLimited(stdin io.Reader, limit int64) (json.RawMessage, error) {
    // If stdin is an interactive terminal there is no payload coming at all: the
    // command was run by hand, or the agent left the console attached instead of
    // wiring up a pipe. Decoding would block waiting for input that never comes,
    // so treat it as empty and return promptly.
    if StdinLooksInteractive(stdin) {
        return nil, errors.New("empty hook input")
    }

r := stdin
    if limit >= 0 {
        r = io.LimitReader(stdin, limit)
    }
    var raw json.RawMessage
    if err := json.NewDecoder(r).Decode(&raw); err != nil {
        if errors.Is(err, io.EOF) {
            return nil, errors.New("empty hook input")
        }
        return nil, fmt.Errorf("failed to parse hook input: %w", err)
    }
    return raw, nil
}

// StdinLooksInteractive reports whether r is an interactive terminal, i.e. no
// piped hook payload is on its way. Hook readers use it to bail out promptly
// instead of blocking on a read that will never complete (issue #1398).
func StdinLooksInteractive(r io.Reader) bool {
    f, ok := r.(*os.File)
    return ok && term.IsTerminal(int(f.Fd())) //nolint:gosec // G115: uintptr->int is safe for fd
}

Mcmd/entire/cli/agent/event.go+61/-8

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

package agent

import (
    "io"
    "strings"
    "testing"
    "time"
)

type hookInput struct {
    SessionID      string `json:"session_id"`
    TranscriptPath string `json:"transcript_path"`
}

// TestReadAndParseHookInput_ReturnsBeforeEOF proves the hook reader returns as
// soon as a complete JSON value has arrived, WITHOUT waiting for stdin to be
// closed. On Windows/Git Bash the agent keeps the pipe's write end open for the
// hook's lifetime; io.ReadAll blocked forever there (issue #1398). We simulate
// that by writing the payload to an io.Pipe and never closing the writer.
func TestReadAndParseHookInput_ReturnsBeforeEOF(t *testing.T) {
    t.Parallel()

pr, pw := io.Pipe()
    // Write a complete payload, then hold the pipe open (never Close) — mimics an
    // agent that keeps stdin open after delivering the JSON.
    go func() {
        if _, err := pw.Write([]byte(`{"session_id":"s1","transcript_path":"/t.jsonl"}`)); err != nil {
            _ = pw.CloseWithError(err)
        }
        // Intentionally no pw.Close() on success: stdin stays open, so EOF never arrives.
    }()

type result struct {
        val *hookInput
        err error
    }
    done := make(chan result, 1)
    go func() {
        v, err := ReadAndParseHookInput[hookInput](pr)
        done <- result{v, err}
    }()

select {
    case r := <-done:
        if r.err != nil {
            t.Fatalf("unexpected error: %v", r.err)
        }
        if r.val == nil || r.val.SessionID != "s1" || r.val.TranscriptPath != "/t.jsonl" {
            t.Fatalf("unexpected value: %+v", r.val)
        }
    case <-time.After(3 * time.Second):
        t.Fatal("ReadAndParseHookInput blocked waiting for EOF — regression of #1398")
    }
}

// TestReadHookInputRawLimited_ReturnsBeforeEOF is the external-agent analogue of
// TestReadAndParseHookInput_ReturnsBeforeEOF: the size-bounded raw reader must
// also return on the first complete JSON value without waiting for stdin close
// (issue #1398).
func TestReadHookInputRawLimited_ReturnsBeforeEOF(t *testing.T) {
    t.Parallel()

pr, pw := io.Pipe()
    go func() {
        if _, err := pw.Write([]byte(`{"session_file":"/t.jsonl"}`)); err != nil {
            _ = pw.CloseWithError(err)
        }
        // No Close(): the write end stays open, so EOF never arrives.
    }()

done := make(chan error, 1)
    go func() {
        _, err := ReadHookInputRawLimited(pr, 10*1024*1024)
        done <- err
    }()

select {
    case err := <-done:
        if err != nil {
            t.Fatalf("unexpected error: %v", err)
        }
    case <-time.After(3 * time.Second):
        t.Fatal("ReadHookInputRawLimited blocked waiting for EOF — regression of #1398")
    }
}

// TestReadHookInputRawLimited_RejectsOversized proves the byte ceiling turns an
// over-limit payload into an error rather than an unbounded read.
func TestReadHookInputRawLimited_RejectsOversized(t *testing.T) {
    t.Parallel()

big := `{"k":"` + strings.Repeat("x", 512) + `"}`
    _, err := ReadHookInputRawLimited(strings.NewReader(big), 64)
    if err == nil {
        t.Fatal("expected error for payload exceeding the limit, got nil")
    }
}

func TestReadAndParseHookInput_EmptyInputEOF(t *testing.T) {
    t.Parallel()

_, err := ReadAndParseHookInput[hookInput](strings.NewReader(""))
    if err == nil || !strings.Contains(err.Error(), "empty hook input") {
        t.Fatalf("want 'empty hook input' error, got: %v", err)
    }
}

func TestReadAndParseHookInput_MalformedJSON(t *testing.T) {
    t.Parallel()

_, err := ReadAndParseHookInput[hookInput](strings.NewReader(`{"session_id": INVALID}`))
    if err == nil || !strings.Contains(err.Error(), "failed to parse hook input") {
        t.Fatalf("want 'failed to parse hook input' error, got: %v", err)
    }
}

func TestReadAndParseHookInput_ValidPayload(t *testing.T) {
    t.Parallel()

got, err := ReadAndParseHookInput[hookInput](strings.NewReader(`{"session_id":"abc","transcript_path":"/x"}`))
    if err != nil {
        t.Fatalf("unexpected error: %v", err)
    }
    if got.SessionID != "abc" || got.TranscriptPath != "/x" {
        t.Fatalf("unexpected value: %+v", got)
    }
}

Acmd/entire/cli/agent/event_test.go+127

231 unmodified lines

232
233
234
235
235
236
237
238
239
240
241
242
243
239
244
245
246
247

231 unmodified lines

func (e *Agent) ParseHookEvent(ctx context.Context, hookName string, stdin io.Reader) (*agent.Event, error) {
    const maxParseHookBytes = 10 * 1024 * 1024 // 10 MB
    data, err := io.ReadAll(io.LimitReader(stdin, maxParseHookBytes))
    // Stream a single (size-bounded) JSON value rather than io.ReadAll, so the
    // hook never blocks waiting for stdin EOF that some agents don't send on
    // Windows/Git Bash (issue #1398). The external "parse-hook" contract receives
    // the host's hook payload — which is JSON — and we forward its raw bytes
    // verbatim to the subprocess, so a plain byte copy is preserved.
    raw, err := agent.ReadHookInputRawLimited(stdin, maxParseHookBytes)
    if err != nil {
        return nil, fmt.Errorf("parse-hook: read stdin: %w", err)
    }
    stdout, err := e.run(ctx, data, "parse-hook", "--hook", hookName)
    stdout, err := e.run(ctx, raw, "parse-hook", "--hook", hookName)
    if err != nil {
        return nil, fmt.Errorf("parse-hook: %w", err)
    }

Mcmd/entire/cli/agent/external/external.go+7/-2

2 unmodified lines

3
4
5
6
6
7
8
136 unmodified lines

145
146
147
149
148
149
150
151
151
152
153
153
154
155
156
157
158
159
160
154
155
156
157

2 unmodified lines

import (
    "context"
    "encoding/json"
    "errors"
    "fmt"
    "io"
    "log/slog"
136 unmodified lines

// ParseHookEvent translates a Pi hook invocation into a normalised lifecycle
// event. Implements agent.HookSupport.
func (a *PiAgent) ParseHookEvent(ctx context.Context, hookName string, stdin io.Reader) (*agent.Event, error) {
    data, err := io.ReadAll(stdin)
    // Stream one JSON value rather than io.ReadAll so the hook never blocks
    // waiting for stdin EOF that some agents don't send on Windows (issue #1398).
    parsed, err := agent.ReadAndParseHookInput[piHookPayload](stdin)
    if err != nil {
        return nil, fmt.Errorf("read pi hook input: %w", err)
        return nil, err
    }
    if len(data) == 0 {
        return nil, errors.New("empty pi hook input")
    }

var payload piHookPayload
    if err := json.Unmarshal(data, &payload); err != nil {
        return nil, fmt.Errorf("parse pi hook payload: %w", err)
    }
    payload := *parsed

sessionID := payload.SessionID
    if sessionID == "" {

Mcmd/entire/cli/agent/pi/lifecycle.go+5/-11

16 unmodified lines

17
18
19
20
21
22
23
394 unmodified lines

418
419
420
420
421
422
423
424

16 unmodified lines

"github.com/entireio/cli/cmd/entire/cli/palette"
    "github.com/entireio/cli/internal/coreapi"
    "github.com/entireio/cli/internal/entireclient/contexts"
    "github.com/entireio/cli/internal/entireclient/tokenstore"
    "github.com/spf13/cobra"
)

394 unmodified lines

if t.activeContext != "" {
        writeAuthStatusLine(w, "Context:", t.activeContext)
    }
    writeAuthStatusLine(w, "Token:", "stored in OS keychain")
    writeAuthStatusLine(w, "Token:", "stored in "+tokenstore.BackendDescription())

// Active sessions on this core. The token is already known good, so a
    // listing failure is non-fatal — note it and carry on.

Mcmd/entire/cli/auth.go+2/-1

19 unmodified lines

20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
56 unmodified lines

96
97
98
85
99
100
101
102
1 unmodified line

104
105
106
93
107
108
109
110

19 unmodified lines

// so a conservative non-zero value is enough to keep the entry usable.
const defaultContextTokenTTL = time.Hour

// ErrCredentialStoreWrite marks a failure writing tokens to the configured
// credential backend (OS keyring or file store), as opposed to claim
// validation or contexts.json failures. Login UX branches on it via
// errors.Is to decide whether pointing the user at the file token store
// would actually help.
var ErrCredentialStoreWrite = errors.New("credential store write failed")

// credStoreWriteError tags an underlying store error with
// ErrCredentialStoreWrite without changing its message.
type credStoreWriteError struct{ inner error }

func (e *credStoreWriteError) Error() string   { return e.inner.Error() }
func (e *credStoreWriteError) Unwrap() []error { return []error{e.inner, ErrCredentialStoreWrite} }

// RecordLoginContext records a freshly obtained login token in the
// shared contexts.json credential model: it derives the issuer (core
// URL), handle, and expiry from the token's own claims, stores the token
56 unmodified lines

refreshSlot := tokenstore.RefreshService(keychainService)
    if refreshToken != "" {
        if err := tokenstore.Set(refreshSlot, handle, refreshToken); err != nil {
            return "", fmt.Errorf("store refresh token in keyring: %w", err)
            return "", fmt.Errorf("store refresh token in credential store: %w", &credStoreWriteError{err})
        }
    } else {
        _ = tokenstore.Delete(refreshSlot, handle) //nolint:errcheck // best-effort cleanup of a stale refresh token
1 unmodified line

encoded := tokenstore.EncodeTokenWithExpiration(rawToken, expiresIn)
    if err := tokenstore.Set(keychainService, handle, encoded); err != nil {
        return "", fmt.Errorf("store login token in keyring: %w", err)
        return "", fmt.Errorf("store login token in credential store: %w", &credStoreWriteError{err})
    }

var name string

Mcmd/entire/cli/auth/contexts.go+16/-2

430 unmodified lines

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

430 unmodified lines

}
    }
}

// The Token: provenance line must reflect the configured credential backend:
// with ENTIRE_TOKEN_STORE=file the token lives in a JSON file, not the OS
// keychain, and claiming otherwise misleads exactly the headless users the
// file backend exists for (#1036).
func TestRunAuthStatus_FileTokenStoreProvenance(t *testing.T) {
    // Not parallel: t.Setenv.
    t.Setenv("ENTIRE_TOKEN_STORE", "file")
    t.Setenv("ENTIRE_TOKEN_STORE_PATH", "/ci/secrets/tokens.json")

target := statusTarget{coreURL: testCoreURL, token: "tok", activeContext: "core"}
    listSessions := func(context.Context, string, string) ([]api.AuthSession, error) { return nil, nil }

var out bytes.Buffer
    if err := runAuthStatus(context.Background(), &out, okProfile, listSessions, target); err != nil {
        t.Fatalf("unexpected error: %v", err)
    }
    got := out.String()
    if !strings.Contains(got, "stored in file /ci/secrets/tokens.json") {
        t.Fatalf("output = %q, want the file-backend provenance line", got)
    }
    if strings.Contains(got, "OS keychain") {
        t.Fatalf("output = %q, must not claim the OS keychain when the file backend is configured", got)
    }
}

Mcmd/entire/cli/auth_test.go+25

4694 unmodified lines

4695
4696
4697
4698
4698
4699
4700
4701

4694 unmodified lines

// Summary.Intent and ReviewPrompt that previously bypassed redaction because
// the dispatcher only matched .jsonl. The PR 1236 fix extended the JSON-aware
// branch to .json. We assert via a low-entropy AWS-key shaped secret (catches
// the 7-layer pipeline) so the test stays deterministic without the OPF binary.
// the regex-only pipeline) so the test stays deterministic without the OPF binary.
func TestRedactBlobBytes_JSONMetadata(t *testing.T) {
    t.Parallel()

Mcmd/entire/cli/checkpoint/checkpoint_test.go+1/-1

447 unmodified lines

448
449
450
451
451
452
453
454
260 unmodified lines

715
716
717
718
718
719
720
721
1532 unmodified lines

2254
2255
2256
2257
2258
2257
2258
2259
2260
2261
2261
2262
2262
2263
2264
2265
2266
15 unmodified lines

2282
2283
2284
2284
2285
2286
2287
2288
2289
2285
2286
2287
2288
2289
2290
2291
2292
2293
2294
35 unmodified lines

2330
2331
2332
2331
2332
2333
2334
2335
2336
2337

447 unmodified lines

}
    }

// Replace prompts with 7-layer-redacted content.
    // Replace prompts with regex-only-redacted content.
    if len(opts.Prompts) > 0 {
        promptContent := RedactedJoinedPrompts(opts.Prompts)
        blobHash, err := CreateBlobFromContent(s.repo, []byte(promptContent))
260 unmodified lines

}
    filePaths.AssetsManifest = manifestPath

// Write prompts via the 7-layer pipeline. OPF runs only in the
    // Write prompts via the regex-only pipeline. OPF runs only in the
    // pre-push rewrite path (manual_commit_opf_rewrite.go).
    if len(opts.Prompts) > 0 {
        promptContent := RedactedJoinedPrompts(opts.Prompts)
1532 unmodified lines

return fmt.Errorf("path traversal detected: %s", relPath)
        }

// Create blob from file with 7-layer secrets redaction.
        // Post-commit emits 7-layer-only blobs; the pre-push rewrite
        // Create blob from file with regex-only secrets redaction (the
        // eight always-on/opt-in layers).
        // Post-commit emits regex-only blobs; the pre-push rewrite
        // (strategy/manual_commit_opf_rewrite.go) walks the resulting
        // tree, re-redacts these blobs with OPF when enabled, and
        // rewrites entire/checkpoints/v1 into 8-layer commits before
        // they leave the local machine.
        // rewrites entire/checkpoints/v1 into OPF-applied (9-layer)
        // commits before they leave the local machine.
        blobHash, mode, err := createRedactedBlobFromFile(ctx, s.repo, path, relPath)
        if err != nil {
            return fmt.Errorf("failed to create blob for %s: %w", path, err)
15 unmodified lines

return nil
}

// createRedactedBlobFromFile reads a file, applies the 7-layer redaction
// pipeline, and creates a git blob. Used by committed-checkpoint writes
// at post-commit time. The OpenAI Privacy Filter is intentionally NOT
// run here — OPF lives in the pre-push rewrite path
// (strategy/manual_commit_opf_rewrite.go), which re-redacts the 7-layer
// blobs into 8-layer commits before they leave the local machine.
// createRedactedBlobFromFile reads a file, applies the regex-only redaction
// pipeline (the eight always-on/opt-in layers), and creates a git blob. Used
// by committed-checkpoint writes at post-commit time. The OpenAI Privacy
// Filter is intentionally NOT run here — OPF lives in the pre-push rewrite
// path (strategy/manual_commit_opf_rewrite.go), which re-redacts the
// regex-only blobs into OPF-applied (9-layer) commits before they leave the
// local machine.
// JSONL files get JSONL-aware redaction; all other files get plain byte redaction.
func createRedactedBlobFromFile(ctx context.Context, repo *git.Repository, filePath, treePath string) (plumbing.Hash, filemode.FileMode, error) {
    info, err := os.Stat(filePath)
35 unmodified lines

// JSON-shaped files (.jsonl or .json) get JSON-aware redaction (falling
// back to plain bytes on parse failure so regex/credential layers
// still apply); other files get plain byte redaction. When
// usePrivacyFilter is true the full 8-layer pipeline (including OPF)
// runs; otherwise the 7-layer pipeline.
// usePrivacyFilter is true the full 9-layer pipeline (the eight regex
// layers plus OPF) runs; otherwise just the eight regex layers.
//
// .json is handled alongside .jsonl because checkpoint metadata files
// (metadata.json, per-session metadata.json) carry free-form fields

Mcmd/entire/cli/checkpoint/persistent.go+16/-14

17 unmodified lines

18
19
20
21
21
22
23
24
25
26
25
26
27
28
29

17 unmodified lines

// TestWriteCommitted_DoesNotEmitOPFAppliedTrailer is the regression guard
// for the architectural promise: standard post-commit condensation writes
// 7-layer-only blobs and MUST NOT mark them with the Entire-OPF-Applied
// regex-only blobs and MUST NOT mark them with the Entire-OPF-Applied
// trailer. The trailer is emitted exclusively by the pre-push rewrite
// path; if a future change accidentally added it to the standard writer,
// the pre-push rewrite would skip those commits (HasOPFApplied true →
// reparent-only, no actual OPF run) and ship 7-layer content as if it
// were 8-layer. This test pins down that contract.
// reparent-only, no actual OPF run) and ship regex-only content as if it
// were OPF-applied. This test pins down that contract.
func TestWriteCommitted_DoesNotEmitOPFAppliedTrailer(t *testing.T) {
    t.Parallel()

Mcmd/entire/cli/checkpoint/persistent_opf_trailer_test.go+3/-3

22 unmodified lines

23
24
25
26
27
26
27
28
29
30
31

22 unmodified lines

return prompts
}

// RedactedJoinedPrompts joins prompts and runs the 7-layer redaction
// pipeline. OPF runs exclusively in the pre-push rewrite (not here),
// RedactedJoinedPrompts joins prompts and runs the regex-only redaction
// pipeline (the eight always-on/opt-in layers). OPF runs exclusively in
// the pre-push rewrite (not here),
// so the writer's hot path stays predictable. Exported so alternate
// persistent backends produce identically-redacted prompt blobs.
func RedactedJoinedPrompts(prompts []string) string {

Mcmd/entire/cli/checkpoint/prompts.go+3/-2

27 unmodified lines

28
29
30
31
32
31
32
33
34
35
36

27 unmodified lines

}

// TestRedactedJoinedPrompts_AppliesSafetyNet verifies the helper joins
// prompts with the canonical separator and runs them through the 7-layer
// pipeline. OPF runs only in the pre-push rewrite path, never here.
// prompts with the canonical separator and runs them through the
// regex-only pipeline. OPF runs only in the pre-push rewrite path, never
// here.
func TestRedactedJoinedPrompts_AppliesSafetyNet(t *testing.T) {
    t.Parallel()
    got := RedactedJoinedPrompts([]string{"hello", "world"})

Mcmd/entire/cli/checkpoint/prompts_test.go+3/-2

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

18 unmodified lines

19
20
21
22
23
24
25
82 unmodified lines

108
109
110
110
111
112
111
112
113
114
115
116
117
118
119
120
121
122
123
124

18 unmodified lines

"github.com/entireio/cli/cmd/entire/cli/gitrepo"
    "github.com/entireio/cli/cmd/entire/cli/logging"
    "github.com/entireio/cli/cmd/entire/cli/paths"
    "github.com/entireio/cli/cmd/entire/cli/settings"
    "github.com/entireio/cli/cmd/entire/cli/strategy"
    "github.com/entireio/cli/cmd/entire/cli/telemetry"
    "github.com/entireio/cli/cmd/entire/cli/versioncheck"
82 unmodified lines

return nil
    }

// Skip if Entire is not enabled
    enabled, err := IsEnabled(cmd.Context())
    if err == nil && !enabled {
    // Skip if Entire is not set up and enabled. This must fail closed: any
    // settings read error (missing file, corrupted JSON, transient I/O
    // failure) is treated as disabled so a hook never silently falls through
    // to full lifecycle work just because settings couldn't be read. Using
    // IsEnabled here previously failed OPEN on error (`err == nil && !enabled`
    // only short-circuits when the read succeeded), which meant a corrupted
    // or unreadable settings file made every hook invocation pay the full
    // dispatch cost instead of exiting fast (#524).
    // settings.IsSetUpAndEnabled is the same fail-closed gate the git hooks
    // use (see PersistentPreRunE in hooks_git_cmd.go).
    if !settings.IsSetUpAndEnabled(cmd.Context()) {
        return nil
    }

Mcmd/entire/cli/hook_registry.go+12/-3

230 unmodified lines

231
232
233
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

230 unmodified lines

require.True(t, os.IsNotExist(statErr), "session-start must not claim the session when checkpoint policy is unreadable")
}

// TestExecuteAgentHookShortCircuitsWhenDisabled is a regression test for #524:
// a hook must not perform any dispatch/strategy work when Entire is
// disabled. Asserted via the same "session was never claimed" signal the
// checkpoint-policy tests above use, rather than a timing assertion.
func TestExecuteAgentHookShortCircuitsWhenDisabled(t *testing.T) {
    setupStopTestRepo(t)
    repoRoot := mustGetwd(t)

entireDir := filepath.Join(repoRoot, ".entire")
    require.NoError(t, os.MkdirAll(entireDir, 0o750))
    require.NoError(t, os.WriteFile(filepath.Join(entireDir, "settings.json"), []byte(`{"enabled":false}`), 0o600))

sessionID := "disabled-session-start"
    payload, err := json.Marshal(map[string]string{
        "session_id":      sessionID,
        "transcript_path": filepath.Join(repoRoot, "transcript.jsonl"),
    })
    require.NoError(t, err)

cmd := &cobra.Command{}
    cmd.SetIn(bytes.NewReader(payload))
    cmd.SetErr(&bytes.Buffer{})
    cmd.SetContext(context.Background())

require.NoError(t, executeAgentHook(cmd, agent.AgentNameClaudeCode, claudecode.HookNameSessionStart, false))

hintPath := filepath.Join(repoRoot, ".git", session.SessionStateDirName, sessionID+".agent")
    _, statErr := os.Stat(hintPath)
    require.True(t, os.IsNotExist(statErr), "disabled hook must not dispatch or claim the session")
}

// TestExecuteAgentHookShortCircuitsWhenSettingsMissing is a regression test
// for #524: a repo that was never `entire enable`d (no .entire/settings.json)
// must short-circuit rather than falling through to full lifecycle dispatch.
func TestExecuteAgentHookShortCircuitsWhenSettingsMissing(t *testing.T) {
    setupStopTestRepo(t)
    repoRoot := mustGetwd(t)
    // Deliberately do NOT create .entire/settings.json.

sessionID := "missing-settings-session-start"
    payload, err := json.Marshal(map[string]string{
        "session_id":      sessionID,
        "transcript_path": filepath.Join(repoRoot, "transcript.jsonl"),
    })
    require.NoError(t, err)

cmd := &cobra.Command{}
    cmd.SetIn(bytes.NewReader(payload))
    cmd.SetErr(&bytes.Buffer{})
    cmd.SetContext(context.Background())

require.NoError(t, executeAgentHook(cmd, agent.AgentNameClaudeCode, claudecode.HookNameSessionStart, false))

hintPath := filepath.Join(repoRoot, ".git", session.SessionStateDirName, sessionID+".agent")
    _, statErr := os.Stat(hintPath)
    require.True(t, os.IsNotExist(statErr), "hook must not dispatch when Entire was never enabled in this repo")
}

// TestExecuteAgentHookShortCircuitsWhenSettingsCorrupted is a regression test
// for #524. Before this fix, IsEnabled() failed OPEN on a settings.Load()
// error (the caller's `err == nil && !enabled` check only short-circuited
// when the read succeeded), so a corrupted settings file made every hook
// invocation pay the full dispatch cost — including, for Stop hooks, a
// multi-second wait on the transcript-flush sentinel (see
// ClaudeCodeAgent.ParseHookEvent) — instead of exiting fast. The gate must
// fail closed on any settings read error.
func TestExecuteAgentHookShortCircuitsWhenSettingsCorrupted(t *testing.T) {
    setupStopTestRepo(t)
    repoRoot := mustGetwd(t)

entireDir := filepath.Join(repoRoot, ".entire")
    require.NoError(t, os.MkdirAll(entireDir, 0o750))
    require.NoError(t, os.WriteFile(filepath.Join(entireDir, "settings.json"), []byte(`{ enabled: false, not valid json`), 0o600))

sessionID := "corrupted-settings-session-start"
    payload, err := json.Marshal(map[string]string{
        "session_id":      sessionID,
        "transcript_path": filepath.Join(repoRoot, "transcript.jsonl"),
    })
    require.NoError(t, err)

cmd := &cobra.Command{}
    cmd.SetIn(bytes.NewReader(payload))
    cmd.SetErr(&bytes.Buffer{})
    cmd.SetContext(context.Background())

require.NoError(t, executeAgentHook(cmd, agent.AgentNameClaudeCode, claudecode.HookNameSessionStart, false))

hintPath := filepath.Join(repoRoot, ".git", session.SessionStateDirName, sessionID+".agent")
    _, statErr := os.Stat(hintPath)
    require.True(t, os.IsNotExist(statErr), "hook must fail closed (not dispatch) when settings are unreadable")
}

// TestExecuteAgentHookStopReturnsFastWhenSettingsCorrupted directly
// regression-tests the reported symptom: `entire hooks claude-code stop`
// against a corrupted settings file must return in well under the
// multi-second transcript-flush-sentinel timeout it used to hit, not just
// skip dispatch. The bound is intentionally generous (this repo has no
// other timing-based tests to match precedent against) — it only needs to
// distinguish "short-circuited" from "waited on the sentinel timeout".
func TestExecuteAgentHookStopReturnsFastWhenSettingsCorrupted(t *testing.T) {
    setupStopTestRepo(t)
    repoRoot := mustGetwd(t)

transcriptPath := filepath.Join(repoRoot, "transcript.jsonl")
    require.NoError(t, os.WriteFile(transcriptPath, []byte(`{"type":"user","message":{"content":"hi"}}`+"\n"), 0o600))

payload, err := json.Marshal(map[string]string{
        "session_id":      "corrupted-settings-stop",
        "transcript_path": transcriptPath,
    })
    require.NoError(t, err)

cmd := &cobra.Command{}
    cmd.SetIn(bytes.NewReader(payload))
    cmd.SetErr(&bytes.Buffer{})
    cmd.SetContext(context.Background())

start := time.Now()
    require.NoError(t, executeAgentHook(cmd, agent.AgentNameClaudeCode, claudecode.HookNameStop, false))
    elapsed := time.Since(start)

require.Lessf(t, elapsed, 1*time.Second,
        "stop hook took %s against a corrupted settings file; want a fast short-circuit, not the transcript-flush-sentinel timeout path", elapsed)
}

// TestExecuteAgentHookCapturesWhenEnabledViaLocalSettingsOnly guards against a
// regression in the #524 fix: `entire enable --local` writes only
// .entire/settings.local.json and never creates the base .entire/settings.json
// (see determineSettingsTarget in setup.go). The disabled-hook gate must
// recognize that local-only enablement — gating on the base file alone
// (settings.IsSetUp) would silently no-op every agent hook for that repo and
// drop all checkpoint capture. Asserted via the same "session was claimed"
// signal (the .agent hint StoreAgentTypeHint writes during SessionStart
// dispatch) the short-circuit tests above assert the *absence* of.
func TestExecuteAgentHookCapturesWhenEnabledViaLocalSettingsOnly(t *testing.T) {
    setupStopTestRepo(t)
    repoRoot := mustGetwd(t)

entireDir := filepath.Join(repoRoot, ".entire")
    require.NoError(t, os.MkdirAll(entireDir, 0o750))
    // Local-only enablement: settings.local.json present, base settings.json absent.
    require.NoError(t, os.WriteFile(filepath.Join(entireDir, "settings.local.json"), []byte(`{"enabled":true}`), 0o600))
    require.NoFileExists(t, filepath.Join(entireDir, "settings.json"))

sessionID := "local-only-session-start"
    payload, err := json.Marshal(map[string]string{
        "session_id":      sessionID,
        "transcript_path": transcriptPath,
    })
    require.NoError(t, err)

cmd := &cobra.Command{}
    cmd.SetIn(bytes.NewReader(payload))
    cmd.SetErr(&bytes.Buffer{})
    cmd.SetContext(context.Background())

require.NoError(t, executeAgentHook(cmd, agent.AgentNameClaudeCode, claudecode.HookNameSessionStart, false))

hintPath := filepath.Join(repoRoot, ".git", session.SessionStateDirName, sessionID+".agent")
    require.FileExists(t, hintPath, "SessionStart must dispatch and claim the session when Entire is enabled via settings.local.json only")
}

func TestAgentHookPolicyFailsWhenRepoCannotOpen(t *testing.T) {
    _, err := agentHookPolicy(context.Background(), filepath.Join(t.TempDir(), "missing"))

Mcmd/entire/cli/hook_registry_test.go+170

1 unmodified line

2
3
4
5
6
5
6
7
8
9
10
8 unmodified lines

19
20
21
23
22
23
24
25
26
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
27
28
29
30

1 unmodified line

import (
    "encoding/json"
    "errors"
    "fmt"
    "io"

"github.com/entireio/cli/cmd/entire/cli/agent"
    "github.com/entireio/cli/cmd/entire/cli/strategy"
)

8 unmodified lines

ToolResponse   json.RawMessage `json:"tool_response"`
}

// parseSubagentCheckpointHookInput parses PostToolUse hook input for subagent checkpoints
// parseSubagentCheckpointHookInput parses PostToolUse hook input for subagent
// checkpoints. It streams a single JSON value rather than reading to EOF so the
// claude-code post-todo hook never blocks waiting for a stdin close that some
// agents don't send on Windows (issue #1398).
func parseSubagentCheckpointHookInput(r io.Reader) (*SubagentCheckpointHookInput, error) {
    data, err := io.ReadAll(r)
    if err != nil {
        return nil, fmt.Errorf("failed to read input: %w", err)
    }

if len(data) == 0 {
        return nil, errors.New("empty input")
    }

var input SubagentCheckpointHookInput
    if err := json.Unmarshal(data, &input); err != nil {
        return nil, fmt.Errorf("failed to parse JSON: %w", err)
    }

return &input, nil
    return agent.ReadAndParseHookInput[SubagentCheckpointHookInput](r)
}

// taskToolInput represents the tool_input structure for the Task tool.

Mcmd/entire/cli/hooks.go+6/-18

325 unmodified lines

326
327
328
329
329
330
331
332

325 unmodified lines

defer g.span.End()
            g.logInvoked(slog.String("remote", remote))

hookErr := g.strategy.PrePush(g.ctx, remote)
            hookErr := g.strategy.PrePushFromGitHook(g.ctx, remote)
            g.logCompleted(hookErr)

// Propagate the error so the hook script exits non-zero and

Mcmd/entire/cli/hooks_git_cmd.go+1/-1

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

42 unmodified lines

}
    })
}

// TestGitPushWithHooks_DefersCheckpointsUntilFirstUserBranchExists ensures the
// user's own branch — not Entire metadata — is the first ref on a fresh remote.
//
// On the git-branch backend, entire/checkpoints/v1 is a real branch a forge
// could pick as the repository default, so its push is deferred until the
// user's branch has landed. On the git-refs backend, checkpoints live under
// refs/entire/*, which a forge cannot select as a default branch, so there is
// no hazard and they publish on the first push.
func TestGitPushWithHooks_DefersCheckpointsUntilFirstUserBranchExists(t *testing.T) {
    t.Parallel()

ForEachBackend(t, func(t *testing.T, backend string) {
        env := NewFeatureBranchEnv(t)
        env.CheckpointStore = backend

bareDir := env.SetupEmptyNamedBareRemote("origin")
        branch := env.GetCurrentBranch()
        checkpointID := createCheckpointedCommit(t, env, "Add auth module", "auth.go", "package auth", "Add auth module")
        if checkpointID == "" {
            t.Fatal("should have a checkpoint ID after condensation")
        }

// The first push must land the user's branch on the empty remote.
        env.GitPushWithHooks("origin", "HEAD")
        if !env.BranchExistsOnRemote(bareDir, branch) {
            t.Fatalf("[%s] first user branch %q should be on remote", backend, branch)
        }

if backend == StoreGitRefs {
            // refs/entire/* can't become a default branch → no deferral.
            if !env.CheckpointExistsOnRemote(bareDir, checkpointID) {
                t.Fatalf("[git-refs] checkpoint %s should publish on the first push (no default-branch hazard)", checkpointID)
            }
            return
        }

// git-branch: the v1 branch must be withheld until the user branch exists.
        if env.CheckpointsPresentOnRemote(bareDir) {
            t.Fatalf("[git-branch] checkpoints must be deferred until after the first user branch push")
        }

// The first push created a remote-tracking ref, so a later push publishes.
        env.WriteFile("later.go", "package later")
        env.GitAdd("later.go")
        env.GitCommit("Later user commit")
        env.GitPushWithHooks("origin", "HEAD")
        if !env.CheckpointExistsOnRemote(bareDir, checkpointID) {
            t.Fatalf("[git-branch] deferred checkpoint %s should be published on a later push", checkpointID)
        }
    })
}

Mcmd/entire/cli/integration_test/real_hook_push_test.go+52

214 unmodified lines

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
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269

214 unmodified lines

t.Errorf("Expected status to show 'manual-commit', got: %s", stdout)
    }
}

// TestHooksRunAfterLocalOnlyEnable is a full-flow reproduction of the
// `entire enable --local` regression: only .entire/settings.local.json
// exists, and the hooks (gated on settings.IsSetUpAndEnabled) silently
// no-op'd because that check only looked at settings.json — so a commit
// produced no checkpoint.
//
// This drives the real hook binary end-to-end: a session, a
// user-prompt-submit, a file change, a stop, and a commit — then asserts the
// commit actually carries an Entire-Checkpoint trailer (i.e. the hooks ran
// and a checkpoint was saved). Complements TestHooksSilentWhenDisabled above,
// which covers the opposite case.
func TestHooksRunAfterLocalOnlyEnable(t *testing.T) {
    t.Parallel()
    env := NewTestEnv(t)
    defer env.Cleanup()

env.InitRepo()
    env.WriteFile("README.md", "# Test")
    env.GitAdd("README.md")
    env.GitCommit("Initial commit")
    env.GitCheckoutNewBranch("feature/local-only")

// Simulate `entire enable --local`: only settings.local.json exists.
    entireDir := filepath.Join(env.RepoDir, ".entire")
    if err := os.MkdirAll(filepath.Join(entireDir, "tmp"), 0o755); err != nil {
        t.Fatalf("mkdir .entire/tmp: %v", err)
    }
    localSettings := `{"enabled":true,"local_dev":true,"strategy_options":{"filtered_fetches":true}}`
    if err := os.WriteFile(filepath.Join(entireDir, "settings.local.json"), []byte(localSettings), 0o644); err != nil {
        t.Fatalf("write settings.local.json: %v", err)
    }
    if _, err := os.Stat(filepath.Join(entireDir, "settings.json")); err == nil {
        t.Fatal("precondition: settings.json must not exist for the enable --local scenario")
    }

session := env.NewSession()
    if err := env.SimulateUserPromptSubmitWithPrompt(session.ID, "Create a hello file"); err != nil {
        t.Fatalf("user-prompt-submit: %v", err)
    }
    env.WriteFile("hello.txt", "hello")
    session.CreateTranscript("Create a hello file", []FileChange{{Path: "hello.txt", Content: "hello"}})
    if err := env.SimulateStop(session.ID, session.TranscriptPath); err != nil {
        t.Fatalf("stop: %v", err)
    }
    env.GitCommitWithShadowHooksAsAgent("add hello", "hello.txt")

cpID := env.GetCheckpointIDFromCommitMessage(env.GetHeadHash())
    if cpID == "" {
        t.Fatal("commit has no Entire-Checkpoint trailer — hooks silently no-op'd with only settings.local.json")
    }
}

Mcmd/entire/cli/integration_test/setup_cmd_test.go+52

//go:build integration

package integration

import ( "os" "strings" "testing"

"github.com/entireio/cli/cmd/entire/cli/paths" )

// TestSupabaseSecretRedaction_FullHookFlow is the end-to-end regression for // issue #1716. It drives the real entire hook binary (UserPromptSubmit -> // mid-turn commit/condensation -> Stop/finalize) on a Claude Code session // whose transcript embeds a Supabase sb_secret_ API key across the two vectors // from the issue report (prompt text and shell-tool input/output), then reads // the real entire/checkpoints/v1 transcript blob back and proves: // - the sb_secret_ value does NOT survive into the condensed blob, // - a REDACTED placeholder is present, // - a plain capture-control marker DID survive (so a zero secret count means // redaction happened, not that capture failed — mirroring the issue's // methodology), // - a sb_publishable_ key (public by design) is NOT over-redacted. // // Every sb_secret_ occurrence uses a low-entropy synthetic token, which the // entropy layer (threshold 4.5) misses regardless of quoting or surrounding // prose, and no *.supabase.co URL is co-present, so the composite betterleaks // Supabase rule does not fire either — so redaction here is attributable to // the deterministic provider-prefix layer added for this fix. func TestSupabaseSecretRedaction_FullHookFlow(t *testing.T) { // Hook subprocesses share settings/env; do not run in parallel. // The sb_secret_ / sb_publishable_ prefixes are assembled from fragments so // a complete Supabase-shaped token never appears verbatim in source, keeping // secret scanners (including GitHub push protection) from flagging these // synthetic fixtures; the runtime values are complete. const ( supabaseSecret = "sb" + "secret" + "probe_20260710_7f91c2d8e4a6b3f0" supabasePublishable = "sb" + "publishable" + "probe_20260710_7f91c2d8e4a6b3f0" captureControl = "CAPTURE_CONTROL_MARKER_9f" )

env := NewFeatureBranchEnv(t) session := env.NewSession()

// Author a Claude Code transcript: a prompt that names the secret (vector 1) // plus the publishable control and capture marker, a Bash tool_use whose // command exports the secret (vector 2 input), the shell tool_result echoing // the secret (vector 2 output), then a file-writing tool use so the commit // has attributable content. prompt := "Configure the backend. The service_role key is " + supabaseSecret + " and the public client key " + supabasePublishable + " is safe to commit. " + captureControl transcript := strings.Join([]string{ {"uuid":"u1","type":"user","message":{"role":"user","content":" + prompt + "},"timestamp":"2026-01-01T00:00:00Z"}, {"uuid":"a1","type":"assistant","message":{"content":[{"type":"tool_use","id":"toolu_1","name":"Bash","input":{"command":"export SUPABASE_SERVICE_ROLE_KEY=' + supabaseSecret + '","description":"set service role key"}}]},"timestamp":"2026-01-01T00:00:01Z"}, {"uuid":"u2","type":"user","message":{"content":[{"type":"tool_result","tool_use_id":"toolu_1","content":"Applied. Wrote key + supabaseSecret + to env. + captureControl + "}]},"timestamp":"2026-01-01T00:00:02Z"}, {"uuid":"a2","type":"assistant","message":{"content":[{"type":"tool_use","id":"toolu_2","name":"Write","input":{"file_path":"feature.go","content":"package main\n"}}]},"timestamp":"2026-01-01T00:00:03Z"}, {"uuid":"u3","type":"user","message":{"content":[{"type":"tool_result","tool_use_id":"toolu_2","content":"Success"}]},"timestamp":"2026-01-01T00:00:04Z"}, {"uuid":"a3","type":"assistant","message":{"content":[{"type":"text","text":"done"}]},"timestamp":"2026-01-01T00:00:05Z"}, }, "\n") + "\n" if err := os.WriteFile(session.TranscriptPath, []byte(transcript), 0o644); err != nil { t.Fatalf("write transcript: %v", err) }

if err := env.SimulateUserPromptSubmitWithPromptAndTranscriptPath(session.ID, prompt, session.TranscriptPath); err != nil { t.Fatalf("UserPromptSubmit: %v", err) }

// Mid-turn commit -> post-commit condensation runs redaction (redact.JSONLBytes). env.WriteFile("feature.go", "package main\n") env.GitCommitWithShadowHooks("add feature", "feature.go")

// Stop -> finalize rewrites the turn checkpoint with the full transcript. if err := env.SimulateStop(session.ID, session.TranscriptPath); err != nil { t.Fatalf("Stop: %v", err) }

if !env.BranchExists(paths.MetadataBranchName) { t.Fatal("entire/checkpoints/v1 should exist after condensation") } cpID := env.GetLatestCheckpointIDFromHistory() if cpID == "" { t.Fatal("no checkpoint id found in history") } sessionPath := ShardedCheckpointPath(cpID) + "/0/"

full, ok := env.ReadFileFromBranch(paths.MetadataBranchName, sessionPath+paths.TranscriptFileName) if !ok { t.Fatalf("full.jsonl missing at %s", sessionPath) }

// Evidence: dump the actual checkpoint blob (equivalent to // git show entire/checkpoints/v1:<path>). t.Logf("checkpoint %s blob %s%s:\n%s", cpID, sessionPath, paths.TranscriptFileName, full)

secretCount := strings.Count(full, supabaseSecret) t.Logf("occurrences in condensed blob: sb_secret_=%d REDACTED=%d publishable=%d capture-control=%d", secretCount, strings.Count(full, "REDACTED"), strings.Count(full, supabasePublishable), strings.Count(full, captureControl))

// Capture control must survive, otherwise a zero secret count is meaningless. if !strings.Contains(full, captureControl) { t.Fatalf("capture-control marker %q missing from blob — transcript content did not reach the checkpoint, so the secret check is inconclusive", captureControl) } // The bug: sb_secret_ must not survive into the checkpoint blob. if secretCount != 0 { t.Fatalf("issue #1716 regression: sb_secret_ key survived redaction into the checkpoint blob (%d occurrences)", secretCount) } if !strings.Contains(full, "REDACTED") { t.Fatal("expected a REDACTED placeholder in the condensed transcript") } // Publishable keys are public by design and must not be over-redacted. if !strings.Contains(full, supabasePublishable) { t.Errorf("sb_publishable_ key was over-redacted; publishable keys are designed to be public and must survive") } }


Acmd/entire/cli/integration\_test/supabase\_secret\_redaction\_test.go+117

312 unmodified lines

313 314 315 316 316 317 318 319 320 321 322 323 324 325 323 324 325 326 327 1476 unmodified lines

1804 1805 1806 1807 1808 1809 1810 1811 1812 1813 1814 1815 1816 1817 1818 1819 1820 1821 1822 1823 1824 1825 1826 1827 1828 18 unmodified lines

1847 1848 1849 1832 1833 1834 1835 1836 1837 1838 1839 1850 1851 1852

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 "" 1476 unmodified lines

// multiple remotes. func (env *TestEnv) SetupNamedBareRemote(remoteName string) string { env.T.Helper() bareDir := env.SetupEmptyNamedBareRemote(remoteName)

// Push HEAD to the remote. cmd := exec.CommandContext(env.T.Context(), "git", "push", "--no-verify", "-u", remoteName, "HEAD") cmd.Dir = env.RepoDir cmd.Env = testutil.GitIsolatedEnv() if output, err := cmd.CombinedOutput(); err != nil { env.T.Fatalf("failed to push to %s: %v\n%s", remoteName, err, output) }

env.setGitConfigBaseline()

return bareDir }

// SetupEmptyNamedBareRemote creates a bare git repository and adds it as a // remote without pushing a branch. Use this to exercise first-push behavior. func (env *TestEnv) SetupEmptyNamedBareRemote(remoteName string) string { env.T.Helper()

ctx := env.T.Context()

18 unmodified lines

env.T.Fatalf("failed to add remote %s: %v\n%s", remoteName, err, output) }

// Push HEAD to the remote cmd = exec.CommandContext(ctx, "git", "push", "--no-verify", "-u", remoteName, "HEAD") cmd.Dir = env.RepoDir cmd.Env = testutil.GitIsolatedEnv() if output, err := cmd.CombinedOutput(); err != nil { env.T.Fatalf("failed to push to %s: %v\n%s", remoteName, err, output) }

env.setGitConfigBaseline()

return bareDir


Mcmd/entire/cli/integration\_test/testenv.go+22/-12

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

16 unmodified lines

17 18 19 20 21 22 23 304 unmodified lines

328 329 330 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

16 unmodified lines

"github.com/entireio/cli/cmd/entire/cli/api" "github.com/entireio/cli/cmd/entire/cli/auth" "github.com/entireio/cli/cmd/entire/cli/interactive" "github.com/entireio/cli/internal/entireclient/tokenstore" "github.com/spf13/cobra" )

304 unmodified lines

// single store every consumer (control plane, data API, git remote // helper, entiredb's CLIs) resolves against. if _, err := auth.RecordLoginContext(token, refreshToken, true); err != nil { return fmt.Errorf("save login: %w", err) return fmt.Errorf("save login: %w", withHeadlessStoreHint(err)) }

fmt.Fprintln(outW, "✓ Login complete.") return nil }

// withHeadlessStoreHint appends file-token-store guidance to a credential // store write failure. The default backend is the OS keyring, which locked // or keyring-less machines (CI, containers, minimal server VMs) can't use — // the raw store error gives those users no way forward (#1036). The hint is // skipped when ENTIRE_TOKEN_STORE=file is already set (suggesting it again // would be nonsense) and for failures the file store wouldn't help with. func withHeadlessStoreHint(err error) error { if !errors.Is(err, auth.ErrCredentialStoreWrite) || tokenstore.FileBackendSelected() { return err }

return fmt.Errorf("%w\n\nIf this machine has no usable OS keyring (headless server, container, CI), store tokens in a file instead:\n\n %s=file entire login\n\nTokens are then written with 0600 permissions to %s (override the location with %s)", err, tokenstore.BackendEnvVar, tokenstore.FileBackendPath(), tokenstore.PathEnvVar) }

// validateReceivedToken runs minimum-trust checks on the access token // the AS handed us before we persist it. The server is the authority // on signature/exp; this is defense in depth aimed at catching gross


Mcmd/entire/cli/login.go+17/-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 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

package cli

import ( "bytes" "fmt" "path/filepath" "strings" "testing" "time"

"github.com/entireio/cli/internal/entireclient/tokenstore" )

// failingTokenStore installs a backend whose Set always fails, standing in // for the locked/absent OS keyring a headless machine hits (#1036). Fault // injection (rather than filesystem permissions) keeps the failure // deterministic even when tests run as root, where permission bits don't // block writes. func failingTokenStore(t *testing.T) { t.Helper() restore := tokenstore.UseFailingBackendForTesting( filepath.Join(t.TempDir(), "tokens.json"), func(string, string) bool { return true }, ) t.Cleanup(restore) }

// loginTestJWT builds a token that passes validateReceivedToken and carries // the iss/handle claims RecordLoginContext keys on. func loginTestJWT(t *testing.T, issuer string) string { t.Helper() exp := time.Now().Add(time.Hour).Unix() return makeJWT(t, {"alg":"RS256"}, fmt.Sprintf({"iss":%q,"handle":"alice","exp":%d}, issuer, exp)) }

// A login that reaches token persistence and fails there must tell headless // users about the file token store: the default backend is the OS keyring, // and on keyring-less machines (CI, containers, minimal server VMs) the raw // store error gives no way forward (#1036). Both store-write sites are // covered: the refresh-token write (refreshToken != "") fails first when a // refresh token is present, and the login-token write is the first store // write when there is none. func TestPersistLogin_StoreWriteFailureIncludesHeadlessHint(t *testing.T) { for name, refreshToken := range map[string]string{ "refresh-token write fails": "refresh-token", "login-token write fails": "", } { t.Run(name, func(t *testing.T) { // Not parallel: mutates the process-global tokenstore backend and // env. TestMain sets ENTIRE_TOKEN_STORE=file process-wide for // spawned-binary isolation; blank it so this test sees the // default-keyring condition a real user hits. t.Setenv("ENTIRE_TOKEN_STORE", "") failingTokenStore(t)

var out bytes.Buffer err := persistLogin(&out, "https://example.test", loginTestJWT(t, "https://example.test"), refreshToken) if err == nil { t.Fatal("persistLogin should fail when the token store rejects writes") } if !strings.Contains(err.Error(), "ENTIRE_TOKEN_STORE=file") { t.Fatalf("store-write failure should point headless users at the file token store, got:\n%v", err) } if !strings.Contains(err.Error(), "ENTIRE_TOKEN_STORE_PATH") { t.Fatalf("hint should mention the path override, got:\n%v", err) } }) } }

// When the user is already on the file backend, suggesting // ENTIRE_TOKEN_STORE=file would be nonsense — the raw error must pass // through without the headless hint. func TestPersistLogin_StoreWriteFailureOnFileBackend_NoHint(t *testing.T) { // Not parallel: mutates the process-global tokenstore backend and env. t.Setenv("ENTIRE_TOKEN_STORE", "file") failingTokenStore(t)

var out bytes.Buffer err := persistLogin(&out, "https://example.test", loginTestJWT(t, "https://example.test"), "refresh-token") if err == nil { t.Fatal("persistLogin should fail when the token store rejects writes") } // Assert on the hint's structural markers, not its prose: the underlying // store error can never contain these, so the assertion stays meaningful // if the hint wording changes. if strings.Contains(err.Error(), "=file entire login") || strings.Contains(err.Error(), "ENTIRE_TOKEN_STORE_PATH") { t.Fatalf("hint must not appear when the file backend is already configured, got:\n%v", err) } if !strings.Contains(err.Error(), "save login") { t.Fatalf("underlying save failure should still surface, got:\n%v", err) } }

// Failures unrelated to the credential store (here: a token whose issuer // doesn't match the login server) must not carry the keyring hint — the // file store wouldn't help. func TestPersistLogin_NonStoreFailure_NoHint(t *testing.T) { // Not parallel: mutates process-global env. t.Setenv("ENTIRE_TOKEN_STORE", "") restore := tokenstore.UseFileBackendForTesting(filepath.Join(t.TempDir(), "tokens.json")) t.Cleanup(restore)

exp := time.Now().Add(time.Hour).Unix() // iss mismatch with baseURL fails validateReceivedToken before any store write. token := makeJWT(t, {"alg":"RS256"}, fmt.Sprintf({"iss":"https://other.test","handle":"alice","exp":%d}, exp))

var out bytes.Buffer err := persistLogin(&out, "https://example.test", token, "refresh-token") if err == nil { t.Fatal("persistLogin should reject a token from the wrong issuer") } if strings.Contains(err.Error(), "ENTIRE_TOKEN_STORE") { t.Fatalf("non-store failure must not carry the token-store hint, got:\n%v", err) } }


Acmd/entire/cli/login\_headless\_hint\_test.go+116

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(2tuiPostRunCompleteGrace + 3time.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 21 unmodified lines

32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 47 unmodified lines

102 103 104 105 106 107 108 109 110 111 112 113 88 89 90 91 92 93 94 95 96 97 98 99 100 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 103 104 105 106 107 108 109 110 111 112 113 129 130 131 132 133 134 135 136 137 138 139 140 115 116 117 118 119 141 142 143 144 145 146 147 148 149 150 151 126 152 153 128 129 154 155 156 157 158 4 unmodified lines

163 164 165 140 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" 21 unmodified lines

TUI elements, which works better with screen readers. `

// Help groups for the root command. AddGroup order is display order. // Visible commands without a GroupID render under "Additional Commands" // (version, labs, agent-help, help) — that placement is intentional. const ( groupSetup = "setup" groupSessions = "sessions" groupAccount = "account" groupControlPlane = "controlplane" )

// inGroup assigns a help group to a command at registration time so all // grouping stays visible in NewRootCmd rather than spread across constructors. func inGroup(c *cobra.Command, groupID string) *cobra.Command { c.GroupID = groupID return c }

func NewRootCmd() *cobra.Command { cmd := &cobra.Command{ Use: "entire", 47 unmodified lines

}, }

// Help groups; AddGroup order is display order in entire --help. cmd.AddGroup( &cobra.Group{ID: groupSetup, Title: "Entire Setup:"}, &cobra.Group{ID: groupSessions, Title: "Sessions & Checkpoints:"}, &cobra.Group{ID: groupAccount, Title: "Account:"}, &cobra.Group{ID: groupControlPlane, Title: "Control Plane:"}, )

// Noun groups (canonical homes for subcommands). cmd.AddCommand(newSessionsCmd()) // 'session' (with 'sessions' as Cobra alias) cmd.AddCommand(newCheckpointGroupCmd()) // 'checkpoint' / 'cp' / 'checkpoints' cmd.AddCommand(newTokensGroupCmd()) // 'tokens' cmd.AddCommand(newAgentGroupCmd()) // 'agent' cmd.AddCommand(newAuthCmd()) // 'auth' cmd.AddCommand(newDoctorCmd()) // 'doctor' (group: trace/logs/bundle) cmd.AddCommand(newLabsCmd()) // 'labs' (experimental workflow discovery) cmd.AddCommand(newPluginGroupCmd()) // 'plugin' (managed install/list/remove) cmd.AddCommand(newImportCmd()) // 'import' (hidden; import pre-existing agent history) cmd.AddCommand(newOrgCmd()) // 'org' — control-plane org management cmd.AddCommand(newProjectCmd()) // 'project' — control-plane project management cmd.AddCommand(newRepoCmd()) // 'repo' — control-plane repo lifecycle cmd.AddCommand(newGrantCmd()) // 'grant' — control-plane access grants cmd.AddCommand(inGroup(newSessionsCmd(), groupSessions)) // 'session' (with 'sessions' as Cobra alias) cmd.AddCommand(inGroup(newCheckpointGroupCmd(), groupSessions)) // 'checkpoint' / 'cp' / 'checkpoints' 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) 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 cmd.AddCommand(newCleanCmd()) cmd.AddCommand(newSetupCmd()) // 'configure' — non-agent settings; agent CRUD lives under 'agent' cmd.AddCommand(newEnableCmd()) cmd.AddCommand(newDisableCmd()) cmd.AddCommand(newStatusCmd()) cmd.AddCommand(newBlameCmd()) cmd.AddCommand(newWhyCmd()) cmd.AddCommand(newLoginCmd()) cmd.AddCommand(newLogoutCmd()) 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)) 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()) cmd.AddCommand(newDispatchCmd()) cmd.AddCommand(newActivityCmd()) cmd.AddCommand(newRecapCmd()) cmd.AddCommand(newAPICmd()) // authenticated passthrough to core/cell APIs cmd.AddCommand(newAgentHelpCmd(cmd)) // visible: agents on transports without context injection discover it via entire help cmd.AddCommand(inGroup(newDispatchCmd(), groupSessions)) cmd.AddCommand(inGroup(newActivityCmd(), groupSessions)) cmd.AddCommand(inGroup(newRecapCmd(), groupSessions)) cmd.AddCommand(inGroup(newAPICmd(), groupControlPlane)) // authenticated passthrough to core/cell APIs cmd.AddCommand(newAgentHelpCmd(cmd)) // visible: agents on transports without context injection discover it via entire help

// Hidden top-level shortcuts. Functional but print a deprecation hint. cmd.AddCommand(hideAsAlias(newResumeCmd(), "entire session resume")) 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+61/-33

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 2 unmodified lines

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

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"}) 2 unmodified lines

} }

func TestRoot_VisibleCommandsAreGrouped(t *testing.T) { t.Parallel()

// 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, "agent-help": true, "help": true, "completion": true, }

wantGroups := map[string]string{ "enable": groupSetup, "disable": groupSetup, "configure": groupSetup, "agent": groupSetup, "plugin": groupSetup, "status": groupSetup, "doctor": groupSetup, "clean": groupSetup, "session": groupSessions, "checkpoint": groupSessions, "recap": groupSessions, "activity": groupSessions, "dispatch": groupSessions, "login": groupAccount, "logout": groupAccount, "auth": groupAccount, "org": groupControlPlane, "project": groupControlPlane, "repo": groupControlPlane, "grant": groupControlPlane, "api": groupControlPlane, }

root := NewRootCmd()

registered := make(map[string]bool) for _, g := range root.Groups() { registered[g.ID] = true }

for _, c := range root.Commands() { 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 != "" { t.Errorf("%q should stay ungrouped, got GroupID %q", name, c.GroupID) } continue } want, ok := wantGroups[name] if !ok { t.Errorf("visible command %q missing from group table; assign it a group or add it to the ungrouped allowlist", name) continue } if c.GroupID != want { t.Errorf("%q GroupID = %q, want %q", name, c.GroupID, want) } if !registered[want] { t.Errorf("group %q used by %q is not registered on root (cobra panics at Execute)", want, name) } } }

func containsString(values []string, want string) bool { for _, value := range values { if value == want {


Mcmd/entire/cli/root\_test.go+88/-6

8 unmodified lines

9 10 11 12 13 14 15 110 unmodified lines

126 127 128 129 130 131 132 244 unmodified lines

377 378 379 380 381 382 383 95 unmodified lines

479 480 481 482 483 484 485 486 487 488 489 490 491 492 2 unmodified lines

495 496 497 487 488 498 499 500 501 492 502 503 504 505 307 unmodified lines

813 814 815 806 807 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 3 unmodified lines

836 837 838 839 840 841 842 843 844 845 846 847 848 818 819 820 821 849 850 851 852 853 854 855 823 856 857 825 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 827 896 897 898 830 899 900 901 902 903 834 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957

8 unmodified lines

"sort" "strings" "time" "unicode/utf8"

tea "charm.land/bubbletea/v2" "github.com/entireio/cli/cmd/entire/cli/api" 110 unmodified lines

query: codeQuery, repoFilters: codeRepos, limit: limitFlag, limitExplicit: cmd.Flags().Changed("limit"), caseSensitive: caseSensitive, jsonOutput: jsonOutput, insecureHTTP: insecureHTTPAuth, 244 unmodified lines

repoFilters []string resolvedRepoIDs []string // ULIDs resolved from repoFilters via repo index limit int limitExplicit bool // user passed --limit; don't override for text display caseSensitive bool jsonOutput bool insecureHTTP bool 95 unmodified lines

}

w := cmd.OutOrStdout() textOutput := !opts.jsonOutput && interactive.IsTerminalWriter(w)

// Text output shows up to maxCodeSearchFiles files with a few matches // each, so fetch a deeper result set than the default --limit (which is // tuned for flat JSON output) unless the user asked for a specific limit. if textOutput && !opts.limitExplicit { opts.limit = codeSearchTextFetchLimit }

// Always fan out via searchAllCells — it fetches the repo index, // resolves slugs to ULIDs, and handles single- vs multi-jurisdiction. 2 unmodified lines

return err }

isTerminal := interactive.IsTerminalWriter(w) if opts.jsonOutput || !isTerminal { if !textOutput { return writeCodeSearchJSON(w, resp) }

writeCodeSearchText(w, resp) writeCodeSearchText(w, resp, newStatusStyles(w), opts.caseSensitive) return nil }

307 unmodified lines

// with an ellipsis so that JSONL/minified files don't blow up the terminal. const maxContextLineLen = 200

// writeCodeSearchText renders code search results in grep-style format. func writeCodeSearchText(w io.Writer, resp *codesearch.SearchResponse) { // Text output display caps: show breadth (files) over depth (in-file matches). // The fetch limit leaves headroom beyond files×matches so per-file overflow // ("+ N matches") counts have data to count. const ( maxCodeSearchFiles = 10 // files shown in text output maxCodeSearchFileMatches = 3 // matches shown per file codeSearchTextFetchLimit = 100 // results fetched for text display )

// writeCodeSearchText renders code search results grouped by file (ripgrep // style): a colored "repo:path" header per file, indented line-numbered // matches beneath it, and a dimmed stats footer. Colors are applied only when // the writer supports them (styles.colorEnabled); piped output stays plain. func writeCodeSearchText(w io.Writer, resp *codesearch.SearchResponse, styles statusStyles, caseSensitive bool) { if len(resp.Results) == 0 { if len(resp.FailedJurisdictions) > 0 { fmt.Fprintf(w, "No code search results found (some regions failed: %s)\n", 3 unmodified lines

} return }

// Group results by repo:path, preserving first-appearance order so the // best-scored file stays on top (results arrive globally score-sorted). type fileGroup struct { key string results []codesearch.Result } var groups []fileGroup idx := make(map[string]int, len(resp.Results)) for _, r := range resp.Results { line := r.ContextLine runes := []rune(line) if len(runes) > maxContextLineLen { line = string(runes[:maxContextLineLen]) + "…" key := r.Repo + ":" + r.Path i, ok := idx[key] if !ok { i = len(groups) idx[key] = i groups = append(groups, fileGroup{key: key}) } fmt.Fprintf(w, "%s:%s:%d: %s\n", r.Repo, r.Path, r.Line, line) groups[i].results = append(groups[i].results, r) } shown := len(resp.Results)

shown := 0 for gi, g := range groups { if gi == maxCodeSearchFiles { break } fmt.Fprintln(w) fmt.Fprintln(w, styles.render(styles.cyan, g.key)) for mi, r := range g.results { if mi == maxCodeSearchFileMatches { break } // Truncate before highlighting but append the ellipsis after, // so the non-ASCII "…" doesn't disable case-insensitive // highlighting (isASCII) for the rest of the line. line := r.ContextLine ellipsis := "" if runes := []rune(line); len(runes) > maxContextLineLen { line = string(runes[:maxContextLineLen]) ellipsis = "…" } lineNo := styles.render(styles.dim, fmt.Sprintf("%d:", r.Line)) fmt.Fprintf(w, " %s %s%s\n", lineNo, highlightCodeMatches(line, resp.Query, styles, caseSensitive), ellipsis) shown++ } // ponytail: overflow counts only what this page fetched (peregrine // has no per-file totals); a hot file shows "+ 97 matches" at most. if extra := len(g.results) - maxCodeSearchFileMatches; extra > 0 { label := "matches" if extra == 1 { label = "match" } fmt.Fprintln(w, styles.render(styles.dim, fmt.Sprintf(" + %d %s", extra, label))) } }

var summary string if resp.Stats.TotalMatches > shown { fmt.Fprintf(w, "\nShowing %d of %d matches across %d files in %d repos (%.0fms)\n", summary = fmt.Sprintf("Showing %d of %d matches across %d files in %d repos (%.0fms)", shown, resp.Stats.TotalMatches, resp.Stats.TotalFiles, resp.Stats.ReposSearched, resp.Stats.DurationMs) } else { fmt.Fprintf(w, "\n%d matches across %d files in %d repos (%.0fms)\n", summary = fmt.Sprintf("%d matches across %d files in %d repos (%.0fms)", resp.Stats.TotalMatches, resp.Stats.TotalFiles, resp.Stats.ReposSearched, resp.Stats.DurationMs) } fmt.Fprintf(w, "\n%s\n", styles.render(styles.dim, summary)) if len(resp.FailedJurisdictions) > 0 { fmt.Fprintf(w, "Warning: results may be incomplete (failed jurisdictions: %s)\n", warning := fmt.Sprintf("Warning: results may be incomplete (failed jurisdictions: %s)", strings.Join(resp.FailedJurisdictions, ", ")) fmt.Fprintln(w, styles.render(styles.yellow, warning)) } }

// highlightCodeMatches bold-red highlights occurrences of query in line // (grep convention). Matching mirrors the search: case-insensitive unless // caseSensitive is set. Case folding is only applied when both strings are // pure ASCII, since Unicode case mappings can change byte widths and // misalign offsets against the original line; non-ASCII input falls back to // exact matching. Returns line unchanged when color is disabled or there's // nothing to highlight. func highlightCodeMatches(line, query string, styles statusStyles, caseSensitive bool) string { if !styles.colorEnabled || query == "" { return line } haystack, needle := line, query if !caseSensitive && isASCII(line) && isASCII(query) { haystack, needle = strings.ToLower(line), strings.ToLower(query) } matchStyle := styles.red.Bold(true) var b strings.Builder i := 0 for { j := strings.Index(haystack[i:], needle) if j < 0 { break } j += i b.WriteString(line[i:j]) b.WriteString(matchStyle.Render(line[j : j+len(needle)])) i = j + len(needle) } if i == 0 { return line // no matches; skip the builder copy } b.WriteString(line[i:]) return b.String() }

// isASCII reports whether s contains only ASCII bytes. func isASCII(s string) bool { for i := range len(s) { if s[i] >= utf8.RuneSelf { return false } } return true }

// writeSearchJSON writes client-side paginated search results as JSON. func writeSearchJSON(w io.Writer, resp *search.Response, limit, page int) error { if limit <= 0 {


Mcmd/entire/cli/search\_cmd.go+130/-14

3 unmodified lines

4 5 6 7 8 9 10 157 unmodified lines

168 169 170 170 171 172 173 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 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 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 36 unmodified lines

323 324 325 221 326 327 328 329 9 unmodified lines

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 2 unmodified lines

371 372 373 245 374 375 376 377

3 unmodified lines

"bytes" "context" "errors" "fmt" "strings" "testing"

157 unmodified lines

}

var buf bytes.Buffer writeCodeSearchText(&buf, resp) writeCodeSearchText(&buf, resp, newStatusStyles(&buf), false)

output := buf.String() if !strings.Contains(output, "entireio/cli:main.go:10: func main() {") { if !strings.Contains(output, "entireio/cli:main.go\n") { t.Errorf("output missing file header:\n%s", output) } if !strings.Contains(output, " 10: func main() {") { t.Errorf("output missing first result:\n%s", output) } if !strings.Contains(output, " 42: \tfmt.Println("hello")") { t.Errorf("output missing second result:\n%s", output) } if !strings.Contains(output, "2 matches across 1 files") { t.Errorf("output missing summary line:\n%s", output) } if strings.Contains(output, "\x1b[") {
t.Errorf("expected no ANSI codes for non-terminal writer:\n%s", output)
}
}

func TestWriteCodeSearchText_GroupsByFile(t *testing.T) {
t.Parallel()

// Interleaved files (score-sorted input) should collapse into one header
// per file, in first-appearance order.
resp := &codesearch.SearchResponse{
Stats: codesearch.Stats{TotalMatches: 3, TotalFiles: 2, ReposSearched: 1, DurationMs: 1},
Results: []codesearch.Result{
{Repo: "r", Path: "a.go", Line: 1, ContextLine: "one"},
{Repo: "r", Path: "b.go", Line: 2, ContextLine: "two"},
{Repo: "r", Path: "a.go", Line: 3, ContextLine: "three"},
},
}

var buf bytes.Buffer
writeCodeSearchText(&buf, resp, newStatusStyles(&buf), false)

output := buf.String()
if got := strings.Count(output, "r:a.go\n"); got != 1 {
t.Errorf("expected exactly 1 header for a.go, got %d:\n%s", got, output)
}
if aIdx, bIdx := strings.Index(output, "r:a.go"), strings.Index(output, "r:b.go"); aIdx > bIdx {
t.Errorf("expected a.go header before b.go:\n%s", output)
}
}

func TestWriteCodeSearchText_CapsFilesAndMatchesPerFile(t *testing.T) {
t.Parallel()

var results []codesearch.Result
// First file has 5 matches — 2 over the per-file cap.
for line := 1; line <= maxCodeSearchFileMatches+2; line++ {
results = append(results, codesearch.Result{Repo: "r", Path: "hot.go", Line: line, ContextLine: "x"})
}
// More files than the file cap.
for f := range maxCodeSearchFiles + 3 {
results = append(results, codesearch.Result{Repo: "r", Path: fmt.Sprintf("f%02d.go", f), Line: 1, ContextLine: "y"})
}
resp := &codesearch.SearchResponse{
Stats: codesearch.Stats{TotalMatches: len(results), TotalFiles: maxCodeSearchFiles + 4, ReposSearched: 1},
Results: results,
}

var buf bytes.Buffer
writeCodeSearchText(&buf, resp, newStatusStyles(&buf), false)
output := buf.String()

if got := strings.Count(output, "r:"); got != maxCodeSearchFiles {
t.Errorf("expected %d file headers, got %d:\n%s", maxCodeSearchFiles, got, output)
}
if !strings.Contains(output, "+ 2 matches") {
t.Errorf("expected '+ 2 matches' overflow for hot.go:\n%s", output)
}
// hot.go shows only the per-file cap: lines 1..3, not 4/5.
if strings.Contains(output, fmt.Sprintf(" %d: x", maxCodeSearchFileMatches+1)) {
t.Errorf("expected at most %d matches for hot.go:\n%s", maxCodeSearchFileMatches, output)
}
}

func TestHighlightCodeMatches(t *testing.T) {
t.Parallel()

styles := statusStyles{colorEnabled: true}

out := highlightCodeMatches("func HandleRequest(w)", "handlerequest", styles, false)
if !strings.Contains(out, "\x1b[") {
t.Errorf("expected ANSI codes in highlighted output, got %q", out)
}
if !strings.HasPrefix(out, "func ") || !strings.HasSuffix(out, "(w)") {
t.Errorf("expected unmatched text preserved around highlight, got %q", out)
}

if out := highlightCodeMatches("no match here", "zzz", styles, false); out != "no match here" {
t.Errorf("expected unchanged line when no match, got %q", out)
}

// Case-sensitive search must not highlight case variants.
if out := highlightCodeMatches("func HandleRequest(w)", "handlerequest", styles, true); out != "func HandleRequest(w)" {
t.Errorf("expected no highlight for case mismatch with caseSensitive, got %q", out)
}

// Non-ASCII input falls back to exact matching (no case folding).
if out := highlightCodeMatches("comment ÉTÉ ici", "été", styles, false); out != "comment ÉTÉ ici" {
t.Errorf("expected no case-folded highlight for non-ASCII input, got %q", out)
}
if out := highlightCodeMatches("comment été ici", "été", styles, false); !strings.Contains(out, "\x1b[") {
t.Errorf("expected exact non-ASCII match highlighted, got %q", out)
}

plain := statusStyles{colorEnabled: false}
if out := highlightCodeMatches("func main()", "main", plain, false); out != "func main()" {
t.Errorf("expected unchanged line when color disabled, got %q", out)
}
}

func TestWriteCodeSearchJSON(t *testing.T) {
36 unmodified lines

}

var buf bytes.Buffer
writeCodeSearchText(&buf, resp)
writeCodeSearchText(&buf, resp, newStatusStyles(&buf), false)

output := buf.String()
if strings.Contains(output, longLine) {
9 unmodified lines

}
}

func TestWriteCodeSearchText_HighlightsTruncatedLines(t *testing.T) {
t.Parallel()

// The appended "…" is non-ASCII; it must not disable case-insensitive
// highlighting for an otherwise ASCII line.
longLine := "FooBar " + strings.Repeat("x", 300)
resp := &codesearch.SearchResponse{
Query: "foobar",
Stats: codesearch.Stats{TotalMatches: 1, TotalFiles: 1, ReposSearched: 1, DurationMs: 1},
Results: []codesearch.Result{{Repo: "r", Path: "f.go", Line: 1, ContextLine: longLine}},
}

var buf bytes.Buffer
writeCodeSearchText(&buf, resp, statusStyles{colorEnabled: true}, false)

output := buf.String()
if !strings.Contains(output, "…") {
t.Errorf("expected truncated line to end with ellipsis:\n%s", output)
}
if !strings.Contains(output, "\x1b[") {
t.Errorf("expected case-insensitive highlight on truncated line:\n%s", output)
}
}

func TestWriteCodeSearchText_Empty(t *testing.T) {
t.Parallel()

2 unmodified lines

}

var buf bytes.Buffer
writeCodeSearchText(&buf, resp)
writeCodeSearchText(&buf, resp, newStatusStyles(&buf), false)

if !strings.Contains(buf.String(), "No code search results found") {
t.Errorf("expected empty results message, got:\n%s", buf.String())\

\
Mcmd/entire/cli/search\_cmd\_test.go+133/-4\
\
```\
283 unmodified lines\
\
284\
285\
286\
287\
287\
288\
289\
290\
1019 unmodified lines\
\
1310\
1311\
1312\
1313\
1313\
1314\
1315\
1316\
1317\
1318\
1319\
1320\
1321\
1322\
1323\
1316\
1324\
1325\
1326\
1327\
\
283 unmodified lines\
\
    // PromptDefault controls whether the pre-push hook asks the user\
    // before running OPF. "" (default) and "ask" both surface the\
    // interactive prompt; "never" skips OPF and pushes 7-layer content;\
    // interactive prompt; "never" skips OPF and pushes regex-only content;\
    // "always" runs without asking. ENTIRE_OPF=yes|no on the push\
    // invocation overrides this setting per-push.\
    PromptDefault string `json:"prompt_default,omitempty"`\
1019 unmodified lines\
\
}\
\
// IsSetUpAndEnabled returns true if Entire is both set up and enabled.\
// This checks if .entire/settings.json exists AND has enabled: true.\
// "Set up" spans either scope — .entire/settings.json OR\
// .entire/settings.local.json — so it must check IsSetUpAny, not IsSetUp.\
// `entire enable --local` writes only settings.local.json and never creates the\
// base file; gating on the base file alone would treat such a local-only repo\
// as inactive and make every hook a silent no-op, dropping all checkpoint\
// capture for that documented workflow. The IsSetUpAny guard is still required\
// so a never-enabled repo (no settings file in any scope) is not treated as\
// enabled by Load's default Enabled: true. Any settings read error is treated\
// as disabled (fail closed).\
// Use this for hooks that should be no-ops when Entire is not active.\
func IsSetUpAndEnabled(ctx context.Context) bool {\
    if !IsSetUp(ctx) {\
    if !IsSetUpAny(ctx) {\
        return false\
    }\
    s, err := Load(ctx)\
```\
\
Mcmd/entire/cli/settings/settings.go+11/-3\
\
```\
9 unmodified lines\
\
10\
11\
12\
13\
14\
15\
16\
1314 unmodified lines\
\
1331\
1332\
1333\
1334\
1335\
1336\
1337\
1338\
1339\
1340\
1341\
1342\
1343\
1344\
1345\
1346\
1347\
1348\
1349\
1350\
1351\
1352\
1353\
1354\
1355\
1356\
1357\
1358\
1359\
1360\
\
9 unmodified lines\
\
    "testing"\
    "time"\
\
    "github.com/entireio/cli/cmd/entire/cli/paths"\
    "github.com/entireio/cli/cmd/entire/cli/session"\
    "github.com/entireio/cli/cmd/entire/cli/testutil"\
)\
1314 unmodified lines\
\
        t.Error("merge(nil, emptyNonNil) should return a non-nil empty map, got nil")\
    }\
}\
\
// Regression: `entire enable --local` writes only .entire/settings.local.json,\
// but the hook activation check (IsSetUpAndEnabled) only looked for\
// .entire/settings.json, so hooks silently no-op'd. It must recognize a\
// local-only setup.\
func TestIsSetUpAndEnabled_LocalSettingsOnly(t *testing.T) {\
    root := t.TempDir()\
    testutil.InitRepo(t, root)\
    entireDir := filepath.Join(root, ".entire")\
    if err := os.MkdirAll(entireDir, 0o755); err != nil {\
        t.Fatal(err)\
    }\
    // Only the local settings file exists (no settings.json), enabled.\
    if err := os.WriteFile(filepath.Join(entireDir, "settings.local.json"), []byte(`{"enabled":true}`), 0o644); err != nil {\
        t.Fatal(err)\
    }\
\
    t.Chdir(root)\
    paths.ClearWorktreeRootCache()\
\
    if IsSetUp(context.Background()) {\
        t.Fatal("precondition: IsSetUp should be false with only settings.local.json")\
    }\
    if !IsSetUpAndEnabled(context.Background()) {\
        t.Fatal("IsSetUpAndEnabled should be true when only settings.local.json exists and is enabled")\
    }\
}\
```\
\
Mcmd/entire/cli/settings/settings\_test.go+28\
\
```\
21 unmodified lines\
\
22\
23\
24\
25\
26\
27\
28\
29\
30\
31\
32\
25\
26\
27\
28\
29\
30\
31\
32\
33\
34\
35\
36\
15 unmodified lines\
\
52\
53\
54\
55\
56\
57\
58\
\
21 unmodified lines\
\
    width        int\
\
    // Styles\
    green  lipgloss.Style\
    red    lipgloss.Style\
    gray   lipgloss.Style\
    bold   lipgloss.Style\
    dim    lipgloss.Style\
    agent  lipgloss.Style // accent (magenta) for agent names\
    cyan   lipgloss.Style\
    yellow lipgloss.Style // yellow for stale warnings\
    green   lipgloss.Style\
    red     lipgloss.Style\
    gray    lipgloss.Style\
    bold    lipgloss.Style\
    dim     lipgloss.Style\
    agent   lipgloss.Style // accent (magenta) for agent names\
    cyan    lipgloss.Style\
    yellow  lipgloss.Style // yellow for stale warnings\
    magenta lipgloss.Style\
}\
\
// newStatusStyles creates styles appropriate for the output writer.\
15 unmodified lines\
\
        s.agent = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color(palette.Accent))\
        s.cyan = lipgloss.NewStyle().Foreground(lipgloss.Color(palette.Info))\
        s.yellow = lipgloss.NewStyle().Foreground(lipgloss.Color(palette.Warning))\
        s.magenta = lipgloss.NewStyle().Foreground(lipgloss.Color(palette.Magenta))\
    }\
\
    return s\
```\
\
Mcmd/entire/cli/status\_style.go+10/-8\
\
```\
441 unmodified lines\
\
442\
443\
444\
445\
445\
446\
447\
448\
\
441 unmodified lines\
\
            })\
        }\
\
        // OpenAI Privacy Filter (opt-in 8th layer).\
        // OpenAI Privacy Filter (opt-in 9th layer).\
        if s.Redaction != nil && s.Redaction.OpenAIPrivacyFilter != nil {\
            opf := s.Redaction.OpenAIPrivacyFilter\
            redact.ConfigurePrivacyFilter(redact.OPFConfig{\
```\
\
Mcmd/entire/cli/strategy/common.go+1/-1\
\
```\
104 unmodified lines\
\
105\
106\
107\
108\
109\
110\
108\
109\
110\
111\
112\
112\
113\
114\
115\
116\
117\
240 unmodified lines\
\
358\
359\
360\
359\
361\
362\
363\
364\
\
104 unmodified lines\
\
    allAgentFiles    map[string]struct{} // Union of all sessions' FilesTouched for cross-session exclusion (nil = single-session)\
}\
\
// redactSessionJSONLBytes runs the 7-layer redaction pipeline over a\
// session transcript at post-commit condensation. OPF is intentionally\
// NOT included here — it runs exclusively in the pre-push rewrite path\
// redactSessionJSONLBytes runs the regex-only redaction pipeline (the\
// eight always-on/opt-in layers) over a session transcript at\
// post-commit condensation. OPF is intentionally NOT included here —\
// it runs exclusively in the pre-push rewrite path\
// (strategy/manual_commit_opf_rewrite.go), which re-redacts the\
// 7-layer blobs and produces 8-layer commits before the push.\
// regex-only blobs and produces OPF-applied (9-layer) commits before\
// the push.\
//\
// Exposed as a var so tests can inject deterministic success/error\
// returns. The signature still takes a context so the var can be\
240 unmodified lines\
\
        summary = generateSummary(ctx, redactedTranscript, sessionData.FilesTouched, state)\
    }\
\
    // Post-commit emits 7-layer-only blobs. OPF runs later in the\
    // Post-commit emits regex-only blobs. OPF runs later in the\
    // pre-push rewrite path, never here.\
    skillEvents := mergeSkillEvents(state.SkillEvents, withSkillEventTurnID(sessionData.SkillEvents, state.TurnID))\
```\
\
Mcmd/entire/cli/strategy/manual\_commit\_condensation.go+7/-5\
\
```\
2840 unmodified lines\
\
2841\
2842\
2843\
2844\
2845\
2846\
2844\
2845\
2846\
2847\
2848\
2849\
44 unmodified lines\
\
2894\
2895\
2896\
2897\
2897\
2898\
2899\
2900\
\
2840 unmodified lines\
\
    // (attribution, files touched, prompts). Hooks run without user interaction\
    // so there is no retry path — preserving partial metadata is better than\
    // losing everything. Persisting an unredacted transcript would be worse.\
    // Run the 7-layer pipeline over the transcript — OPF runs later in\
    // the pre-push rewrite path, which re-redacts these 7-layer blobs\
    // and produces 8-layer commits before the push goes out.\
    // Run the regex-only pipeline over the transcript — OPF runs later in\
    // the pre-push rewrite path, which re-redacts these regex-only blobs\
    // and produces OPF-applied (9-layer) commits before the push goes out.\
    // Externalize inline images BEFORE redaction, mirroring CondenseSession, so the\
    // finalized (authoritative, full-session) transcript keeps its placeholders and\
    // matching assets instead of re-inlining what condensation lifted out. Opt-in;\
44 unmodified lines\
\
        redactedTranscript = redact.RedactedBytes{}\
    }\
\
    // Post-commit emits 7-layer-only blobs; the writer joins + redacts\
    // Post-commit emits regex-only blobs; the writer joins + redacts\
    // via checkpoint.redactedJoinedPrompts. OPF runs later, once per\
    // push, in the pre-push rewrite path.\
    stores, err := checkpoint.Open(ctx, repo, checkpoint.OpenOptions{})\
```\
\
Mcmd/entire/cli/strategy/manual\_commit\_hooks.go+4/-4\
\
```\
20 unmodified lines\
\
21\
22\
23\
24\
25\
24\
25\
26\
27\
28\
\
20 unmodified lines\
\
type OPFDecision int\
\
const (\
    OPFRun   OPFDecision = iota // run the rewrite, push 8-layer\
    OPFSkip                     // skip the rewrite, push 7-layer\
    OPFRun   OPFDecision = iota // run the rewrite, push OPF-applied (9-layer)\
    OPFSkip                     // skip the rewrite, push regex-only (8-layer)\
    OPFAbort                    // cancel the push entirely (Ctrl-C / non-TTY abort)\
)\
```\
\
Mcmd/entire/cli/strategy/manual\_commit\_opf\_prompt.go+2/-2\
\
```\
1\
2\
3\
4\
5\
6\
4\
5\
6\
7\
8\
9\
70 unmodified lines\
\
80\
81\
82\
83\
84\
85\
86\
83\
84\
85\
86\
87\
88\
89\
90\
91\
92\
93\
93\
94\
95\
96\
158 unmodified lines\
\
255\
256\
257\
258\
258\
259\
260\
261\
31 unmodified lines\
\
293\
294\
295\
296\
296\
297\
298\
299\
187 unmodified lines\
\
487\
488\
489\
490\
490\
491\
492\
493\
\
// Pre-push OPF rewrite for entire/checkpoints/v1.\
//\
// This is the ONLY production code path that runs the OPF-augmented\
// redaction entry points. Post-commit condensation stays on 7-layer\
// for predictable latency; OPF runs here, once per push, after the\
// user opted in via settings.\
// redaction entry points. Post-commit condensation stays on the\
// regex-only pipeline for predictable latency; OPF runs here, once per\
// push, after the user opted in via settings.\
package strategy\
\
import (\
70 unmodified lines\
\
}\
\
// OPFRuntimeFailedError: the OPF circuit breaker tripped mid-rewrite.\
// Some blobs were silently downgraded to 7-layer; tagging those commits\
// as Entire-OPF-Applied would be a privacy regression (future pushes\
// would skip them while their content is 7-layer-only). Abort before\
// CAS so the user fixes their OPF install and retries.\
// Some blobs were silently downgraded to regex-only; tagging those\
// commits as Entire-OPF-Applied would be a privacy regression (future\
// pushes would skip them while their content is regex-only). Abort\
// before CAS so the user fixes their OPF install and retries.\
type OPFRuntimeFailedError struct {\
    OPFCommand string\
}\
\
func (e *OPFRuntimeFailedError) Error() string {\
    return fmt.Sprintf("OPF runtime failed during pre-push rewrite (command=%q); "+\
        "aborting push so 7-layer content isn't tagged as 8-layer-applied. "+\
        "aborting push so regex-only content isn't tagged as OPF-applied. "+\
        "Run `%s --help` to verify your OPF install, then retry. Or set "+\
        "ENTIRE_OPF=no on the push to skip OPF for this push only.",\
        e.OPFCommand, e.OPFCommand)\
158 unmodified lines\
\
    // this push (an earlier process step tripped the breaker), abort\
    // before tagging any commits as OPF-applied. Without this, the\
    // per-blob fallback inside the no-OPF cases of BatchBytesWithPrivacyFilter\
    // could let 7-layer content slip out with the trailer attached.\
    // could let regex-only content slip out with the trailer attached.\
    if redact.OPFBreakerTripped() {\
        return plumbing.ZeroHash, &OPFRuntimeFailedError{OPFCommand: redact.OPFCommand()}\
    }\
31 unmodified lines\
\
            }\
            pc.startIdx = len(globalBlobs)\
            // Whole-tree redaction: each v1 commit tree is cumulative, so the\
            // newest commit can still carry older shards that were 7-layer-only\
            // newest commit can still carry older shards that were regex-only\
            // before this rewrite. Redacting the whole tree for every unapplied\
            // commit keeps the final rewritten tip from reintroducing an\
            // un-OPF-redacted older shard. collect and apply walk the tree the\
187 unmodified lines\
\
//\
// Correctness note: each v1 commit tree is cumulative. During a multi-commit\
// rewrite, the newest original commit can still contain older shards that were\
// 7-layer-only before this rewrite. The collect/apply walkers redact the whole\
// regex-only before this rewrite. The collect/apply walkers redact the whole\
// tree for every unapplied commit so the final rewritten tip cannot\
// reintroduce an older un-OPF-redacted shard.\
func rebuildV1Commit(ctx context.Context, repo *git.Repository, oldCommit *object.Commit, parent plumbing.Hash, redactedByPath map[string][]byte) (plumbing.Hash, error) {\
```\
\
Mcmd/entire/cli/strategy/manual\_commit\_opf\_rewrite.go+11/-11\
\
```\
109 unmodified lines\
\
110\
111\
112\
113\
114\
115\
116\
117\
118\
119\
120\
11 unmodified lines\
\
132\
133\
134\
130\
135\
136\
137\
138\
97 unmodified lines\
\
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\
419 unmodified lines\
\
689\
690\
691\
659\
660\
692\
693\
694\
695\
696\
\
109 unmodified lines\
\
// setupV1Repo creates a repo + one v1 checkpoint with "PERSONABC" in\
// both the transcript and prompt. Returns the repo and the v1 tip.\
func setupV1Repo(t *testing.T) (*git.Repository, plumbing.Hash) {\
    _, repo, tip := setupV1RepoInDir(t)\
    return repo, tip\
}\
\
func setupV1RepoInDir(t *testing.T) (string, *git.Repository, plumbing.Hash) {\
    t.Helper()\
    tempDir := t.TempDir()\
    testutil.InitRepo(t, tempDir)\
11 unmodified lines\
\
    require.NoError(t, err)\
\
    tip := addV1Checkpoint(t, repo, "a1b2c3d4e5f6", "test-session", "Hello, PERSONABC asked", "Look up PERSONABC")\
    return repo, tip\
    return tempDir, repo, tip\
}\
\
func addV1Checkpoint(t *testing.T, repo *git.Repository, cpIDString, sessionID, transcript, prompt string) plumbing.Hash {\
97 unmodified lines\
\
    }))\
}\
\
func TestPrePushFromGitHook_DeferralStillRunsOPF(t *testing.T) {\
    fake := &fakeOPFForRewrite{}\
    configureFakeOPF(t, fake)\
\
    dir, repo, originalTip := setupV1RepoInDir(t)\
    remoteDir := filepath.Join(t.TempDir(), "origin.git")\
    _, err := git.PlainInit(remoteDir, true)\
    require.NoError(t, err)\
    _, err = repo.CreateRemote(&gitconfig.RemoteConfig{Name: "origin", URLs: []string{remoteDir}})\
    require.NoError(t, err)\
\
    t.Chdir(dir)\
    paths.ClearWorktreeRootCache()\
    t.Cleanup(paths.ClearWorktreeRootCache)\
\
    // The empty remote defers Entire's automatic metadata push. The OPF rewrite\
    // still must run because the user's outer git push may include v1 directly.\
    require.NoError(t, NewManualCommitStrategy().PrePushFromGitHook(t.Context(), "origin"))\
\
    ref, err := repo.Reference(plumbing.NewBranchReferenceName(paths.MetadataBranchName), true)\
    require.NoError(t, err)\
    require.NotEqual(t, originalTip, ref.Hash(), "OPF rewrite must advance the local v1 ref before deferral")\
    commit, err := repo.CommitObject(ref.Hash())\
    require.NoError(t, err)\
    require.True(t, trailers.HasOPFApplied(commit.Message))\
    require.Equal(t, 1, fake.batchCallCount())\
}\
\
func TestRewriteUnpushedV1WithOPF_MultiCommitTipCarriesPriorRedactedShards(t *testing.T) {\
    configureFakeOPF(t, &fakeOPFForRewrite{})\
    repo, _ := setupV1Repo(t)\
419 unmodified lines\
\
// Fail-closed regression: when the OPF runtime fails and the breaker\
// trips, the rewrite must NOT CAS the ref. Otherwise the new commits\
// would carry Entire-OPF-Applied: true while their content is 7-layer\
// only, and future pushes would skip them — silently shipping unredacted\
// would carry Entire-OPF-Applied: true while their content is regex-only,\
// and future pushes would skip them — silently shipping unredacted\
// content to the remote.\
func TestRewriteUnpushedV1WithOPF_BreakerTrippedMidRewrite_AbortsBeforeCAS(t *testing.T) {\
    configureFakeOPF(t, &fakeRuntimeAlwaysFails{})\
```\
\
Mcmd/entire/cli/strategy/manual\_commit\_opf\_rewrite\_test.go+36/-3\
\
```\
6 unmodified lines\
\
7\
8\
9\
10\
11\
12\
13\
14\
22 unmodified lines\
\
37\
38\
39\
40\
41\
42\
43\
44\
45\
46\
47\
48\
49\
50\
51\
52\
53\
7 unmodified lines\
\
61\
62\
63\
51\
52\
64\
65\
66\
67\
68\
69\
70\
71\
72\
73\
74\
75\
76\
77\
78\
79\
80\
11 unmodified lines\
\
92\
93\
94\
74\
75\
76\
95\
96\
97\
98\
99\
100\
12 unmodified lines\
\
113\
114\
115\
95\
116\
117\
118\
119\
17 unmodified lines\
\
137\
138\
139\
140\
141\
142\
143\
144\
145\
146\
147\
148\
149\
150\
151\
10 unmodified lines\
\
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\
217\
218\
219\
220\
221\
222\
223\
224\
225\
226\
227\
228\
229\
230\
231\
232\
233\
234\
235\
236\
237\
238\
239\
240\
\
6 unmodified lines\
\
    "io"\
    "log/slog"\
    "os"\
    "os/exec"\
    "strings"\
\
    git "github.com/go-git/go-git/v6"\
    "github.com/go-git/go-git/v6/plumbing"\
22 unmodified lines\
\
//   - push_sessions: false to disable automatic pushing of checkpoints\
//   - checkpoint_remote: {"provider": "github", "repo": "org/repo"} to push to a separate repo\
func (s *ManualCommitStrategy) PrePush(ctx context.Context, remote string) error {\
    return s.prePush(ctx, remote, false)\
}\
\
// PrePushFromGitHook handles a push initiated by Git's pre-push hook. Unlike\
// direct callers, it protects an empty user remote from receiving checkpoint\
// metadata before the user's first normal branch is published.\
func (s *ManualCommitStrategy) PrePushFromGitHook(ctx context.Context, remote string) error {\
    return s.prePush(ctx, remote, true)\
}\
\
func (s *ManualCommitStrategy) prePush(ctx context.Context, remote string, protectFirstUserBranch bool) error {\
    // Load settings once for remote resolution and push_sessions check.\
    // Spanned because checkpoint-remote resolution can perform a one-time\
    // network fetch of the metadata branch (fetchMetadataBranchIfMissing),\
7 unmodified lines\
\
    }\
\
    // git-refs primary: push the per-checkpoint refs recorded in the push queue\
    // instead of the single v1 branch. (A configured git-branch mirror's v1 ref\
    // is not pushed here yet — mirror push for downgrade safety is a later step.)\
    // instead of the single v1 branch. Those refs live under refs/entire/, not\
    // refs/heads/, so a forge can never pick them as a repository's default\
    // branch — the empty-remote guard below is unnecessary for this backend.\
    // (A configured git-branch mirror's v1 ref is not pushed here yet — mirror\
    // push for downgrade safety is a later step.)\
    if cpCfg, _ := settings.LoadCheckpointsConfig(ctx); checkpoint.PrimaryIsRefs(cpCfg) { //nolint:errcheck // fail-soft: a bad checkpoints block already surfaces via Open; default to no refs push\
        return s.prePushCheckpointRefs(ctx, ps)\
    }\
\
    // git-branch primary: entire/checkpoints/v1 is a real refs/heads branch, so\
    // on an otherwise-empty remote a forge like GitHub would select it as the\
    // default. Defer publication until the user's own branch exists there.\
    deferAutomaticCheckpointPush := protectFirstUserBranch && deferCheckpointPushOnEmptyRemote(ctx, ps)\
\
    refs := checkpoint.ResolveRefs(ctx)\
    repo, repoErr := OpenRepository(ctx)\
    if repoErr != nil {\
11 unmodified lines\
\
    // OPF pre-push rewrite: if OPF is configured, resolve the user's\
    // decision (env > settings > prompt > non-TTY auto-run), then\
    // re-redact unpushed v1 commits with the 8-layer pipeline before\
    // pushing. Skipped entirely when OPF is off, so the common-case\
    // fast path is unchanged.\
    // re-redact unpushed v1 commits with OPF (producing the OPF-applied,\
    // 9-layer pipeline) before pushing. Skipped entirely when OPF is off,\
    // so the common-case fast path is unchanged.\
    if redact.OPFEnabled() {\
        cfg, _ := settings.Load(ctx) //nolint:errcheck // Load already failed at hook init; fall back to nil\
        var opfCfg *settings.OPFSettings\
12 unmodified lines\
\
            return errOPFAbortedByUser\
        case OPFSkip:\
            // User opted out for this push (or settings/env say\
            // "never"). Push 7-layer content as-is.\
            // "never"). Push regex-only (8-layer) content as-is.\
            logging.Info(ctx, "OPF skipped for this push (user choice or settings)")\
        case OPFRun:\
            _, opfSpan := perf.Start(ctx, "opf_pre_push_rewrite")\
17 unmodified lines\
\
        }\
    }\
\
    if deferAutomaticCheckpointPush {\
        // Do this only after OPF has had a chance to rewrite v1: the outer\
        // user push may explicitly include the metadata branch.\
        logging.Info(ctx, "automatic checkpoint push deferred until the remote has a branch",\
            slog.String("remote", ps.remote),\
        )\
        return nil\
    }\
\
    // Thread the span's context into the push so the network push and any\
    // fetch+rebase recovery nest beneath it as child steps in the perf trace.\
    pushCtx, pushCheckpointsSpan := perf.Start(ctx, "push_checkpoint_refs")\
10 unmodified lines\
\
    return nil\
}\
\
// deferCheckpointPushOnEmptyRemote reports whether publication of the git-branch\
// v1 metadata should be held back because the push remote may be brand new.\
//\
// Hosting providers such as GitHub make the first branch pushed to an empty\
// repository its default, so the pre-push hook must not publish\
// entire/checkpoints/v1 ahead of the user's own first branch. The check is\
// purely local: if a remote-tracking ref for this remote already exists\
// (refs/remotes/<remote>/*), the remote has been fetched from or pushed to\
// before and therefore already has at least one branch, so publishing cannot\
// make our metadata the default. Otherwise defer — git records a\
// remote-tracking ref after the first successful push, so the deferred metadata\
// publishes on the next push.\
//\
// It deliberately performs no ls-remote/fetch. A network round trip on the\
// pre-push path can trigger an SSH security-key touch prompt (and doing so per\
// push URL would multiply those prompts), which is a poor pre-push UX. This is\
// also why it uses only the remote git handed the hook rather than resolving\
// every configured push URL.\
//\
// A separate checkpoint remote is exempt: it is a dedicated metadata store, not\
// the repository the user pushes to.\
func deferCheckpointPushOnEmptyRemote(ctx context.Context, ps pushSettings) bool {\
    if ps.hasCheckpointURL() {\
        return false\
    }\
\
    // The hazard only arises for a configured remote (the `git remote add\
    // origin …` then first-push flow). Pushing straight to a bare URL hands that\
    // URL to the hook as the remote arg, and git never records a\
    // refs/remotes/<url>/* tracking ref for it — so a tracking-ref check would\
    // defer the metadata forever. Publish for a non-configured (URL) target\
    // rather than strand it; the first-branch scenario always uses a named\
    // remote.\
    if !isConfiguredRemote(ctx, ps.remote) {\
        return false\
    }\
\
    // Known limitation, accepted for the no-network design: a tracking ref left\
    // over from before a remote was deleted and recreated empty under the same\
    // URL reads as "established", so v1 would publish to the now-empty remote.\
    // Detecting that requires asking the remote — the network round trip we\
    // deliberately avoid here. The scenario is rare and its default branch is\
    // recoverable by resetting it on the forge.\
    return !remoteHasTrackingRefs(ctx, ps.remote)\
}\
\
// isConfiguredRemote reports whether name is a configured git remote, as\
// opposed to a bare URL that git passes through verbatim when a push targets a\
// URL directly. Local and best-effort (reads config, no network); any error is\
// treated as "not a configured remote".\
func isConfiguredRemote(ctx context.Context, name string) bool {\
    if name == "" {\
        return false\
    }\
    return exec.CommandContext(ctx, "git", "remote", "get-url", name).Run() == nil\
}\
\
// remoteHasTrackingRefs reports whether any refs/remotes/<remote>/* ref exists\
// locally. Its presence means the remote has been fetched from or pushed to\
// before and so already has at least one branch. Local-only and best-effort:\
// any error is treated as "no tracking refs" so the caller fails safe (defers).\
func remoteHasTrackingRefs(ctx context.Context, remote string) bool {\
    if remote == "" {\
        return false\
    }\
    cmd := exec.CommandContext(ctx, "git", "for-each-ref", "--count=1", "refs/remotes/"+remote+"/")\
    out, err := cmd.Output()\
    if err != nil {\
        return false\
    }\
    return strings.TrimSpace(string(out)) != ""\
}\
\
// prePushCheckpointRefs drains the per-checkpoint push queue and batch-pushes the\
// recorded refs fast-forward-only (git-refs primary; never a force push — a\
// diverged ref is recovered via fetch+replay). Transient push failures are logged and\
```\
\
Mcmd/entire/cli/strategy/manual\_commit\_push.go+109/-6\
\
```\
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\
\
package strategy\
\
import (\
    "context"\
    "os/exec"\
    "testing"\
\
    "github.com/entireio/cli/cmd/entire/cli/testutil"\
\
    "github.com/stretchr/testify/require"\
)\
\
// TestDeferCheckpointPushOnEmptyRemote_UsesLocalTrackingRefs verifies the guard\
// decides purely from local remote-tracking refs, with no network access: a\
// remote with no refs/remotes/<remote>/* is treated as possibly-empty (defer),\
// and one with any tracking ref is treated as established (publish).\
func TestDeferCheckpointPushOnEmptyRemote_UsesLocalTrackingRefs(t *testing.T) {\
    // No t.Parallel: uses t.Chdir.\
    dir := t.TempDir()\
    testutil.InitRepo(t, dir)\
\
    run := func(args ...string) {\
        t.Helper()\
        cmd := exec.CommandContext(t.Context(), "git", args...)\
        cmd.Dir = dir\
        require.NoError(t, cmd.Run(), "git %v", args)\
    }\
    run("commit", "--allow-empty", "-m", "init")\
    // A deliberately unreachable URL: the guard must never dial it.\
    run("remote", "add", "origin", "https://example.invalid/repo.git")\
\
    t.Chdir(dir)\
    ctx := context.Background()\
    ps := pushSettings{remote: "origin"}\
\
    // No remote-tracking refs yet → possibly a brand-new remote → defer.\
    require.True(t, deferCheckpointPushOnEmptyRemote(ctx, ps),\
        "a remote with no tracking refs must defer")\
\
    // A push straight to a bare URL is not a configured remote; git never records\
    // a tracking ref for it, so the guard must publish rather than defer forever.\
    require.False(t,\
        deferCheckpointPushOnEmptyRemote(ctx, pushSettings{remote: "https://example.invalid/repo.git"}),\
        "a bare-URL push target must not defer")\
\
    // git records a remote-tracking ref after the first successful push; simulate\
    // that locally (no network). The remote is now established → publish.\
    run("update-ref", "refs/remotes/origin/main", "HEAD")\
    require.False(t, deferCheckpointPushOnEmptyRemote(ctx, ps),\
        "a remote with a tracking ref must not defer")\
\
    // A configured separate checkpoint remote is always exempt.\
    require.False(t,\
        deferCheckpointPushOnEmptyRemote(ctx, pushSettings{remote: "origin", checkpointURL: "https://example.invalid/cp.git"}),\
        "a dedicated checkpoint remote is exempt from the guard")\
}\
```\
\
Acmd/entire/cli/strategy/manual\_commit\_push\_test.go+56\
\
```\
119 unmodified lines\
\
120\
121\
122\
123\
124\
125\
126\
127\
128\
129\
130\
131\
132\
133\
134\
135\
\
119 unmodified lines\
\
        t.Fatalf("error = %q, want it to mention 'cannot combine'", err)\
    }\
}\
\
func TestTrailCheckoutHasWorktreeFlag(t *testing.T) {\
    t.Parallel()\
\
    cmd := newTrailCheckoutCmd()\
    flag := cmd.Flags().Lookup("worktree")\
    if flag == nil {\
        t.Fatal("worktree flag not registered")\
    }\
    if flag.Value.Type() != "bool" {\
        t.Fatalf("worktree flag type = %q, want bool", flag.Value.Type())\
    }\
}\
```\
\
Mcmd/entire/cli/trail\_checkout\_cmd\_test.go+13\
\
```\
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\
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\
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\
469\
470\
471\
472\
473\
474\
475\
476\
477\
478\
479\
480\
481\
482\
483\
484\
485\
486\
487\
488\
489\
490\
491\
492\
493\
494\
495\
496\
497\
498\
499\
500\
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\
\
package cli\
\
import (\
    "bytes"\
    "context"\
    "errors"\
    "fmt"\
    "io"\
    "io/fs"\
    "os"\
    "os/exec"\
    "path/filepath"\
    "strings"\
    "time"\
\
    "github.com/go-git/go-git/v6/plumbing/format/gitignore"\
\
    "github.com/entireio/cli/cmd/entire/cli/checkpoint/remote"\
    "github.com/entireio/cli/cmd/entire/cli/interactive"\
    "github.com/entireio/cli/cmd/entire/cli/osroot"\
    "github.com/entireio/cli/cmd/entire/cli/paths"\
)\
\
const (\
    trailWorktreesRelDir      = ".entire/worktrees"\
    trailWorktreeFallbackName = "branch"\
)\
\
func defaultTrailWorktreePath(repoRoot, branch string, trailNumber int) string {\
    name := fmt.Sprintf("trail-%d-%s", trailNumber, sanitizeTrailWorktreeName(branch))\
    return filepath.Join(repoRoot, filepath.FromSlash(trailWorktreesRelDir), name)\
}\
\
func sanitizeTrailWorktreeName(branch string) string {\
    name := strings.Map(func(r rune) rune {\
        if r >= 'a' && r <= 'z' || r >= 'A' && r <= 'Z' || r >= '0' && r <= '9' || r == '-' || r == '_' || r == '.' {\
            return r\
        }\
        return '-'\
    }, strings.TrimSpace(branch))\
    name = strings.Trim(name, "-.")\
    if name == "" {\
        return trailWorktreeFallbackName\
    }\
    return name\
}\
\
// gitCommonDirForTrailWorktree returns the absolute git common dir, which is\
// the main repo's .git directory even when run from a linked worktree.\
// session.GetGitCommonDir is not reused here because it returns relative\
// rev-parse results as-is; this feature needs an absolute path for the\
// worktree location and the printed cd hint.\
func gitCommonDirForTrailWorktree(ctx context.Context) (string, error) {\
    cmd := exec.CommandContext(ctx, "git", "rev-parse", "--git-common-dir")\
    output, err := cmd.Output()\
    if err != nil {\
        return "", gitOutputError("failed to get git common dir", err)\
    }\
    gitDir := strings.TrimSpace(string(output))\
    if !filepath.IsAbs(gitDir) {\
        cwd, wdErr := os.Getwd() //nolint:forbidigo // must resolve relative git common dir in cwd context\
        if wdErr != nil {\
            return "", fmt.Errorf("failed to get current directory: %w", wdErr)\
        }\
        gitDir = filepath.Join(cwd, gitDir)\
    }\
    return filepath.Clean(gitDir), nil\
}\
\
func trailWorktreeBaseRoot(ctx context.Context) (string, error) {\
    gitDir, err := gitCommonDirForTrailWorktree(ctx)\
    if err != nil {\
        return "", err\
    }\
    if filepath.Base(gitDir) != ".git" {\
        return "", fmt.Errorf("git common dir %q is not a .git directory", gitDir)\
    }\
    return filepath.Dir(gitDir), nil\
}\
\
// ensureTrailWorktreeIgnoreRule appends the .entire/worktrees/ rule to an\
// existing repo-root .gitignore when the directory isn't already ignored.\
// Already ignored, or no .gitignore at all → silent no-op: the CLI doesn't\
// impose ignore policy on a repo that hasn't opted into one, and committing\
// the appended rule stays the user's choice.\
func ensureTrailWorktreeIgnoreRule(ctx context.Context, w io.Writer, root string) error {\
    check := exec.CommandContext(ctx, "git", "check-ignore", "-q", trailWorktreesRelDir+"/")\
    check.Dir = root\
    err := check.Run()\
    if err == nil {\
        return nil\
    }\
    var exitErr *exec.ExitError\
    if !errors.As(err, &exitErr) || exitErr.ExitCode() != 1 {\
        return fmt.Errorf("failed to check ignore status of %s: %w", trailWorktreesRelDir, err)\
    }\
\
    appended, err := appendIgnoreRule(filepath.Join(root, ".gitignore"))\
    if err != nil {\
        return err\
    }\
    if appended {\
        fmt.Fprintln(w, "Added .entire/worktrees/ to .gitignore — commit it to keep the rule.")\
    }\
    return nil\
}\
\
func appendIgnoreRule(path string) (bool, error) {\
    const rule = trailWorktreesRelDir + "/"\
    content, err := os.ReadFile(path) //nolint:gosec // path derived from repo root / git common dir\
    if errors.Is(err, os.ErrNotExist) {\
        return false, nil\
    }\
    if err != nil {\
        return false, fmt.Errorf("failed to read %s: %w", path, err)\
    }\
    for line := range strings.SplitSeq(string(content), "\n") {\
        if strings.TrimSpace(line) == rule {\
            return false, nil\
        }\
    }\
    prefix := ""\
    if len(content) > 0 && !strings.HasSuffix(string(content), "\n") {\
        prefix = "\n"\
    }\
    updated := string(content) + prefix + rule + "\n"\
    if err := os.WriteFile(path, []byte(updated), 0o600); err != nil { //nolint:gosec // path derived from repo root / git common dir\
        return false, fmt.Errorf("failed to update %s: %w", path, err)\
    }\
    return true, nil\
}\
\
const worktreeIncludeFile = ".worktreeinclude"\
\
// copyWorktreeIncludeFiles copies ignored files matching .worktreeinclude\
// patterns from the main worktree root into a freshly created worktree.\
// Per-file failures warn and skip; they never fail the checkout.\
func copyWorktreeIncludeFiles(ctx context.Context, errW io.Writer, root, dest string) error {\
    patterns, err := loadWorktreeIncludePatterns(root)\
    if err != nil {\
        return err\
    }\
    if len(patterns) == 0 {\
        return nil\
    }\
    ignored, err := listIgnoredFiles(ctx, root)\
    if err != nil {\
        return err\
    }\
    matches := matchIncludePatterns(patterns, ignored)\
    if len(matches) == 0 {\
        return nil\
    }\
    // dest is a fresh checkout of the trail branch, whose content the invoking\
    // user did not author. os.Root confines writes to dest even if the branch\
    // contains a tracked symlinked directory pointing outside it.\
    destRoot, err := os.OpenRoot(dest)\
    if err != nil {\
        return fmt.Errorf("failed to open worktree root: %w", err)\
    }\
    defer destRoot.Close()\
    for _, rel := range matches {\
        if err := copyIncludedFile(filepath.Join(root, rel), destRoot, rel); err != nil {\
            fmt.Fprintf(errW, "warning: skipped %s: %v\n", filepath.ToSlash(rel), err)\
        }\
    }\
    return nil\
}\
\
// loadWorktreeIncludePatterns reads .worktreeinclude from root. A missing\
// file means nothing gets copied. Lines are gitignore-style patterns; blank\
// lines and #-comments are skipped.\
func loadWorktreeIncludePatterns(root string) ([]string, error) {\
    data, err := os.ReadFile(filepath.Join(root, worktreeIncludeFile)) //nolint:gosec // path derived from repo root\
    if errors.Is(err, fs.ErrNotExist) {\
        return nil, nil\
    }\
    if err != nil {\
        return nil, fmt.Errorf("failed to read %s: %w", worktreeIncludeFile, err)\
    }\
    var patterns []string\
    for raw := range strings.SplitSeq(string(data), "\n") {\
        line := strings.TrimRight(raw, "\r")\
        if strings.TrimSpace(line) == "" || strings.HasPrefix(line, "#") {\
            continue\
        }\
        patterns = append(patterns, line)\
    }\
    return patterns, nil\
}\
\
// listIgnoredFiles returns untracked files ignored by repo ignore rules,\
// relative to root. Paths under .entire/worktrees are excluded: sibling trail\
// worktrees' own ignored files (e.g. their .env) appear in the listing at the\
// main root and would otherwise be copied into every new worktree.\
func listIgnoredFiles(ctx context.Context, root string) ([]string, error) {\
    cmd := exec.CommandContext(ctx, "git", "ls-files", "--others", "--ignored", "--exclude-standard", "-z")\
    cmd.Dir = root\
    output, err := cmd.Output()\
    if err != nil {\
        return nil, fmt.Errorf("failed to list ignored files: %w", err)\
    }\
    var files []string\
    for f := range bytes.SplitSeq(output, []byte{0}) {\
        if len(f) > 0 && !isManagedTrailWorktreePath(string(f)) {\
            files = append(files, string(f))\
        }\
    }\
    return files, nil\
}\
\
func matchIncludePatterns(patterns, files []string) []string {\
    ps := make([]gitignore.Pattern, 0, len(patterns))\
    for _, pattern := range patterns {\
        ps = append(ps, gitignore.ParsePattern(pattern, nil))\
    }\
    matcher := gitignore.NewMatcher(ps)\
    included := make([]string, 0, len(files))\
    for _, file := range files {\
        rel, ok := cleanRelativeIncludeFile(file)\
        if !ok || !matcher.Match(strings.Split(filepath.ToSlash(rel), "/"), false) {\
            continue\
        }\
        included = append(included, rel)\
    }\
    return included\
}\
\
func isManagedTrailWorktreePath(rel string) bool {\
    slash := filepath.ToSlash(rel)\
    return slash == trailWorktreesRelDir || strings.HasPrefix(slash, trailWorktreesRelDir+"/")\
}\
\
func cleanRelativeIncludeFile(rel string) (string, bool) {\
    if rel == "" || filepath.IsAbs(rel) {\
        return "", false\
    }\
    clean := filepath.Clean(filepath.FromSlash(rel))\
    if clean == "." || paths.IsRelativeTraversal(clean) {\
        return "", false\
    }\
    return clean, true\
}\
\
// copyIncludedFile copies src into destRoot at rel. destRoot confines all\
// writes to the worktree root, so a tracked symlinked directory in the\
// branch cannot redirect the copy outside the worktree.\
func copyIncludedFile(src string, destRoot *os.Root, rel string) error {\
    srcInfo, err := os.Lstat(src)\
    if err != nil {\
        return err //nolint:wrapcheck // lstat error is sufficient for caller context\
    }\
    if !srcInfo.Mode().IsRegular() {\
        return errors.New("source is not a regular file")\
    }\
    in, err := os.Open(src) //nolint:gosec // src derived from repo root + .worktreeinclude\
    if err != nil {\
        return err //nolint:wrapcheck // open error is sufficient for caller context\
    }\
    defer in.Close()\
    openedInfo, err := in.Stat()\
    if err != nil {\
        return err //nolint:wrapcheck // stat error is sufficient for caller context\
    }\
    if !openedInfo.Mode().IsRegular() || !os.SameFile(srcInfo, openedInfo) {\
        return errors.New("source changed while opening")\
    }\
    if err := osroot.MkdirAll(destRoot, filepath.Dir(rel), 0o750); err != nil {\
        return err //nolint:wrapcheck // mkdir error is sufficient for caller context\
    }\
    out, err := destRoot.OpenFile(rel, os.O_WRONLY|os.O_CREATE|os.O_EXCL, srcInfo.Mode().Perm())\
    if err != nil {\
        return err //nolint:wrapcheck // openfile error is sufficient for caller context\
    }\
    copied := false\
    defer func() {\
        if !copied {\
            _ = out.Close()\
            _ = osroot.Remove(destRoot, rel) //nolint:errcheck // best-effort cleanup after a failed copy\
        }\
    }()\
    if _, err := io.Copy(out, in); err != nil {\
        return err //nolint:wrapcheck // copy error is sufficient for caller context\
    }\
    if err := out.Close(); err != nil {\
        return err //nolint:wrapcheck // close error is sufficient for caller context\
    }\
    if err := destRoot.Chmod(rel, srcInfo.Mode().Perm()); err != nil {\
        return err //nolint:wrapcheck // chmod error is sufficient for caller context\
    }\
    copied = true\
    return nil\
}\
\
// checkoutTrailWorktree checks branch out into a managed worktree under\
// <main-root>/.entire/worktrees instead of switching the current checkout.\
// The final output line is a shell-safe `cd '<path>'` hint.\
func checkoutTrailWorktree(ctx context.Context, w, errW io.Writer, branch string, force bool, trailNumber int) error {\
    // The trail number disambiguates the worktree directory: sanitized branch\
    // names are lossy (feature/x and feature-x collide), so an unnumbered\
    // trail cannot get a unique location.\
    if trailNumber <= 0 {\
        return fmt.Errorf("trail for branch %q has no number yet; cannot check out into a worktree", branch)\
    }\
    if err := ValidateBranchName(ctx, branch); err != nil {\
        return err\
    }\
\
    root, err := trailWorktreeBaseRoot(ctx)\
    if err != nil {\
        return fmt.Errorf("failed to find main worktree root: %w", err)\
    }\
\
    match, found, err := findWorktreeForBranch(ctx, branch, root)\
    if err != nil {\
        return err\
    }\
    if found {\
        // Lstat: a symlink planted at the registered path must not pass as a\
        // healthy worktree directory.\
        switch info, statErr := os.Lstat(match.path); {\
        case statErr == nil && !info.IsDir():\
            return fmt.Errorf("branch %q is registered to %s, which is not a directory", branch, match.path)\
        case statErr == nil:\
            if !match.managed {\
                return fmt.Errorf("branch %q is already checked out at %s", branch, match.path)\
            }\
            if err := validateTrailWorktreeReuse(ctx, match.path, branch); err != nil {\
                return staleTrailWorktreeError(branch, match.path)\
            }\
            printTrailWorktreeLocation(w, errW, "Worktree already exists at "+match.path, match.path)\
            return nil\
        case errors.Is(statErr, fs.ErrNotExist):\
            return staleTrailWorktreeError(branch, match.path)\
        default:\
            return fmt.Errorf("failed to check worktree at %s: %w", match.path, statErr)\
        }\
    }\
\
    proceed, err := ensureTrailWorktreeBranchAvailable(ctx, errW, branch, force)\
    if err != nil {\
        return err\
    }\
    if !proceed {\
        fmt.Fprintf(errW, "Checkout of branch %s cancelled.\n", branch)\
        return nil\
    }\
\
    if err := ensureTrailWorktreeIgnoreRule(ctx, errW, root); err != nil {\
        return err\
    }\
\
    worktreePath := defaultTrailWorktreePath(root, branch, trailNumber)\
    if err := os.MkdirAll(filepath.Dir(worktreePath), 0o750); err != nil {\
        return fmt.Errorf("failed to create worktree parent: %w", err)\
    }\
    add := exec.CommandContext(ctx, "git", "worktree", "add", worktreePath, branch)\
    add.Dir = root\
    if output, err := add.CombinedOutput(); err != nil {\
        return fmt.Errorf("failed to create worktree: %s: %w", strings.TrimSpace(string(output)), err)\
    }\
\
    if err := copyWorktreeIncludeFiles(ctx, errW, root, worktreePath); err != nil {\
        fmt.Fprintf(errW, "warning: could not copy %s files: %v\n", worktreeIncludeFile, err)\
    }\
\
    printTrailWorktreeLocation(w, errW, "Worktree ready at "+worktreePath, worktreePath)\
    return nil\
}\
\
// printTrailWorktreeLocation reports where the worktree lives. On a terminal\
// it prints the note and a copy-paste cd hint. Otherwise stdout carries only\
// the bare path — so `cd "$(entire trail checkout <n> --worktree)"` works —\
// and the note goes to stderr.\
func printTrailWorktreeLocation(w, errW io.Writer, note, path string) {\
    if interactive.IsTerminalWriter(w) {\
        fmt.Fprintln(w, note)\
        fmt.Fprintf(w, "cd %s\n", shellQuote(path))\
        return\
    }\
    fmt.Fprintln(errW, note)\
    fmt.Fprintln(w, path)\
}\
\
func validateTrailWorktreeReuse(ctx context.Context, path, branch string) error {\
    expectedCommonDir, err := gitCommonDirForTrailWorktree(ctx)\
    if err != nil {\
        return err\
    }\
\
    showTop := exec.CommandContext(ctx, "git", "-C", path, "rev-parse", "--show-toplevel")\
    output, err := showTop.Output()\
    if err != nil {\
        return err //nolint:wrapcheck // caller reports a prune hint, not this low-level probe\
    }\
    if normalizeWorktreePath(strings.TrimSpace(string(output))) != normalizeWorktreePath(path) {\
        return errors.New("path is not a worktree root")\
    }\
\
    showCommon := exec.CommandContext(ctx, "git", "-C", path, "rev-parse", "--git-common-dir")\
    output, err = showCommon.Output()\
    if err != nil {\
        return err //nolint:wrapcheck // caller reports a prune hint, not this low-level probe\
    }\
    commonDir := strings.TrimSpace(string(output))\
    if !filepath.IsAbs(commonDir) {\
        commonDir = filepath.Join(path, commonDir)\
    }\
    if normalizeWorktreePath(commonDir) != normalizeWorktreePath(expectedCommonDir) {\
        return errors.New("path belongs to another repository")\
    }\
\
    showBranch := exec.CommandContext(ctx, "git", "-C", path, "branch", "--show-current")\
    output, err = showBranch.Output()\
    if err != nil {\
        return err //nolint:wrapcheck // caller reports a prune hint, not this low-level probe\
    }\
    if strings.TrimSpace(string(output)) != branch {\
        return fmt.Errorf("worktree is not on branch %q", branch)\
    }\
    return nil\
}\
\
// gitOutputError formats a failed git invocation, including git's stderr\
// (captured by cmd.Output in ExitError.Stderr) when it carries a diagnostic —\
// the bare error is usually just "exit status 128".\
func gitOutputError(action string, err error) error {\
    var exitErr *exec.ExitError\
    if errors.As(err, &exitErr) && len(bytes.TrimSpace(exitErr.Stderr)) > 0 {\
        return fmt.Errorf("%s: %s: %w", action, bytes.TrimSpace(exitErr.Stderr), err)\
    }\
    return fmt.Errorf("%s: %w", action, err)\
}\
\
func staleTrailWorktreeError(branch, path string) error {\
    return fmt.Errorf("branch %q is registered to a missing worktree at %s; run 'git worktree prune' to clear it", branch, path)\
}\
\
// trailWorktreeMatch describes an existing worktree that has a branch checked\
// out; managed means it lives under <root>/.entire/worktrees.\
type trailWorktreeMatch struct {\
    path    string\
    managed bool\
}\
\
// findWorktreeForBranch returns the worktree that has branch checked out,\
// with found reporting whether any worktree does.\
func findWorktreeForBranch(ctx context.Context, branch, root string) (match trailWorktreeMatch, found bool, err error) {\
    cmd := exec.CommandContext(ctx, "git", "worktree", "list", "--porcelain")\
    cmd.Dir = root\
    output, err := cmd.Output()\
    if err != nil {\
        return trailWorktreeMatch{}, false, gitOutputError("failed to list worktrees", err)\
    }\
    // Empty currentRoot: match any worktree, including the current checkout.\
    path, found := parseWorktreeForBranch(string(output), branch, "")\
    if !found {\
        return trailWorktreeMatch{}, false, nil\
    }\
    managedRoot := normalizeWorktreePath(filepath.Join(root, filepath.FromSlash(trailWorktreesRelDir)))\
    normalized := normalizeWorktreePath(path)\
    managed := normalized == managedRoot || strings.HasPrefix(normalized, managedRoot+string(filepath.Separator))\
    return trailWorktreeMatch{path: path, managed: managed}, true, nil\
}\
\
// ensureTrailWorktreeBranchAvailable makes sure branch exists locally,\
// fetching it from origin when it only exists there. It returns false when\
// the user declined the fetch prompt. --force and non-interactive runs fetch\
// without prompting.\
func ensureTrailWorktreeBranchAvailable(ctx context.Context, w io.Writer, branch string, force bool) (bool, error) {\
    exists, err := BranchExistsLocally(ctx, branch)\
    if err != nil {\
        return false, fmt.Errorf("failed to check branch: %w", err)\
    }\
    if exists {\
        return true, nil\
    }\
\
    remoteExists, err := BranchExistsOnRemote(ctx, branch)\
    if err != nil {\
        return false, fmt.Errorf("failed to check remote branch: %w", err)\
    }\
    if !remoteExists {\
        return false, fmt.Errorf("branch %q not found locally or on origin", branch)\
    }\
    if !force && interactive.CanPromptInteractively() {\
        shouldFetch, err := promptFetchFromRemote(branch)\
        if err != nil {\
            return false, err\
        }\
        if !shouldFetch {\
            return false, nil\
        }\
    }\
\
    fmt.Fprintf(w, "Fetching branch '%s' from origin...\n", branch)\
    if err := fetchTrailWorktreeBranch(ctx, branch); err != nil {\
        return false, err\
    }\
    return true, nil\
}\
\
// fetchTrailWorktreeBranch fetches origin's branch directly into refs/heads\
// so `git worktree add` can check it out without touching the current\
// checkout.\
func fetchTrailWorktreeBranch(ctx context.Context, branch string) error {\
    ctx, cancel := context.WithTimeout(ctx, 2*time.Minute)\
    defer cancel()\
\
    refSpec := fmt.Sprintf("refs/heads/%s:refs/heads/%s", branch, branch)\
    // NoFilter: the worktree checkout needs full branch content; a partial\
    // clone would leave blobs missing.\
    output, err := remote.Fetch(ctx, remote.FetchOptions{\
        Remote:   "origin",\
        RefSpecs: []string{refSpec},\
        NoFilter: true,\
    })\
    if err != nil {\
        if errors.Is(ctx.Err(), context.DeadlineExceeded) {\
            return errors.New("fetch timed out after 2 minutes")\
        }\
        return fmt.Errorf("failed to fetch branch from origin: %s: %w", strings.TrimSpace(string(output)), err)\
    }\
    return nil\
}\
```\
\
Acmd/entire/cli/trail\_checkout\_worktree.go+525\
\
```\
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\
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\
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\
469\
470\
471\
472\
473\
474\
475\
476\
477\
478\
479\
480\
481\
482\
483\
484\
485\
486\
487\
488\
489\
490\
491\
492\
493\
494\
495\
496\
497\
498\
499\
500\
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\
557\
558\
559\
560\
561\
562\
563\
564\
565\
566\
567\
568\
569\
570\
571\
572\
573\
574\
575\
576\
577\
578\
579\
580\
581\
582\
583\
584\
585\
586\
587\
588\
589\
590\
591\
592\
593\
594\
595\
596\
597\
598\
599\
600\
601\
602\
603\
604\
605\
606\
607\
608\
609\
610\
611\
612\
613\
614\
615\
616\
617\
618\
619\
620\
621\
622\
623\
624\
625\
626\
627\
628\
629\
630\
631\
632\
633\
634\
635\
636\
637\
638\
639\
640\
641\
642\
643\
644\
645\
646\
647\
648\
649\
650\
651\
652\
653\
654\
655\
656\
657\
658\
659\
660\
661\
662\
663\
664\
665\
666\
667\
668\
669\
670\
671\
672\
673\
674\
675\
\
package cli\
\
import (\
    "bytes"\
    "context"\
    "os"\
    "os/exec"\
    "path/filepath"\
    "slices"\
    "strings"\
    "testing"\
\
    "github.com/entireio/cli/cmd/entire/cli/paths"\
    "github.com/entireio/cli/cmd/entire/cli/testutil"\
)\
\
const testEnvFile = ".env"\
\
func TestDefaultTrailWorktreePath(t *testing.T) {\
    t.Parallel()\
\
    tests := []struct {\
        name        string\
        branch      string\
        trailNumber int\
        want        string\
    }{\
        {"slash branch", "peter/feature.auth", 123, filepath.Join("/repo", ".entire", "worktrees", "trail-123-peter-feature.auth")},\
        {"plain branch", "feature-other", 7, filepath.Join("/repo", ".entire", "worktrees", "trail-7-feature-other")},\
    }\
    for _, tt := range tests {\
        t.Run(tt.name, func(t *testing.T) {\
            t.Parallel()\
            if got := defaultTrailWorktreePath("/repo", tt.branch, tt.trailNumber); got != tt.want {\
                t.Fatalf("defaultTrailWorktreePath() = %q, want %q", got, tt.want)\
            }\
        })\
    }\
}\
\
func TestSanitizeTrailWorktreeName(t *testing.T) {\
    t.Parallel()\
\
    tests := []struct {\
        in   string\
        want string\
    }{\
        {"feature/test", "feature-test"},\
        {"Feat_1.2-x", "Feat_1.2-x"},\
        {"weird name!", "weird-name"},\
        {"---", trailWorktreeFallbackName},\
        {"  spaced  ", "spaced"},\
    }\
    for _, tt := range tests {\
        if got := sanitizeTrailWorktreeName(tt.in); got != tt.want {\
            t.Fatalf("sanitizeTrailWorktreeName(%q) = %q, want %q", tt.in, got, tt.want)\
        }\
    }\
}\
\
func TestAppendIgnoreRule(t *testing.T) {\
    t.Parallel()\
\
    path := filepath.Join(t.TempDir(), "gitignore")\
    if err := os.WriteFile(path, nil, 0o600); err != nil {\
        t.Fatalf("seed: %v", err)\
    }\
    for i := range 2 {\
        appended, err := appendIgnoreRule(path)\
        if err != nil {\
            t.Fatalf("appendIgnoreRule: %v", err)\
        }\
        wantAppended := i == 0\
        if appended != wantAppended {\
            t.Fatalf("appendIgnoreRule appended = %v, want %v", appended, wantAppended)\
        }\
    }\
    content, err := os.ReadFile(path)\
    if err != nil {\
        t.Fatalf("read: %v", err)\
    }\
    if got := strings.Count(string(content), ".entire/worktrees/"); got != 1 {\
        t.Fatalf("rule count = %d, want 1; content: %q", got, string(content))\
    }\
    if !strings.HasSuffix(string(content), "\n") {\
        t.Fatalf("content %q missing trailing newline", string(content))\
    }\
}\
\
func TestAppendIgnoreRule_AddsNewlineBeforeRule(t *testing.T) {\
    t.Parallel()\
\
    path := filepath.Join(t.TempDir(), "gitignore")\
    if err := os.WriteFile(path, []byte("node_modules"), 0o600); err != nil {\
        t.Fatalf("seed: %v", err)\
    }\
    appended, err := appendIgnoreRule(path)\
    if err != nil {\
        t.Fatalf("appendIgnoreRule: %v", err)\
    }\
    if !appended {\
        t.Fatal("appendIgnoreRule appended = false, want true")\
    }\
    content, err := os.ReadFile(path)\
    if err != nil {\
        t.Fatalf("read: %v", err)\
    }\
    if got, want := string(content), "node_modules\n.entire/worktrees/\n"; got != want {\
        t.Fatalf("content = %q, want %q", got, want)\
    }\
}\
\
func TestAppendIgnoreRule_MissingFileNoop(t *testing.T) {\
    t.Parallel()\
\
    path := filepath.Join(t.TempDir(), "gitignore")\
    appended, err := appendIgnoreRule(path)\
    if err != nil {\
        t.Fatalf("appendIgnoreRule: %v", err)\
    }\
    if appended {\
        t.Fatal("appendIgnoreRule appended = true, want false")\
    }\
    if _, err := os.Stat(path); !os.IsNotExist(err) {\
        t.Fatalf("gitignore stat = %v, want not exist", err)\
    }\
}\
\
func TestEnsureTrailWorktreeIgnoreRule_AppendsGitignore(t *testing.T) {\
    testutil.IsolateGitConfigEnv(t)\
\
    repoDir := t.TempDir()\
    testutil.InitRepo(t, repoDir)\
    testutil.WriteFile(t, repoDir, ".gitignore", "node_modules\n")\
    t.Chdir(repoDir)\
\
    var out bytes.Buffer\
    if err := ensureTrailWorktreeIgnoreRule(context.Background(), &out, repoDir); err != nil {\
        t.Fatalf("ensureTrailWorktreeIgnoreRule: %v", err)\
    }\
    content, err := os.ReadFile(filepath.Join(repoDir, ".gitignore"))\
    if err != nil {\
        t.Fatalf("read .gitignore: %v", err)\
    }\
    if !strings.Contains(string(content), ".entire/worktrees/") {\
        t.Fatalf(".gitignore = %q, want .entire/worktrees/ rule", string(content))\
    }\
    if !strings.Contains(out.String(), ".gitignore") {\
        t.Fatalf("output = %q, want notice mentioning .gitignore", out.String())\
    }\
}\
\
func TestEnsureTrailWorktreeIgnoreRule_MissingGitignoreNoop(t *testing.T) {\
    testutil.IsolateGitConfigEnv(t)\
\
    repoDir := t.TempDir()\
    testutil.InitRepo(t, repoDir)\
    t.Chdir(repoDir)\
\
    var out bytes.Buffer\
    if err := ensureTrailWorktreeIgnoreRule(context.Background(), &out, repoDir); err != nil {\
        t.Fatalf("ensureTrailWorktreeIgnoreRule: %v", err)\
    }\
    if out.Len() != 0 {\
        t.Fatalf("output = %q, want silence", out.String())\
    }\
    if _, err := os.Stat(filepath.Join(repoDir, ".gitignore")); !os.IsNotExist(err) {\
        t.Fatalf(".gitignore stat = %v, want not exist", err)\
    }\
}\
\
func TestEnsureTrailWorktreeIgnoreRule_AlreadyIgnoredIsSilentNoop(t *testing.T) {\
    testutil.IsolateGitConfigEnv(t)\
\
    repoDir := t.TempDir()\
    testutil.InitRepo(t, repoDir)\
    testutil.WriteFile(t, repoDir, ".gitignore", ".entire/\n")\
    t.Chdir(repoDir)\
\
    var out bytes.Buffer\
    if err := ensureTrailWorktreeIgnoreRule(context.Background(), &out, repoDir); err != nil {\
        t.Fatalf("ensureTrailWorktreeIgnoreRule: %v", err)\
    }\
    if out.Len() != 0 {\
        t.Fatalf("output = %q, want silence", out.String())\
    }\
    content, err := os.ReadFile(filepath.Join(repoDir, ".gitignore"))\
    if err != nil {\
        t.Fatalf("read .gitignore: %v", err)\
    }\
    if got, want := string(content), ".entire/\n"; got != want {\
        t.Fatalf(".gitignore = %q, want untouched %q", got, want)\
    }\
}\
\
func TestMatchIncludePatterns(t *testing.T) {\
    t.Parallel()\
\
    files := []string{\
        testEnvFile,\
        "config/.env.local",\
        "/abs/.env",\
        "../escape/.env",\
        "node_modules/pkg/x.js",\
    }\
    got := matchIncludePatterns([]string{testEnvFile, "*.local"}, files)\
    want := []string{testEnvFile, filepath.Join("config", ".env.local")}\
    if !slices.Equal(got, want) {\
        t.Fatalf("matchIncludePatterns() = %v, want %v", got, want)\
    }\
}\
\
func TestListIgnoredFiles_ExcludesManagedWorktreePaths(t *testing.T) {\
    testutil.IsolateGitConfigEnv(t)\
\
    repoDir := t.TempDir()\
    testutil.InitRepo(t, repoDir)\
    testutil.WriteFile(t, repoDir, ".gitignore", ".env\n.entire/\n")\
    testutil.WriteFile(t, repoDir, testEnvFile, "SECRET=1\n")\
    testutil.WriteFile(t, repoDir, ".entire/worktrees/other/.env", "SECRET=2\n")\
\
    got, err := listIgnoredFiles(context.Background(), repoDir)\
    if err != nil {\
        t.Fatalf("listIgnoredFiles: %v", err)\
    }\
    want := []string{testEnvFile}\
    if !slices.Equal(got, want) {\
        t.Fatalf("listIgnoredFiles() = %v, want %v", got, want)\
    }\
}\
\
func TestLoadWorktreeIncludePatterns(t *testing.T) {\
    t.Parallel()\
\
    root := t.TempDir()\
    content := "# secrets\n\n.env\n*.local\n"\
    if err := os.WriteFile(filepath.Join(root, ".worktreeinclude"), []byte(content), 0o600); err != nil {\
        t.Fatalf("seed: %v", err)\
    }\
    got, err := loadWorktreeIncludePatterns(root)\
    if err != nil {\
        t.Fatalf("loadWorktreeIncludePatterns: %v", err)\
    }\
    want := []string{".env", "*.local"}\
    if !slices.Equal(got, want) {\
        t.Fatalf("patterns = %v, want %v", got, want)\
    }\
}\
\
func TestLoadWorktreeIncludePatterns_MissingFile(t *testing.T) {\
    t.Parallel()\
\
    got, err := loadWorktreeIncludePatterns(t.TempDir())\
    if err != nil {\
        t.Fatalf("loadWorktreeIncludePatterns: %v", err)\
    }\
    if len(got) != 0 {\
        t.Fatalf("patterns = %v, want none", got)\
    }\
}\
\
func TestCopyWorktreeIncludeFiles(t *testing.T) {\
    testutil.IsolateGitConfigEnv(t)\
\
    repoDir := t.TempDir()\
    testutil.InitRepo(t, repoDir)\
    testutil.WriteFile(t, repoDir, ".gitignore", testEnvFile+"\n")\
    testutil.WriteFile(t, repoDir, ".worktreeinclude", testEnvFile+"\n")\
    testutil.WriteFile(t, repoDir, testEnvFile, "SECRET=1\n")\
    testutil.WriteFile(t, repoDir, "sub/"+testEnvFile, "SECRET=2\n")\
    testutil.GitAdd(t, repoDir, ".gitignore", ".worktreeinclude")\
    testutil.GitCommit(t, repoDir, "init")\
\
    dest := t.TempDir()\
    var errOut bytes.Buffer\
    if err := copyWorktreeIncludeFiles(context.Background(), &errOut, repoDir, dest); err != nil {\
        t.Fatalf("copyWorktreeIncludeFiles: %v; stderr: %s", err, errOut.String())\
    }\
    for _, rel := range []string{testEnvFile, "sub/" + testEnvFile} {\
        if _, err := os.Stat(filepath.Join(dest, filepath.FromSlash(rel))); err != nil {\
            t.Fatalf("copied file %s missing: %v", rel, err)\
        }\
    }\
}\
\
func TestCopyWorktreeIncludeFiles_NoIncludeFileCopiesNothing(t *testing.T) {\
    testutil.IsolateGitConfigEnv(t)\
\
    repoDir := t.TempDir()\
    testutil.InitRepo(t, repoDir)\
    testutil.WriteFile(t, repoDir, ".gitignore", testEnvFile+"\n")\
    testutil.WriteFile(t, repoDir, testEnvFile, "SECRET=1\n")\
\
    dest := t.TempDir()\
    var errOut bytes.Buffer\
    if err := copyWorktreeIncludeFiles(context.Background(), &errOut, repoDir, dest); err != nil {\
        t.Fatalf("copyWorktreeIncludeFiles: %v", err)\
    }\
    if _, err := os.Stat(filepath.Join(dest, testEnvFile)); !os.IsNotExist(err) {\
        t.Fatalf("%s stat = %v, want not exist", testEnvFile, err)\
    }\
}\
\
func TestCopyWorktreeIncludeFiles_SkipsSymlinkWithWarning(t *testing.T) {\
    testutil.IsolateGitConfigEnv(t)\
\
    const symlinkPath = "link.env"\
    repoDir := t.TempDir()\
    testutil.InitRepo(t, repoDir)\
    testutil.WriteFile(t, repoDir, ".gitignore", symlinkPath+"\ntarget.txt\n")\
    testutil.WriteFile(t, repoDir, ".worktreeinclude", symlinkPath+"\n")\
    testutil.WriteFile(t, repoDir, "target.txt", "x\n")\
    if err := os.Symlink(filepath.Join(repoDir, "target.txt"), filepath.Join(repoDir, symlinkPath)); err != nil {\
        t.Skipf("symlinks unsupported: %v", err)\
    }\
\
    dest := t.TempDir()\
    var errOut bytes.Buffer\
    if err := copyWorktreeIncludeFiles(context.Background(), &errOut, repoDir, dest); err != nil {\
        t.Fatalf("copyWorktreeIncludeFiles: %v", err)\
    }\
    if !strings.Contains(errOut.String(), "warning: skipped "+symlinkPath) {\
        t.Fatalf("stderr = %q, want skip warning for %s", errOut.String(), symlinkPath)\
    }\
    if _, err := os.Stat(filepath.Join(dest, symlinkPath)); !os.IsNotExist(err) {\
        t.Fatalf("%s stat = %v, want not exist", symlinkPath, err)\
    }\
}\
\
func TestCopyWorktreeIncludeFiles_RefusesSymlinkedDirEscape(t *testing.T) {\
    testutil.IsolateGitConfigEnv(t)\
\
    repoDir := t.TempDir()\
    testutil.InitRepo(t, repoDir)\
    testutil.WriteFile(t, repoDir, ".gitignore", "sub/"+testEnvFile+"\n")\
    testutil.WriteFile(t, repoDir, ".worktreeinclude", "sub/"+testEnvFile+"\n")\
    testutil.WriteFile(t, repoDir, "sub/"+testEnvFile, "SECRET=1\n")\
\
    dest := t.TempDir()\
    outside := t.TempDir()\
    if err := os.Symlink(outside, filepath.Join(dest, "sub")); err != nil {\
        t.Skipf("symlinks unsupported: %v", err)\
    }\
\
    var errOut bytes.Buffer\
    if err := copyWorktreeIncludeFiles(context.Background(), &errOut, repoDir, dest); err != nil {\
        t.Fatalf("copyWorktreeIncludeFiles: %v", err)\
    }\
    if !strings.Contains(errOut.String(), "warning: skipped sub/"+testEnvFile) {\
        t.Fatalf("stderr = %q, want skip warning for sub/%s", errOut.String(), testEnvFile)\
    }\
    if _, err := os.Stat(filepath.Join(outside, testEnvFile)); !os.IsNotExist(err) {\
        t.Fatalf("%s stat in outside dir = %v, want not exist", testEnvFile, err)\
    }\
}\
\
func newTrailWorktreeTestRepo(t *testing.T) string {\
    t.Helper()\
    testutil.IsolateGitConfigEnv(t)\
    paths.ClearWorktreeRootCache()\
    t.Cleanup(paths.ClearWorktreeRootCache)\
\
    repoDir := t.TempDir()\
    testutil.InitRepo(t, repoDir)\
    testutil.WriteFile(t, repoDir, "README.md", "test\n")\
    testutil.GitAdd(t, repoDir, "README.md")\
    testutil.GitCommit(t, repoDir, "initial")\
    return repoDir\
}\
\
func currentBranchInDir(t *testing.T, dir string) string {\
    t.Helper()\
    cmd := exec.CommandContext(context.Background(), "git", "branch", "--show-current")\
    cmd.Dir = dir\
    output, err := cmd.Output()\
    if err != nil {\
        t.Fatalf("git branch --show-current failed: %v", err)\
    }\
    return strings.TrimSpace(string(output))\
}\
\
func TestCheckoutTrailWorktree_CreatesWorktree(t *testing.T) {\
    repoDir := newTrailWorktreeTestRepo(t)\
    runGit(t, repoDir, "branch", "feature/test")\
    testutil.WriteFile(t, repoDir, ".worktreeinclude", ".env\n")\
    testutil.WriteFile(t, repoDir, ".env", "SECRET=1\n")\
    testutil.WriteFile(t, repoDir, ".gitignore", ".env\n")\
    testutil.GitAdd(t, repoDir, ".worktreeinclude", ".gitignore")\
    testutil.GitCommit(t, repoDir, "add include config")\
    startBranch := currentBranchInDir(t, repoDir)\
    t.Chdir(repoDir)\
\
    var out, errOut bytes.Buffer\
    if err := checkoutTrailWorktree(context.Background(), &out, &errOut, "feature/test", false, 7); err != nil {\
        t.Fatalf("checkoutTrailWorktree: %v; stderr: %s", err, errOut.String())\
    }\
\
    wantPath := filepath.Join(repoDir, ".entire", "worktrees", "trail-7-feature-test")\
    if got, want := out.String(), wantPath+"\n"; got != want {\
        t.Fatalf("stdout = %q, want bare path %q for script use", got, want)\
    }\
    if !strings.Contains(errOut.String(), "Worktree ready at "+wantPath) {\
        t.Fatalf("stderr = %q, want progress notice", errOut.String())\
    }\
    if got := currentBranchInDir(t, repoDir); got != startBranch {\
        t.Fatalf("current branch = %q, want unchanged %q", got, startBranch)\
    }\
    if got := currentBranchInDir(t, wantPath); got != "feature/test" {\
        t.Fatalf("worktree branch = %q, want feature/test", got)\
    }\
    if _, err := os.Stat(filepath.Join(wantPath, ".env")); err != nil {\
        t.Fatalf(".worktreeinclude copy missing: %v", err)\
    }\
    gitignoreContent, err := os.ReadFile(filepath.Join(repoDir, ".gitignore"))\
    if err != nil {\
        t.Fatalf("read .gitignore: %v", err)\
    }\
    if !strings.Contains(string(gitignoreContent), ".entire/worktrees/") {\
        t.Fatalf(".gitignore = %q, want .entire/worktrees/ rule", string(gitignoreContent))\
    }\
}\
\
func TestCheckoutTrailWorktree_FromLinkedWorktreeCreatesSibling(t *testing.T) {\
    repoDir := newTrailWorktreeTestRepo(t)\
    runGit(t, repoDir, "branch", "feature/first")\
    runGit(t, repoDir, "branch", "feature/second")\
    t.Chdir(repoDir)\
\
    var out1, err1 bytes.Buffer\
    if err := checkoutTrailWorktree(context.Background(), &out1, &err1, "feature/first", false, 7); err != nil {\
        t.Fatalf("first checkout: %v; stderr: %s", err, err1.String())\
    }\
    firstPath := filepath.Join(repoDir, ".entire", "worktrees", "trail-7-feature-first")\
    t.Chdir(firstPath)\
\
    var out2, err2 bytes.Buffer\
    if err := checkoutTrailWorktree(context.Background(), &out2, &err2, "feature/second", false, 8); err != nil {\
        t.Fatalf("second checkout: %v; stderr: %s", err, err2.String())\
    }\
\
    wantPath := filepath.Join(repoDir, ".entire", "worktrees", "trail-8-feature-second")\
    if _, err := os.Stat(wantPath); err != nil {\
        t.Fatalf("sibling worktree missing: %v", err)\
    }\
    nested := filepath.Join(firstPath, ".entire", "worktrees", "trail-8-feature-second")\
    if _, err := os.Stat(nested); !os.IsNotExist(err) {\
        t.Fatalf("nested worktree stat = %v, want not exist", err)\
    }\
}\
\
func TestCheckoutTrailWorktree_BranchCheckedOutInMainWorktree(t *testing.T) {\
    repoDir := newTrailWorktreeTestRepo(t)\
    startBranch := currentBranchInDir(t, repoDir)\
    t.Chdir(repoDir)\
\
    var out, errOut bytes.Buffer\
    err := checkoutTrailWorktree(context.Background(), &out, &errOut, startBranch, false, 1)\
    if err == nil || !strings.Contains(err.Error(), "already checked out at") {\
        t.Fatalf("error = %v, want already-checked-out error", err)\
    }\
\
    if _, statErr := os.Stat(filepath.Join(repoDir, ".entire", "worktrees")); !os.IsNotExist(statErr) {\
        t.Fatalf(".entire/worktrees stat = %v, want not exist", statErr)\
    }\
    if _, statErr := os.Stat(filepath.Join(repoDir, ".gitignore")); !os.IsNotExist(statErr) {\
        t.Fatalf(".gitignore stat = %v, want no ignore rule written before the failure", statErr)\
    }\
}\
\
func TestCheckoutTrailWorktree_ReusesExistingWorktree(t *testing.T) {\
    repoDir := newTrailWorktreeTestRepo(t)\
    runGit(t, repoDir, "branch", "feature/reuse")\
    t.Chdir(repoDir)\
\
    var out1, err1 bytes.Buffer\
    if err := checkoutTrailWorktree(context.Background(), &out1, &err1, "feature/reuse", false, 9); err != nil {\
        t.Fatalf("first checkout: %v; stderr: %s", err, err1.String())\
    }\
    var out2, err2 bytes.Buffer\
    if err := checkoutTrailWorktree(context.Background(), &out2, &err2, "feature/reuse", false, 9); err != nil {\
        t.Fatalf("second checkout: %v; stderr: %s", err, err2.String())\
    }\
    wantPath := filepath.Join(repoDir, ".entire", "worktrees", "trail-9-feature-reuse")\
    gotPath := strings.TrimSuffix(out2.String(), "\n")\
    if strings.Contains(gotPath, "\n") || normalizeWorktreePath(gotPath) != normalizeWorktreePath(wantPath) {\
        t.Fatalf("second stdout = %q, want bare path %q for script use", out2.String(), wantPath)\
    }\
    if !strings.Contains(err2.String(), "Worktree already exists") {\
        t.Fatalf("second stderr = %q, want existing-worktree message", err2.String())\
    }\
}\
\
func TestCheckoutTrailWorktree_FetchesRemoteOnlyBranch(t *testing.T) {\
    testutil.IsolateGitConfigEnv(t)\
    paths.ClearWorktreeRootCache()\
    t.Cleanup(paths.ClearWorktreeRootCache)\
\
    tmp := t.TempDir()\
    originDir := filepath.Join(tmp, "origin.git")\
    seedDir := filepath.Join(tmp, "seed")\
    repoDir := filepath.Join(tmp, "local")\
    runGit(t, tmp, "init", "--bare", originDir)\
    testutil.InitRepo(t, seedDir)\
    testutil.WriteFile(t, seedDir, "README.md", "test\n")\
    testutil.GitAdd(t, seedDir, "README.md")\
    testutil.GitCommit(t, seedDir, "initial")\
    runGit(t, seedDir, "checkout", "-b", "feature/remote")\
    testutil.WriteFile(t, seedDir, "remote.txt", "remote\n")\
    testutil.GitAdd(t, seedDir, "remote.txt")\
    testutil.GitCommit(t, seedDir, "remote branch")\
    runGit(t, seedDir, "remote", "add", "origin", originDir)\
    runGit(t, seedDir, "push", "origin", "--all")\
    runGit(t, tmp, "clone", originDir, repoDir)\
    t.Chdir(repoDir)\
\
    var out, errOut bytes.Buffer\
    if err := checkoutTrailWorktree(context.Background(), &out, &errOut, "feature/remote", false, 12); err != nil {\
        t.Fatalf("checkoutTrailWorktree: %v; stderr: %s", err, errOut.String())\
    }\
\
    wantPath := filepath.Join(repoDir, ".entire", "worktrees", "trail-12-feature-remote")\
    if got := currentBranchInDir(t, wantPath); got != "feature/remote" {\
        t.Fatalf("worktree branch = %q, want feature/remote", got)\
    }\
    if _, err := os.Stat(filepath.Join(wantPath, "remote.txt")); err != nil {\
        t.Fatalf("remote branch file missing: %v", err)\
    }\
}\
\
func TestCheckoutTrailWorktree_RejectsUnnumberedTrail(t *testing.T) {\
    repoDir := newTrailWorktreeTestRepo(t)\
    runGit(t, repoDir, "branch", "feature/unnumbered")\
    t.Chdir(repoDir)\
\
    var out, errOut bytes.Buffer\
    err := checkoutTrailWorktree(context.Background(), &out, &errOut, "feature/unnumbered", false, 0)\
    if err == nil || !strings.Contains(err.Error(), "has no number yet") {\
        t.Fatalf("error = %v, want no-number rejection", err)\
    }\
    if _, statErr := os.Stat(filepath.Join(repoDir, ".entire", "worktrees")); !os.IsNotExist(statErr) {\
        t.Fatalf(".entire/worktrees stat = %v, want not exist", statErr)\
    }\
}\
\
func TestCheckoutTrailWorktree_StaleManagedWorktreeErrorsWithPruneHint(t *testing.T) {\
    repoDir := newTrailWorktreeTestRepo(t)\
    runGit(t, repoDir, "branch", "feature/stale")\
    t.Chdir(repoDir)\
\
    var out1, err1 bytes.Buffer\
    if err := checkoutTrailWorktree(context.Background(), &out1, &err1, "feature/stale", false, 4); err != nil {\
        t.Fatalf("first checkout: %v; stderr: %s", err, err1.String())\
    }\
    worktreePath := filepath.Join(repoDir, ".entire", "worktrees", "trail-4-feature-stale")\
    if err := os.RemoveAll(worktreePath); err != nil {\
        t.Fatalf("remove worktree dir: %v", err)\
    }\
\
    var out2, err2 bytes.Buffer\
    err := checkoutTrailWorktree(context.Background(), &out2, &err2, "feature/stale", false, 4)\
    if err == nil || !strings.Contains(err.Error(), "git worktree prune") {\
        t.Fatalf("error = %v, want prune hint", err)\
    }\
    if _, statErr := os.Stat(worktreePath); !os.IsNotExist(statErr) {\
        t.Fatalf("worktree path stat = %v, want not recreated", statErr)\
    }\
}\
\
func TestCheckoutTrailWorktree_StaleManagedWorktreeDirectoryErrorsWithPruneHint(t *testing.T) {\
    repoDir := newTrailWorktreeTestRepo(t)\
    runGit(t, repoDir, "branch", "feature/stale-dir")\
    t.Chdir(repoDir)\
\
    var out1, err1 bytes.Buffer\
    if err := checkoutTrailWorktree(context.Background(), &out1, &err1, "feature/stale-dir", false, 4); err != nil {\
        t.Fatalf("first checkout: %v; stderr: %s", err, err1.String())\
    }\
    worktreePath := filepath.Join(repoDir, ".entire", "worktrees", "trail-4-feature-stale-dir")\
    if err := os.RemoveAll(worktreePath); err != nil {\
        t.Fatalf("remove worktree dir: %v", err)\
    }\
    if err := os.MkdirAll(worktreePath, 0o750); err != nil {\
        t.Fatalf("replace worktree dir: %v", err)\
    }\
\
    var out2, err2 bytes.Buffer\
    err := checkoutTrailWorktree(context.Background(), &out2, &err2, "feature/stale-dir", false, 4)\
    if err == nil || !strings.Contains(err.Error(), "git worktree prune") {\
        t.Fatalf("error = %v, want prune hint", err)\
    }\
    if strings.Contains(out2.String(), "Worktree already exists") {\
        t.Fatalf("output = %q, want no reuse message", out2.String())\
    }\
}\
\
func TestCheckoutTrailWorktree_StaleNonManagedWorktreeErrors(t *testing.T) {\
    repoDir := newTrailWorktreeTestRepo(t)\
    runGit(t, repoDir, "branch", "feature/manual")\
    manualPath := filepath.Join(t.TempDir(), "manual")\
    runGit(t, repoDir, "worktree", "add", manualPath, "feature/manual")\
    if err := os.RemoveAll(manualPath); err != nil {\
        t.Fatalf("remove manual worktree: %v", err)\
    }\
    t.Chdir(repoDir)\
\
    var out, errOut bytes.Buffer\
    err := checkoutTrailWorktree(context.Background(), &out, &errOut, "feature/manual", false, 5)\
    if err == nil || !strings.Contains(err.Error(), "git worktree prune") {\
        t.Fatalf("error = %v, want prune hint", err)\
    }\
}\
\
func TestCheckoutTrailWorktree_RegisteredPathNotADirectory(t *testing.T) {\
    repoDir := newTrailWorktreeTestRepo(t)\
    runGit(t, repoDir, "branch", "feature/swapped")\
    t.Chdir(repoDir)\
\
    var out1, err1 bytes.Buffer\
    if err := checkoutTrailWorktree(context.Background(), &out1, &err1, "feature/swapped", false, 6); err != nil {\
        t.Fatalf("first checkout: %v; stderr: %s", err, err1.String())\
    }\
    worktreePath := filepath.Join(repoDir, ".entire", "worktrees", "trail-6-feature-swapped")\
    if err := os.RemoveAll(worktreePath); err != nil {\
        t.Fatalf("remove worktree dir: %v", err)\
    }\
    if err := os.Symlink(repoDir, worktreePath); err != nil {\
        t.Skipf("symlinks unsupported: %v", err)\
    }\
\
    var out2, err2 bytes.Buffer\
    err := checkoutTrailWorktree(context.Background(), &out2, &err2, "feature/swapped", false, 6)\
    if err == nil || !strings.Contains(err.Error(), "is not a directory") {\
        t.Fatalf("error = %v, want not-a-directory rejection", err)\
    }\
}\
\
func TestFindWorktreeForBranch_SurfacesGitError(t *testing.T) {\
    testutil.IsolateGitConfigEnv(t)\
\
    _, _, err := findWorktreeForBranch(context.Background(), "any", t.TempDir())\
    if err == nil || !strings.Contains(err.Error(), "not a git repository") {\
        t.Fatalf("error = %v, want git stderr in message", err)\
    }\
}\
\
func TestGitCommonDirForTrailWorktree_SurfacesGitError(t *testing.T) {\
    testutil.IsolateGitConfigEnv(t)\
    t.Chdir(t.TempDir())\
\
    _, err := gitCommonDirForTrailWorktree(context.Background())\
    if err == nil || !strings.Contains(err.Error(), "not a git repository") {\
        t.Fatalf("error = %v, want git stderr in message", err)\
    }\
}\
\
func TestCheckoutTrailWorktree_RejectsInvalidBranch(t *testing.T) {\
    t.Parallel()\
\
    var out, errOut bytes.Buffer\
    err := checkoutTrailWorktree(context.Background(), &out, &errOut, "-bad", false, 1)\
    if err == nil || !strings.Contains(err.Error(), "invalid branch") {\
        t.Fatalf("error = %v, want invalid branch", err)\
    }\
}\
\
func TestCheckoutTrailWorktree_UnknownBranch(t *testing.T) {\
    repoDir := newTrailWorktreeTestRepo(t)\
    t.Chdir(repoDir)\
\
    var out, errOut bytes.Buffer\
    err := checkoutTrailWorktree(context.Background(), &out, &errOut, "feature/nope", false, 3)\
    if err == nil || !strings.Contains(err.Error(), "not found locally or on origin") {\
        t.Fatalf("error = %v, want branch-not-found", err)\
    }\
}\
```\
\
Acmd/entire/cli/trail\_checkout\_worktree\_test.go+675\
\
```\
10 unmodified lines\
\
11\
12\
13\
14\
14\
15\
16\
3 unmodified lines\
\
20\
21\
22\
23\
24\
25\
26\
224 unmodified lines\
\
251\
252\
253\
254\
254\
255\
256\
257\
258\
259\
260\
261\
262\
263\
264\
265\
266\
267\
268\
269\
270\
256\
271\
272\
273\
259\
274\
275\
261\
262\
263\
264\
276\
277\
278\
279\
280\
266\
281\
282\
283\
284\
285\
286\
287\
269\
288\
289\
290\
272\
291\
292\
293\
275\
294\
295\
277\
278\
296\
297\
298\
280\
299\
300\
301\
302\
343 unmodified lines\
\
646\
647\
648\
630\
631\
632\
649\
650\
651\
635\
652\
653\
654\
655\
656\
637\
657\
658\
659\
640\
660\
661\
662\
643\
663\
664\
645\
665\
666\
647\
667\
668\
649\
650\
669\
670\
671\
672\
673\
674\
2 unmodified lines\
\
677\
678\
679\
659\
680\
681\
682\
683\
661\
684\
685\
686\
687\
688\
689\
690\
664\
691\
692\
693\
694\
695\
696\
697\
667\
698\
699\
700\
701\
702\
703\
704\
705\
706\
707\
673\
708\
709\
675\
710\
711\
712\
713\
714\
715\
716\
717\
718\
719\
720\
721\
722\
723\
724\
725\
726\
42 unmodified lines\
\
769\
770\
771\
772\
773\
774\
775\
776\
777\
778\
779\
780\
781\
782\
783\
784\
785\
786\
787\
788\
789\
790\
791\
792\
793\
500 unmodified lines\
\
1294\
1295\
1296\
1297\
1298\
1299\
1300\
4 unmodified lines\
\
1305\
1306\
1307\
1308\
1309\
1310\
1311\
1312\
1313\
1314\
1315\
1316\
1317\
8 unmodified lines\
\
1326\
1327\
1328\
1254\
1329\
1330\
1331\
1332\
1333\
1334\
1335\
1336\
1337\
1338\
1339\
1264\
1340\
1341\
1342\
1343\
1344\
1345\
1346\
1347\
1348\
12 unmodified lines\
\
1361\
1362\
1363\
1364\
1365\
1366\
1367\
1368\
1369\
1370\
1371\
4 unmodified lines\
\
1376\
1377\
1378\
1293\
1379\
1380\
1381\
1382\
\
10 unmodified lines\
\
    "os/exec"\
    "strconv"\
    "strings"\
    "text/tabwriter"\
    "time"\
\
    "github.com/entireio/cli/cmd/entire/cli/api"\
3 unmodified lines\
\
    "github.com/entireio/cli/cmd/entire/cli/trail"\
\
    "charm.land/huh/v2"\
    "charm.land/lipgloss/v2"\
    "github.com/go-git/go-git/v6"\
    "github.com/go-git/go-git/v6/plumbing"\
    "github.com/spf13/cobra"\
224 unmodified lines\
\
}\
\
func printTrailDetails(w io.Writer, m *trail.Metadata, webURL, bodyText string) {\
    fmt.Fprintf(w, "Trail: %s\n", m.Title)\
    // Color the same fields as the list view (STATUS/PHASE/AUTHOR); everything\
    // else stays plain. Values are pre-colored, so alignment is unaffected.\
    styles := newStatusStyles(w)\
    status := string(m.Status)\
    if style, ok := trailStatusColor(styles, m.Status); ok {\
        status = styles.render(style, status)\
    }\
    author := m.AuthorLogin()\
    if styles.colorEnabled && author != "" {\
        author = styles.render(styles.cyan, author)\
    }\
\
    // Field labels and the title line render yellow (matching the list header).\
    label := func(s string) string { return styles.render(styles.yellow, s) }\
\
    fmt.Fprintf(w, "%s\n", styles.render(styles.yellow, "Trail: "+m.Title))\
    if m.Number > 0 {\
        fmt.Fprintf(w, "  Number:  %d\n", m.Number)\
        fmt.Fprintf(w, "  %s%d\n", label("Number:  "), m.Number)\
    }\
    if !m.TrailID.IsEmpty() {\
        fmt.Fprintf(w, "  ID:      %s\n", m.TrailID)\
        fmt.Fprintf(w, "  %s%s\n", label("ID:      "), m.TrailID)\
    }\
    fmt.Fprintf(w, "  Branch:  %s\n", m.Branch)\
    fmt.Fprintf(w, "  Base:    %s\n", m.Base)\
    fmt.Fprintf(w, "  Status:  %s\n", m.Status)\
    fmt.Fprintf(w, "  Author:  %s\n", m.AuthorLogin())\
    fmt.Fprintf(w, "  %s%s\n", label("Branch:  "), m.Branch)\
    fmt.Fprintf(w, "  %s%s\n", label("Base:    "), m.Base)\
    fmt.Fprintf(w, "  %s%s\n", label("Status:  "), status)\
    fmt.Fprintf(w, "  %s%s\n", label("Author:  "), author)\
    if strings.TrimSpace(m.Phase) != "" {\
        fmt.Fprintf(w, "  Phase:   %s\n", trailPhaseDisplay(m.Phase))\
        phase := trailPhaseDisplay(m.Phase)\
        if styles.colorEnabled {\
            phase = styles.render(styles.yellow, phase)\
        }\
        fmt.Fprintf(w, "  %s%s\n", label("Phase:   "), phase)\
    }\
    if webURL != "" {\
        fmt.Fprintf(w, "  URL:     %s\n", webURL)\
        fmt.Fprintf(w, "  %s%s\n", label("URL:     "), webURL)\
    }\
    if len(m.Labels) > 0 {\
        fmt.Fprintf(w, "  Labels:  %s\n", strings.Join(m.Labels, ", "))\
        fmt.Fprintf(w, "  %s%s\n", label("Labels:  "), strings.Join(m.Labels, ", "))\
    }\
    if len(m.Assignees) > 0 {\
        fmt.Fprintf(w, "  Assignees: %s\n", strings.Join(m.Assignees, ", "))\
        fmt.Fprintf(w, "  %s%s\n", label("Assignees: "), strings.Join(m.Assignees, ", "))\
    }\
    fmt.Fprintf(w, "  Created: %s\n", m.CreatedAt.Format("2006-01-02T15:04:05Z07:00"))\
    fmt.Fprintf(w, "  Updated: %s\n", m.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"))\
    fmt.Fprintf(w, "  %s%s\n", label("Created: "), m.CreatedAt.Format("2006-01-02T15:04:05Z07:00"))\
    fmt.Fprintf(w, "  %s%s\n", label("Updated: "), m.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"))\
    if strings.TrimSpace(bodyText) != "" {\
        fmt.Fprintf(w, "\nDescription:\n%s\n", bodyText)\
        fmt.Fprintf(w, "\n%s\n%s\n", label("Description:"), bodyText)\
    }\
}\
\
343 unmodified lines\
\
}\
\
func printTrailRows(w io.Writer, trails []*trail.Metadata, showAuthor, showStatus bool) {\
    // tabwriter aligns by display columns instead of bytes, so multi-byte\
    // branch names or logins don't throw off the table.\
    tw := tabwriter.NewWriter(w, 0, 0, 2, ' ', 0)\
    styles := newStatusStyles(w)\
    showPhase := trailListHasPhase(trails)\
    showURL := trailListHasURL(trails)\
    columns := []string{"NUM", "BRANCH", "TITLE"}\
\
    // The leading two-space indent is folded into the first column so the shared\
    // table renderer (columnWidths/writeTableRow) reproduces the list's layout.\
    headers := []string{"  NUM", "BRANCH", "TITLE"}\
    if showStatus {\
        columns = append(columns, "STATUS")\
        headers = append(headers, "STATUS")\
    }\
    if showPhase {\
        columns = append(columns, "PHASE")\
        headers = append(headers, "PHASE")\
    }\
    if showAuthor {\
        columns = append(columns, "AUTHOR")\
        headers = append(headers, "AUTHOR")\
    }\
    columns = append(columns, "UPDATED")\
    headers = append(headers, "UPDATED")\
    if showURL {\
        columns = append(columns, "URL")\
        headers = append(headers, "URL")\
    }\
    fmt.Fprintln(tw, "  "+strings.Join(columns, "\t"))\
    for _, t := range trails {\
\
    rows := make([][]string, len(trails))\
    for i, t := range trails {\
        number := "-"\
        if t.Number > 0 {\
            number = strconv.Itoa(t.Number)\
2 unmodified lines\
\
        if title == "" {\
            title = "(untitled)"\
        }\
        fields := []string{number, t.Branch, title}\
        fields := []string{"  " + number, t.Branch, title}\
        // Cells are pre-colored here; columnWidths/writeTableRow measure width\
        // with lipgloss.Width (ANSI-agnostic), so color never shifts columns.\
        if showStatus {\
            fields = append(fields, trailStatusDisplay(t.Status))\
            status := trailStatusDisplay(t.Status)\
            if style, ok := trailStatusColor(styles, t.Status); ok {\
                status = styles.render(style, status)\
            }\
            fields = append(fields, status)\
        }\
        if showPhase {\
            fields = append(fields, trailPhaseDisplay(t.Phase))\
            phase := trailPhaseDisplay(t.Phase)\
            if styles.colorEnabled && phase != "-" {\
                phase = styles.render(styles.yellow, phase)\
            }\
            fields = append(fields, phase)\
        }\
        if showAuthor {\
            fields = append(fields, t.AuthorLogin())\
            author := t.AuthorLogin()\
            if styles.colorEnabled && author != "" {\
                author = styles.render(styles.cyan, author)\
            }\
            fields = append(fields, author)\
        }\
        fields = append(fields, timeAgo(t.UpdatedAt))\
        if showURL {\
            fields = append(fields, t.URL)\
        }\
        fmt.Fprintln(tw, "  "+strings.Join(fields, "\t"))\
        rows[i] = fields\
    }\
    _ = tw.Flush()\
\
    widths := columnWidths(headers, rows)\
    var b strings.Builder\
    // Header row is yellow; data cells are already pre-colored, so they pass\
    // through a disabled style. tblSt only supplies the color-enabled gate for\
    // the header style.\
    tblSt := newTableStyles(w)\
    headerStyle := func(int) lipgloss.Style { return styles.yellow }\
    plain := func(int) lipgloss.Style { return lipgloss.Style{} }\
    writeTableRow(&b, headers, widths, headerStyle, tblSt)\
    for _, r := range rows {\
        writeTableRow(&b, r, widths, plain, tableStyles{})\
    }\
    fmt.Fprint(w, b.String())\
}\
\
func trailListHasPhase(trails []*trail.Metadata) bool {\
42 unmodified lines\
\
    return strings.ReplaceAll(string(status), "_", " ")\
}\
\
// trailStatusColor returns the style for a trail status: open green, merged\
// magenta, closed red. draft (the in-progress/building state) and any unknown\
// status stay uncolored. The colors avoid AUTHOR's cyan and PHASE's yellow so\
// the columns stay distinguishable.\
func trailStatusColor(styles statusStyles, status trail.Status) (lipgloss.Style, bool) {\
    switch status {\
    case trail.StatusOpen:\
        return styles.green, true\
    case trail.StatusMerged:\
        return styles.magenta, true\
    case trail.StatusClosed:\
        return styles.red, true\
    case trail.StatusDraft:\
        return lipgloss.Style{}, false\
    default:\
        return lipgloss.Style{}, false\
    }\
}\
\
// trailCountDisplay renders a count as "shown/total" when --limit truncated\
// the list, so a capped page doesn't read as the total number of matches.\
func trailCountDisplay(shown, total int) string {\
500 unmodified lines\
\
func newTrailCheckoutCmd() *cobra.Command {\
    var trailSelector string\
    var force bool\
    var worktree bool\
\
    cmd := &cobra.Command{\
        Use:   "checkout [<trail>]",\
4 unmodified lines\
\
branch. Without one, the trail for the current branch is used. The trail's branch\
is checked out, fetching it from origin first when it only exists there.\
\
With --worktree, the branch is checked out into a git worktree under\
.entire/worktrees at the repo root instead of switching this checkout, and the\
command prints a cd command for the new worktree. Gitignored files matching\
.worktreeinclude patterns are copied into the worktree. When stdout is not a\
terminal, only the worktree path is printed, so scripts can use\
cd "$(entire trail checkout <trail> --worktree)".\
\
This must be run from within a clone of the repository the trail belongs to; the\
trail is looked up against that repository's origin remote.`,\
        Args: cobra.MaximumNArgs(1),\
8 unmodified lines\
\
            if err := ensureNoTrailRepoOverride(cmd, "trail checkout"); err != nil {\
                return err\
            }\
            return runTrailCheckout(cmd.Context(), cmd.OutOrStdout(), cmd.ErrOrStderr(), trailInsecureHTTP(cmd), selector, force)\
            return runTrailCheckout(cmd.Context(), cmd.OutOrStdout(), cmd.ErrOrStderr(), trailInsecureHTTP(cmd), selector, trailCheckoutOptions{Force: force, Worktree: worktree})\
        },\
    }\
\
    cmd.Flags().StringVar(&trailSelector, "trail", "", "Trail to check out (number, id, or branch; defaults to the current branch's trail)")\
    cmd.Flags().BoolVarP(&force, "force", "f", false, "Skip the prompt before fetching a remote-only branch")\
    cmd.Flags().BoolVar(&worktree, "worktree", false, "Check out the trail branch in a worktree under .entire/worktrees instead of switching this checkout")\
\
    return cmd\
}\
\
func runTrailCheckout(ctx context.Context, w, errW io.Writer, insecureHTTP bool, selector string, force bool) error {\
type trailCheckoutOptions struct {\
    Force    bool\
    Worktree bool\
}\
\
func runTrailCheckout(ctx context.Context, w, errW io.Writer, insecureHTTP bool, selector string, opts trailCheckoutOptions) error {\
    // checkout rejects --repo (it operates on the local clone), so the enablement\
    // cache always tracks the local origin here.\
    return runAuthenticatedTrailAPI(ctx, errW, insecureHTTP, "", func(ctx context.Context, client *api.Client) error {\
12 unmodified lines\
\
            return fmt.Errorf("%s has no branch to check out", describeTrailRef(found))\
        }\
\
        if opts.Worktree {\
            fmt.Fprintf(errW, "Checking out %s in a worktree\n", describeTrailRef(found))\
            return checkoutTrailWorktree(ctx, w, errW, branch, opts.Force, found.Number)\
        }\
\
        currentBranch, _ := GetCurrentBranch(ctx) //nolint:errcheck // best-effort; a detached HEAD just means "not already on the branch"\
        if currentBranch == branch {\
            fmt.Fprintf(w, "Already on branch %s for %s.\n", branch, describeTrailRef(found))\
4 unmodified lines\
\
        // switchToBranchForResume handles local vs. remote-only branches, the\
        // uncommitted-changes guard, and the fetch prompt; reuse it rather than\
        // re-deriving that logic here.\
        proceed, err := switchToBranchForResume(ctx, w, errW, branch, force)\
        proceed, err := switchToBranchForResume(ctx, w, errW, branch, opts.Force)\
        if err != nil {\
            return err\
        }\
```\
\
Mcmd/entire/cli/trail\_cmd.go+121/-35\
\
```\
50 unmodified lines\
\
51\
52\
53\
54\
54\
55\
56\
57\
58\
\
50 unmodified lines\
\
    AgentTrailerKey = "Entire-Agent"\
\
    // OPFAppliedTrailerKey marks an entire/checkpoints/v1 commit whose blobs\
    // have been redacted by the OpenAI Privacy Filter (8-layer pipeline).\
    // have been redacted by the OpenAI Privacy Filter (the opt-in 9th,\
    // network-backed layer, applied on top of the 8 regex layers).\
    // Format: literal "true"; the trailer is omitted entirely when OPF was\
    // not applied. The pre-push rewrite path treats commits lacking this\
    // trailer as candidates to OPF-redact before they reach the remote.\
```\
\
Mcmd/entire/cli/trailers/trailers.go+2/-1\
\
```\
350 unmodified lines\
\
351\
352\
353\
354\
355\
354\
355\
356\
357\
358\
359\
\
350 unmodified lines\
\
// TestHasOPFApplied covers the Entire-OPF-Applied trailer reader. The\
// trailer marks a v1 commit whose blobs have been redacted by the\
// OpenAI Privacy Filter (8-layer); commits without it carry 7-layer\
// content and are eligible for the pre-push rewrite to add OPF.\
// OpenAI Privacy Filter (OPF-applied, 9-layer); commits without it carry\
// regex-only (8-layer) content and are eligible for the pre-push rewrite\
// to add OPF.\
func TestHasOPFApplied(t *testing.T) {\
    t.Parallel()\
    cases := []struct {\
```\
\
Mcmd/entire/cli/trailers/trailers\_test.go+3/-2\
\
````\
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\
\
````\
15 unmodified lines\
\
16\
17\
18\
19\
19\
20\
21\
22\
23\
24\
25\
23\
24\
25\
26\
27\
27\
28\
29\
30\
31\
31 unmodified lines\
\
63\
64\
65\
65\
66\
67\
68\
69\
74 unmodified lines\
\
144\
145\
146\
146\
147\
148\
149\
150\
1 unmodified line\
\
152\
153\
154\
154\
155\
156\
157\
158\
3 unmodified lines\
\
162\
163\
164\
164\
165\
166\
165\
166\
167\
168\
169\
170\
170\
171\
171\
172\
173\
174\
175\
26 unmodified lines\
\
202\
203\
204\
204\
205\
206\
207\
205\
206\
207\
208\
209\
210\
211\
11 unmodified lines\
\
223\
224\
225\
225\
226\
227\
228\
229\
\
15 unmodified lines\
\
### What Entire redacts automatically\
\
Entire automatically scans transcript and metadata content before writing it to the `entire/checkpoints/v1` branch. Five always-on secret detection methods run during condensation, plus a conditional sixth pass for user-defined secret rules (see [Customizing redaction](#customizing-redaction) below), an opt-in seventh pass for PII (see [Optional PII redaction](#optional-pii-redaction) below), and an opt-in eighth pass that shells out to the OpenAI Privacy Filter model (see [Optional OpenAI Privacy Filter](#optional-openai-privacy-filter-opf) below):\
Entire automatically scans transcript and metadata content before writing it to the `entire/checkpoints/v1` branch. Six always-on secret detection methods run during condensation, plus a conditional seventh pass for user-defined secret rules (see [Customizing redaction](#customizing-redaction) below), an opt-in eighth pass for PII (see [Optional PII redaction](#optional-pii-redaction) below), and an opt-in ninth pass that shells out to the OpenAI Privacy Filter model (see [Optional OpenAI Privacy Filter](#optional-openai-privacy-filter-opf) below):\
\
1. **Entropy scoring** — Identifies high-entropy strings (Shannon entropy > 4.5) that look like randomly generated secrets, even if they don't match a known pattern.\
2. **Pattern matching** — Uses [Betterleaks](https://github.com/betterleaks/betterleaks) built-in rules to detect known secret formats.\
3. **Credentialed URI detection** — Redacts URLs with embedded passwords, such as `scheme://user:password@host`.\
4. **Database connection-string detection** — Redacts JDBC, Postgres keyword DSN, SQL Server, and ODBC-style connection strings containing passwords.\
5. **Bounded credential value detection** — Redacts password-like config values such as `DB_PASSWORD=...` and `PGPASSWORD=...` while preserving the surrounding key.\
3. **Provider token prefixes** — Deterministically redacts known secret-key prefixes (e.g. Supabase `sb_secret_`, `sbp_`) regardless of entropy or surrounding context.\
4. **Credentialed URI detection** — Redacts URLs with embedded passwords, such as `scheme://user:password@host`.\
5. **Database connection-string detection** — Redacts JDBC, Postgres keyword DSN, SQL Server, and ODBC-style connection strings containing passwords.\
6. **Bounded credential value detection** — Redacts password-like config values such as `DB_PASSWORD=...` and `PGPASSWORD=...` while preserving the surrounding key.\
\
Detected secrets are replaced with `REDACTED` before the data is ever written to a git object. The five secret-detection passes above are **always on** and cannot be disabled. User-defined rules (inline `custom_redactions` and rule packs) add a sixth secret-detection pass that only runs when configured.\
Detected secrets are replaced with `REDACTED` before the data is ever written to a git object. The six secret-detection passes above are **always on** and cannot be disabled. User-defined rules (inline `custom_redactions` and rule packs) add a seventh secret-detection pass that only runs when configured.\
\
### Optional PII redaction\
\
31 unmodified lines\
\
### Optional OpenAI Privacy Filter (`opf`)\
\
A separate, **opt-in** layer that shells out to the [OpenAI Privacy Filter](https://github.com/openai/privacy-filter) (`opf`) — a 1.5B-parameter token-classification model that finds names, emails, phone numbers, addresses, dates, URLs, account numbers, and secrets that pure regex can miss. Disabled by default. Runs *in addition to* the seven built-in layers, **only at push time** — never per-turn and never at commit time. Local commits stay on the fast 7-layer pipeline so per-commit latency is unchanged; OPF only re-redacts checkpoints right before they leave the machine via `git push`.\
A separate, **opt-in** layer that shells out to the [OpenAI Privacy Filter](https://github.com/openai/privacy-filter) (`opf`) — a 1.5B-parameter token-classification model that finds names, emails, phone numbers, addresses, dates, URLs, account numbers, and secrets that pure regex can miss. Disabled by default. Runs *in addition to* the eight built-in layers, **only at push time** — never per-turn and never at commit time. Local commits stay on the fast 8-layer pipeline so per-commit latency is unchanged; OPF only re-redacts checkpoints right before they leave the machine via `git push`.\
\
Prerequisites:\
\
74 unmodified lines\
\
```\
\
- **Yes** runs OPF for this push only.\
- **No** skips OPF for this push only; the 7-layer-redacted content reaches the remote.\
- **No** skips OPF for this push only; the 8-layer-redacted content reaches the remote.\
- **Always** runs OPF this push AND writes `prompt_default: "always"` to `.entire/settings.local.json` so future pushes don't ask.\
- **Ctrl-C** aborts the push entirely — `git push` exits non-zero, no refs go to the remote.\
\
1 unmodified line\
\
**CI consideration**: if you've enabled OPF locally and your CI runs `git push` (e.g. an agent-driven workflow), the CI push will attempt to run OPF too. If the `opf` binary isn't installed in CI, the push will abort with `OPFRuntimeFailedError` rather than silently shipping under-redacted content — by design, since "I enabled OPF" should mean "no content leaves my machines without OPF." The remedies are (a) install `opf` in CI, (b) set `ENTIRE_OPF=no` for CI pushes, or (c) set `prompt_default: "never"` if you only want OPF on interactive pushes.\
\
OPF failures at push time are **fail-closed**: if OPF is not on PATH, fails to start, or times out during the pre-push rewrite, the per-process circuit breaker trips and the rewrite aborts the push with `OPF runtime failed; aborting push`. Nothing reaches the remote. The intent is that "the user enabled OPF" means "I do not want unredacted content leaving this machine" — falling back to 7-layer silently on the push path would violate that contract. Fix the install or set `ENTIRE_OPF=no` for a one-off push.\
OPF failures at push time are **fail-closed**: if OPF is not on PATH, fails to start, or times out during the pre-push rewrite, the per-process circuit breaker trips and the rewrite aborts the push with `OPF runtime failed; aborting push`. Nothing reaches the remote. The intent is that "the user enabled OPF" means "I do not want unredacted content leaving this machine" — falling back to 8-layer silently on the push path would violate that contract. Fix the install or set `ENTIRE_OPF=no` for a one-off push.\
\
(The circuit breaker is per-process, so a broken install costs one warning instead of one timeout per blob — but the push still aborts.)\
\
3 unmodified lines\
\
OPF execution lives in the pre-push hook. The flow:\
\
1. **Post-commit** writes the checkpoint with **7-layer-only** redaction to your local `entire/checkpoints/v1` branch. Fast, predictable, no OPF cost on the hot path.\
2. **Pre-push** (`git push`): if OPF is enabled, the hook re-reads each unpushed `entire/checkpoints/v1` commit, runs the OpenAI Privacy Filter over its blobs to add the categories the regex layers don't catch (person names, addresses, etc.), and builds **new commits** carrying an `Entire-OPF-Applied: true` trailer. The local v1 ref fast-forwards atomically to the new tip, and the (now 8-layer-redacted) commits are what get pushed.\
3. The original 7-layer-only commits become **unreachable** in the local git object database and eventually get swept by `git gc`.\
1. **Post-commit** writes the checkpoint with **8-layer-only** redaction to your local `entire/checkpoints/v1` branch. Fast, predictable, no OPF cost on the hot path.\
2. **Pre-push** (`git push`): if OPF is enabled, the hook re-reads each unpushed `entire/checkpoints/v1` commit, runs the OpenAI Privacy Filter over its blobs to add the categories the regex layers don't catch (person names, addresses, etc.), and builds **new commits** carrying an `Entire-OPF-Applied: true` trailer. The local v1 ref fast-forwards atomically to the new tip, and the (now 9-layer-redacted) commits are what get pushed.\
3. The original 8-layer-only commits become **unreachable** in the local git object database and eventually get swept by `git gc`.\
\
This means:\
\
- **The remote only ever sees 8-layer-redacted content** when OPF is enabled.\
- **Local-only commits are 7-layer-redacted** until the moment you push. If you never push, OPF never runs.\
- **The remote only ever sees 9-layer-redacted content** when OPF is enabled.\
- **Local-only commits are 8-layer-redacted** until the moment you push. If you never push, OPF never runs.\
- **Re-running pre-push is idempotent** — commits already carrying the trailer get re-parented into the chain but are not re-redacted.\
\
#### Force-pushed remote, bootstrap, and concurrent pushes\
26 unmodified lines\
\
| Location | Redaction level | Lifetime | Reaches remote? |\
|---|---|---|---|\
| `.entire/<session>.jsonl` | **None — raw** | Until session is deleted (managed by the agent) | No |\
| Shadow branch `entire/<commit>-<worktree>` | 7-layer | Auto-deleted after the next successful push (only when its session has ended cleanly) | No |\
| Unreachable git objects after pre-push rewrite | 7-layer | Until `git gc --prune` (default `gc.pruneExpire` is 2 weeks) | No |\
| Reflog `git reflog show entire/checkpoints/v1` | 7-layer tips | Default `gc.reflogExpire` is 90 days | No |\
| `<remote>/entire/checkpoints/v1` | 8-layer (after OPF rewrite) | Until you delete the branch on the remote | Yes |\
| Shadow branch `entire/<commit>-<worktree>` | 8-layer | Auto-deleted after the next successful push (only when its session has ended cleanly) | No |\
| Unreachable git objects after pre-push rewrite | 8-layer | Until `git gc --prune` (default `gc.pruneExpire` is 2 weeks) | No |\
| Reflog `git reflog show entire/checkpoints/v1` | 8-layer tips | Default `gc.reflogExpire` is 90 days | No |\
| `<remote>/entire/checkpoints/v1` | 9-layer (after OPF rewrite) | Until you delete the branch on the remote | Yes |\
\
The `.entire/<session>.jsonl` files are raw working state owned by the agent (Claude Code, etc.) — Entire reads from them but does not redact them in place, because the agent is reading and writing them continuously and editing under the agent's feet would corrupt the session.\
\
11 unmodified lines\
\
```fish\
# After enabling OPF, run an agent turn that includes a name in the prompt,\
# e.g. "Create notes.txt with: Alice Johnson reviewed the proposal."\
# Commit (this stays on the fast 7-layer pipeline), then push. OPF runs\
# Commit (this stays on the fast 8-layer pipeline), then push. OPF runs\
# during the pre-push step:\
git commit -m "demo"\
git push   # → "→ OpenAI Privacy Filter: scanning N checkpoints (~30s)…"\
````\
\
Mdocs/security-and-privacy.md+19/-18\
\
```\
100 unmodified lines\
\
101\
102\
103\
104\
105\
106\
107\
108\
109\
110\
111\
112\
\
100 unmodified lines\
\
    testutil.Git(t, work, "update-ref", "refs/heads/entire/checkpoints/v1", k2)\
    testutil.Git(t, work, "remote", "add", "origin", originBare)\
\
    // The remote already carries the v1 branch (seeded above). In a real repo\
    // that means we hold a remote-tracking ref for it, so record one here: the\
    // first-user-branch guard treats a remote with tracking refs as established\
    // (non-empty) and runs the sync instead of deferring the push.\
    testutil.Git(t, work, "update-ref", "refs/remotes/origin/entire/checkpoints/v1", r1)\
\
    // Drive the real pre-push hook: non-ff vs the remote forces the sync/rebase\
    // path that reads the alternate-resident checkpoint commits via go-git.\
    cmd := exec.Command(entire.BinPath(), "hooks", "git", "pre-push", "origin")\
```\
\
Me2e/tests/alternates\_test.go+6\
\
```\
4 unmodified lines\
\
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\
24 unmodified lines\
\
66\
67\
68\
69\
70\
71\
72\
73\
74\
75\
76\
77\
78\
79\
80\
81\
82\
83\
84\
85\
86\
\
4 unmodified lines\
\
    "encoding/json"\
    "errors"\
    "fmt"\
    "io"\
    "os"\
    "path/filepath"\
    "runtime"\
    "sync"\
    "time"\
\
    "github.com/gofrs/flock"\
)\
\
// goosWindows is runtime.GOOS on Windows, where unix permission bits don't\
// exist (Go reports synthetic modes) so permission checks are skipped.\
const goosWindows = "windows"\
\
// loosePermsWarnW receives the loose-permissions warning. Package-level so\
// tests can capture it; production always writes to stderr (matching the\
// unlock warning in withFileLock).\
var loosePermsWarnW io.Writer = os.Stderr\
\
// fileStore persists credentials as a JSON file on disk.\
// The file format is: { "service": { "user": "password" } }\
type fileStore struct {\
    path string\
    mu   sync.Mutex\
    // warnedLoosePerms dedupes the loose-permissions warning to once per\
    // store instance — effectively once per CLI invocation, since\
    // currentBackend caches a single fileStore for the process. Like the\
    // rest of the store's state it relies on mu, which every production\
    // caller of load (Get/Set/Delete) holds; tests that call load directly\
    // are single-goroutine.\
    warnedLoosePerms bool\
}\
\
// withFileLock runs fn while holding an exclusive flock on f.path + ".lock".\
24 unmodified lines\
\
}\
\
func (f *fileStore) load() (map[string]map[string]string, error) {\
    // The file holds bearer tokens; warn (once per store) when it is\
    // readable or writable by group/others. Deliberately a warning, not a\
    // refusal: externally provisioned files (CI secret mounts, read-only\
    // volumes) often carry modes the user cannot change, a hard refusal\
    // would also block the login rewrite that restores 0600, and diagnostic\
    // commands must keep working so the user can see their auth state.\
    // Files written by save() are always 0600, so this only fires on files\
    // created or chmod-ed outside this store. Windows has no unix permission\
    // bits — Go reports synthetic modes there — so the check is unix-only.\
    if runtime.GOOS != goosWindows && !f.warnedLoosePerms {\
        if info, statErr := os.Stat(f.path); statErr == nil && info.Mode().Perm()&0o077 != 0 {\
            f.warnedLoosePerms = true\
            fmt.Fprintf(loosePermsWarnW, "Warning: token store %s is accessible by group/others (mode %04o) and holds bearer tokens; run: chmod 0600 %s\n", f.path, info.Mode().Perm(), f.path)\
        }\
    }\
    data, err := os.ReadFile(f.path)\
    if err != nil {\
        if os.IsNotExist(err) {\
```\
\
Minternal/entireclient/tokenstore/file.go+33\
\
```\
3 unmodified lines\
\
4\
5\
6\
7\
8\
9\
10\
11\
227 unmodified lines\
\
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\
\
3 unmodified lines\
\
    "errors"\
    "os"\
    "path/filepath"\
    "runtime"\
    "strings"\
    "testing"\
)\
\
227 unmodified lines\
\
        t.Fatalf("file permissions = %o, want 0600", perm)\
    }\
}\
\
// looseStoreFile creates a store file and then chmods it explicitly —\
// os.WriteFile's mode is masked by the process umask (a hardened umask like\
// 077 would silently produce 0600), while chmod is not.\
func looseStoreFile(t *testing.T, s *fileStore, perm os.FileMode) {\
    t.Helper()\
    if err := os.WriteFile(s.path, []byte(`{"svc":{"alice":"tokval"}}`), 0o600); err != nil {\
        t.Fatal(err)\
    }\
    if err := os.Chmod(s.path, perm); err != nil {\
        t.Fatal(err)\
    }\
}\
\
// captureLoosePermsWarnings redirects the loose-permissions warning writer\
// to a buffer for the duration of the test. Tests using it must not be\
// parallel (package-global writer).\
func captureLoosePermsWarnings(t *testing.T) *strings.Builder {\
    t.Helper()\
    var buf strings.Builder\
    prev := loosePermsWarnW\
    loosePermsWarnW = &buf\
    t.Cleanup(func() { loosePermsWarnW = prev })\
    return &buf\
}\
\
// The store file holds bearer tokens: a group/other-accessible file draws a\
// warning naming the file and the chmod remediation, but operations still\
// work. Deliberately not a refusal — externally provisioned files (CI secret\
// mounts, read-only volumes) can carry modes the user cannot change, and a\
// hard failure would also block the login rewrite that restores 0600.\
func TestFileStore_WarnsOnLoosePermissionsButWorks(t *testing.T) {\
    if runtime.GOOS == goosWindows {\
        t.Skip("unix permission semantics")\
    }\
    for name, perm := range map[string]os.FileMode{\
        "group-readable": 0o640,\
        "world-readable": 0o604,\
        "group-writable": 0o620,\
    } {\
        t.Run(name, func(t *testing.T) {\
            s := newTestStore(t)\
            looseStoreFile(t, s, perm)\
            warnings := captureLoosePermsWarnings(t)\
\
            got, err := s.Get("svc", "alice")\
            if err != nil {\
                t.Fatalf("Get on a %s file must still work, got error: %v", name, err)\
            }\
            if got != "tokval" {\
                t.Fatalf("Get = %q, want %q", got, "tokval")\
            }\
            warned := warnings.String()\
            if !strings.Contains(warned, "chmod 0600") || !strings.Contains(warned, s.path) {\
                t.Fatalf("warning should name the file and the chmod remediation, got: %q", warned)\
            }\
        })\
    }\
}\
\
// Set must also warn on (and still repair) a loose file: login's rewrite is\
// exactly how a loose store gets restored to 0600.\
func TestFileStore_SetOnLooseFileWarnsAndRestores0600(t *testing.T) {\
    if runtime.GOOS == goosWindows {\
        t.Skip("unix permission semantics")\
    }\
    s := newTestStore(t)\
    looseStoreFile(t, s, 0o644)\
    warnings := captureLoosePermsWarnings(t)\
\
    if err := s.Set("svc", "alice", "fresh"); err != nil {\
        t.Fatalf("Set on a loose file must still work, got: %v", err)\
    }\
    if !strings.Contains(warnings.String(), "chmod 0600") {\
        t.Fatalf("Set should emit the loose-permissions warning, got: %q", warnings.String())\
    }\
    info, err := os.Stat(s.path)\
    if err != nil {\
        t.Fatal(err)\
    }\
    if perm := info.Mode().Perm(); perm != 0o600 {\
        t.Fatalf("save must restore 0600 on rewrite, got %04o", perm)\
    }\
}\
\
// The warning is emitted once per store instance, not once per operation.\
func TestFileStore_LoosePermissionWarningIsDeduped(t *testing.T) {\
    if runtime.GOOS == goosWindows {\
        t.Skip("unix permission semantics")\
    }\
    s := newTestStore(t)\
    looseStoreFile(t, s, 0o640)\
    warnings := captureLoosePermsWarnings(t)\
\
    for range 3 {\
        if _, err := s.Get("svc", "alice"); err != nil {\
            t.Fatal(err)\
        }\
    }\
    if n := strings.Count(warnings.String(), "chmod 0600"); n != 1 {\
        t.Fatalf("warning should be emitted once, got %d occurrences:\n%s", n, warnings.String())\
    }\
}\
\
// A correctly-permissioned file draws no warning.\
func TestFileStore_Reads0600FileWithoutWarning(t *testing.T) {\
    s := newTestStore(t)\
    if err := os.WriteFile(s.path, []byte(`{"svc":{"alice":"tokval"}}`), 0o600); err != nil {\
        t.Fatal(err)\
    }\
    if err := os.Chmod(s.path, 0o600); err != nil {\
        t.Fatal(err)\
    }\
    warnings := captureLoosePermsWarnings(t)\
    got, err := s.Get("svc", "alice")\
    if err != nil {\
        t.Fatalf("Get on a 0600 file should succeed, got: %v", err)\
    }\
    if got != "tokval" {\
        t.Fatalf("Get = %q, want %q", got, "tokval")\
    }\
    if warnings.Len() != 0 {\
        t.Fatalf("no warning expected for a 0600 file, got: %q", warnings.String())\
    }\
}\
\
// BackendDescription pins: user-facing provenance wording must track the env\
// the way resolveBackendLocked does. Not parallel: t.Setenv.\
func TestBackendDescription_Keyring(t *testing.T) {\
    t.Setenv(BackendEnvVar, "")\
    got := BackendDescription()\
    if got != keyringProviderName() {\
        t.Fatalf("BackendDescription() = %q, want the per-OS keyring name %q", got, keyringProviderName())\
    }\
    if strings.HasPrefix(got, "file ") {\
        t.Fatalf("BackendDescription() = %q, must not claim the file backend when env is unset", got)\
    }\
}\
\
func TestBackendDescription_FileWithExplicitPath(t *testing.T) {\
    t.Setenv(BackendEnvVar, "file")\
    t.Setenv(PathEnvVar, "/ci/secrets/tokens.json")\
    if got := BackendDescription(); got != "file /ci/secrets/tokens.json" {\
        t.Fatalf("BackendDescription() = %q, want %q", got, "file /ci/secrets/tokens.json")\
    }\
}\
\
// The default file location is tokens.json in the per-user config dir — this\
// is production routing (resolveBackendLocked uses the same helper), so a\
// typo'd default would relocate real users' token files.\
func TestFileBackendPath_DefaultsToConfigDirTokensJSON(t *testing.T) {\
    cfgDir := t.TempDir()\
    t.Setenv(PathEnvVar, "")\
    t.Setenv("ENTIRE_CONFIG_DIR", cfgDir)\
    want := filepath.Join(cfgDir, "tokens.json")\
    if got := FileBackendPath(); got != want {\
        t.Fatalf("FileBackendPath() = %q, want %q", got, want)\
    }\
}\
\
// The warning's production destination is stderr. Pinned because every other\
// warning test swaps the writer via captureLoosePermsWarnings — without this,\
// changing the default to io.Discard would silently delete the feature in\
// production while the whole suite stays green (verified by mutation).\
// Not parallel: reads the package-global writer that other tests swap.\
func TestLoosePermsWarnWriter_DefaultsToStderr(t *testing.T) {\
    if loosePermsWarnW != os.Stderr {\
        t.Fatalf("loosePermsWarnW default = %T, want os.Stderr", loosePermsWarnW)\
    }\
}\
```\
\
Minternal/entireclient/tokenstore/file\_test.go+172\
\
```\
116 unmodified lines\
\
117\
118\
119\
120\
120\
121\
122\
123\
\
116 unmodified lines\
\
    switch runtime.GOOS {\
    case "darwin":\
        return "macOS Keychain"\
    case "windows":\
    case goosWindows:\
        return "Windows Credential Manager"\
    case "linux", "freebsd", "openbsd", "netbsd", "dragonfly":\
        return "Secret Service (D-Bus)"\
```\
\
Minternal/entireclient/tokenstore/keyring\_timeout.go+1/-1\
\
```\
99 unmodified lines\
\
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\
104\
105\
106\
107\
108\
109\
143\
144\
145\
146\
147\
\
99 unmodified lines\
\
    return backend\
}\
\
// BackendEnvVar selects the credential backend: set to "file" to use the\
// JSON file store instead of the OS keyring. PathEnvVar overrides where the\
// file store lives (default: tokens.json in the per-user config directory).\
// Exported so user-facing guidance (e.g. login's headless hint) names the\
// same variables this package actually reads.\
const (\
    BackendEnvVar = "ENTIRE_TOKEN_STORE"\
    PathEnvVar    = "ENTIRE_TOKEN_STORE_PATH"\
)\
\
// FileBackendSelected reports whether the environment selects the file\
// backend — the single predicate shared by backend resolution, provenance\
// wording, and login's headless hint, so they can never disagree.\
func FileBackendSelected() bool {\
    return os.Getenv(BackendEnvVar) == "file"\
}\
\
// BackendDescription names the credential backend the current environment\
// resolves to, for user-facing provenance lines (e.g. `entire auth status`).\
// It mirrors resolveBackendLocked's env semantics — the production resolution\
// — rather than introspecting the live backend, so test-only overrides don't\
// leak into user-facing wording.\
func BackendDescription() string {\
    if FileBackendSelected() {\
        return "file " + FileBackendPath()\
    }\
    return keyringProviderName()\
}\
\
// FileBackendPath resolves where the file backend stores (or would store)\
// tokens: PathEnvVar when set, else tokens.json in the per-user config\
// directory. Exported so user-facing guidance can name the concrete path.\
func FileBackendPath() string {\
    if path := os.Getenv(PathEnvVar); path != "" {\
        return path\
    }\
    return filepath.Join(userdirs.Config(), "tokens.json")\
}\
\
func resolveBackendLocked() store {\
    if os.Getenv("ENTIRE_TOKEN_STORE") == "file" {\
        path := os.Getenv("ENTIRE_TOKEN_STORE_PATH")\
        if path == "" {\
            path = filepath.Join(userdirs.Config(), "tokens.json")\
        }\
        return &fileStore{path: path}\
    if FileBackendSelected() {\
        return &fileStore{path: FileBackendPath()}\
    }\
    // Under `go test`, never fall through to the real OS keyring: a test\
    // that forgets tokenstore.UseFileBackendForTesting would otherwise write\
```\
\
Minternal/entireclient/tokenstore/tokenstore.go+41/-6\
\
```\
27 unmodified lines\
\
28\
29\
30\
31\
32\
33\
34\
31\
32\
33\
34\
35\
36\
37\
37\
38\
39\
40\
41\
2 unmodified lines\
\
44\
45\
46\
46\
47\
48\
49\
50\
50\
51\
52\
53\
54\
60 unmodified lines\
\
115\
116\
117\
117\
118\
119\
120\
121\
8 unmodified lines\
\
130\
131\
132\
132\
133\
134\
135\
136\
9 unmodified lines\
\
146\
147\
148\
148\
149\
150\
151\
151\
152\
153\
154\
155\
\
27 unmodified lines\
\
// returns a non-nil error. Callers running this for privacy-critical\
// operations (e.g. the pre-push rewrite) must abort rather than\
// proceed with partially-redacted content. The per-blob\
// JSONLContentWithPrivacyFilter falls back to 7-layer on batch\
// failure; this batched variant intentionally does not, because the\
// only caller (cross-blob walker) needs an explicit signal that OPF\
// did not finish.\
// JSONLContentWithPrivacyFilter falls back to the regex-only pipeline\
// (the eight always-on/opt-in layers, no OPF) on batch failure; this\
// batched variant intentionally does not, because the only caller\
// (cross-blob walker) needs an explicit signal that OPF did not\
// finish.\
//\
// When OPF is unconfigured, disabled, has no enabled categories, or\
// the per-process circuit breaker has tripped, returns 7-layer-only\
// the per-process circuit breaker has tripped, returns regex-only\
// output for every blob with no error. This matches the existing\
// non-batched paths and keeps the caller's hot-path code clean.\
func BatchBytesWithPrivacyFilter(ctx context.Context, inputs []NamedBlob) ([][]byte, error) {\
2 unmodified lines\
\
    }\
    cfg := getOPFConfig()\
    if cfg == nil || !cfg.Enabled || cfg.runtime == nil || opfBreakerTripped.Load() {\
        return apply7LayerToBlobs(inputs), nil\
        return applyRegexLayersToBlobs(inputs), nil\
    }\
    cats := enabledCategories(cfg)\
    if len(cats) == 0 {\
        return apply7LayerToBlobs(inputs), nil\
        return applyRegexLayersToBlobs(inputs), nil\
    }\
\
    // Pass 1: collect unique prose-shaped leaves across every blob.\
60 unmodified lines\
\
//\
// JSON parse failures fall back to whole-content treatment, matching\
// RedactBlobBytes's behavior: a malformed JSON blob still gets the\
// 7-layer pipeline applied, just without leaf-by-leaf precision.\
// regex-only pipeline applied, just without leaf-by-leaf precision.\
func collectLeaves(in NamedBlob, add func(string)) {\
    if isJSONLikeName(in.Name) {\
        if _, err := jsonlContentImpl(string(in.Content), func(v string) string {\
8 unmodified lines\
\
}\
\
// applyToBlob produces the redacted bytes for a single blob, combining\
// the 7 regex layers with the cached OPF spans for each leaf. The\
// the always-on/opt-in regex layers with the cached OPF spans for each leaf. The\
// per-leaf closure mirrors JSONLContentWithPrivacyFilter's Pass 3.\
func applyToBlob(in NamedBlob, spansByInput map[string][]Span, cfg *OPFConfig) []byte {\
    applier := func(v string) string {\
9 unmodified lines\
\
    return []byte(applier(string(in.Content)))\
}\
\
// apply7LayerToBlobs is the OPF-disabled fast path: each blob gets\
// applyRegexLayersToBlobs is the OPF-disabled fast path: each blob gets\
// regex-only redaction with no shell-out. Returned slice is index-aligned\
// with inputs.\
func apply7LayerToBlobs(inputs []NamedBlob) [][]byte {\
func applyRegexLayersToBlobs(inputs []NamedBlob) [][]byte {\
    out := make([][]byte, len(inputs))\
    for i, in := range inputs {\
        if isJSONLikeName(in.Name) {\
```\
\
Mredact/batch.go+12/-11\
\
```\
201 unmodified lines\
\
202\
203\
204\
205\
205\
206\
207\
208\
68 unmodified lines\
\
277\
278\
279\
280\
280\
281\
282\
283\
284\
285\
284\
285\
286\
287\
288\
6 unmodified lines\
\
295\
296\
297\
298\
298\
299\
300\
301\
302\
302\
303\
304\
305\
306\
307\
307\
308\
309\
310\
\
201 unmodified lines\
\
// TestBatchBytesWithPrivacyFilter_FailsClosedOnBatchError is the\
// fail-closed contract: when the OPF runtime errors, callers must see\
// the error rather than silently get 7-layer-only output tagged as if\
// the error rather than silently get regex-only output tagged as if\
// OPF ran. This is the privacy-critical difference vs\
// JSONLContentWithPrivacyFilter (which silently falls back).\
func TestBatchBytesWithPrivacyFilter_FailsClosedOnBatchError(t *testing.T) {\
68 unmodified lines\
\
    }\
}\
\
// TestBatchBytesWithPrivacyFilter_OPFDisabledReturns7Layer covers the\
// TestBatchBytesWithPrivacyFilter_OPFDisabledReturnsRegexOnly covers the\
// "OPF turned off in settings" path: every blob gets regex-only\
// redaction, no shell-out happens, no error. Without this, a user with\
// OPF disabled would get a hard error from the new API instead of the\
// fast 7-layer path they expect.\
func TestBatchBytesWithPrivacyFilter_OPFDisabledReturns7Layer(t *testing.T) {\
// fast regex-only path they expect.\
func TestBatchBytesWithPrivacyFilter_OPFDisabledReturnsRegexOnly(t *testing.T) {\
    resetOPFConfig()\
    t.Cleanup(resetOPFConfig)\
    // No ConfigurePrivacyFilter call → cfg == nil\
6 unmodified lines\
\
        t.Fatalf("OPF-disabled path should not error: %v", err)\
    }\
    if !strings.Contains(string(got[0]), "REDACTED") {\
        t.Errorf("7-layer fallback should still redact AWS key, got %q", string(got[0]))\
        t.Errorf("regex-only fallback should still redact AWS key, got %q", string(got[0]))\
    }\
}\
\
// TestBatchBytesWithPrivacyFilter_BreakerTrippedReturns7Layer ensures\
// TestBatchBytesWithPrivacyFilter_BreakerTrippedReturnsRegexOnly ensures\
// that once the circuit breaker has tripped (e.g. an earlier batch\
// failed and the strategy aborted), subsequent calls in the same\
// process don't pay another shell-out cost. They short-circuit to\
// regex-only with no error.\
func TestBatchBytesWithPrivacyFilter_BreakerTrippedReturns7Layer(t *testing.T) {\
func TestBatchBytesWithPrivacyFilter_BreakerTrippedReturnsRegexOnly(t *testing.T) {\
    fake := &fakeRuntime{spans: []Span{{Start: 0, End: 5, Label: "private_person"}}}\
    configureFakeOPF(t, fake, map[string]bool{"private_person": true})\
    opfBreakerTripped.Store(true)\
```\
\
Mredact/batch\_test.go+7/-7\
\
```\
87 unmodified lines\
\
88\
89\
90\
91\
91\
92\
93\
94\
6 unmodified lines\
\
101\
102\
103\
104\
104\
105\
106\
106\
107\
108\
109\
75 unmodified lines\
\
185\
186\
187\
188\
188\
189\
190\
191\
\
87 unmodified lines\
\
// OPFEnabled reports whether the OpenAI Privacy Filter is configured\
// and turned on for this process. Callers gate pre-push rewrite work\
// on this: when false, the pre-push hook pushes the local 7-layer\
// on this: when false, the pre-push hook pushes the local regex-only\
// checkpoint branch verbatim with no extra processing. Independent of\
// the circuit breaker — a tripped breaker still reports Enabled=true\
// because the runtime config didn't change; the rewrite logic itself\
6 unmodified lines\
\
// OPFBreakerTripped reports whether the per-process OPF circuit breaker\
// has been tripped — i.e. an OPF invocation failed at some point during\
// this process's lifetime. The pre-push rewrite uses this to detect\
// when OPF silently fell back to 7-layer mid-rewrite and abort before\
// when OPF silently fell back to regex-only mid-rewrite and abort before\
// CAS-ing the new ref; otherwise the rewritten commits would carry the\
// Entire-OPF-Applied: true trailer despite containing only 7-layer\
// Entire-OPF-Applied: true trailer despite containing only regex-only\
// content, and the next push would skip them.\
func OPFBreakerTripped() bool {\
    return opfBreakerTripped.Load()\
75 unmodified lines\
\
// in the pre-push rewrite path (strategy/manual_commit_opf_rewrite.go),\
// whose hook is installed without a `2>/dev/null` redirect, so plain\
// stderr reaches the user's terminal during `git push`. Post-commit\
// condensation never invokes OPF (it calls the 7-layer functions\
// condensation never invokes OPF (it calls the regex-layer functions\
// directly via RedactBlobBytes(..., usePrivacyFilter=false)), so the\
// historical `/dev/tty` routing that survived the post-commit hook's\
// stderr redirect is no longer needed. Tests override this directly.\
```\
\
Mredact/opf.go+4/-4\
\
```\
607 unmodified lines\
\
608\
609\
610\
611\
611\
612\
613\
614\
11 unmodified lines\
\
626\
627\
628\
629\
629\
630\
631\
632\
\
607 unmodified lines\
\
// TestJSONLContentWithPrivacyFilter_ShortReturnTripsBreaker pins the\
// privacy contract: if the OPF runtime returns fewer span slices than\
// inputs, we treat it as a runtime failure (trip the breaker + 7-layer\
// inputs, we treat it as a runtime failure (trip the breaker + regex-only\
// fallback) rather than silently produce under-redacted output. The\
// per-blob caller in the pre-push rewrite then catches the tripped\
// breaker via OPFBreakerTripped() and aborts before CAS.\
11 unmodified lines\
\
    content := `{"a":"Alice met Bob","b":"Charlie sat down","c":"Eve walked home"}`\
    _, err := JSONLContentWithPrivacyFilter(context.Background(), content)\
    if err != nil {\
        t.Fatalf("short return should fall back to 7-layer (no error), got %v", err)\
        t.Fatalf("short return should fall back to regex-only (no error), got %v", err)\
    }\
    if !opfBreakerTripped.Load() {\
        t.Error("short return must trip the OPF breaker so the rewrite's post-loop check aborts the push")\
```\
\
Mredact/opf\_test.go+2/-2\
\
```\
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\
\
package redact\
\
import "regexp"\
\
// Provider-specific deterministic secret patterns.\
//\
// Detection here is purely prefix + length based: it never depends on\
// entropy or the surrounding key name, so it catches low-entropy\
// credential formats the other secret layers don't reliably flag.\
//\
// The betterleaks layer's coverage of these differs per prefix (verified\
// against the vendored betterleaks v1.5.0 rule source):\
//   - sb_secret_: the supabase-project-api-key rule is a *composite* rule\
//     (RequiredRules: supabase-project-url) that only fires when a matching\
//     "*.supabase.co" URL is present in the same content, on top of an\
//     entropy<=4.0 filter. A secret captured on its own therefore passes\
//     straight through regardless of entropy.\
//   - sbp_: the supabase-management-token rule fires standalone (no\
//     RequiredRules), but only matches an exact 40-character lowercase body\
//     and is further filtered by entropy<=3.5 and a two-digit minimum. A\
//     high-entropy 40-char sbp_ token captured alone IS caught by\
//     betterleaks; what this layer adds for sbp_ is coverage of bodies at\
//     other lengths, lower entropy, or without two digits.\
//\
// Supabase (https://supabase.com/docs/guides/getting-started/api-keys):\
//   - sb_secret_...      secret API key (replaces the legacy service_role\
//     key; bypasses row-level security, server-side\
//     only) — SENSITIVE, always redacted.\
//   - sbp_...            personal access token used by the Supabase CLI and\
//     Management API — SENSITIVE, always redacted.\
//   - sb_publishable_... publishable key (replaces the legacy anon key). It\
//     is designed to be embedded in client-side code and\
//     is protected by row-level security, so it is NOT a\
//     secret and is intentionally NOT redacted here.\
//     Redacting it would be false-positive noise; this\
//     matches betterleaks, which also ships no\
//     publishable-key rule.\
//\
// The current real key bodies are 31 chars (sb_secret_) and 40 chars\
// (sbp_); charsets mirror betterleaks (sb_secret_ keys are mixed-case\
// base64url, sbp_ tokens are lowercase). The {20,} floor comfortably\
// catches the current and plausibly-longer future formats while rejecting\
// short identifier-like collisions such as "sb_secret_short".\
//\
// Known false-positive class: because the body charset includes `_` and\
// the {20,} length check is open-ended, sufficiently long snake_case\
// identifiers that merely start with a provider prefix are redacted even\
// though they aren't secrets — e.g. `sb_secret_key_rotation_handler`, or\
// mid-word inside a longer identifier like\
// `libsbp_something_long_enough_value`. This is\
// accepted: over-redaction is the safe direction here (see\
// TestString_SupabaseProviderTokenLongIdentifierOverRedaction), and adding\
// anchors or capping the body length to eliminate it would reopen the\
// low-entropy under-redaction gap below.\
//\
// The prefix is deliberately NOT preceded by a \b word boundary. \b requires\
// the character before the prefix to be a non-word char, so a secret glued to\
// a preceding word character — an underscore-joined name (FOO_sb_secret_…) or,\
// in the JSONL fall-back / raw redact.Bytes path, a JSON escape whose trailing\
// letter abuts the prefix (…line1\nsb_secret_…, where the byte before "sb" is\
// the literal 'n') — would slip past. Because these low-entropy secrets are\
// backed up by no other layer, missing them means the raw key reaches the\
// checkpoint blob. Dropping the anchor is redaction-completeness-safe: any\
// high-entropy incidental match would already be caught by the entropy layer,\
// and a mid-word identifier collision (documented above) only ever\
// over-redacts, never under-redacts.\
var providerTokenPatterns = []*regexp.Regexp{\
    regexp.MustCompile(`sb_secret_[A-Za-z0-9_-]{20,}`),\
    regexp.MustCompile(`sbp_[a-z0-9_-]{20,}`),\
}\
\
// detectProviderTokens returns tagged regions for every occurrence of a\
// known provider secret-token prefix in s. Regions use the empty label so\
// they render as the bare "REDACTED" token, consistent with the other\
// always-on secret layers.\
func detectProviderTokens(s string) []taggedRegion {\
    var regions []taggedRegion\
    for _, pat := range providerTokenPatterns {\
        for _, loc := range pat.FindAllStringIndex(s, -1) {\
            regions = append(regions, taggedRegion{region: region{loc[0], loc[1]}})\
        }\
    }\
    return regions\
}\
```\
\
Aredact/providers.go+84\
\
```\
156 unmodified lines\
\
157\
158\
159\
160\
161\
162\
163\
164\
165\
166\
160\
161\
162\
163\
164\
165\
166\
167\
168\
169\
170\
171\
172\
173\
174\
172\
175\
176\
174\
175\
176\
177\
178\
179\
180\
181\
182\
41 unmodified lines\
\
224\
225\
226\
224\
227\
228\
229\
230\
231\
232\
233\
234\
235\
236\
229\
237\
238\
239\
232\
240\
241\
242\
235\
243\
244\
245\
238\
246\
247\
248\
249\
374 unmodified lines\
\
624\
625\
626\
619\
627\
628\
629\
630\
\
156 unmodified lines\
\
}\
\
// String replaces secrets and PII in s using layered detection:\
// 1. Entropy-based: high-entropy alphanumeric sequences (threshold 4.5)\
// 2. Pattern-based: betterleaks regex rules (260+ known secret formats)\
// 3. Credentialed URIs: URLs containing userinfo passwords\
// 4. Database connection strings: JDBC, keyword DSNs, and semicolon strings\
// 5. User-defined custom rules: configured via ConfigureCustomRules\
// 6. Bounded credential key/value pairs: DB_PASSWORD=...\
// 7. PII detection: email, phone, address patterns (only when configured via ConfigurePII)\
//  1. Entropy-based: high-entropy alphanumeric sequences (threshold 4.5)\
//  2. Pattern-based: betterleaks regex rules (260+ known secret formats)\
//  3. Provider token prefixes: deterministic prefix rules for credential\
//     formats betterleaks misses in isolation (e.g. Supabase sb_secret_)\
//  4. Credentialed URIs: URLs containing userinfo passwords\
//  5. Database connection strings: JDBC, keyword DSNs, and semicolon strings\
//  6. User-defined custom rules: configured via ConfigureCustomRules\
//  7. Bounded credential key/value pairs: DB_PASSWORD=...\
//  8. PII detection: email, phone, address patterns (only when configured via ConfigurePII)\
//\
// A string is redacted if ANY method flags it.\
func String(s string) string {\
    return applyRegions(s, detectAllLayers(s))\
}\
\
// detectAllLayers runs the seven always-on/opt-in regex-based redaction\
// detectAllLayers runs the eight always-on/opt-in regex-based redaction\
// layers and returns their tagged regions. The OpenAI Privacy Filter\
// (layer 8) is NOT included — callers that want it append detectOPF spans\
// to the result before passing to applyRegions. See StringWithPrivacyFilter\
// for the augmented flow.\
// (the final, network-backed layer) is NOT included — callers that want it\
// append detectOPF spans to the result before passing to applyRegions. See\
// StringWithPrivacyFilter for the augmented flow.\
func detectAllLayers(s string) []taggedRegion {\
    var regions []taggedRegion\
\
41 unmodified lines\
\
        }\
    }\
\
    // 3. Credentialed URIs (secrets — always on).\
    // 3. Provider-specific deterministic token prefixes (secrets — always on).\
    // Catches low-entropy credential formats (e.g. Supabase sb_secret_) that\
    // the entropy and betterleaks layers miss when captured in isolation.\
    regions = append(regions, detectProviderTokens(s)...)\
\
    // 4. Credentialed URIs (secrets — always on).\
    for _, loc := range credentialedURIPattern.FindAllStringIndex(s, -1) {\
        regions = append(regions, taggedRegion{region: region{loc[0], loc[1]}})\
    }\
\
    // 4. Database and connection-string detection (secrets — always on).\
    // 5. Database and connection-string detection (secrets — always on).\
    regions = append(regions, detectConnectionStrings(s)...)\
\
    // 5. User-defined custom rules (secrets — only runs when configured).\
    // 6. User-defined custom rules (secrets — only runs when configured).\
    regions = append(regions, detectCustomRules(getCustomRulesConfig(), s)...)\
\
    // 6. Bounded credential key/value detection (secrets — always on).\
    // 7. Bounded credential key/value detection (secrets — always on).\
    regions = append(regions, detectCredentialValues(s)...)\
\
    // 7. PII detection (opt-in — only runs when configured).\
    // 8. PII detection (opt-in — only runs when configured).\
    regions = append(regions, detectPII(getPIIConfig(), s)...)\
\
    return regions\
374 unmodified lines\
\
// JSONLContentWithPrivacyFilter augments JSONLContent with the OpenAI\
// Privacy Filter via batched inference. Walks the content twice: pass 1\
// collects unique prose-shaped leaves into a single RedactBatch call;\
// pass 2 applies the seven regex layers per leaf plus the cached OPF spans\
// pass 2 applies the eight regex layers per leaf plus the cached OPF spans\
// for that leaf. One OPF shell-out covers the whole transcript instead of\
// one per leaf — without batching, a typical 500-leaf transcript would\
// take many minutes per commit.\
```\
\
Mredact/redact.go+25/-17\
\
```\
333 unmodified lines\
\
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\
469\
470\
471\
472\
473\
474\
475\
476\
477\
478\
479\
480\
481\
482\
483\
484\
485\
486\
487\
488\
489\
490\
491\
492\
493\
494\
495\
496\
497\
498\
499\
500\
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\
557\
558\
559\
560\
561\
562\
563\
564\
565\
566\
567\
568\
569\
570\
571\
572\
573\
574\
575\
576\
577\
578\
579\
580\
581\
582\
583\
584\
585\
586\
587\
588\
589\
590\
591\
592\
593\
594\
595\
596\
597\
598\
599\
600\
601\
602\
603\
604\
605\
606\
607\
608\
609\
610\
611\
612\
613\
614\
615\
616\
617\
618\
619\
620\
621\
622\
623\
624\
625\
626\
627\
628\
629\
630\
631\
632\
633\
634\
635\
636\
637\
638\
\
333 unmodified lines\
\
    }\
}\
\
// supabaseSecretPrefix, supabasePersonalPrefix, and supabasePublishablePrefix\
// assemble the Supabase credential prefixes from fragments so a complete token\
// never appears verbatim in source. This mirrors openSSHPrivateKeyMarker above\
// and keeps secret scanners (including GitHub push protection) from flagging\
// synthetic test fixtures; the assembled runtime values exercise the redactor\
// exactly as a real token would.\
func supabaseSecretPrefix() string      { return "sb" + "_secret_" }\
func supabasePersonalPrefix() string    { return "sb" + "p_" }\
func supabasePublishablePrefix() string { return "sb" + "_publishable_" }\
\
// TestString_SupabaseProviderTokens covers issue #1716: Supabase sb_secret_\
// API keys and sbp_ personal access tokens are low-entropy and, captured in\
// isolation, are missed by the entropy layer (threshold 4.5). betterleaks\
// coverage differs per prefix: its sb_secret_ rule is a composite rule that\
// only fires when a *.supabase.co URL is co-present, so a bare sb_secret_\
// value never reaches its filter at all; its sbp_ rule fires standalone but\
// requires an exact 40-character lowercase body, so bodies of another length\
// (like the probe values below) never match its regex regardless of entropy.\
// The deterministic provider-prefix layer must catch both regardless of\
// entropy, body length, or the surrounding variable name.\
func TestString_SupabaseProviderTokens(t *testing.T) {\
    t.Parallel()\
\
    secret := supabaseSecretPrefix() + "probe_20260710_7f91c2d8e4a6b3f0" // entropy 4.199\
    realSecret := supabaseSecretPrefix() + "9uM4GhB0STF5R4K3HxQtlg_bzWW6DRj"\
    sbpToken := supabasePersonalPrefix() + "test_probe_20260710_test_probe_2026071"\
    // Real Supabase key bodies are base64url, which includes '-'. No other\
    // fixture in this test contains a hyphen, so the charset's '-' member is\
    // otherwise unpinned: narrowing [A-Za-z0-9_-] / [a-z0-9_-] to drop the\
    // hyphen would still pass every other case here while silently truncating\
    // (not merely shrinking) the match at the first hyphen in a real key,\
    // leaking the remainder raw — the #1716 failure mode recurring via an\
    // innocent charset "tidy-up".\
    secretWithHyphen := supabaseSecretPrefix() + "probe-20260710-7f91c2d8e4a6b3f0"\
    sbpTokenWithHyphen := supabasePersonalPrefix() + "probe-20260710-7f91c2d8e4a6b3f0"\
\
    // Both probe values sit below the entropy threshold, proving entropy-only\
    // detection would miss them (the issue reports entropy 4.199 for sb_secret_).\
    for _, low := range []string{secret, sbpToken} {\
        if e := shannonEntropy(low); e > entropyThreshold {\
            t.Fatalf("value %q has entropy %.3f > %.1f; not a low-entropy regression case", low, e, entropyThreshold)\
        }\
    }\
\
    assertStringRedactionCases(t, []stringRedactionCase{\
        {\
            name:  "sb_secret_ standalone (issue #1716 repro value)",\
            input: secret,\
            want:  "REDACTED",\
        },\
        {\
            name:  "sb_secret_ at start of line",\
            input: secret + " is the service_role key",\
            want:  "REDACTED is the service_role key",\
        },\
        {\
            name:  "sb_secret_ at end of line",\
            input: "service_role key: " + secret,\
            want:  "service_role key: REDACTED",\
        },\
        {\
            // Canonical .env form. The chosen token value is low-entropy\
            // (quoting has no effect on secretPattern matching), so the\
            // entropy layer misses it, isolating the deterministic provider\
            // layer.\
            name:  "sb_secret_ in env-style double-quoted assignment",\
            input: `SUPABASE_SERVICE_ROLE_KEY="` + secret + `"`,\
            want:  `SUPABASE_SERVICE_ROLE_KEY="REDACTED"`,\
        },\
        {\
            name:  "sb_secret_ single-quoted value",\
            input: "key: '" + secret + "'",\
            want:  "key: 'REDACTED'",\
        },\
        {\
            name:  "sb_secret_ multiple occurrences",\
            input: secret + " then " + secret,\
            want:  "REDACTED then REDACTED",\
        },\
        {\
            name:  "sb_secret_ real-shaped mixed-case body",\
            input: `SUPABASE_SERVICE_ROLE_KEY="` + realSecret + `"`,\
            want:  `SUPABASE_SERVICE_ROLE_KEY="REDACTED"`,\
        },\
        {\
            name:  "sbp_ personal access token (38-char body, betterleaks' rule requires exactly 40)",\
            input: "SUPABASE_ACCESS_TOKEN=" + sbpToken,\
            want:  "SUPABASE_ACCESS_TOKEN=REDACTED",\
        },\
        {\
            name:  "sb_secret_ body with an early hyphen (real base64url shape)",\
            input: secretWithHyphen,\
            want:  "REDACTED",\
        },\
        {\
            name:  "sbp_ body with an early hyphen (real base64url shape)",\
            input: sbpTokenWithHyphen,\
            want:  "REDACTED",\
        },\
    })\
}\
\
// TestString_SupabaseProviderTokenLengthBoundaries pins the {20,} body-length\
// floor shared by both provider patterns as an explicit boundary rather than\
// an emergent property of an unrelated fixture: a body of exactly 20 chars\
// must redact, and a body of exactly 19 chars must be preserved. Before this\
// test, the floor was pinned only accidentally — via key_rotation_handler\
// (sb_secret_) happening to have a 20-char body, with no equivalent coverage\
// for sbp_ at all. Each case fails if either pattern's minimum is tightened\
// to {21,}.\
func TestString_SupabaseProviderTokenLengthBoundaries(t *testing.T) {\
    t.Parallel()\
\
    const (\
        body20 = "boundary_probe_2026x" // exactly 20 chars\
        body19 = "boundary_probe_2026"  // exactly 19 chars\
    )\
    if len(body20) != 20 || len(body19) != 19 {\
        t.Fatalf("fixture bodies are %d/%d chars, want 20/19", len(body20), len(body19))\
    }\
\
    secret20 := supabaseSecretPrefix() + body20\
    secret19 := supabaseSecretPrefix() + body19\
    sbp20 := supabasePersonalPrefix() + body20\
    sbp19 := supabasePersonalPrefix() + body19\
\
    assertStringRedactionCases(t, []stringRedactionCase{\
        {\
            name:  "sb_secret_ with exactly 20-char body redacts",\
            input: secret20,\
            want:  "REDACTED",\
        },\
        {\
            name:  "sb_secret_ with exactly 19-char body is preserved",\
            input: secret19,\
            want:  secret19,\
        },\
        {\
            name:  "sbp_ with exactly 20-char body redacts",\
            input: sbp20,\
            want:  "REDACTED",\
        },\
        {\
            name:  "sbp_ with exactly 19-char body is preserved",\
            input: sbp19,\
            want:  sbp19,\
        },\
    })\
}\
\
// TestString_SupabaseProviderTokenOverRedactionGuards pins that the\
// deterministic provider layer does not over-redact. Publishable keys are\
// designed to be embedded in client code and are intentionally not targeted by\
// this layer (a low-entropy publishable value therefore passes through it; a\
// high-entropy real one would still be caught by the entropy layer). A bare\
// prefix or a prefix with a too-short body is not a credential.\
func TestString_SupabaseProviderTokenOverRedactionGuards(t *testing.T) {\
    t.Parallel()\
\
    publishable := supabasePublishablePrefix() + "probe_20260710_7f91c2d8e4a6b3f0"\
    shortSecret := supabaseSecretPrefix() + "short"\
    shortToken := supabasePersonalPrefix() + "short"\
\
    assertStringRedactionCases(t, []stringRedactionCase{\
        {\
            // The publishable fixture is low-entropy (quoting has no effect on\
            // secretPattern matching), so the entropy layer does not flag it,\
            // proving the provider layer itself does not target publishable\
            // keys.\
            name:  "publishable key is not targeted by the provider layer",\
            input: `NEXT_PUBLIC_SUPABASE_KEY="` + publishable + `"`,\
            want:  `NEXT_PUBLIC_SUPABASE_KEY="` + publishable + `"`,\
        },\
        {\
            name:  "sb_secret_ with too-short body is preserved",\
            input: shortSecret,\
            want:  shortSecret,\
        },\
        {\
            name:  "sbp_ with too-short body is preserved",\
            input: shortToken,\
            want:  shortToken,\
        },\
        {\
            name:  "bare sb_secret_ prefix in prose is preserved",\
            input: "the " + supabaseSecretPrefix() + " prefix identifies Supabase secret keys",\
            want:  "the " + supabaseSecretPrefix() + " prefix identifies Supabase secret keys",\
        },\
    })\
}\
\
// TestString_SupabaseProviderTokenLongIdentifierOverRedaction documents a\
// known, accepted false-positive class: because the body charset includes\
// underscore and the length check is {20,} with no upper bound, sufficiently\
// long snake_case identifiers that merely start with a provider prefix are\
// redacted even though they are not secrets — including mid-word, since the\
// prefix is deliberately not anchored (see the package comment in\
// providers.go). This is intentional: over-redaction is the safe direction,\
// and reintroducing a \b anchor or a body-length cap to "fix" this would\
// reopen the low-entropy under-redaction gap the provider layer exists to\
// close. This test pins the tradeoff so it isn't silently reversed.\
func TestString_SupabaseProviderTokenLongIdentifierOverRedaction(t *testing.T) {\
    t.Parallel()\
\
    assertStringRedactionCases(t, []stringRedactionCase{\
        {\
            name:  "long snake_case identifier starting with sb_secret_ is over-redacted",\
            input: "func " + supabaseSecretPrefix() + "key_rotation_handler() {}",\
            want:  "func REDACTED() {}",\
        },\
        {\
            name:  "sbp_ mid-word inside a longer identifier is over-redacted",\
            input: "call lib" + supabasePersonalPrefix() + "something_long_enough_value()",\
            want:  "call libREDACTED()",\
        },\
    })\
}\
\
// TestJSONLContent_SupabaseSecretRedacted drives the secret through the\
// field-aware JSONL path used by checkpoint condensation, mirroring a Claude\
// Code transcript line where the secret lives in a message-content leaf.\
func TestJSONLContent_SupabaseSecretRedacted(t *testing.T) {\
    t.Parallel()\
    secret := supabaseSecretPrefix() + "probe_20260710_7f91c2d8e4a6b3f0"\
    line := `{"type":"user","message":{"role":"user","content":"the service_role key is ` + secret + ` now"}}`\
    got, err := JSONLContent(line)\
    if err != nil {\
        t.Fatalf("JSONLContent error: %v", err)\
    }\
    if strings.Contains(got, secret) {\
        t.Fatalf("secret survived JSONL redaction: %q", got)\
    }\
    if !strings.Contains(got, "REDACTED") {\
        t.Fatalf("expected REDACTED placeholder in %q", got)\
    }\
}\
\
// TestString_SupabaseProviderTokenBoundaries pins that the provider layer\
// redacts a Supabase secret even when the prefix abuts a preceding *word*\
// character. A \b anchor before the prefix only fires after a non-word\
// character, so a secret glued to a preceding letter/digit/underscore — an\
// underscore-joined name, or (in the raw redact.Bytes / JSONL fall-back path\
// that runs String on undecoded text) a JSON escape whose trailing letter sits\
// against the prefix, e.g. "…line1\nsb_secret_…" where the byte before "sb" is\
// the literal 'n' — would slip past. These bodies are deliberately low-entropy,\
// so no other layer backs the provider layer up: a miss reaches the blob raw.\
// Each case fails if the leading \b anchor is reintroduced.\
func TestString_SupabaseProviderTokenBoundaries(t *testing.T) {\
    t.Parallel()\
\
    secret := supabaseSecretPrefix() + "probe_20260710_7f91c2d8e4a6b3f0"\
    sbpToken := supabasePersonalPrefix() + "test_probe_20260710_test_probe_2026071"\
\
    assertStringRedactionCases(t, []stringRedactionCase{\
        {\
            name:  "sb_secret_ glued to a preceding word char",\
            input: "x" + secret,\
            want:  "xREDACTED",\
        },\
        {\
            // Raw-text fall-back shape: the transcript line failed to parse as\
            // JSON, so String runs on the undecoded bytes where "\n" is a literal\
            // backslash-n and the 'n' abuts the prefix.\
            name:  "sb_secret_ preceded by a literal JSON escape letter",\
            input: `first line\n` + secret,\
            want:  `first line\nREDACTED`,\
        },\
        {\
            name:  "sbp_ preceded by a literal JSON escape letter",\
            input: `first line\n` + sbpToken,\
            want:  `first line\nREDACTED`,\
        },\
    })\
}\
\
// TestJSONLContent_SupabaseSecretMalformedLineFallback drives the secret\
// through the JSONL fall-back branch (jsonlContentImpl calls the per-leaf\
// redactor on the raw line when json.Unmarshal fails), with the secret glued to\
// a literal "\n" escape so the byte before the prefix is a word char. This is\
// the realistic path by which a malformed/truncated transcript line could leak\
// a low-entropy Supabase secret; it must still be redacted.\
func TestJSONLContent_SupabaseSecretMalformedLineFallback(t *testing.T) {\
    t.Parallel()\
    secret := supabaseSecretPrefix() + "probe_20260710_7f91c2d8e4a6b3f0"\
    // Trailing garbage after the closing brace makes json.Unmarshal fail, forcing\
    // the raw-line fall-back; inside, "\n" is a literal backslash-n before "sb".\
    line := `{"content":"line1\n` + secret + `"} <-- truncated`\
    got, err := JSONLContent(line)\
    if err != nil {\
        t.Fatalf("JSONLContent error: %v", err)\
    }\
    if strings.Contains(got, secret) {\
        t.Fatalf("secret survived JSONL fall-back redaction: %q", got)\
    }\
    if !strings.Contains(got, "REDACTED") {\
        t.Fatalf("expected REDACTED placeholder in %q", got)\
    }\
}\
\
func TestString_CredentialedURIs(t *testing.T) {\
    tests := []struct {\
        name  string\
```\
\
Mredact/redact\_test.go+299