Merge remote-tracking branch 'github/main' into fix/agent-help-trails-availability · Entire

Home

Log in

Merge remote-tracking branch 'github/main' into fix/agent-help-trails-availability

aadd7a4→main·

dipree·yesterday·63 files·+4,263 added/-360 removed

# Conflicts: # cmd/entire/cli/trail_context_cache.go

Changes

63

176 unmodified lines

177
178
179
180
180
181
182
183

176 unmodified lines

/tmp/entire-bin hooks claude-code post-task

# Verify task checkpoint created
/tmp/entire-bin rewind --list | jq '.[] | select(.is_task_checkpoint == true)'
/tmp/entire-bin checkpoint list --pending --json | jq '.[] | select(.is_task_checkpoint == true)'
```

### Test User Commits (Condensation)

M.claude/skills/test-repo/SKILL.md+1/-1

150 unmodified lines

151
152
153
154
154
155
156
157

150 unmodified lines

echo "==> Listing rewind points..."
cd "$REPO_DIR"

"$BIN_PATH" rewind --list
"$BIN_PATH" checkpoint list --pending --json
;;

create-changes)

M.claude/skills/test-repo/test-harness.sh+1/-1

79 unmodified lines

80
81
82
83
83
84
85
86

79 unmodified lines

2. `entire.exe enable` — in a git repo with an agent installed
3. Start an agent session, make file changes
4. `git add . && git commit -m "test"` — hooks should fire (prepare-commit-msg, post-commit)
5. `entire.exe rewind --list` — should show checkpoint(s)
5. `entire.exe checkpoint list --pending --json` — should show checkpoint(s)
6. `entire.exe explain` — pager should use `more` by default

## Architecture Notes

MWINDOWS.md+1/-1

360 unmodified lines

361
362
363
364
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384

360 unmodified lines

ExtractAllModifiedFiles(transcriptData []byte, fromOffset int, subagentsDir string) ([]string, error)

// CalculateTotalTokenUsage computes token usage including all spawned subagents.
    // The subagentsDir parameter specifies where subagent transcripts are stored.
    // The subagentsDir parameter specifies where subagent transcripts are stored
    // (an empty subagentsDir skips subagent accounting and leaves SubagentTokens nil).
    //
    // CONTRACT — the returned SubagentTokens is a CUMULATIVE-SINCE-SESSION-START
    // snapshot, NOT a delta scoped to fromOffset like the main-agent fields
    // (InputTokens/OutputTokens/...). Implementations MUST discover spawned agent
    // IDs from the FULL transcript prefix [0,end) — so a subagent spawned before\
    // fromOffset is still found (#329) — and re-read each subagent transcript from\
    // line 0 on every call. Consequently a subagent's full total repeats on every\
    // call after it is first discovered.\
    //\
    // Callers that accumulate across checkpoints/turns therefore MUST NOT sum\
    // SubagentTokens across calls: replace the running total with the latest\
    // snapshot, and rescope any window delta by subtracting a previously captured\
    // baseline (see accumulateTokenUsage / resetCheckpointWindow and\
    // session.State.SubagentTokensBaseline in cmd/entire/cli/strategy, and\
    // rescopeSubagentTokensToDeltas in cmd/entire/cli/agentimport for the import\
    // path). An implementation that instead returned per-window deltas would\
    // silently break that accounting with no compile-time or test signal.\
    CalculateTotalTokenUsage(transcriptData []byte, fromOffset int, subagentsDir string) (*TokenUsage, error)\
}\
```\
\
Mcmd/entire/cli/agent/agent.go+19/-1\
\
```\
394 unmodified lines\
\
395\
396\
397\
398\
399\
398\
399\
400\
401\
401\
402\
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\
43 unmodified lines\
\
475\
476\
477\
452\
453\
478\
479\
480\
481\
482\
483\
484\
485\
486\
487\
488\
489\
490\
491\
492\
493\
494\
495\
496\
497\
\
394 unmodified lines\
\
    // Calculate token usage from parsed transcript\
    mainUsage := CalculateTokenUsage(parsed)\
\
    // Extract spawned agent IDs from the same parsed transcript\
    agentIDs := ExtractSpawnedAgentIDs(parsed)\
    if subagentsDir == "" {\
        return mainUsage, nil\
    }\
\
    // Calculate subagent token usage (skip when subagentsDir is empty to avoid reading from cwd)\
    if len(agentIDs) > 0 && subagentsDir != "" {\
    // Extract spawned agent IDs from the FULL transcript (startLine=0), not the\
    // sliced portion. A subagent spawned before this checkpoint's startLine can\
    // keep writing to its transcript in later turns; scanning only the slice\
    // would miss it and undercount subagent token usage (#329).\
    //\
    // PERF (considered, retained deliberately): this re-parses the full\
    // transcript in addition to the sliced parse above — two JSONL parses per\
    // call, growing with session length. A single-pass version was rejected as\
    // not worth the risk: ParseFromBytes silently drops malformed lines, so a\
    // parsed-entry index does not correspond to a raw line number and naively\
    // slicing the full parse at startLine would misattribute main-agent usage;\
    // doing it safely would mean threading raw-line numbers through the shared\
    // transcript parser used by every agent. A cheap line scan for the Task\
    // marker instead of a full parse would duplicate ExtractSpawnedAgentIDs'\
    // nested tool_result decoding. The common no-subagent case already avoids\
    // this cost entirely via the subagentsDir == "" short-circuit above.\
    fullParsed, err := transcript.ParseFromBytes(transcriptData)\
    if err != nil {\
        return nil, fmt.Errorf("failed to parse full transcript: %w", err)\
    }\
    agentIDs := ExtractSpawnedAgentIDs(fullParsed)\
\
    // Calculate subagent token usage. This re-reads each subagent transcript from\
    // line 0 on every call, so mainUsage.SubagentTokens is a cumulative-since-\
    // session-start snapshot — see the CalculateTotalTokenUsage interface contract\
    // in cmd/entire/cli/agent for how callers must accumulate it.\
    if len(agentIDs) > 0 {\
        subagentUsage := &agent.TokenUsage{}\
        for agentID := range agentIDs {\
            agentPath := filepath.Join(subagentsDir, fmt.Sprintf("agent-%s.jsonl", agentID))\
43 unmodified lines\
\
        }\
    }\
\
    // Find spawned subagents and collect their modified files (skip when subagentsDir is empty to avoid reading from cwd)\
    agentIDs := ExtractSpawnedAgentIDs(parsed)\
    if subagentsDir == "" {\
        return files, nil\
    }\
\
    // Find spawned subagents from the FULL transcript (startLine=0): a subagent\
    // spawned before this checkpoint's startLine may keep modifying files in\
    // later turns, and scanning only the slice would miss it (#329). Main-agent\
    // file extraction above stays scoped to the slice.\
    //\
    // PERF: the second full-transcript parse is retained deliberately for the\
    // same reasons documented on CalculateTotalTokenUsage above; the common\
    // no-subagent case is short-circuited by the subagentsDir == "" guard.\
    fullParsed, err := transcript.ParseFromBytes(transcriptData)\
    if err != nil {\
        return nil, fmt.Errorf("failed to parse full transcript: %w", err)\
    }\
    agentIDs := ExtractSpawnedAgentIDs(fullParsed)\
    for agentID := range agentIDs {\
        agentPath := filepath.Join(subagentsDir, fmt.Sprintf("agent-%s.jsonl", agentID))\
        agentLines, agentErr := transcript.ParseFromFileAtLine(agentPath, 0)\
```\
\
Mcmd/entire/cli/agent/claudecode/transcript.go+44/-6\
\
```\
886 unmodified lines\
\
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\
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\
958\
959\
960\
961\
962\
963\
964\
\
886 unmodified lines\
\
        t.Errorf("missing expected file %q", f)\
    }\
}\
\
// Regression for #329: a subagent spawned BEFORE the checkpoint's startLine\
// must still be discovered, because it can keep modifying files in later turns.\
// The Task spawn/result live in lines before startLine; only the full transcript\
// scan finds them.\
func TestExtractAllModifiedFiles_FindsSubagentSpawnedBeforeStartLine(t *testing.T) {\
    t.Parallel()\
\
    tmpDir := t.TempDir()\
    subagentsDir := tmpDir + "/tasks/toolu_task1"\
    c := &ClaudeCodeAgent{}\
    if err := os.MkdirAll(subagentsDir, 0o755); err != nil {\
        t.Fatalf("failed to create subagents dir: %v", err)\
    }\
\
    transcriptData := buildJSONL(\
        makeTaskToolUseLine(t, "a1", "toolu_taskA"),        // line 0 (before startLine)\
        makeTaskResultLine(t, "uA", "toolu_taskA", "subA"), // line 1 (before startLine)\
        makeWriteToolLine(t, "a2", "/repo/main.go"),        // line 2 (>= startLine)\
    )\
    writeJSONLFile(t, subagentsDir+"/agent-subA.jsonl",\
        makeWriteToolLine(t, "sa1", "/repo/helper.go"),\
    )\
\
    files, err := c.ExtractAllModifiedFiles(transcriptData, 2, subagentsDir)\
    if err != nil {\
        t.Fatalf("ExtractAllModifiedFiles() error: %v", err)\
    }\
\
    got := make(map[string]bool, len(files))\
    for _, f := range files {\
        got[f] = true\
    }\
    if !got["/repo/main.go"] {\
        t.Errorf("missing main-agent file /repo/main.go: %v", files)\
    }\
    if !got["/repo/helper.go"] {\
        t.Errorf("subagent spawned before startLine was not discovered; missing /repo/helper.go: %v", files)\
    }\
}\
\
// Regression for #329: subagent token usage must be counted even when the\
// subagent was spawned before the checkpoint's startLine.\
func TestCalculateTotalTokenUsage_CountsSubagentSpawnedBeforeStartLine(t *testing.T) {\
    t.Parallel()\
\
    tmpDir := t.TempDir()\
    subagentsDir := tmpDir + "/tasks/toolu_task1"\
    c := &ClaudeCodeAgent{}\
    if err := os.MkdirAll(subagentsDir, 0o755); err != nil {\
        t.Fatalf("failed to create subagents dir: %v", err)\
    }\
\
    // Subagent spawned in lines 0-1 (before startLine=2); main usage on line 2.\
    transcriptData := buildJSONL(\
        makeTaskToolUseLine(t, "a1", "toolu_taskB"),\
        makeTaskResultLine(t, "uB", "toolu_taskB", "subB"),\
        `{"type":"assistant","uuid":"a2","message":{"id":"m2","usage":{"input_tokens":300,"output_tokens":150}}}`,\
    )\
    writeJSONLFile(t, subagentsDir+"/agent-subB.jsonl",\
        `{"type":"assistant","uuid":"sa1","message":{"id":"sm1","usage":{"input_tokens":50,"output_tokens":25}}}`,\
    )\
\
    usage, err := c.CalculateTotalTokenUsage(transcriptData, 2, subagentsDir)\
    if err != nil {\
        t.Fatalf("CalculateTotalTokenUsage() error: %v", err)\
    }\
    if usage.SubagentTokens == nil {\
        t.Fatal("subagent spawned before startLine was not counted (SubagentTokens is nil)")\
    }\
    if usage.SubagentTokens.InputTokens != 50 || usage.SubagentTokens.OutputTokens != 25 {\
        t.Errorf("subagent tokens = input %d output %d, want input 50 output 25",\
            usage.SubagentTokens.InputTokens, usage.SubagentTokens.OutputTokens)\
    }\
}\
```\
\
Mcmd/entire/cli/agent/claudecode/transcript\_test.go+75\
\
```\
365 unmodified lines\
\
366\
367\
368\
369\
370\
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\
35 unmodified lines\
\
433\
434\
435\
412\
436\
437\
438\
439\
440\
441\
442\
443\
444\
445\
446\
447\
448\
449\
450\
451\
452\
453\
454\
455\
\
365 unmodified lines\
\
    mainUsage := CalculateTokenUsage(parsed)\
\
    agentIDs := ExtractSpawnedAgentIDs(parsed)\
    if len(agentIDs) > 0 && subagentsDir != "" {\
    if subagentsDir == "" {\
        return mainUsage, nil\
    }\
\
    // Extract spawned agent IDs from the FULL transcript (startLine=0): a\
    // subagent spawned before this checkpoint's startLine can keep writing to\
    // its transcript, so scanning only the slice would undercount it (#329).\
    //\
    // PERF (considered, retained deliberately): this re-parses the full\
    // transcript in addition to the sliced parse above — two JSONL parses per\
    // call, growing with session length. A single-pass version was rejected:\
    // the Droid parser drops non-message / malformed lines, so a parsed-entry\
    // index does not map to a raw line number and naively slicing the full parse\
    // at startLine would misattribute main-agent usage; doing it safely would\
    // mean threading raw-line numbers through the shared parser. The common\
    // no-subagent case already avoids this via the subagentsDir == "" guard.\
    fullParsed, _, err := ParseDroidTranscriptFromBytes(data, 0)\
    if err != nil {\
        return nil, fmt.Errorf("failed to parse full transcript: %w", err)\
    }\
    agentIDs := ExtractSpawnedAgentIDs(fullParsed)\
    // This re-reads each subagent transcript from line 0 on every call below, so\
    // mainUsage.SubagentTokens ends up cumulative-since-session-start — see the\
    // CalculateTotalTokenUsage interface contract in cmd/entire/cli/agent for how\
    // callers must accumulate it (shared with Claude Code).\
    if len(agentIDs) > 0 {\
        subagentUsage := &agent.TokenUsage{}\
        for agentID := range agentIDs {\
            agentPath := filepath.Join(subagentsDir, fmt.Sprintf("agent-%s.jsonl", agentID))\
35 unmodified lines\
\
        fileSet[f] = true\
    }\
\
    agentIDs := ExtractSpawnedAgentIDs(parsed)\
    if subagentsDir == "" {\
        return files, nil\
    }\
\
    // Find spawned subagents from the FULL transcript (startLine=0): a subagent\
    // spawned before this checkpoint's startLine may keep modifying files in\
    // later turns, and scanning only the slice would miss it (#329). Main-agent\
    // file extraction above stays scoped to the slice.\
    //\
    // PERF: the second full-transcript parse is retained deliberately for the\
    // same reasons documented on CalculateTotalTokenUsageFromBytes above; the\
    // common no-subagent case is short-circuited by the subagentsDir == "" guard.\
    fullParsed, _, err := ParseDroidTranscriptFromBytes(data, 0)\
    if err != nil {\
        return nil, fmt.Errorf("failed to parse full transcript: %w", err)\
    }\
    agentIDs := ExtractSpawnedAgentIDs(fullParsed)\
    for agentID := range agentIDs {\
        agentPath := filepath.Join(subagentsDir, fmt.Sprintf("agent-%s.jsonl", agentID))\
        agentLines, _, agentErr := ParseDroidTranscript(agentPath, 0)\
```\
\
Mcmd/entire/cli/agent/factoryaidroid/transcript.go+40/-3\
\
```\
1223 unmodified lines\
\
1224\
1225\
1226\
1227\
1228\
1229\
1230\
1231\
1232\
1233\
1234\
1235\
1236\
1237\
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\
\
1223 unmodified lines\
\
        t.Errorf("missing expected file %q", f)\
    }\
}\
\
// Regression for #329: a subagent spawned BEFORE the checkpoint's startLine must\
// still be discovered for file extraction (it can keep modifying files later).\
func TestExtractAllModifiedFilesFromBytes_FindsSubagentSpawnedBeforeStartLine(t *testing.T) {\
    t.Parallel()\
\
    tmpDir := t.TempDir()\
    subagentsDir := tmpDir + "/tasks/toolu_task1"\
    if err := os.MkdirAll(subagentsDir, 0o755); err != nil {\
        t.Fatalf("failed to create subagents dir: %v", err)\
    }\
\
    data := joinJSONL(\
        makeTaskToolUseLine(t, "a1", "toolu_taskC"),        // line 0 (before startLine)\
        makeTaskResultLine(t, "uC", "toolu_taskC", "sub1"), // line 1 (before startLine)\
        makeWriteToolLine(t, "a2", "/repo/main.go"),        // line 2 (>= startLine)\
    )\
    writeJSONLFile(t, subagentsDir+"/agent-sub1.jsonl",\
        makeWriteToolLine(t, "sa1", "/repo/helper.go"),\
    )\
\
    files, err := ExtractAllModifiedFilesFromBytes(data, 2, subagentsDir)\
    if err != nil {\
        t.Fatalf("ExtractAllModifiedFilesFromBytes() error: %v", err)\
    }\
\
    got := make(map[string]bool, len(files))\
    for _, f := range files {\
        got[f] = true\
    }\
    if !got["/repo/main.go"] {\
        t.Errorf("missing main-agent file /repo/main.go: %v", files)\
    }\
    if !got["/repo/helper.go"] {\
        t.Errorf("subagent spawned before startLine was not discovered; missing /repo/helper.go: %v", files)\
    }\
}\
\
// Regression for #329: subagent token usage must be counted even when the\
// subagent was spawned before the checkpoint's startLine.\
func TestCalculateTotalTokenUsageFromBytes_CountsSubagentSpawnedBeforeStartLine(t *testing.T) {\
    t.Parallel()\
\
    tmpDir := t.TempDir()\
    subagentsDir := tmpDir + "/tasks/toolu_task1"\
    if err := os.MkdirAll(subagentsDir, 0o755); err != nil {\
        t.Fatalf("failed to create subagents dir: %v", err)\
    }\
\
    data := joinJSONL(\
        makeTaskToolUseLine(t, "a1", "toolu_taskD"),           // line 0 (before startLine)\
        makeTaskResultLine(t, "uD", "toolu_taskD", "sub1"),    // line 1 (before startLine)\
        makeAssistantTokenLine(t, "a2", "msg_main", 300, 150), // line 2\
    )\
    writeJSONLFile(t, subagentsDir+"/agent-sub1.jsonl",\
        makeAssistantTokenLine(t, "sa1", "msg_sub", 50, 25),\
    )\
\
    usage, err := CalculateTotalTokenUsageFromBytes(data, 2, subagentsDir)\
    if err != nil {\
        t.Fatalf("CalculateTotalTokenUsageFromBytes() error: %v", err)\
    }\
    if usage.SubagentTokens == nil {\
        t.Fatal("subagent spawned before startLine was not counted (SubagentTokens is nil)")\
    }\
    if usage.SubagentTokens.InputTokens != 50 || usage.SubagentTokens.OutputTokens != 25 {\
        t.Errorf("subagent tokens = input %d output %d, want input 50 output 25",\
            usage.SubagentTokens.InputTokens, usage.SubagentTokens.OutputTokens)\
    }\
}\
```\
\
Mcmd/entire/cli/agent/factoryaidroid/transcript\_test.go+70\
\
```\
44 unmodified lines\
\
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\
\
44 unmodified lines\
\
    sum.SubagentTokens = AddTokenUsage(aSub, bSub)\
    return sum\
}\
\
// SubtractTokenUsage returns a-b, recursing into subagent usage and clamping\
// every field at zero (a nil operand is treated as zero). Neither input is\
// mutated. Used to rescope a cumulative-since-session-start snapshot (e.g.\
// subagent token usage, which is always re-read from the start of each\
// subagent transcript) down to a delta since a previously captured baseline.\
func SubtractTokenUsage(a, b *TokenUsage) *TokenUsage {\
    if a == nil {\
        return nil\
    }\
    if b == nil {\
        b = &TokenUsage{}\
    }\
    diff := &TokenUsage{\
        InputTokens:         clampSubtract(a.InputTokens, b.InputTokens),\
        CacheCreationTokens: clampSubtract(a.CacheCreationTokens, b.CacheCreationTokens),\
        CacheReadTokens:     clampSubtract(a.CacheReadTokens, b.CacheReadTokens),\
        OutputTokens:        clampSubtract(a.OutputTokens, b.OutputTokens),\
        APICallCount:        clampSubtract(a.APICallCount, b.APICallCount),\
    }\
    diff.SubagentTokens = SubtractTokenUsage(a.SubagentTokens, b.SubagentTokens)\
    return diff\
}\
\
// clampSubtract returns a-b, floored at zero so a stale or racy baseline\
// never produces a negative delta.\
func clampSubtract(a, b int) int {\
    if a < b {\
        return 0\
    }\
    return a - b\
}\
```\
\
Mcmd/entire/cli/agent/types/token\_usage.go+32\
\
```\
72 unmodified lines\
\
73\
74\
75\
76\
76\
77\
78\
79\
80\
81\
82\
83\
84\
85\
86\
87\
88\
89\
90\
91\
92\
93\
94\
95\
96\
97\
98\
\
72 unmodified lines\
\
// command and can afford to refresh an absent or stale enablement decision rather\
// than incorrectly treating an unknown cache entry as "trails unavailable".\
func agentHelpRepoContext(ctx context.Context) (repoLine string, trailsEnabled bool) {\
    return agentHelpRepoContextWithRefresh(ctx, refreshTrailsEnabledCacheIfStaleForScope)\
    return agentHelpRepoContextWithRefresh(ctx, refreshAgentHelpTrailsEnabledCacheIfStaleForScope)\
}\
\
// refreshAgentHelpTrailsEnabledCacheIfStaleForScope refreshes synchronously\
// because agent-help is an explicit command whose output must reflect the\
// current availability decision. SessionStart uses the detached\
// refreshTrailsEnabledCacheIfStaleForScope path instead to avoid hook latency.\
func refreshAgentHelpTrailsEnabledCacheIfStaleForScope(ctx context.Context, scope trailEnablementScope) error {\
    if cachedTrailsEnablementForScope(ctx, scope, time.Now()) != trailEnablementCacheUnknown {\
        return nil\
    }\
    if !scope.Supported {\
        return saveTrailsEnabledForScope(ctx, scope, false, time.Now())\
    }\
    client, err := NewAuthenticatedAPIClient(ctx, false)\
    if err != nil {\
        return err\
    }\
    _, err = refreshTrailsEnabledCacheForScope(ctx, client, scope)\
    return err\
}\
\
// agentHelpRepoContextWithRefresh keeps the refresh dependency explicit so the\
```\
\
Mcmd/entire/cli/agent\_help\_cmd.go+20/-1\
\
```\
43 unmodified lines\
\
44\
45\
46\
47\
47\
48\
49\
50\
51\
52\
53\
54\
55\
56\
57\
58\
59\
164 unmodified lines\
\
224\
225\
226\
227\
228\
229\
230\
231\
232\
233\
234\
235\
236\
\
43 unmodified lines\
\
    UUID               string\
    Prompt, Model      string\
    CreatedAt          time.Time\
    Tokens             *types.TokenUsage\
    // Tokens is this turn's token usage. Every field is a per-turn delta:\
    // main-agent fields are scoped to the turn's [LineStart, LineEnd) slice by\
    // the token helpers, and SubagentTokens is rescoped from the cumulative\
    // snapshot those helpers return to a per-turn increment by\
    // rescopeSubagentTokensToDeltas (see linesplit.go). That invariant lets\
    // callers sum turns freely: writeSessionState sums them for the session\
    // total and each imported checkpoint stores its own turn's delta, so a\
    // subagent's tokens are counted exactly once rather than re-added on every\
    // turn after it is discovered.\
    Tokens *types.TokenUsage\
}\
\
// Importer is the per-agent seam: it locates an agent's transcripts for a repo\
164 unmodified lines\
\
        if turn.Model != "" {\
            model = turn.Model\
        }\
        // turn.Tokens holds per-turn deltas for every field, including\
        // SubagentTokens (rescoped from a cumulative snapshot in\
        // rescopeSubagentTokensToDeltas — see the Turn.Tokens doc). Summing\
        // them therefore yields the correct session total: main-agent fields\
        // add up, and the subagent deltas sum back to the final cumulative\
        // subagent snapshot exactly once instead of being multiplied by the\
        // number of turns after each subagent was first discovered.\
        tokens = types.AddTokenUsage(tokens, turn.Tokens)\
    }\
    if started.IsZero() {\
```\
\
Mcmd/entire/cli/agentimport/agentimport.go+17/-1\
\
```\
31 unmodified lines\
\
32\
33\
34\
35\
36\
37\
38\
35\
36\
37\
38\
39\
40\
41\
42\
43\
\
31 unmodified lines\
\
    return discoverSessionFiles(dir, now, sessionFilter, jsonlSessionResolver(".jsonl", identitySessionID))\
}\
\
// SplitTurns produces one Turn per user-prompt line. Token usage for each turn\
// is computed on the slice [LineStart, LineEnd) so turns don't double-count\
// later turns. tool_result lines (Type == "user" but no text content) do not\
// start a turn.\
// SplitTurns produces one Turn per user-prompt line. Main-agent token usage for\
// each turn is computed on the slice [LineStart, LineEnd) so turns don't\
// double-count later turns; subagent token usage is discovered from the full\
// prefix and rescoped to a per-turn delta by splitLineTurns (see\
// rescopeSubagentTokensToDeltas). tool_result lines (Type == "user" but no text\
// content) do not start a turn.\
func (claudeImporter) SplitTurns(sf SessionFile, full []byte) ([]Turn, error) {\
    subagentsDir := filepath.Join(filepath.Dir(sf.Path), sf.SessionID, "subagents")\
    ag := &claudecode.ClaudeCodeAgent{}\
```\
\
Mcmd/entire/cli/agentimport/claude.go+6/-4\
\
```\
33 unmodified lines\
\
34\
35\
36\
37\
38\
37\
38\
39\
40\
41\
42\
43\
\
33 unmodified lines\
\
}\
\
// SplitTurns produces one Turn per user-prompt envelope, bounded by the next.\
// Token usage (including spawned subagents) is delegated to the Factory agent;\
// the model is read once from the session's adjacent settings file. Droid\
// Token usage is delegated to the Factory agent; spawned-subagent usage comes\
// back as a cumulative snapshot and is rescoped to a per-turn delta by\
// splitLineTurns (see rescopeSubagentTokensToDeltas). The model is read once\
// from the session's adjacent settings file. Droid\
// envelopes carry no per-message timestamp (the agent stamps events with\
// time.Now() at hook time), so every turn falls back to the transcript file's\
// modtime — the same fallback the Gemini importer uses.\
```\
\
Mcmd/entire/cli/agentimport/factory.go+4/-2\
\
```\
1\
2\
3\
3\
4\
5\
6\
7\
8\
9\
10\
45 unmodified lines\
\
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\
\
package agentimport\
\
import "time"\
import (\
    "time"\
\
    "github.com/entireio/cli/cmd/entire/cli/agent/types"\
)\
\
// parseTimestamp parses an RFC3339 timestamp, returning the zero time when the\
// string is empty or unparseable. Shared by the importers that read a per-turn\
45 unmodified lines\
\
        turn.LineStart, turn.LineEnd = start, end\
        turns = append(turns, *turn)\
    }\
    rescopeSubagentTokensToDeltas(turns)\
    return turns, nil\
}\
\
// rescopeSubagentTokensToDeltas converts each turn's SubagentTokens from the\
// cumulative-since-session-start snapshot the token helpers return into the\
// per-turn increment (this turn's cumulative minus the previous turn's).\
//\
// The subagent-aware token helpers (claudecode/factoryaidroid\
// CalculateTotalTokenUsage) discover spawned agent IDs from the full transcript\
// prefix [0,end) — so a subagent spawned before the current turn is still found\
// (#329) — and re-read each agent-<id>.jsonl from line 0. That makes a turn's\
// SubagentTokens a cumulative snapshot that repeats every already-discovered\
// subagent's full total on every later turn, unlike the main-agent fields\
// (InputTokens/OutputTokens/...), which are scoped to the turn's own\
// [start,end) slice and are genuine per-turn deltas.\
//\
// Both import consumers sum per-turn token usage: writeSessionState folds the\
// turns together with AddTokenUsage for the session total, and every imported\
// checkpoint stores its turn's TokenUsage (downstream consumers sum those).\
// Summing the cumulative snapshot multiplies a subagent's tokens by the number\
// of turns after it is first discovered (trail finding 019f5ea3). Rescoping to\
// per-turn deltas fixes both without special-casing either: each checkpoint\
// carries only the subagent usage attributable to its turn, and summing the\
// deltas reconstructs the final cumulative total exactly once.\
//\
// This mirrors the live path, which keeps the latest cumulative snapshot in\
// state.TokenUsage (accumulateTokenUsage replaces rather than adds\
// SubagentTokens) and rescopes each checkpoint window to "cumulative minus a\
// captured baseline" via types.SubtractTokenUsage and\
// SessionState.SubagentTokensBaseline (see cmd/entire/cli/strategy). Here the\
// baseline for turn k is turn k-1's cumulative snapshot. The cumulative is\
// monotonic non-decreasing across turns (discovered-agent set only grows and\
// each subagent file total is fixed), so the clamped subtraction is exact and\
// the deltas sum back to the final snapshot.\
//\
// Turns without a discovered subagent have a nil SubagentTokens and are left\
// untouched, so this is a no-op for the non-subagent-aware importers that route\
// through splitLineTurns (cursor/pi/codex/copilot).\
func rescopeSubagentTokensToDeltas(turns []Turn) {\
    var prevCumulative *types.TokenUsage\
    for i := range turns {\
        if turns[i].Tokens == nil {\
            continue\
        }\
        cumulative := turns[i].Tokens.SubagentTokens\
        turns[i].Tokens.SubagentTokens = types.SubtractTokenUsage(cumulative, prevCumulative)\
        // Only advance the baseline when this turn carried a snapshot. A turn\
        // whose agent-<id>.jsonl transiently failed to read has a nil cumulative\
        // (CalculateTotalTokenUsage continue-s past the error); resetting\
        // prevCumulative to nil here would make the next non-nil snapshot subtract\
        // nothing and re-report the full cumulative, reintroducing the\
        // double-counting this rescoping removes. Mirrors the live path, where\
        // accumulateTokenUsage only replaces SubagentTokens when the incoming\
        // snapshot is non-nil.\
        if cumulative != nil {\
            prevCumulative = cumulative\
        }\
    }\
}\
```\
\
Mcmd/entire/cli/agentimport/linesplit.go+63/-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\
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\
\
package agentimport\
\
import (\
    "context"\
    "os"\
    "path/filepath"\
    "strings"\
    "testing"\
\
    "github.com/entireio/cli/cmd/entire/cli/agent/types"\
)\
\
// These regression tests pin the trail-817 fix: the subagent-aware importers\
// (Claude Code, Factory AI Droid) get their per-turn SubagentTokens as a\
// cumulative-since-session-start snapshot (agent IDs are discovered from the\
// full transcript prefix and each subagent transcript is re-read from line 0),\
// so a subagent spawned in an early turn repeats its full total on every later\
// turn. Both import consumers sum per-turn usage — writeSessionState for the\
// session total and the per-imported-checkpoint TokenUsage that downstream sums\
// — so before the fix a subagent's tokens were multiplied by the number of\
// turns after it was first discovered. rescopeSubagentTokensToDeltas\
// (linesplit.go) rescopes those snapshots to per-turn deltas so the total is\
// counted exactly once. Reverting that call makes both assertions below fail.\
\
const (\
    // The single spawned subagent's on-disk transcript totals, asserted to be\
    // counted exactly once across a 3-turn session.\
    wantSubagentInput  = 50\
    wantSubagentOutput = 25\
    wantSubagentCalls  = 1\
\
    // Main-agent totals summed across the three turns (per-slice deltas), kept\
    // intact by the fix.\
    wantMainInput  = 600 // 100 + 200 + 300\
    wantMainOutput = 300 // 50 + 100 + 150\
    wantMainCalls  = 3\
)\
\
// sumTurnSubagentTokens sums each turn's SubagentTokens the way both the session\
// total and any downstream sum of per-checkpoint TokenUsage would. With the fix\
// the turns hold per-turn deltas, so this reconstructs the subagent total once;\
// without it each turn holds the cumulative snapshot and this multiplies.\
func sumTurnSubagentTokens(turns []Turn) types.TokenUsage {\
    var sum types.TokenUsage\
    for _, tr := range turns {\
        if tr.Tokens == nil || tr.Tokens.SubagentTokens == nil {\
            continue\
        }\
        s := tr.Tokens.SubagentTokens\
        sum.InputTokens += s.InputTokens\
        sum.CacheCreationTokens += s.CacheCreationTokens\
        sum.CacheReadTokens += s.CacheReadTokens\
        sum.OutputTokens += s.OutputTokens\
        sum.APICallCount += s.APICallCount\
    }\
    return sum\
}\
\
func assertSubagentCountedOnce(t *testing.T, label string, got *types.TokenUsage) {\
    t.Helper()\
    if got == nil {\
        t.Fatalf("%s: SubagentTokens is nil, want input=%d output=%d calls=%d",\
            label, wantSubagentInput, wantSubagentOutput, wantSubagentCalls)\
    }\
    if got.InputTokens != wantSubagentInput || got.OutputTokens != wantSubagentOutput ||\
        got.APICallCount != wantSubagentCalls {\
        t.Errorf("%s: subagent tokens counted more than once: got input=%d output=%d calls=%d, "+\
            "want input=%d output=%d calls=%d (cumulative snapshot summed across turns)",\
            label, got.InputTokens, got.OutputTokens, got.APICallCount,\
            wantSubagentInput, wantSubagentOutput, wantSubagentCalls)\
    }\
}\
\
func writeSubagentTranscript(t *testing.T, sf SessionFile, agentID, line string) {\
    t.Helper()\
    subagentsDir := filepath.Join(filepath.Dir(sf.Path), sf.SessionID, "subagents")\
    if err := os.MkdirAll(subagentsDir, 0o755); err != nil {\
        t.Fatalf("mkdir subagents dir: %v", err)\
    }\
    agentPath := filepath.Join(subagentsDir, "agent-"+agentID+".jsonl")\
    if err := os.WriteFile(agentPath, []byte(line+"\n"), 0o600); err != nil {\
        t.Fatalf("write subagent transcript: %v", err)\
    }\
}\
\
// TestRescopeSubagentTokensToDeltas_NilCumulativeThenReappears pins finding\
// 019f5ebc-cf27: when a turn's cumulative SubagentTokens snapshot is transiently\
// nil (the subagent's agent-<id>.jsonl failed to read, so CalculateTotalTokenUsage\
// continue-d past it and left SubagentTokens nil) and a later turn's snapshot\
// reappears non-nil, prevCumulative must NOT be reset to nil for the nil turn —\
// otherwise SubtractTokenUsage(cumulative, nil) on the reappearing turn returns\
// the full cumulative again and reintroduces the double-counting the PR fixes.\
// The deltas must still sum to the final cumulative exactly once.\
func TestRescopeSubagentTokensToDeltas_NilCumulativeThenReappears(t *testing.T) {\
    turns := []Turn{\
        {Tokens: &types.TokenUsage{InputTokens: 10, SubagentTokens: &types.TokenUsage{InputTokens: 100, OutputTokens: 50, APICallCount: 1}}},\
        // Transient read failure: main-agent tokens present, subagent snapshot nil.\
        {Tokens: &types.TokenUsage{InputTokens: 20}},\
        // Snapshot reappears, having grown to 300/150.\
        {Tokens: &types.TokenUsage{InputTokens: 30, SubagentTokens: &types.TokenUsage{InputTokens: 300, OutputTokens: 150, APICallCount: 3}}},\
    }\
\
    rescopeSubagentTokensToDeltas(turns)\
\
    // Turn 0 delta = 100-0 = 100.\
    if turns[0].Tokens.SubagentTokens == nil || turns[0].Tokens.SubagentTokens.InputTokens != 100 {\
        t.Fatalf("turn0 subagent delta = %#v, want input=100", turns[0].Tokens.SubagentTokens)\
    }\
    // Turn 1 had a nil snapshot: its delta stays nil.\
    if turns[1].Tokens.SubagentTokens != nil {\
        t.Fatalf("turn1 subagent delta = %#v, want nil", turns[1].Tokens.SubagentTokens)\
    }\
    // Turn 2 delta must be rescoped against turn 0's cumulative (100), NOT nil:\
    // 300-100 = 200, not the full 300.\
    if turns[2].Tokens.SubagentTokens == nil || turns[2].Tokens.SubagentTokens.InputTokens != 200 {\
        t.Fatalf("turn2 subagent delta = %#v, want input=200 (300 cumulative minus turn0 baseline 100)",\
            turns[2].Tokens.SubagentTokens)\
    }\
\
    // The per-turn deltas must sum to the final cumulative (300) exactly once.\
    sum := sumTurnSubagentTokens(turns)\
    if sum.InputTokens != 300 || sum.OutputTokens != 150 || sum.APICallCount != 3 {\
        t.Fatalf("summed subagent deltas = input=%d output=%d calls=%d, want 300/150/3 (counted once)",\
            sum.InputTokens, sum.OutputTokens, sum.APICallCount)\
    }\
}\
\
// TestImport_ClaudeSubagentTokensCountedOnceAcrossTurns builds a Claude session\
// where a subagent is spawned in the first turn and two more user-prompt turns\
// follow, then asserts the subagent's tokens are counted exactly once both in\
// the summed per-turn/per-checkpoint usage and in the imported session total.\
func TestImport_ClaudeSubagentTokensCountedOnceAcrossTurns(t *testing.T) {\
    importRepo(t) // chdir into a repo for session-state storage; no t.Parallel (t.Chdir)\
\
    dir := t.TempDir()\
    sf := SessionFile{Path: filepath.Join(dir, "s.jsonl"), SessionID: "s"}\
\
    // Turn 1 spawns subagent "subX" (Task tool_use + tool_result carrying the\
    // agentId), then three user-prompt turns each with their own assistant\
    // usage. The tool_result line is type "user" but has no text, so it does\
    // not start a turn.\
    full := []byte(strings.Join([]string{\
        `{"type":"user","uuid":"u1","message":{"role":"user","content":"first"}}`,\
        `{"type":"assistant","uuid":"a0","message":{"content":[{"type":"tool_use","id":"toolu_task1","name":"Task","input":{"prompt":"go"}}]}}`,\
        `{"type":"user","uuid":"r1","message":{"content":[{"type":"tool_result","tool_use_id":"toolu_task1","content":"agentId: subX"}]}}`,\
        `{"type":"assistant","uuid":"a1","message":{"id":"m1","content":[{"type":"text","text":"ok"}],"usage":{"input_tokens":100,"output_tokens":50}}}`,\
        `{"type":"user","uuid":"u2","message":{"role":"user","content":"second"}}`,\
        `{"type":"assistant","uuid":"a2","message":{"id":"m2","content":[{"type":"text","text":"ok2"}],"usage":{"input_tokens":200,"output_tokens":100}}}`,\
        `{"type":"user","uuid":"u3","message":{"role":"user","content":"third"}}`,\
        `{"type":"assistant","uuid":"a3","message":{"id":"m3","content":[{"type":"text","text":"ok3"}],"usage":{"input_tokens":300,"output_tokens":150}}}`,\
    }, "\n") + "\n")\
    if err := os.WriteFile(sf.Path, full, 0o600); err != nil {\
        t.Fatalf("write transcript: %v", err)\
    }\
    writeSubagentTranscript(t, sf, "subX",\
        `{"type":"assistant","uuid":"sa1","message":{"id":"sm1","content":[{"type":"text","text":"sub"}],"usage":{"input_tokens":50,"output_tokens":25}}}`)\
\
    turns, err := claudeImporter{}.SplitTurns(sf, full)\
    if err != nil {\
        t.Fatalf("SplitTurns: %v", err)\
    }\
    assertSubagentTurns(t, claudeImporter{}, sf, turns)\
}\
\
// TestImport_FactorySubagentTokensCountedOnceAcrossTurns is the Factory AI Droid\
// analogue: Droid envelopes, subagent spawned in the first turn, three prompt\
// turns, subagent tokens counted exactly once.\
func TestImport_FactorySubagentTokensCountedOnceAcrossTurns(t *testing.T) {\
    importRepo(t)\
\
    dir := t.TempDir()\
    sf := SessionFile{Path: filepath.Join(dir, "s.jsonl"), SessionID: "s"}\
\
    full := []byte(strings.Join([]string{\
        `{"type":"message","id":"u1","message":{"role":"user","content":"first"}}`,\
        `{"type":"message","id":"a0","message":{"role":"assistant","content":[{"type":"tool_use","id":"toolu_task1","name":"Task","input":{"prompt":"go"}}]}}`,\
        `{"type":"message","id":"r1","message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"toolu_task1","content":"agentId: subX"}]}}`,\
        `{"type":"message","id":"a1","message":{"role":"assistant","id":"m1","content":[{"type":"text","text":"ok"}],"usage":{"input_tokens":100,"output_tokens":50}}}`,\
        `{"type":"message","id":"u2","message":{"role":"user","content":"second"}}`,\
        `{"type":"message","id":"a2","message":{"role":"assistant","id":"m2","content":[{"type":"text","text":"ok2"}],"usage":{"input_tokens":200,"output_tokens":100}}}`,\
        `{"type":"message","id":"u3","message":{"role":"user","content":"third"}}`,\
        `{"type":"message","id":"a3","message":{"role":"assistant","id":"m3","content":[{"type":"text","text":"ok3"}],"usage":{"input_tokens":300,"output_tokens":150}}}`,\
    }, "\n") + "\n")\
    if err := os.WriteFile(sf.Path, full, 0o600); err != nil {\
        t.Fatalf("write transcript: %v", err)\
    }\
    writeSubagentTranscript(t, sf, "subX",\
        `{"type":"message","id":"se1","message":{"role":"assistant","id":"sm1","content":[{"type":"text","text":"sub"}],"usage":{"input_tokens":50,"output_tokens":25}}}`)\
\
    turns, err := factoryImporter{}.SplitTurns(sf, full)\
    if err != nil {\
        t.Fatalf("SplitTurns: %v", err)\
    }\
    assertSubagentTurns(t, factoryImporter{}, sf, turns)\
}\
\
// assertSubagentTurns runs the shared assertions for a 3-turn session with one\
// spawned subagent: the summed per-turn (== per-checkpoint) subagent tokens and\
// the imported session total each count the subagent exactly once, while the\
// main-agent totals still sum across turns.\
func assertSubagentTurns(t *testing.T, imp Importer, sf SessionFile, turns []Turn) {\
    t.Helper()\
    ctx := context.Background()\
    if len(turns) != 3 {\
        t.Fatalf("want 3 turns, got %d", len(turns))\
    }\
\
    // Per-checkpoint proof: summing each turn's stored TokenUsage.SubagentTokens\
    // (which is exactly what writeTurn persists per imported checkpoint) must\
    // reconstruct the subagent total once, not 3x.\
    perCheckpoint := sumTurnSubagentTokens(turns)\
    assertSubagentCountedOnce(t, "sum of per-turn SubagentTokens", &perCheckpoint)\
\
    // Session-total proof: the imported session.State.TokenUsage folds the\
    // turns via writeSessionState the same way production Run does.\
    if err := writeSessionState(ctx, imp, sf, turns); err != nil {\
        t.Fatalf("writeSessionState: %v", err)\
    }\
    st := loadState(t, sf.SessionID)\
    if st == nil || st.TokenUsage == nil {\
        t.Fatalf("no imported session token usage written: %+v", st)\
    }\
    assertSubagentCountedOnce(t, "session total SubagentTokens", st.TokenUsage.SubagentTokens)\
\
    // The main-agent fields are genuine per-slice deltas and must still sum.\
    if st.TokenUsage.InputTokens != wantMainInput || st.TokenUsage.OutputTokens != wantMainOutput ||\
        st.TokenUsage.APICallCount != wantMainCalls {\
        t.Errorf("main-agent totals = input=%d output=%d calls=%d, want input=%d output=%d calls=%d",\
            st.TokenUsage.InputTokens, st.TokenUsage.OutputTokens, st.TokenUsage.APICallCount,\
            wantMainInput, wantMainOutput, wantMainCalls)\
    }\
}\
```\
\
Acmd/entire/cli/agentimport/subagent\_tokens\_test.go+232\
\
```\
13 unmodified lines\
\
14\
15\
16\
17\
18\
19\
20\
21\
83 unmodified lines\
\
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\
\
13 unmodified lines\
\
    "time"\
\
    "github.com/entireio/cli/cmd/entire/cli/agent"\
    _ "github.com/entireio/cli/cmd/entire/cli/agent/claudecode" // register claude-code so its .claude protected dir is discoverable\
    "github.com/entireio/cli/cmd/entire/cli/agent/types"\
    "github.com/entireio/cli/cmd/entire/cli/checkpoint/id"\
    "github.com/entireio/cli/cmd/entire/cli/paths"\
    "github.com/entireio/cli/cmd/entire/cli/testutil"\
83 unmodified lines\
\
    }\
}\
\
// fakePluginAgent is a minimal agent stub used to prove that protected dirs\
// and files reported by an external-plugin-style agent (via the AllProtectedDirs\
// / AllProtectedFiles union) are honored by the first-checkpoint path, not just\
// the built-in claude-code .claude dir.\
type fakePluginAgent struct{}\
\
var (\
    _ agent.Agent                  = (*fakePluginAgent)(nil)\
    _ agent.ProtectedFilesProvider = (*fakePluginAgent)(nil)\
)\
\
func (fakePluginAgent) Name() types.AgentName                { return "terminalhire-plugin" }\
func (fakePluginAgent) Type() types.AgentType                { return "TerminalHire" }\
func (fakePluginAgent) Description() string                  { return "fake external plugin for tests" }\
func (fakePluginAgent) IsPreview() bool                      { return true }\
func (fakePluginAgent) ProtectedDirs() []string              { return []string{".terminalhire"} }\
func (fakePluginAgent) ProtectedFiles() []string             { return []string{".terminalhirerc"} }\
func (fakePluginAgent) GetSessionID(*agent.HookInput) string { return "" }\
\
func (fakePluginAgent) DetectPresence(context.Context) (bool, error) { return false, nil }\
func (fakePluginAgent) ReadTranscript(string) ([]byte, error)        { return nil, nil }\
func (fakePluginAgent) ChunkTranscript(_ context.Context, c []byte, _ int) ([][]byte, error) {\
    return [][]byte{c}, nil\
}\
func (fakePluginAgent) ReassembleTranscript(chunks [][]byte) ([]byte, error) {\
    var out []byte\
    for _, c := range chunks {\
        out = append(out, c...)\
    }\
    return out, nil\
}\
func (fakePluginAgent) GetSessionDir(string) (string, error)                      { return "", nil }\
func (fakePluginAgent) ResolveSessionFile(dir, sid string) string                 { return dir + "/" + sid }\
func (fakePluginAgent) ReadSession(*agent.HookInput) (*agent.AgentSession, error) { return nil, nil } //nolint:nilnil // test stub\
func (fakePluginAgent) WriteSession(context.Context, *agent.AgentSession) error   { return nil }\
func (fakePluginAgent) FormatResumeCommand(string) string                         { return "" }\
\
// TestCollectChangedFiles_ExcludesProtectedDirs verifies that the\
// first-checkpoint path keeps agent-protected dirs (e.g. .claude) and the\
// .entire infrastructure dir out of the checkpoint snapshot, while ordinary\
// untracked files are still captured. Regression for protected-dir content\
// leaking into the shadow tree on session start.\
func TestCollectChangedFiles_ExcludesProtectedDirs(t *testing.T) {\
    t.Parallel()\
\
    // Register an external-plugin-style agent so its protected dir/file join the\
    // AllProtectedDirs/AllProtectedFiles union alongside the built-in .claude.\
    // Registration is additive and concurrency-safe; no test asserts the exact set.\
    agent.Register("terminalhire-plugin", func() agent.Agent { return fakePluginAgent{} })\
\
    tempDir := t.TempDir()\
    // Resolve symlinks so the repo root matches git's resolved path.\
    // On macOS, t.TempDir() returns /var/... but git resolves to /private/var/...\
    tempDir, err := filepath.EvalSymlinks(tempDir)\
    require.NoError(t, err)\
\
    testutil.InitRepo(t, tempDir)\
    testutil.WriteFile(t, tempDir, "base.txt", "base")\
    testutil.GitAdd(t, tempDir, "base.txt")\
    testutil.GitCommit(t, tempDir, "init")\
\
    // Disable any global core.excludesFile so a developer/CI-runner gitignore\
    // convention (e.g. one that ignores .claude) can't mask the leak. The fix\
    // must exclude protected dirs on its own, independent of gitignore state.\
    cfgCmd := exec.CommandContext(context.Background(), "git", "config", "core.excludesFile", os.DevNull)\
    cfgCmd.Dir = tempDir\
    require.NoError(t, cfgCmd.Run())\
\
    // Planted untracked, non-gitignored files.\
    testutil.WriteFile(t, tempDir, ".claude/marker.txt", "MARKER-secret")         // built-in agent-protected dir\
    testutil.WriteFile(t, tempDir, ".terminalhire/profile.json", "MARKER-plugin") // plugin-protected dir\
    testutil.WriteFile(t, tempDir, ".terminalhirerc", "MARKER-plugin-file")       // plugin-protected file\
    testutil.WriteFile(t, tempDir, ".entire/state.json", "{}")                    // infrastructure\
    testutil.WriteFile(t, tempDir, "src/keep.txt", "user work")                   // ordinary\
\
    repo, err := git.PlainOpen(tempDir)\
    require.NoError(t, err)\
\
    result, err := collectChangedFiles(context.Background(), repo)\
    require.NoError(t, err)\
\
    require.NotContains(t, result.Changed, ".claude/marker.txt",\
        "built-in agent protected dir content must not be captured into the checkpoint")\
    require.NotContains(t, result.Changed, ".terminalhire/profile.json",\
        "external-plugin protected dir content must not be captured into the checkpoint")\
    require.NotContains(t, result.Changed, ".terminalhirerc",\
        "external-plugin protected file must not be captured into the checkpoint")\
    require.NotContains(t, result.Changed, ".entire/state.json",\
        "infrastructure dir must not be captured into the checkpoint")\
    require.Contains(t, result.Changed, "src/keep.txt",\
        "ordinary untracked files must still be captured")\
}\
\
// TestWriteCommitted_AgentField verifies that the Agent field is written\
// to both metadata.json and the commit message trailer.\
func TestWriteCommitted_AgentField(t *testing.T) {\
```\
\
Mcmd/entire/cli/checkpoint/checkpoint\_test.go+95\
\
```\
1194 unmodified lines\
\
1195\
1196\
1197\
1198\
1199\
1200\
1201\
1202\
1203\
1204\
1205\
1206\
1207\
1208\
1209\
1210\
1211\
1212\
1213\
1214\
1215\
1216\
1217\
1218\
1219\
1220\
1221\
1222\
1223\
1224\
1225\
1226\
1227\
1228\
1229\
45 unmodified lines\
\
1275\
1276\
1277\
1249\
1278\
1279\
1280\
1252\
1281\
1282\
1283\
1284\
1285\
1286\
1258\
1287\
1288\
1289\
1290\
2 unmodified lines\
\
1293\
1294\
1295\
1267\
1268\
1296\
1297\
1298\
1299\
1300\
\
1194 unmodified lines\
\
    return kept\
}\
\
// isProtectedCheckpointPath reports whether a repo-relative path must be kept\
// out of checkpoint snapshots: the .entire infrastructure dir, or any\
// registered agent's declared protected dir/file (e.g. .claude, or an external\
// plugin's protected_dirs).\
//\
// This mirrors shouldIgnoreSessionTrackingPath in the cli package. The two\
// cannot share an implementation because cli imports checkpoint, so the logic\
// is duplicated deliberately. The first-checkpoint path (collectChangedFiles)\
// must apply the same exclusions as the session-tracking and rewind paths, or\
// protected-dir content is captured into the shadow tree on session start\
// (see the DetectFileChanges / isProtectedPath call sites).\
func isProtectedCheckpointPath(relPath string) bool {\
    cleanPath := filepath.Clean(filepath.FromSlash(relPath))\
    if paths.IsInfrastructurePath(cleanPath) {\
        return true\
    }\
    for _, file := range agent.AllProtectedFiles() {\
        if paths.Equal(cleanPath, file) {\
            return true\
        }\
    }\
    for _, dir := range agent.AllProtectedDirs() {\
        if paths.IsProtectedSubpath(filepath.Clean(filepath.FromSlash(dir)), cleanPath) {\
            return true\
        }\
    }\
    return false\
}\
\
// collectChangedFiles returns all changed files from git status for the first checkpoint.\
//\
// For the first checkpoint, we need to capture:\
45 unmodified lines\
\
        filename := entry[3:] // No TrimSpace needed with -z format\
\
        // Handle R/C (rename/copy) first - they have a second entry we must skip\
        // even if the new filename is an infrastructure path\
        // even if the new filename is a protected path\
        if staging == 'R' || staging == 'C' {\
            // Renamed or copied: current entry is new name, next entry is old name\
            if !paths.IsInfrastructurePath(filename) {\
            if !isProtectedCheckpointPath(filename) {\
                changedSeen[filename] = struct{}{}\
            }\
            // The old name follows as the next NUL-separated entry - must always skip it\
            if i+1 < len(entries) && entries[i+1] != "" {\
                oldName := entries[i+1]\
                if staging == 'R' && !paths.IsInfrastructurePath(oldName) {\
                if staging == 'R' && !isProtectedCheckpointPath(oldName) {\
                    // For renames, old file is effectively deleted\
                    deletedSeen[oldName] = struct{}{}\
                }\
2 unmodified lines\
\
            continue\
        }\
\
        // Skip .entire directory for non-R/C entries\
        if paths.IsInfrastructurePath(filename) {\
        // Skip .entire and agent-protected dirs/files for non-R/C entries\
        if isProtectedCheckpointPath(filename) {\
            continue\
        }\
```\
\
Mcmd/entire/cli/checkpoint/ephemeral.go+34/-5\
\
```\
7 unmodified lines\
\
8\
9\
10\
11\
12\
13\
14\
19 unmodified lines\
\
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\
318 unmodified lines\
\
521\
522\
523\
524\
525\
526\
527\
528\
529\
530\
531\
\
7 unmodified lines\
\
    "os"\
    "os/exec"\
    "path/filepath"\
    "regexp"\
    "strconv"\
    "strings"\
    "sync"\
19 unmodified lines\
\
var sshTokenWarningOnce sync.Once //nolint:gochecknoglobals // intentional per-process gate\
\
// nonInteractiveSSHKey marks a context whose checkpoint git subprocesses must\
// never block on an interactive SSH prompt (e.g. a key passphrase when no\
// ssh-agent is running).\
type nonInteractiveSSHKey struct{}\
\
// WithNonInteractiveSSH marks ctx so every checkpoint git command spawned under\
// it runs SSH with BatchMode=yes, failing fast instead of hanging on an\
// interactive prompt. Set this at best-effort, non-interactive entry points such\
// as the git pre-push hook: a blocked passphrase prompt there would hang the\
// user's own `git push` until the checkpoint push budget kills it, with no way\
// to type the passphrase. Foreground commands (resume, explain) leave it unset\
// so they can still prompt.\
//\
// BatchMode tradeoffs (issue #1523):\
//   - Passphrase-protected keys with no ssh-agent: fail fast (desired).\
//   - Touch-only security keys (sk-, user-presence only): still work — touch is\
//     not a terminal passphrase read.\
//   - PIN-protected FIDO2 keys (verify-required): PIN entry goes through ssh's\
//     passphrase reader, so BatchMode suppresses it and the push fails. Load the\
//     key into ssh-agent beforehand, or set an explicit BatchMode=no via\
//     GIT_SSH_COMMAND / core.sshCommand (respected; we do not override it).\
func WithNonInteractiveSSH(ctx context.Context) context.Context {\
    return context.WithValue(ctx, nonInteractiveSSHKey{}, true)\
}\
\
// IsNonInteractiveSSH reports whether ctx was marked with WithNonInteractiveSSH.\
func IsNonInteractiveSSH(ctx context.Context) bool {\
    return nonInteractiveSSHFromContext(ctx)\
}\
\
func nonInteractiveSSHFromContext(ctx context.Context) bool {\
    v, ok := ctx.Value(nonInteractiveSSHKey{}).(bool)\
    return ok && v\
}\
\
// LooksLikeSSHAuthFailure reports whether errText looks like an SSH\
// authentication failure (passphrase/PIN unavailable under BatchMode, missing\
// agent identity, publickey rejection, etc.). Used to print an actionable\
// ssh-agent hint from the pre-push checkpoint path.\
func LooksLikeSSHAuthFailure(errText string) bool {\
    if errText == "" {\
        return false\
    }\
    lower := strings.ToLower(errText)\
    // Keep needles auth-specific. Do not match git's generic\
    // "Could not read from remote repository" epilogue — that also appears on\
    // network failures where an ssh-agent hint would be wrong. Real auth\
    // failures always include a Permission denied / auth-methods line too.\
    needles := []string{\
        "permission denied (publickey)",\
        "permission denied (keyboard-interactive",\
        "permission denied (password)",\
        "too many authentication failures",\
        "no more authentication methods to try",\
    }\
    for _, n := range needles {\
        if strings.Contains(lower, n) {\
            return true\
        }\
    }\
    // Generic publickey denial without the parenthetical form.\
    if strings.Contains(lower, "permission denied") && strings.Contains(lower, "publickey") {\
        return true\
    }\
    return false\
}\
\
// batchModeOptionRe matches an explicit BatchMode ssh option (e.g.\
// "-o BatchMode=yes" or "BatchMode=no"), case-insensitively. Anchored with \b\
// so it doesn't false-positive on unrelated text that merely contains\
// "BatchMode" as a substring of a longer token.\
var batchModeOptionRe = regexp.MustCompile(`(?i)\bBatchMode\s*=\s*\S+`)\
\
// hasExplicitBatchMode reports whether sshCmd already sets a BatchMode option,\
// with any value. A user-supplied BatchMode=no is a deliberate choice and must\
// be respected, not silently overridden to yes.\
func hasExplicitBatchMode(sshCmd string) bool {\
    return batchModeOptionRe.MatchString(sshCmd)\
}\
\
// envLookup returns the value of the last occurrence of key in env (matching\
// exec.Cmd's last-wins semantics for duplicate entries) and whether it was\
// found.\
func envLookup(env []string, key string) (string, bool) {\
    prefix := key + "="\
    for i := len(env) - 1; i >= 0; i-- {\
        if v, ok := strings.CutPrefix(env[i], prefix); ok {\
            return v, true\
        }\
    }\
    return "", false\
}\
\
// gitConfigSSHCommand looks up core.sshCommand via `git config`, run with env\
// so the lookup honors any HOME/GIT_CONFIG_* overrides present in env (e.g. in\
// tests). Returns "" if unset or the lookup fails.\
func gitConfigSSHCommand(ctx context.Context, env []string) string {\
    cmd := exec.CommandContext(ctx, "git", "config", "--get", "core.sshCommand")\
    cmd.Env = env\
    out, err := cmd.Output()\
    if err != nil {\
        return ""\
    }\
    return strings.TrimSpace(string(out))\
}\
\
// effectiveSSHCommand resolves the ssh invocation git itself would use, in\
// git's own precedence order: the GIT_SSH_COMMAND environment variable, then\
// the core.sshCommand git config value, then the GIT_SSH environment\
// variable, falling back to plain "ssh" when none are set.\
func effectiveSSHCommand(ctx context.Context, env []string) string {\
    if v, ok := envLookup(env, "GIT_SSH_COMMAND"); ok {\
        if trimmed := strings.TrimSpace(v); trimmed != "" {\
            return trimmed\
        }\
    }\
    if v := gitConfigSSHCommand(ctx, env); v != "" {\
        return v\
    }\
    if v, ok := envLookup(env, "GIT_SSH"); ok {\
        if trimmed := strings.TrimSpace(v); trimmed != "" {\
            return trimmed\
        }\
    }\
    return "ssh"\
}\
\
// withBatchModeSSH returns env with GIT_SSH_COMMAND set so ssh runs with\
// BatchMode=yes. The base ssh invocation is resolved via effectiveSSHCommand\
// (env GIT_SSH_COMMAND > core.sshCommand > GIT_SSH > plain "ssh") so a custom\
// ssh command configured via core.sshCommand isn't silently discarded. The\
// flag is only appended when BatchMode isn't already explicitly set — an\
// existing BatchMode=no is a deliberate user choice and is left untouched —\
// so the result is idempotent.\
func withBatchModeSSH(ctx context.Context, env []string) []string {\
    const key = "GIT_SSH_COMMAND="\
    base := effectiveSSHCommand(ctx, env)\
    out := make([]string, 0, len(env)+1)\
    for _, e := range env {\
        if strings.HasPrefix(e, key) {\
            continue\
        }\
        out = append(out, e)\
    }\
    if !hasExplicitBatchMode(base) {\
        base += " -o BatchMode=yes"\
    }\
    return append(out, key+base)\
}\
\
// applyNonInteractiveSSH sets BatchMode SSH on cmd when ctx is marked\
// non-interactive (see WithNonInteractiveSSH). No-op otherwise, so foreground\
// commands keep their interactive prompt behavior.\
func applyNonInteractiveSSH(ctx context.Context, cmd *exec.Cmd) {\
    if !nonInteractiveSSHFromContext(ctx) {\
        return\
    }\
    if cmd.Env == nil {\
        cmd.Env = os.Environ()\
    }\
    cmd.Env = withBatchModeSSH(ctx, cmd.Env)\
}\
\
// FetchOptions configures a git fetch operation.\
type FetchOptions struct {\
    Remote   string   // remote name or URL (required)\
318 unmodified lines\
\
        c := exec.CommandContext(ctx, "git", finalArgs...)\
        c.Stdin = nil // Disconnect stdin to prevent hanging in hook context\
        terminateOnCancel(c)\
        // Fail fast on interactive SSH prompts (e.g. a key passphrase with no\
        // ssh-agent) when the caller marked ctx non-interactive. HTTPS token\
        // auth rebuilds cmd.Env below (SSH is not used there), so this only\
        // takes effect on the SSH/no-token paths that actually run ssh.\
        applyNonInteractiveSSH(ctx, c)\
        return c\
    }\
```\
\
Mcmd/entire/cli/checkpoint/remote/git.go+169\
\
```\
1088 unmodified lines\
\
1089\
1090\
1091\
1092\
1093\
1094\
1095\
1096\
1097\
1098\
1099\
1100\
1101\
1102\
1103\
1104\
1105\
1106\
1107\
1108\
1109\
1110\
1111\
1112\
1113\
1114\
1115\
1116\
1117\
1118\
1119\
1120\
1121\
1122\
1123\
1124\
1125\
1126\
1127\
1128\
1129\
1130\
1131\
1132\
1133\
1134\
1135\
1136\
1137\
1138\
1139\
1140\
1141\
1142\
1143\
1144\
1145\
1146\
1147\
1148\
1149\
1150\
1151\
1152\
1153\
1154\
1155\
1156\
1157\
1158\
1159\
1160\
1161\
1162\
1163\
1164\
1165\
1166\
1167\
1168\
1169\
1170\
1171\
1172\
1173\
1174\
1175\
1176\
1177\
1178\
1179\
1180\
1181\
1182\
1183\
1184\
1185\
1186\
1187\
1188\
1189\
1190\
1191\
1192\
1193\
1194\
1195\
1196\
1197\
1198\
1199\
1200\
1201\
1202\
1203\
1204\
1205\
1206\
1207\
1208\
1209\
1210\
1211\
1212\
1213\
1214\
1215\
1216\
1217\
1218\
1219\
1220\
1221\
1222\
1223\
1224\
1225\
1226\
1227\
1228\
1229\
1230\
1231\
1232\
1233\
1234\
1235\
1236\
1237\
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\
\
1088 unmodified lines\
\
    assert.True(t, gitConfigBool(context.Background(), repoDir, "remote."+url+".skipFetchAll"),\
        "stamp must land even though the parent context is cancelled")\
}\
\
// isolatedSSHEnv returns a hermetic env slice for withBatchModeSSH tests: a\
// fresh HOME with no .gitconfig and system/global config lookups disabled, so\
// the effective ssh command resolution isn't polluted by the host machine's\
// real git config. extra entries (e.g. GIT_SSH_COMMAND, GIT_SSH, or a\
// GIT_CONFIG_GLOBAL pointing at a fixture config) are appended on top.\
func isolatedSSHEnv(t *testing.T, extra ...string) []string {\
    t.Helper()\
    env := []string{\
        "PATH=" + os.Getenv("PATH"),\
        "HOME=" + t.TempDir(),\
        "GIT_CONFIG_NOSYSTEM=1",\
    }\
    return append(env, extra...)\
}\
\
func TestWithBatchModeSSH(t *testing.T) {\
    t.Parallel()\
\
    // gitConfigFile writes a minimal gitconfig with core.sshCommand set and\
    // returns a GIT_CONFIG_GLOBAL env entry pointing at it.\
    gitConfigFile := func(t *testing.T, sshCommand string) string {\
        t.Helper()\
        dir := t.TempDir()\
        path := filepath.Join(dir, "gitconfig")\
        content := fmt.Sprintf("[core]\n\tsshCommand = %s\n", sshCommand)\
        require.NoError(t, os.WriteFile(path, []byte(content), 0o600))\
        return "GIT_CONFIG_GLOBAL=" + path\
    }\
\
    tests := []struct {\
        name string\
        in   func(t *testing.T) []string\
        want string\
    }{\
        {\
            name: "no existing GIT_SSH_COMMAND or config defaults to ssh",\
            in:   func(t *testing.T) []string { return isolatedSSHEnv(t) },\
            want: "ssh -o BatchMode=yes",\
        },\
        {\
            name: "preserves and extends a custom ssh command",\
            in: func(t *testing.T) []string {\
                return isolatedSSHEnv(t, "GIT_SSH_COMMAND=ssh -i /home/me/.ssh/id")\
            },\
            want: "ssh -i /home/me/.ssh/id -o BatchMode=yes",\
        },\
        {\
            name: "GIT_SSH_COMMAND with explicit BatchMode=yes is left untouched",\
            in: func(t *testing.T) []string {\
                return isolatedSSHEnv(t, "GIT_SSH_COMMAND=ssh -o BatchMode=yes")\
            },\
            want: "ssh -o BatchMode=yes",\
        },\
        {\
            name: "GIT_SSH_COMMAND with explicit BatchMode=no is respected, not overridden",\
            in: func(t *testing.T) []string {\
                return isolatedSSHEnv(t, "GIT_SSH_COMMAND=ssh -o BatchMode=no")\
            },\
            want: "ssh -o BatchMode=no",\
        },\
        {\
            name: "blank GIT_SSH_COMMAND falls back to ssh",\
            in: func(t *testing.T) []string {\
                return isolatedSSHEnv(t, "GIT_SSH_COMMAND=   ")\
            },\
            want: "ssh -o BatchMode=yes",\
        },\
        {\
            name: "core.sshCommand git config is used as the base when env is unset",\
            in: func(t *testing.T) []string {\
                cfg := gitConfigFile(t, "ssh -i /home/me/.ssh/work_key")\
                return isolatedSSHEnv(t, cfg)\
            },\
            want: "ssh -i /home/me/.ssh/work_key -o BatchMode=yes",\
        },\
        {\
            name: "GIT_SSH_COMMAND env takes precedence over core.sshCommand config",\
            in: func(t *testing.T) []string {\
                cfg := gitConfigFile(t, "ssh -i /home/me/.ssh/work_key")\
                return isolatedSSHEnv(t, cfg, "GIT_SSH_COMMAND=ssh -i /home/me/.ssh/personal_key")\
            },\
            want: "ssh -i /home/me/.ssh/personal_key -o BatchMode=yes",\
        },\
        {\
            name: "GIT_SSH is used only when neither env GIT_SSH_COMMAND nor config are set",\
            in: func(t *testing.T) []string {\
                return isolatedSSHEnv(t, "GIT_SSH=/usr/local/bin/custom-ssh")\
            },\
            want: "/usr/local/bin/custom-ssh -o BatchMode=yes",\
        },\
        {\
            name: "unrelated substring containing BatchMode-like text does not count as explicit",\
            in: func(t *testing.T) []string {\
                return isolatedSSHEnv(t, `GIT_SSH_COMMAND=ssh -o ProxyCommand="connect -H proxy NoBatchModeHereEither"`)\
            },\
            want: `ssh -o ProxyCommand="connect -H proxy NoBatchModeHereEither" -o BatchMode=yes`,\
        },\
    }\
\
    for _, tt := range tests {\
        t.Run(tt.name, func(t *testing.T) {\
            t.Parallel()\
            out := withBatchModeSSH(context.Background(), tt.in(t))\
            got, ok := envToMap(out)["GIT_SSH_COMMAND"]\
            assert.True(t, ok, "GIT_SSH_COMMAND should be set")\
            assert.Equal(t, tt.want, got)\
        })\
    }\
}\
\
func TestWithBatchModeSSH_PreservesOtherVarsWithoutDuplicating(t *testing.T) {\
    t.Parallel()\
\
    env := isolatedSSHEnv(t, "GIT_SSH_COMMAND=ssh")\
    env = append(env, "SOME_OTHER_VAR=value")\
    out := withBatchModeSSH(context.Background(), env)\
\
    count := 0\
    for _, e := range out {\
        if strings.HasPrefix(e, "GIT_SSH_COMMAND=") {\
            count++\
        }\
    }\
    assert.Equal(t, 1, count, "should not duplicate GIT_SSH_COMMAND")\
\
    m := envToMap(out)\
    assert.Equal(t, "value", m["SOME_OTHER_VAR"])\
    assert.Equal(t, "ssh -o BatchMode=yes", m["GIT_SSH_COMMAND"])\
}\
\
// TestNewCommand_NonInteractiveSSH verifies that a checkpoint git command built\
// under a non-interactive context carries GIT_SSH_COMMAND with BatchMode=yes, so\
// an SSH push cannot hang on a passphrase prompt (issue #1523). Without the\
// marker, the command is left untouched so foreground commands keep interactive\
// prompting.\
func TestNewCommand_NonInteractiveSSH(t *testing.T) {\
    // Not parallel: manipulates the checkpoint token env var.\
    t.Setenv(CheckpointTokenEnvVar, "") // ensure SSH/no-token path\
\
    t.Run("marked context adds BatchMode", func(t *testing.T) {\
        ctx := WithNonInteractiveSSH(context.Background())\
        cmd := newCommand(ctx, "push", "--no-verify", "origin", "entire/checkpoints/v1")\
        sshCmd, ok := envToMap(cmd.Env)["GIT_SSH_COMMAND"]\
        assert.True(t, ok, "non-interactive command must set GIT_SSH_COMMAND")\
        assert.Contains(t, sshCmd, "BatchMode=yes")\
    })\
\
    t.Run("unmarked context leaves env untouched", func(t *testing.T) {\
        cmd := newCommand(context.Background(), "push", "--no-verify", "origin", "entire/checkpoints/v1")\
        // No token and no marker: newCommand should not populate cmd.Env, so no\
        // BatchMode is injected and the process inherits the parent environment.\
        assert.Nil(t, cmd.Env, "unmarked command should not set a custom env")\
    })\
}\
\
func TestLooksLikeSSHAuthFailure(t *testing.T) {\
    t.Parallel()\
    cases := []struct {\
        in   string\
        want bool\
    }{\
        {"git push: Permission denied (publickey).", true},\
        {"Permission denied (publickey,password).", true},\
        {"ERROR: Permission denied (publickey).\r\nfatal: Could not read from remote repository.", true},\
        {"fatal: Could not read from remote repository.", false}, // generic transport epilogue, not auth\
        {"ssh: connect to host example.com port 22: Connection refused\nfatal: Could not read from remote repository.", false},\
        {"enter passphrase for key '/home/me/.ssh/id_rsa':", false},\
        {"non-fast-forward", false},\
        {"Connection timed out", false},\
        {"", false},\
    }\
    for _, tt := range cases {\
        t.Run(tt.in, func(t *testing.T) {\
            t.Parallel()\
            assert.Equal(t, tt.want, LooksLikeSSHAuthFailure(tt.in))\
        })\
    }\
}\
\
func TestIsNonInteractiveSSH(t *testing.T) {\
    t.Parallel()\
    assert.False(t, IsNonInteractiveSSH(context.Background()))\
    assert.True(t, IsNonInteractiveSSH(WithNonInteractiveSSH(context.Background())))\
}\
```\
\
Mcmd/entire/cli/checkpoint/remote/git\_test.go+185\
\
```\
52 unmodified lines\
\
53\
54\
55\
56\
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\
66\
81\
82\
83\
84\
85\
86\
87\
88\
89\
90\
91\
92\
93\
94\
95\
71\
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\
\
52 unmodified lines\
\
    return cmd\
}\
\
// newCheckpointListCmd wraps the existing branch-default list view.\
// newCheckpointListCmd wraps the existing branch-default list view and adds\
// machine-readable (--json) and pending-rewind-point (--pending) modes.\
//\
// Dataset/format matrix:\
//\
//	(default)            condensed checkpoints on the branch, human view (pager)\
//	--json               condensed checkpoints as JSON (branchCheckpointJSON shape)\
//	--pending            live shadow-branch rewind points, human list\
//	--pending --json     live shadow-branch rewind points as JSON — the drop-in\
//	                     replacement for the deprecated `rewind --list` bridge\
//\
// The condensed dataset (entire/checkpoints/v1 for the branch) and the pending\
// dataset (strategy.GetRewindPoints; task checkpoints, logs-only points,\
// condensation IDs) are deliberately distinct — see issue #1767.\
func newCheckpointListCmd() *cobra.Command {\
    var sessionFlag string\
    var noPagerFlag bool\
    var jsonFlag bool\
    var pendingFlag bool\
\
    cmd := &cobra.Command{\
        Use:   "list",\
        Short: "List checkpoints on the current branch",\
        Long: `List checkpoints on the current branch.\
\
Optionally filter by session ID with --session.`,\
By default shows condensed checkpoints from the checkpoints branch for the\
current branch. Use --pending to list the live session's shadow-branch rewind\
points instead (task checkpoints, logs-only points, condensation IDs).\
\
Output modes:\
  --json             Machine-readable JSON instead of the human view.\
  --pending          Select the live shadow-branch rewind-point dataset.\
  --pending --json   Rewind points as JSON (replaces the deprecated rewind --list).\
\
Optionally filter condensed checkpoints by session ID with --session\
(not applicable with --pending).`,\
        RunE: func(cmd *cobra.Command, _ []string) error {\
            if checkDisabledGuard(cmd.Context(), cmd.OutOrStdout()) {\
                return nil\
            }\
            return runExplainBranchWithFilter(cmd.Context(), cmd.OutOrStdout(), cmd.ErrOrStderr(), noPagerFlag, sessionFlag)\
            ctx := cmd.Context()\
            w := cmd.OutOrStdout()\
            errW := cmd.ErrOrStderr()\
\
            // --session filters the condensed dataset only; the pending dataset\
            // mirrors the historical rewind --list, which had no session filter.\
            if pendingFlag && sessionFlag != "" {\
                return errors.New("--session cannot be combined with --pending")\
            }\
\
            switch {\
            case pendingFlag && jsonFlag:\
                return runCheckpointPendingListJSON(ctx, w)\
            case pendingFlag:\
                return runCheckpointPendingListHuman(ctx, w)\
            case jsonFlag:\
                return runExplainListJSON(ctx, w, errW, sessionFlag, 0)\
            default:\
                return runExplainBranchWithFilter(ctx, w, errW, noPagerFlag, sessionFlag)\
            }\
        },\
    }\
\
    cmd.Flags().StringVar(&sessionFlag, "session", "", "Filter checkpoints by session ID (or prefix)")\
    cmd.Flags().BoolVar(&noPagerFlag, "no-pager", false, "Disable pager output")\
    cmd.Flags().BoolVar(&jsonFlag, "json", false, "Output as JSON instead of the human view")\
    cmd.Flags().BoolVar(&pendingFlag, "pending", false, "List the live session's shadow-branch rewind points instead of condensed checkpoints")\
    return cmd\
}\
```\
\
Mcmd/entire/cli/checkpoint\_group.go+49/-3\
\
```\
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\
\
package cli\
\
import (\
    "context"\
    "fmt"\
    "io"\
    "time"\
\
    "github.com/entireio/cli/cmd/entire/cli/jsonutil"\
    "github.com/entireio/cli/cmd/entire/cli/strategy"\
)\
\
// pendingRewindPointJSON is the machine-readable shape emitted by\
// `entire checkpoint list --pending --json` (and the deprecated `rewind --list`\
// bridge). It is byte-for-byte the JSON that `rewind --list` historically\
// produced, so downstream consumers (integration and e2e test harnesses,\
// external scripts) that parsed `rewind --list` keep working unchanged after\
// repointing to `checkpoint list --pending --json`.\
//\
// The field set, JSON names, omitempty markers, and the RFC3339 Date encoding\
// are load-bearing — this is a stable contract. CondensationID carries the\
// checkpoint ID (RewindPoint.CheckpointID) for logs-only points; it is empty\
// for shadow-branch (uncommitted) points. Do not change these without\
// migrating every consumer.\
type pendingRewindPointJSON struct {\
    ID               string `json:"id"`\
    Message          string `json:"message"`\
    MetadataDir      string `json:"metadata_dir"`\
    Date             string `json:"date"`\
    IsTaskCheckpoint bool   `json:"is_task_checkpoint"`\
    ToolUseID        string `json:"tool_use_id,omitempty"`\
    IsLogsOnly       bool   `json:"is_logs_only"`\
    CondensationID   string `json:"condensation_id,omitempty"`\
    SessionID        string `json:"session_id,omitempty"`\
    SessionPrompt    string `json:"session_prompt,omitempty"`\
}\
\
// pendingRewindPointsLimit caps how many live shadow-branch rewind points the\
// pending views request. Matches the historical `rewind --list` cap of 20 so\
// the migrated output is identical.\
const pendingRewindPointsLimit = 20\
\
// runCheckpointPendingListJSON emits the live shadow-branch rewind points as\
// JSON. This is the drop-in replacement for (and the implementation behind)\
// the deprecated `rewind --list` bridge: same dataset (strategy.GetRewindPoints),\
// same cap, same JSON shape.\
func runCheckpointPendingListJSON(ctx context.Context, w io.Writer) error {\
    start := GetStrategy(ctx)\
\
    points, err := start.GetRewindPoints(ctx, pendingRewindPointsLimit)\
    if err != nil {\
        return fmt.Errorf("failed to find rewind points: %w", err)\
    }\
\
    output := make([]pendingRewindPointJSON, len(points))\
    for i, p := range points {\
        output[i] = pendingRewindPointJSON{\
            ID:               p.ID,\
            Message:          p.Message,\
            MetadataDir:      p.MetadataDir,\
            Date:             p.Date.Format(time.RFC3339),\
            IsTaskCheckpoint: p.IsTaskCheckpoint,\
            ToolUseID:        p.ToolUseID,\
            IsLogsOnly:       p.IsLogsOnly,\
            CondensationID:   p.CheckpointID.String(),\
            SessionID:        p.SessionID,\
            SessionPrompt:    p.SessionPrompt,\
        }\
    }\
\
    data, err := jsonutil.MarshalIndentWithNewline(output, "", "  ")\
    if err != nil {\
        return err //nolint:wrapcheck // parity with the former rewind --list path\
    }\
    fmt.Fprintln(w, string(data))\
    return nil\
}\
\
// runCheckpointPendingListHuman prints the live shadow-branch rewind points in\
// a human-readable list. `rewind --list` was JSON-only, so there is no legacy\
// human output to mirror; this renders each point with the same label format\
// the former interactive rewind picker used (see rewindPointLabel).\
func runCheckpointPendingListHuman(ctx context.Context, w io.Writer) error {\
    start := GetStrategy(ctx)\
\
    points, err := start.GetRewindPoints(ctx, pendingRewindPointsLimit)\
    if err != nil {\
        return fmt.Errorf("failed to find rewind points: %w", err)\
    }\
\
    if len(points) == 0 {\
        fmt.Fprintln(w, "No pending rewind points found.")\
        fmt.Fprintln(w, "Pending rewind points are created automatically during active agent sessions.")\
        return nil\
    }\
\
    multi := hasMultipleSessions(points)\
    for _, p := range points {\
        fmt.Fprintln(w, rewindPointLabel(p, multi))\
    }\
    return nil\
}\
\
// hasMultipleSessions reports whether the points span more than one session,\
// which controls whether per-line session identifiers are shown.\
func hasMultipleSessions(points []strategy.RewindPoint) bool {\
    sessionIDs := make(map[string]bool)\
    for _, p := range points {\
        if p.SessionID != "" {\
            sessionIDs[p.SessionID] = true\
        }\
    }\
    return len(sessionIDs) > 1\
}\
\
// rewindPointLabel renders a single rewind point as a display label. Shared by\
// the interactive rewind picker (runRewindInteractive) and the\
// `checkpoint list --pending` human view so both stay in sync. When\
// hasMultipleSessions is true, a sanitized session prompt is appended to help\
// disambiguate concurrent sessions.\
func rewindPointLabel(p strategy.RewindPoint, hasMultipleSessions bool) string {\
    timestamp := p.Date.Format("2006-01-02 15:04")\
\
    sessionLabel := ""\
    if hasMultipleSessions && p.SessionPrompt != "" {\
        sessionLabel = fmt.Sprintf(" [%s]", sanitizeForTerminal(p.SessionPrompt))\
    }\
\
    switch {\
    case p.IsLogsOnly:\
        // Committed checkpoint - show commit sha (this is the real user commit)\
        shortID := p.ID\
        if len(shortID) >= 7 {\
            shortID = shortID[:7]\
        }\
        return fmt.Sprintf("%s (%s) %s%s", shortID, timestamp, sanitizeForTerminal(p.Message), sessionLabel)\
    case p.IsTaskCheckpoint:\
        // Task checkpoint (uncommitted) - no sha shown\
        return fmt.Sprintf("        (%s) [Task] %s%s", timestamp, sanitizeForTerminal(p.Message), sessionLabel)\
    default:\
        // Shadow checkpoint (uncommitted) - no sha shown (internal commit)\
        return fmt.Sprintf("        (%s) %s%s", timestamp, sanitizeForTerminal(p.Message), sessionLabel)\
    }\
}\
```\
\
Acmd/entire/cli/checkpoint\_list.go+144\
\
```\
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\
\
package cli\
\
import (\
    "bytes"\
    "context"\
    "encoding/json"\
    "os"\
    "path/filepath"\
    "strings"\
    "testing"\
    "time"\
\
    "github.com/entireio/cli/cmd/entire/cli/checkpoint"\
    "github.com/entireio/cli/cmd/entire/cli/jsonutil"\
    "github.com/entireio/cli/cmd/entire/cli/paths"\
    "github.com/entireio/cli/cmd/entire/cli/testutil"\
    "github.com/go-git/go-git/v6"\
    "github.com/go-git/go-git/v6/plumbing/object"\
    "github.com/stretchr/testify/require"\
)\
\
// TestRewindListBridge_ForwardsToPendingJSON verifies the deprecated\
// `rewind --list` bridge still works for external scripts: the JSON payload\
// matches `checkpoint list --pending --json`, and stderr carries the\
// migration hint. Cobra prints the command-level Deprecated notice to stdout\
// before RunE (Printf); consumers already tolerate that — the bridge itself\
// must not add further stdout noise (hint goes to stderr only).\
func TestRewindListBridge_ForwardsToPendingJSON(t *testing.T) {\
    setupCheckpointListRepo(t)\
\
    canonical := runListCmd(t, "--pending", "--json")\
\
    cmd := newRewindCmd()\
    var stdout, stderr bytes.Buffer\
    cmd.SetArgs([]string{"--list"})\
    cmd.SetOut(&stdout)\
    cmd.SetErr(&stderr)\
    require.NoError(t, cmd.Execute(), "rewind --list failed; stderr: %s", stderr.String())\
\
    out := stdout.String()\
    jsonStart := strings.IndexAny(out, "[{")\
    require.GreaterOrEqual(t, jsonStart, 0, "stdout must contain a JSON payload; got: %q", out)\
    require.Equal(t, canonical, out[jsonStart:],\
        "rewind --list JSON payload must match checkpoint list --pending --json")\
    require.Contains(t, stderr.String(),\
        "note: 'rewind --list' is deprecated; use 'entire checkpoint list --pending --json'",\
        "stderr must carry the migration hint")\
    require.NotContains(t, out[jsonStart:], "deprecated",\
        "bridge must not inject deprecation text into the JSON payload")\
}\
\
// TestPendingRewindPointJSON_MatchesRewindListContract pins the machine-readable\
// shape emitted by `checkpoint list --pending --json`. It must stay byte-for-byte\
// compatible with the JSON `rewind --list` historically produced, so consumers that\
// parsed rewind --list keep working after repointing. Field names, omitempty\
// behavior, and the RFC3339 date encoding are the contract.\
func TestPendingRewindPointJSON_MatchesRewindListContract(t *testing.T) {\
    t.Parallel()\
\
    fixed := time.Date(2026, 7, 9, 12, 30, 0, 0, time.UTC)\
\
    // A logs-only (committed) point: has condensation_id, session_id,\
    // session_prompt; tool_use_id is empty and must be omitted.\
    logsOnly := pendingRewindPointJSON{\
        ID:               "abc123def456",\
        Message:          "user commit",\
        MetadataDir:      ".entire/metadata/s1",\
        Date:             fixed.Format(time.RFC3339),\
        IsTaskCheckpoint: false,\
        IsLogsOnly:       true,\
        CondensationID:   "deadbeefcafe",\
        SessionID:        "s1",\
        SessionPrompt:    "do the thing",\
    }\
    // A task (shadow, uncommitted) point: has tool_use_id; condensation_id,\
    // session_id, session_prompt are empty and must be omitted. metadata_dir and\
    // the two bools have no omitempty and must always render.\
    task := pendingRewindPointJSON{\
        ID:               "0f1e2d3c",\
        Message:          "task step",\
        MetadataDir:      "",\
        Date:             fixed.Format(time.RFC3339),\
        IsTaskCheckpoint: true,\
        ToolUseID:        "toolu_123",\
        IsLogsOnly:       false,\
    }\
\
    data, err := jsonutil.MarshalIndentWithNewline([]pendingRewindPointJSON{logsOnly, task}, "", "  ")\
    require.NoError(t, err)\
\
    var got []map[string]any\
    require.NoError(t, json.Unmarshal(data, &got))\
    require.Len(t, got, 2)\
\
    // logs-only point: exact key set.\
    require.ElementsMatch(t,\
        []string{"id", "message", "metadata_dir", "date", "is_task_checkpoint", "is_logs_only", "condensation_id", "session_id", "session_prompt"},\
        keysOf(got[0]),\
        "logs-only point keys must match the rewind --list contract (tool_use_id omitted when empty)")\
    require.Equal(t, "abc123def456", got[0]["id"])\
    require.Equal(t, "deadbeefcafe", got[0]["condensation_id"])\
    require.Equal(t, fixed.Format(time.RFC3339), got[0]["date"])\
\
    // task point: tool_use_id present; condensation_id/session_id/session_prompt\
    // omitted; metadata_dir + bools always present.\
    require.ElementsMatch(t,\
        []string{"id", "message", "metadata_dir", "date", "is_task_checkpoint", "tool_use_id", "is_logs_only"},\
        keysOf(got[1]),\
        "task point keys must match the rewind --list contract")\
    require.Equal(t, "toolu_123", got[1]["tool_use_id"])\
    require.Equal(t, true, got[1]["is_task_checkpoint"])\
    require.Empty(t, got[1]["metadata_dir"])\
}\
\
func keysOf(m map[string]any) []string {\
    ks := make([]string, 0, len(m))\
    for k := range m {\
        ks = append(ks, k)\
    }\
    return ks\
}\
\
// TestRunCheckpointPendingList_EmptyReturnsEmptyArray verifies the pending JSON\
// view emits `[]` (not `null`) when there are no rewind points — the drop-in\
// contract downstream JSON parsers rely on.\
func TestRunCheckpointPendingList_EmptyReturnsEmptyArray(t *testing.T) {\
    setupCheckpointListRepo(t)\
\
    var stdout bytes.Buffer\
    require.NoError(t, runCheckpointPendingListJSON(context.Background(), &stdout))\
    // Byte-for-byte match with the historical `rewind --list` output: an empty\
    // array, and the trailing double newline (MarshalIndentWithNewline appends\
    // one, Fprintln another). Consumers parse via json.Unmarshal, which tolerates\
    // trailing whitespace; the exactness protects the drop-in contract.\
    require.Equal(t, "[]\n\n", stdout.String())\
    require.Equal(t, "[]", strings.TrimSpace(stdout.String()))\
}\
\
// TestRunCheckpointPendingListHuman_Empty pins the human-view message shown when\
// no pending rewind points exist.\
func TestRunCheckpointPendingListHuman_Empty(t *testing.T) {\
    setupCheckpointListRepo(t)\
\
    var stdout bytes.Buffer\
    require.NoError(t, runCheckpointPendingListHuman(context.Background(), &stdout))\
    require.Contains(t, stdout.String(), "No pending rewind points found.")\
}\
\
// TestCheckpointListCmd_Routing drives the real `checkpoint list` command\
// end-to-end and asserts each flag combination routes to the right\
// dataset/renderer. The repo is seeded with a shadow checkpoint so the\
// condensed dataset is non-empty; the pending dataset stays empty because\
// GetRewindPoints requires active-session state (created by lifecycle hooks,\
// exercised by the integration canary), which a raw ephemeral-store seed does\
// not register — this also demonstrates the two datasets are distinct.\
func TestCheckpointListCmd_Routing(t *testing.T) {\
    setupCheckpointListRepoWithShadowCheckpoint(t)\
\
    // --json → condensed dataset, branchCheckpointJSON shape.\
    condensed := runListCmd(t, "--json")\
    require.True(t, json.Valid([]byte(condensed)), "condensed --json must be valid JSON, got: %s", condensed)\
    require.Contains(t, condensed, `"checkpoint_id"`, "condensed --json must use branchCheckpointJSON shape")\
    require.NotContains(t, condensed, `"metadata_dir"`, "condensed --json must not carry pending-only fields")\
\
    // --pending (human) → pending renderer, empty here.\
    pendingHuman := runListCmd(t, "--pending")\
    require.Contains(t, pendingHuman, "No pending rewind points found.",\
        "--pending (human) must route to the pending human renderer")\
\
    // --pending --json → pending JSON renderer, empty array (distinct from the\
    // human renderer above and from the condensed dataset).\
    pendingJSON := runListCmd(t, "--pending", "--json")\
    require.JSONEq(t, "[]", pendingJSON,\
        "--pending --json must route to the pending JSON renderer")\
    require.NotContains(t, pendingJSON, `"checkpoint_id"`, "pending --json must never carry condensed-only fields")\
}\
\
// TestCheckpointListCmd_SessionWithPendingErrors verifies --session is rejected\
// with --pending (the pending dataset is session-agnostic, mirroring rewind --list).\
func TestCheckpointListCmd_SessionWithPendingErrors(t *testing.T) {\
    setupCheckpointListRepo(t)\
\
    cmd := newCheckpointListCmd()\
    cmd.SetArgs([]string{"--pending", "--session", "abc"})\
    cmd.SetOut(&bytes.Buffer{})\
    cmd.SetErr(&bytes.Buffer{})\
    err := cmd.Execute()\
    require.Error(t, err)\
    require.Contains(t, err.Error(), "--session cannot be combined with --pending")\
}\
\
// runListCmd executes `checkpoint list <args>` via the real cobra command and\
// returns stdout. Entire must already be enabled in CWD (setup helpers do this).\
func runListCmd(t *testing.T, args ...string) string {\
    t.Helper()\
    cmd := newCheckpointListCmd()\
    var stdout, stderr bytes.Buffer\
    cmd.SetArgs(args)\
    cmd.SetOut(&stdout)\
    cmd.SetErr(&stderr)\
    require.NoError(t, cmd.Execute(), "checkpoint list %v failed; stderr: %s", args, stderr.String())\
    return stdout.String()\
}\
\
// setupCheckpointListRepo initializes an enabled Entire repo with one commit in a\
// temp CWD. No checkpoints are seeded.\
func setupCheckpointListRepo(t *testing.T) (*git.Repository, string) {\
    t.Helper()\
    tmpDir := t.TempDir()\
    t.Chdir(tmpDir)\
\
    testutil.InitRepo(t, tmpDir)\
    repo, err := git.PlainOpen(tmpDir)\
    require.NoError(t, err)\
\
    w, err := repo.Worktree()\
    require.NoError(t, err)\
    require.NoError(t, os.WriteFile(filepath.Join(tmpDir, "test.txt"), []byte("initial"), 0o644))\
    _, err = w.Add("test.txt")\
    require.NoError(t, err)\
    _, err = w.Commit("initial commit", &git.CommitOptions{\
        Author: &object.Signature{Name: "Test", Email: "test@test.com", When: time.Now()},\
    })\
    require.NoError(t, err)\
\
    enableEntire(t, tmpDir)\
    return repo, tmpDir\
}\
\
// setupCheckpointListRepoWithShadowCheckpoint extends setupCheckpointListRepo by\
// seeding a checkpoint on the v1 metadata branch with real code changes, so the\
// condensed branch view is non-empty for routing tests.\
func setupCheckpointListRepoWithShadowCheckpoint(t *testing.T) {\
    t.Helper()\
    repo, tmpDir := setupCheckpointListRepo(t)\
\
    sessionID := "2026-07-09-list-test-session"\
    metadataDir := filepath.Join(tmpDir, ".entire", "metadata", sessionID)\
    require.NoError(t, os.MkdirAll(metadataDir, 0o750))\
    require.NoError(t, os.WriteFile(filepath.Join(metadataDir, paths.PromptFileName), []byte("seed prompt"), 0o644))\
    require.NoError(t, os.WriteFile(filepath.Join(metadataDir, "full.jsonl"), []byte(`{"test": true}`), 0o644))\
\
    head, err := repo.Head()\
    require.NoError(t, err)\
    baseCommit := head.Hash().String()[:7]\
\
    store := checkpoint.NewEphemeralStore(repo, checkpoint.DefaultV1Refs())\
    _, err = store.Write(context.Background(), checkpoint.Step{\
        SessionID:         sessionID,\
        BaseCommit:        baseCommit,\
        ModifiedFiles:     []string{"test.txt"},\
        MetadataDir:       ".entire/metadata/" + sessionID,\
        MetadataDirAbs:    metadataDir,\
        CommitMessage:     "First checkpoint (baseline)",\
        AuthorName:        "Test",\
        AuthorEmail:       "test@test.com",\
        IsFirstCheckpoint: true,\
    })\
    require.NoError(t, err)\
\
    require.NoError(t, os.WriteFile(filepath.Join(tmpDir, "test.txt"), []byte("second modification"), 0o644))\
    _, err = store.Write(context.Background(), checkpoint.Step{\
        SessionID:         sessionID,\
        BaseCommit:        baseCommit,\
        ModifiedFiles:     []string{"test.txt"},\
        MetadataDir:       ".entire/metadata/" + sessionID,\
        MetadataDirAbs:    metadataDir,\
        CommitMessage:     "Second checkpoint with code changes",\
        AuthorName:        "Test",\
        AuthorEmail:       "test@test.com",\
        IsFirstCheckpoint: false,\
    })\
    require.NoError(t, err)\
\
    // Sanity: the seeded checkpoint must be visible to the condensed branch view,\
    // otherwise the routing assertions below would pass vacuously on an empty array.\
    points, _, err := getBranchCheckpoints(context.Background(), repo, 10)\
    require.NoError(t, err)\
    require.NotEmpty(t, points, "seed must produce at least one branch checkpoint")\
}\
```\
\
Acmd/entire/cli/checkpoint\_list\_test.go+280\
\
```\
24 unmodified lines\
\
25\
26\
27\
28\
28\
29\
30\
31\
\
24 unmodified lines\
\
func isCheckpointPolicyWarningExcludedCommand(name string) bool {\
    switch name {\
    case "hooks", "__send_analytics", "curl-bash-post-install":\
    case "hooks", "__send_analytics", "__refresh_trail_enablement", "curl-bash-post-install":\
        return true\
    default:\
        return false\
```\
\
Mcmd/entire/cli/checkpoint\_policy\_warning.go+1/-1\
\
```\
46 unmodified lines\
\
47\
48\
49\
50\
51\
52\
53\
54\
55\
56\
57\
58\
\
46 unmodified lines\
\
    sendAnalytics := &cobra.Command{Use: "__send_analytics", Hidden: true}\
    root.AddCommand(sendAnalytics)\
\
    refreshTrailEnablement := &cobra.Command{Use: "__refresh_trail_enablement", Hidden: true}\
    root.AddCommand(refreshTrailEnablement)\
\
    require.True(t, ShouldCheckCheckpointPolicyWarning(visible))\
    require.True(t, ShouldCheckCheckpointPolicyWarning(hiddenAlias))\
    require.False(t, ShouldCheckCheckpointPolicyWarning(gitHook))\
    require.False(t, ShouldCheckCheckpointPolicyWarning(sendAnalytics))\
    require.False(t, ShouldCheckCheckpointPolicyWarning(refreshTrailEnablement))\
}\
```\
\
Mcmd/entire/cli/checkpoint\_policy\_warning\_test.go+4\
\
```\
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\
\
package execx\
\
import (\
    "context"\
    "io"\
    "os"\
    "os/exec"\
    "testing"\
)\
\
// SpawnDetached re-execs the current executable as a detached, fire-and-forget\
// child running args, surviving the parent's exit (new session on Unix,\
// CREATE_NEW_PROCESS_GROUP | DETACHED_PROCESS on Windows, via detachFromTTY).\
// The child runs in dir (os.TempDir() when empty, so the child never holds the\
// parent's working directory), inherits the parent's environment, and has its\
// stdout/stderr discarded. Best-effort: every error is swallowed — callers\
// treat the spawn as advisory background work.\
//\
// In-process `go test` runs are a no-op: the current executable is the test\
// binary, and re-execing it would fork the whole suite. Tests exercise the\
// call sites through their spawn seams instead.\
func SpawnDetached(dir string, args ...string) {\
    if testing.Testing() {\
        return\
    }\
    executable, err := os.Executable()\
    if err != nil {\
        return\
    }\
\
    // context.Background(): the child must outlive the parent, so it is never\
    // tied to a cancellable context.\
    cmd := exec.CommandContext(context.Background(), executable, args...)\
    detachFromTTY(cmd)\
    cmd.Dir = dir\
    if cmd.Dir == "" {\
        cmd.Dir = os.TempDir()\
    }\
    cmd.Env = os.Environ()\
    cmd.Stdout = io.Discard\
    cmd.Stderr = io.Discard\
\
    if err := cmd.Start(); err != nil {\
        return\
    }\
    // Release the process so it can run independently of the parent.\
    //nolint:errcheck // best effort — the child continues regardless\
    _ = cmd.Process.Release()\
}\
```\
\
Acmd/entire/cli/execx/spawn\_detached.go+49\
\
```\
103 unmodified lines\
\
104\
105\
106\
107\
108\
107\
108\
109\
110\
110\
111\
112\
113\
153 unmodified lines\
\
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\
\
103 unmodified lines\
\
    // Disable Entire\
    env.SetEnabled(false)\
\
    // Try to run checkpoint rewind --list - should show disabled message (not error)\
    stdout, err := env.RunCLIWithError("checkpoint", "rewind", "--list")\
    // Try to run checkpoint list --pending --json - should show disabled message (not error)\
    stdout, err := env.RunCLIWithError("checkpoint", "list", "--pending", "--json")\
    if err != nil {\
        t.Fatalf("checkpoint rewind --list command failed unexpectedly: %v\nOutput: %s", err, stdout)\
        t.Fatalf("checkpoint list --pending --json command failed unexpectedly: %v\nOutput: %s", err, stdout)\
    }\
    if !strings.Contains(stdout, "Entire is disabled") {\
        t.Errorf("Expected disabled message, got: %s", stdout)\
153 unmodified lines\
\
        t.Fatal("commit has no Entire-Checkpoint trailer — hooks silently no-op'd with only settings.local.json")\
    }\
}\
\
// TestEnableReenablesProjectScopeAfterProjectDisable is a full-flow\
// reproduction of a re-enable regression: after `entire disable --project`,\
// running `entire enable --checkpoint-remote ...` with no --project/--local\
// reported success but wrote the enabled flag to .entire/settings.local.json,\
// leaving the project .entire/settings.json the user disabled still\
// enabled=false.\
//\
// Because settings.local.json (enabled:true) overrides settings.json in the\
// merged view that both `entire status` and IsEnabled read, status actually\
// reported ENABLED — the effective state was correct and only the committed\
// file was stale. That still bites anyone without the local file (a fresh\
// clone, a teammate) and leaves the committed source of truth wrong.\
//\
// This drives the real entire binary end-to-end — enable, disable --project,\
// then a setup-flag re-enable — and asserts the PROJECT settings.json (the file\
// the user actually disabled) is enabled again.\
func TestEnableReenablesProjectScopeAfterProjectDisable(t *testing.T) {\
    t.Parallel()\
    env := NewTestEnv(t)\
    defer env.Cleanup()\
\
    env.InitRepo()\
\
    // First-time setup via the real binary; a plain enable writes the project\
    // .entire/settings.json.\
    env.RunCLI("enable", "--agent", "claude-code", "--telemetry=false")\
    assertProjectSettingsEnabled(t, env, true)\
\
    // Disable at the project scope → settings.json enabled=false.\
    env.RunCLI("disable", "--project")\
    assertProjectSettingsEnabled(t, env, false)\
\
    // Re-enable with a setup flag but WITHOUT --project/--local. Pre-fix the\
    // enabled flag landed in settings.local.json, so the project file the user\
    // disabled stayed enabled=false.\
    env.RunCLI("enable", "--checkpoint-remote", "github:org/repo", "--skip-push-sessions", "--telemetry=false")\
\
    assertProjectSettingsEnabled(t, env, true)\
\
    // And no local override may contradict it: settings.local.json must be\
    // absent or itself enabled:true, so the merged view can't silently flip\
    // back to disabled by accident of the flow.\
    assertLocalSettingsAbsentOrEnabled(t, env)\
}\
\
// assertLocalSettingsAbsentOrEnabled asserts that .entire/settings.local.json,\
// if present, does not carry an enabled:false override that would mask the\
// committed project scope.\
func assertLocalSettingsAbsentOrEnabled(t *testing.T, env *TestEnv) {\
    t.Helper()\
    localPath := filepath.Join(env.RepoDir, ".entire", "settings.local.json")\
    data, err := os.ReadFile(localPath)\
    if os.IsNotExist(err) {\
        return\
    }\
    if err != nil {\
        t.Fatalf("read .entire/settings.local.json: %v", err)\
    }\
    var s struct {\
        Enabled *bool `json:"enabled"`\
    }\
    if err := json.Unmarshal(data, &s); err != nil {\
        t.Fatalf("parse .entire/settings.local.json: %v\ncontent: %s", err, data)\
    }\
    if s.Enabled != nil && !*s.Enabled {\
        t.Fatalf("settings.local.json carries enabled:false, which would mask the re-enabled project scope\ncontent: %s", data)\
    }\
}\
\
// assertProjectSettingsEnabled reads .entire/settings.json (the project scope,\
// never settings.local.json) and asserts its enabled flag matches want.\
func assertProjectSettingsEnabled(t *testing.T, env *TestEnv, want bool) {\
    t.Helper()\
    settingsPath := filepath.Join(env.RepoDir, ".entire", "settings.json")\
    data, err := os.ReadFile(settingsPath)\
    if err != nil {\
        t.Fatalf("read .entire/settings.json: %v", err)\
    }\
    var s struct {\
        Enabled bool `json:"enabled"`\
    }\
    if err := json.Unmarshal(data, &s); err != nil {\
        t.Fatalf("parse .entire/settings.json: %v\ncontent: %s", err, data)\
    }\
    if s.Enabled != want {\
        t.Fatalf("project settings.json enabled=%v, want %v — enabled flag written to the wrong scope\ncontent: %s",\
            s.Enabled, want, data)\
    }\
}\
```\
\
Mcmd/entire/cli/integration\_test/setup\_cmd\_test.go+93/-3\
\
```\
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\
\
//go:build integration\
\
package integration\
\
import (\
    "os"\
    "os/exec"\
    "path/filepath"\
    "strings"\
    "testing"\
\
    "github.com/entireio/cli/cmd/entire/cli/testutil"\
)\
\
// TestSubmoduleWorktree_SessionCreatesCheckpoint is a full-flow regression test\
// for sessions run inside a git submodule: the working tree's .git is a FILE\
// pointing at the superproject's modules dir ("gitdir: ../.git/modules/<name>").\
// GetWorktreeID must recognize that layout; before it did, it returned\
// "unexpected gitdir format", session initialization failed, and no checkpoint\
// was ever created for work done inside a submodule.\
//\
// It builds a real submodule, points the harness at the submodule worktree, and\
// drives the real hook binary end-to-end (user-prompt-submit, a file change, and\
// stop). It then asserts a rewind point exists — i.e. session init succeeded and\
// a checkpoint was saved for work done inside the submodule.\
func TestSubmoduleWorktree_SessionCreatesCheckpoint(t *testing.T) {\
    t.Parallel()\
    env := NewTestEnv(t)\
\
    root := env.T.TempDir()\
    if resolved, err := filepath.EvalSymlinks(root); err == nil {\
        root = resolved\
    }\
    upstream := filepath.Join(root, "upstream")\
    super := filepath.Join(root, "super")\
\
    runGit := func(dir string, args ...string) {\
        t.Helper()\
        cmd := exec.CommandContext(t.Context(), "git", args...)\
        cmd.Dir = dir\
        cmd.Env = testutil.GitIsolatedEnv()\
        if out, err := cmd.CombinedOutput(); err != nil {\
            t.Fatalf("git -C %s %v: %v\n%s", dir, args, err, out)\
        }\
    }\
    writeFileAt := func(path, content string) {\
        t.Helper()\
        if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {\
            t.Fatalf("mkdir: %v", err)\
        }\
        if err := os.WriteFile(path, []byte(content), 0o644); err != nil {\
            t.Fatalf("write %s: %v", path, err)\
        }\
    }\
\
    // Upstream repo that the submodule points at.\
    if err := os.MkdirAll(upstream, 0o755); err != nil {\
        t.Fatalf("mkdir upstream: %v", err)\
    }\
    runGit(upstream, "init")\
    runGit(upstream, "config", "user.name", "Test User")\
    runGit(upstream, "config", "user.email", "test@example.com")\
    runGit(upstream, "config", "commit.gpgsign", "false")\
    writeFileAt(filepath.Join(upstream, "lib.txt"), "lib")\
    runGit(upstream, "add", "lib.txt")\
    runGit(upstream, "commit", "-m", "upstream init")\
\
    // Superproject with the upstream added as a submodule at ./sub. The local\
    // file transport is disabled by default (CVE-2022-39253), so allow it for\
    // this hermetic setup.\
    if err := os.MkdirAll(super, 0o755); err != nil {\
        t.Fatalf("mkdir super: %v", err)\
    }\
    runGit(super, "init")\
    runGit(super, "config", "user.name", "Test User")\
    runGit(super, "config", "user.email", "test@example.com")\
    runGit(super, "config", "commit.gpgsign", "false")\
    writeFileAt(filepath.Join(super, "README.md"), "# super")\
    runGit(super, "add", "README.md")\
    runGit(super, "commit", "-m", "super init")\
    runGit(super, "-c", "protocol.file.allow=always", "submodule", "add", upstream, "sub")\
    runGit(super, "commit", "-m", "add submodule sub")\
\
    sub := filepath.Join(super, "sub")\
\
    // Confirm the precondition: the submodule's .git is a FILE whose gitdir\
    // points into the superproject's modules directory.\
    gitFileContent, err := os.ReadFile(filepath.Join(sub, ".git"))\
    if err != nil {\
        t.Fatalf("read submodule .git file: %v", err)\
    }\
    if !strings.Contains(filepath.ToSlash(string(gitFileContent)), ".git/modules/") {\
        t.Fatalf("submodule .git file is not a modules gitdir (submodule precondition): %q", gitFileContent)\
    }\
\
    // Point the harness at the submodule worktree and drive the real flow there.\
    env.RepoDir = sub\
    env.GitCheckoutNewBranch("feature/sub-work")\
    env.InitEntire()\
\
    session := env.NewSession()\
    if err := env.SimulateUserPromptSubmitWithPrompt(session.ID, "Create app.txt"); err != nil {\
        t.Fatalf("user-prompt-submit: %v", err)\
    }\
    env.WriteFile("app.txt", "hello")\
    session.CreateTranscript("Create app.txt", []FileChange{{Path: "app.txt", Content: "hello"}})\
    if err := env.SimulateStop(session.ID, session.TranscriptPath); err != nil {\
        t.Fatalf("stop: %v", err)\
    }\
\
    // End-to-end proof via the real `checkpoint rewind --list`: a checkpoint was\
    // created for the work done inside the submodule. Without the fix, session\
    // init failed on the submodule gitdir, so no checkpoint (and no rewind point)\
    // exists.\
    if points := env.GetRewindPoints(); len(points) == 0 {\
        t.Fatal("no rewind point after a session inside a submodule — session init failed on the submodule gitdir, so no checkpoint was created")\
    }\
}\
```\
\
Acmd/entire/cli/integration\_test/submodule\_worktree\_test.go+118\
\
```\
719 unmodified lines\
\
720\
721\
722\
723\
724\
725\
723\
724\
725\
726\
727\
728\
729\
1 unmodified line\
\
731\
732\
733\
733\
734\
735\
736\
737\
\
719 unmodified lines\
\
func (env *TestEnv) GetRewindPoints() []RewindPoint {\
    env.T.Helper()\
\
    // Run rewind --list using the shared binary. Parse stdout only — the\
    // deprecated command prints a notice on stderr that would break the JSON.\
    cmd := exec.Command(getTestBinary(), "checkpoint", "rewind", "--list")\
    // Run `checkpoint list --pending --json` using the shared binary. This is\
    // the drop-in replacement for the deprecated `rewind --list` bridge; the JSON shape is\
    // identical. Parse stdout only — any notice goes to stderr.\
    cmd := exec.Command(getTestBinary(), "checkpoint", "list", "--pending", "--json")\
    cmd.Dir = env.RepoDir\
    cmd.Env = env.cliEnv()\
\
1 unmodified line\
\
    cmd.Stderr = &stderr\
    output, err := cmd.Output()\
    if err != nil {\
        env.T.Fatalf("rewind --list failed: %v\nOutput: %s\nStderr: %s", err, output, stderr.String())\
        env.T.Fatalf("checkpoint list --pending --json failed: %v\nOutput: %s\nStderr: %s", err, output, stderr.String())\
    }\
\
    // Parse JSON output\
```\
\
Mcmd/entire/cli/integration\_test/testenv.go+5/-4\
\
```\
110 unmodified lines\
\
111\
112\
113\
114\
114\
115\
116\
117\
118\
118\
119\
120\
121\
122\
123\
124\
125\
126\
127\
128\
129\
130\
131\
132\
133\
134\
135\
136\
137\
138\
6 unmodified lines\
\
145\
146\
147\
148\
149\
150\
151\
152\
153\
12 unmodified lines\
\
166\
167\
168\
169\
170\
171\
172\
173\
174\
\
110 unmodified lines\
\
}\
\
// experimentalCommandMarkers are substrings that only appear in root help when\
// experimental commands are visible.\
// experimental commands are visible. Do not pin cobra's Use/Short column\
// padding — group membership and longest-command width shift the spaces.\
var experimentalCommandMarkers = []string{\
    "Experimental commands:",\
    "review",\
    "tokens                 Analyze token usage across sessions and checkpoints",\
}\
\
// rootHelpHasTokensCommand reports whether root help lists the experimental\
// `tokens` command with its Short description, ignoring Use/Short padding.\
func rootHelpHasTokensCommand(got string) bool {\
    for _, line := range strings.Split(got, "\n") {\
        fields := strings.Fields(line)\
        if len(fields) == 0 || fields[0] != "tokens" {\
            continue\
        }\
        if strings.Contains(line, "Analyze token usage across sessions and checkpoints") {\
            return true\
        }\
    }\
    return false\
}\
\
// 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.\
6 unmodified lines\
\
            t.Fatalf("release root help should not include %q, got:\n%s", marker, got)\
        }\
    }\
    if rootHelpHasTokensCommand(got) {\
        t.Fatalf("release root help should not list tokens, got:\n%s", got)\
    }\
}\
\
// TestRootHelp_DevShowsExperimentalGroup verifies a developer build\
12 unmodified lines\
\
            t.Fatalf("dev root help should include %q, got:\n%s", marker, got)\
        }\
    }\
    if !rootHelpHasTokensCommand(got) {\
        t.Fatalf("dev root help should list tokens with its Short description, got:\n%s", got)\
    }\
}\
\
// summaryColumns returns, for each non-empty rendered row, the rune offset at\
```\
\
Mcmd/entire/cli/labs\_test.go+23/-2\
\
```\
1 unmodified line\
\
2\
3\
4\
5\
6\
7\
8\
9\
10\
11\
12\
13\
14\
15\
16\
17\
18\
19\
20\
21\
22\
2212 unmodified lines\
\
2235\
2236\
2237\
2238\
2239\
2240\
2241\
2242\
2243\
2244\
2245\
2246\
2247\
2248\
2249\
2250\
2251\
2252\
2253\
2254\
2255\
2256\
2257\
2258\
2259\
2260\
2261\
2262\
2263\
2264\
2265\
2266\
2267\
2268\
2269\
2270\
2271\
2272\
2273\
2274\
2275\
2276\
2277\
2278\
2279\
2280\
2281\
2282\
2283\
2284\
2285\
2286\
2287\
2288\
2289\
2290\
2291\
2292\
2293\
2294\
2295\
2296\
2297\
2298\
2299\
2300\
2301\
2302\
2303\
2304\
2305\
2306\
2307\
2308\
2309\
2310\
2311\
2312\
2313\
2314\
2315\
2316\
2317\
2318\
2319\
2320\
2321\
2322\
2323\
2324\
2325\
2326\
2327\
2328\
2329\
2330\
2331\
2332\
2333\
2334\
2335\
2336\
2337\
2338\
2339\
2340\
2341\
2342\
2343\
2344\
2345\
2346\
2347\
2348\
2349\
2350\
2351\
2352\
2353\
2354\
2355\
2356\
2357\
2358\
2359\
2360\
2361\
2362\
2363\
2364\
2365\
2366\
2367\
2368\
2369\
2370\
2371\
2372\
2373\
2374\
2375\
2376\
2377\
2378\
2379\
2380\
2381\
2382\
2383\
2384\
2385\
2386\
2387\
2388\
2389\
2390\
2391\
2392\
2393\
2394\
2395\
2396\
2397\
2398\
2399\
2400\
2401\
2402\
2403\
2404\
2405\
2406\
2407\
2408\
2409\
2410\
2411\
2412\
2413\
2414\
2415\
2416\
2417\
2418\
2419\
2420\
2421\
2422\
2423\
2424\
2425\
2426\
2427\
2428\
2429\
2430\
2431\
2432\
2433\
2434\
2435\
2436\
2437\
2438\
2439\
2440\
2441\
2442\
2443\
2444\
2445\
2446\
2447\
2448\
2449\
2450\
2451\
2452\
2453\
2454\
2455\
2456\
2457\
2458\
2459\
2460\
2461\
2462\
2463\
2464\
2465\
2466\
2467\
2468\
2469\
2470\
2471\
2472\
2473\
\
1 unmodified line\
\
import (\
    "context"\
    "net"\
    "net/http"\
    "net/http/httptest"\
    "os"\
    "os/exec"\
    "path/filepath"\
    "strings"\
    "sync/atomic"\
    "testing"\
    "time"\
\
    "github.com/entireio/cli/cmd/entire/cli/agent"\
    "github.com/entireio/cli/cmd/entire/cli/agent/opencode"\
    "github.com/entireio/cli/cmd/entire/cli/agent/types"\
    "github.com/entireio/cli/cmd/entire/cli/api"\
    "github.com/entireio/cli/cmd/entire/cli/investigate"\
    "github.com/entireio/cli/cmd/entire/cli/paths"\
    "github.com/entireio/cli/cmd/entire/cli/review"\
2212 unmodified lines\
\
        t.Fatalf("back-to-back checkpoint B after stale hook = %d, want 3", got)\
    }\
}\
\
// TestHandleLifecycleSessionStart_NoSynchronousNetworkForTrailEnablement\
// guards against SessionStart hooks stalling agent startup: the\
// trails-enablement cache refresh must be handed off to a detached subprocess,\
// never performed inline on the SessionStart hook path. A slow/unreachable API\
// host previously added up to trailEnablementSessionStartRefreshTimeout (1s) of\
// synchronous latency to every session start once the hourly cache went stale.\
//\
// The deterministic guarantee is the spawn seam: SessionStart must invoke the\
// detached-refresh spawn exactly once and return without doing the network work\
// itself. As a production-shaped backstop the API base points at a blackholed\
// https host that accepts the TCP connection but never answers — so a\
// regression that dials inline both contacts that host (dialed > 0) and burns\
// the ~1s session-start budget instead of returning immediately. (Plain http\
// would be rejected by api.RequireSecureURL before any dial, so the host must\
// be https to actually exercise the synchronous-dial path.)\
func TestHandleLifecycleSessionStart_NoSynchronousNetworkForTrailEnablement(t *testing.T) {\
    setupStopTestRepo(t)\
    runGitInDir(t, ".", "remote", "add", "origin", "https://github.com/entirehq/example.git")\
\
    // Blackhole https host: accept connections but never complete the TLS\
    // handshake or respond, so an inline dial stalls until a timeout fires\
    // (mirrors the unreachable-host case that motivated the detached refresh)\
    // rather than failing fast.\
    var dialed int32\
    var lc net.ListenConfig\
    ln, err := lc.Listen(context.Background(), "tcp", "127.0.0.1:0")\
    require.NoError(t, err)\
    defer ln.Close()\
    go func() {\
        for {\
            conn, acceptErr := ln.Accept()\
            if acceptErr != nil {\
                return\
            }\
            atomic.AddInt32(&dialed, 1)\
            _ = conn // hold open; never respond\
        }\
    }()\
    t.Setenv("ENTIRE_API_BASE_URL", "https://"+ln.Addr().String())\
\
    var spawnCount int32\
    prevSpawn := trailRefreshSpawn\
    trailRefreshSpawn = func(worktreeRoot string) {\
        atomic.AddInt32(&spawnCount, 1)\
        if worktreeRoot == "" {\
            t.Error("expected non-empty worktree root passed to trail refresh spawn")\
        }\
    }\
    t.Cleanup(func() { trailRefreshSpawn = prevSpawn })\
\
    ag := newMockHookResponseAgent()\
    event := &agent.Event{\
        Type:      agent.SessionStart,\
        SessionID: "test-no-sync-trail-dial",\
        Timestamp: time.Now(),\
    }\
\
    start := time.Now()\
    err = handleLifecycleSessionStart(context.Background(), ag, event)\
    elapsed := time.Since(start)\
\
    require.NoError(t, err)\
    // Deterministic guarantee: the network-capable refresh is delegated to the\
    // detached spawn exactly once, never run inline.\
    if got := atomic.LoadInt32(&spawnCount); got != 1 {\
        t.Fatalf("expected exactly one detached trail-enablement refresh spawn, got %d", got)\
    }\
    // Backstops: SessionStart neither contacted the API host nor blocked.\
    if got := atomic.LoadInt32(&dialed); got != 0 {\
        t.Fatalf("SessionStart dialed the trails-enablement API synchronously; the refresh must run out of process")\
    }\
    if elapsed > time.Second {\
        t.Fatalf("handleLifecycleSessionStart took %v; trails-enablement refresh must be detached, not synchronous", elapsed)\
    }\
}\
\
// TestRunTrailEnablementRefresh_BoundedByTimeoutAgainstUnresponsiveHost\
// verifies the deferred refresh work still completes (or at least\
// gives up) within its own bounded timeout when the API host never\
// responds — the network work that used to block SessionStart must still\
// happen, just out of the hook's critical path, and it must not hang forever.\
func TestRunTrailEnablementRefresh_BoundedByTimeoutAgainstUnresponsiveHost(t *testing.T) {\
    setupStopTestRepo(t)\
    runGitInDir(t, ".", "remote", "add", "origin", "https://github.com/entirehq/example.git")\
\
    var lc net.ListenConfig\
    ln, err := lc.Listen(context.Background(), "tcp", "127.0.0.1:0")\
    require.NoError(t, err)\
    defer ln.Close()\
    var accepted int32\
    go func() {\
        for {\
            conn, acceptErr := ln.Accept()\
            if acceptErr != nil {\
                return\
            }\
            atomic.AddInt32(&accepted, 1)\
            // Accept the connection but never write anything back (no TLS\
            // handshake, no HTTP response) — simulates a blackholed/firewalled\
            // host, which is what triggered the original 1s stall per call.\
            _ = conn\
        }\
    }()\
    t.Setenv("ENTIRE_API_BASE_URL", "https://"+ln.Addr().String())\
\
    start := time.Now()\
    refreshErr := runTrailEnablementRefresh(context.Background())\
    elapsed := time.Since(start)\
\
    // Best-effort: network failure must not surface as a hard error.\
    require.NoError(t, refreshErr)\
    if elapsed > trailEnablementRefreshTimeout+2*time.Second {\
        t.Fatalf("runTrailEnablementRefresh took %v, expected to give up within roughly %v", elapsed, trailEnablementRefreshTimeout)\
    }\
    // Prove the test actually exercised the network path rather than passing\
    // via an early return (e.g. scope resolution or auth failing before any\
    // dial): the blackholed listener must have accepted at least one\
    // connection attempt.\
    if got := atomic.LoadInt32(&accepted); got == 0 {\
        t.Fatalf("expected at least one dial attempt against the unresponsive host, got %d", got)\
    }\
}\
\
// TestNewRefreshTrailEnablementCmd_APIFailureExitsZero guards against the\
// detached __refresh_trail_enablement subprocess exiting non-zero on a\
// transient network/API failure. The refresh is best-effort cache warming\
// with stdout/stderr discarded (see newRefreshTrailEnablementCmd) — there is\
// no one watching the exit code, so a failing TrailsEnabled call must be\
// logged (already covered by TestRefreshTrailEnablementCmd_LogsBackgroundFailureToFile-\
// style tests) and swallowed, never propagated as a command error, mirroring\
// __send_analytics.\
func TestNewRefreshTrailEnablementCmd_APIFailureExitsZero(t *testing.T) {\
    setupStopTestRepo(t)\
    runGitInDir(t, ".", "remote", "add", "origin", "https://github.com/entirehq/example.git")\
\
    srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {\
        w.WriteHeader(http.StatusInternalServerError)\
    }))\
    t.Cleanup(srv.Close)\
\
    prevClient := trailRefreshAPIClient\
    trailRefreshAPIClient = func(context.Context, bool) (*api.Client, error) {\
        return api.NewClientWithBaseURL("test-token", srv.URL), nil\
    }\
    t.Cleanup(func() { trailRefreshAPIClient = prevClient })\
\
    cmd := newRefreshTrailEnablementCmd()\
    cmd.SetArgs([]string{})\
    require.NoError(t, cmd.ExecuteContext(context.Background()),\
        "detached refresh command must exit 0 even when the API call fails (best-effort cache warming)")\
}\
\
// TestRefreshTrailEnablementCmd_LogsBackgroundFailureToFile guards\
// diagnosability: the detached __refresh_trail_enablement child runs with\
// stdout/stderr discarded, so a failing background refresh must still leave a\
// trail in .entire/logs/entire.log instead of vanishing. The command runs in a\
// repo with no origin remote, so the scope resolves-and-fails locally (no\
// network) and that failure has to be logged to the repo's log file.\
func TestRefreshTrailEnablementCmd_LogsBackgroundFailureToFile(t *testing.T) {\
    setupStopTestRepo(t)\
    t.Setenv("ENTIRE_LOG_LEVEL", "debug")\
\
    cmd := newRefreshTrailEnablementCmd()\
    cmd.SetArgs([]string{})\
    require.NoError(t, cmd.ExecuteContext(context.Background()))\
\
    root, err := paths.WorktreeRoot(context.Background())\
    require.NoError(t, err)\
    logData, err := os.ReadFile(filepath.Join(root, ".entire", "logs", "entire.log"))\
    require.NoError(t, err)\
    require.Contains(t, string(logData), "trails enablement refresh skipped: scope unresolved",\
        "background refresh failure must be diagnosable in .entire/logs/entire.log")\
}\
\
// TestRefreshTrailEnablementCmd_NoStrayLogsOutsideWorktree guards the file-init\
// against running outside a resolvable worktree. logging.Init falls back to the\
// current directory when paths.WorktreeRoot fails, so the command must guard on\
// WorktreeRoot (as resume/rewind/reset/explain do) or a child whose worktree was\
// removed/relocated between spawn and exec would MkdirAll a stray .entire/logs/\
// wherever it happens to be running.\
func TestRefreshTrailEnablementCmd_NoStrayLogsOutsideWorktree(t *testing.T) {\
    dir := t.TempDir() // a plain temp dir, not a git worktree\
    t.Chdir(dir)\
    paths.ClearWorktreeRootCache()\
    session.ClearGitCommonDirCache()\
    t.Setenv("ENTIRE_LOG_LEVEL", "debug")\
\
    cmd := newRefreshTrailEnablementCmd()\
    cmd.SetArgs([]string{})\
    require.NoError(t, cmd.ExecuteContext(context.Background()))\
\
    _, statErr := os.Stat(filepath.Join(dir, ".entire", "logs"))\
    require.True(t, os.IsNotExist(statErr),\
        "must not create a stray .entire/logs outside a resolvable worktree")\
}\
\
// TestTrailRefreshRecentlySpawned_ThrottlesWithinWindow verifies the spawn-side\
// guard: within trailRefreshSpawnThrottle of a recorded spawn,\
// further spawns are suppressed; once the window passes a fresh spawn is allowed\
// and re-recorded. Without this, an unreachable host — which never writes the\
// cache, so the hourly TTL never starts — would fork a refresh child on every\
// SessionStart.\
func TestTrailRefreshRecentlySpawned_ThrottlesWithinWindow(t *testing.T) {\
    commonDir := t.TempDir()\
    now := time.Now()\
\
    require.False(t, trailRefreshRecentlySpawned(commonDir, now),\
        "first call records the spawn and is not throttled")\
    require.True(t, trailRefreshRecentlySpawned(commonDir, now.Add(time.Second)),\
        "a second attempt within the window is throttled")\
    require.False(t, trailRefreshRecentlySpawned(commonDir, now.Add(trailRefreshSpawnThrottle)),\
        "at the window boundary the spawn is allowed and re-recorded")\
    require.True(t, trailRefreshRecentlySpawned(commonDir, now.Add(trailRefreshSpawnThrottle+time.Second)),\
        "an attempt within the window of the re-recorded spawn is throttled")\
}\
\
// TestSpawnDetachedTrailEnablementRefresh_CollapsesBurst verifies the throttle is\
// actually wired into the spawn path: a burst of SessionStart-driven attempts for\
// the same repo forks a single child, not one per hook.\
func TestSpawnDetachedTrailEnablementRefresh_CollapsesBurst(t *testing.T) {\
    setupStopTestRepo(t)\
\
    var spawnCount int32\
    prevSpawn := trailRefreshSpawn\
    trailRefreshSpawn = func(string) { atomic.AddInt32(&spawnCount, 1) }\
    t.Cleanup(func() { trailRefreshSpawn = prevSpawn })\
\
    spawnDetachedTrailEnablementRefresh(context.Background())\
    spawnDetachedTrailEnablementRefresh(context.Background())\
    spawnDetachedTrailEnablementRefresh(context.Background())\
\
    if got := atomic.LoadInt32(&spawnCount); got != 1 {\
        t.Fatalf("expected the burst to collapse to a single detached spawn, got %d", got)\
    }\
}\
```\
\
Mcmd/entire/cli/lifecycle\_test.go+241\
\
```\
20 unmodified lines\
\
21\
22\
23\
24\
25\
26\
27\
106 unmodified lines\
\
134\
135\
136\
136\
137\
138\
139\
140\
141\
138\
142\
143\
144\
145\
146\
147\
148\
149\
150\
151\
152\
153\
154\
155\
156\
157\
158\
2 unmodified lines\
\
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\
\
20 unmodified lines\
\
    EntireMetadataDir = ".entire/metadata"\
\
    osWindows = "windows"\
    osDarwin  = "darwin"\
)\
\
// Metadata file names\
106 unmodified lines\
\
}\
\
// IsInfrastructurePath returns true if the path is part of CLI infrastructure\
// (i.e., inside the .entire directory)\
// (i.e., inside the .entire directory). It is used only to EXCLUDE infra paths\
// from checkpoints/tracking, so it matches case-insensitively on\
// case-insensitive filesystems via IsProtectedSubpath. Do not use it as a\
// containment/allow gate.\
func IsInfrastructurePath(path string) bool {\
    return IsSubpath(EntireDir, path)\
    return IsProtectedSubpath(EntireDir, path)\
}\
\
// IsSubpath reports whether child is lexically under parent (or equal to it).\
// It uses filepath.Rel, which cleans both inputs and is traversal-resistant:\
// a crafted child like "/a/b/../../../etc/passwd" that escapes parent will\
// produce a relative path starting with ".." and be rejected.\
//\
// Matching is case-SENSITIVE. This is the correct primitive for fail-closed\
// containment/allow checks (e.g. validating an attacker-influenced path stays\
// under an Entire-owned dir): on a case-sensitive volume a differently-cased\
// path names a different directory, so folding it in would fail open. For\
// EXCLUSION decisions that must also catch case variants on Windows/macOS, use\
// IsProtectedSubpath instead.\
func IsSubpath(parent, child string) bool {\
    rel, err := filepath.Rel(parent, child)\
    if err != nil {\
2 unmodified lines\
\
    return !IsRelativeTraversal(rel)\
}\
\
// IsProtectedSubpath reports whether child is under parent for the purpose of\
// EXCLUDING protected/infrastructure content from checkpoints and tracking.\
// Unlike IsSubpath it honors OS case-insensitivity (see CaseInsensitiveFS), so\
// a case variant of a protected dir (".Claude" vs ".claude") is still excluded\
// on Windows/macOS.\
//\
// SECURITY: never use this for allow/containment decisions. Case-folding widens\
// what counts as "inside" parent, which is safe only when the effect is to\
// exclude more. On a case-sensitive volume under a case-insensitive GOOS it\
// over-matches; for a fail-closed gate that would fail open. Use IsSubpath there.\
func IsProtectedSubpath(parent, child string) bool {\
    if CaseInsensitiveFS() {\
        return IsSubpath(strings.ToLower(parent), strings.ToLower(child))\
    }\
    return IsSubpath(parent, child)\
}\
\
// CaseInsensitiveFS reports whether path comparisons should be case-insensitive\
// on the host OS. This is OS-based, not volume-based: Windows and macOS default\
// to case-insensitive filesystems, Linux to case-sensitive. Keying on GOOS keeps\
// the result deterministic. It must only influence EXCLUSION decisions (see\
// IsProtectedSubpath / Equal): on an atypical volume (e.g. a case-sensitive\
// macOS APFS volume) it treats a differently-cased path as matching, which is\
// safe only when the effect is to exclude more, never to widen an allow gate.\
func CaseInsensitiveFS() bool {\
    return runtime.GOOS == osWindows || runtime.GOOS == osDarwin\
}\
\
// Equal reports whether two paths refer to the same location, honoring the host\
// OS's case sensitivity (see CaseInsensitiveFS). Both inputs are cleaned and\
// slash-normalized before comparison. Like IsProtectedSubpath, this is intended\
// for EXCLUSION matching (e.g. protected files), not fail-closed containment.\
func Equal(a, b string) bool {\
    a = filepath.Clean(filepath.FromSlash(a))\
    b = filepath.Clean(filepath.FromSlash(b))\
    if CaseInsensitiveFS() {\
        return strings.EqualFold(a, b)\
    }\
    return a == b\
}\
\
// IsRelativeTraversal reports whether rel escapes its base directory.\
// It accepts both OS-native paths and Git-style slash-normalized paths.\
func IsRelativeTraversal(rel string) bool {\
```\
\
Mcmd/entire/cli/paths/paths.go+54/-2\
\
```\
95 unmodified lines\
\
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\
\
95 unmodified lines\
\
    }\
}\
\
func TestCaseInsensitiveFS(t *testing.T) {\
    t.Parallel()\
    want := runtime.GOOS == osWindows || runtime.GOOS == osDarwin\
    if got := CaseInsensitiveFS(); got != want {\
        t.Errorf("CaseInsensitiveFS() = %v, want %v (GOOS=%s)", got, want, runtime.GOOS)\
    }\
}\
\
// TestIsSubpath_AlwaysCaseSensitive locks in that IsSubpath — the fail-closed\
// containment primitive used by allow gates (rewind/utils) — never folds case\
// on any OS. A differently-cased path must not count as contained, or a\
// crafted, attacker-influenced value could fail open on a case-sensitive volume.\
func TestIsSubpath_AlwaysCaseSensitive(t *testing.T) {\
    t.Parallel()\
    if IsSubpath(".entire/metadata", ".Entire/metadata") {\
        t.Error("IsSubpath must be case-sensitive (fail-closed); .Entire/metadata must not be under .entire/metadata")\
    }\
    if !IsSubpath(".claude", ".claude/marker.txt") {\
        t.Error("IsSubpath(.claude, .claude/marker.txt) = false, want true")\
    }\
    if IsSubpath(".claude", ".claude/../../etc/passwd") {\
        t.Error("IsSubpath must reject traversal")\
    }\
}\
\
// TestIsProtectedSubpath_CaseSensitivity asserts OS-based folding for the\
// EXCLUSION helper: case variants match on Windows/macOS (where they name the\
// same on-disk path), stay distinct on case-sensitive Linux, and traversal is\
// always rejected.\
func TestIsProtectedSubpath_CaseSensitivity(t *testing.T) {\
    t.Parallel()\
    got := IsProtectedSubpath(".claude", ".Claude/marker.txt")\
    if got != CaseInsensitiveFS() {\
        t.Errorf("IsProtectedSubpath(.claude, .Claude/marker.txt) = %v, want %v (GOOS=%s)",\
            got, CaseInsensitiveFS(), runtime.GOOS)\
    }\
    if !IsProtectedSubpath(".claude", ".claude/marker.txt") {\
        t.Error("IsProtectedSubpath(.claude, .claude/marker.txt) = false, want true")\
    }\
    if IsProtectedSubpath(".claude", ".Claude/../../etc/passwd") {\
        t.Error("IsProtectedSubpath must reject traversal even when case-folding")\
    }\
}\
\
func TestEqual_CaseSensitivity(t *testing.T) {\
    t.Parallel()\
    if !Equal(".terminalhirerc", ".terminalhirerc") {\
        t.Error("Equal should match identical paths")\
    }\
    got := Equal(".terminalhirerc", ".TerminalHireRC")\
    if got != CaseInsensitiveFS() {\
        t.Errorf("Equal(case variant) = %v, want %v (GOOS=%s)",\
            got, CaseInsensitiveFS(), runtime.GOOS)\
    }\
    if Equal(".terminalhirerc", "other") {\
        t.Error("Equal should not match distinct paths")\
    }\
}\
\
func TestToRelativePath_MSYSPaths(t *testing.T) {\
    t.Parallel()\
    if runtime.GOOS != "windows" {\
```\
\
Mcmd/entire/cli/paths/paths\_test.go+59\
\
```\
35 unmodified lines\
\
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\
43\
44\
64\
46\
47\
48\
65\
66\
67\
68\
51\
52\
53\
54\
55\
69\
57\
70\
71\
\
35 unmodified lines\
\
    }\
\
    gitdir := strings.TrimPrefix(line, "gitdir: ")\
    if worktreeID, found := parseWorktreeID(gitdir); found {\
        return worktreeID, nil\
    }\
\
    return "", fmt.Errorf("unexpected gitdir format (no worktrees): %s", gitdir)\
}\
\
func parseWorktreeID(gitdir string) (string, bool) {\
    gitdir = strings.TrimSuffix(strings.ReplaceAll(gitdir, "\\", "/"), "/")\
\
    // Submodule gitdirs live under .git/modules/<path>. If that submodule\
    // repository has its own linked worktree, the gitdir ends with\
    // .git/modules/<path>/worktrees/<id>. A /worktrees/ segment before the\
    // final /modules/ belongs to the superproject's worktree, not the submodule.\
    if modulesIndex := strings.LastIndex(gitdir, "/modules/"); modulesIndex >= 0 {\
        afterModules := gitdir[modulesIndex+len("/modules/"):]\
        if _, worktreeID, found := strings.Cut(afterModules, "/worktrees/"); found {\
            return strings.TrimSuffix(worktreeID, "/"), true\
        }\
        return "", true\
    }\
\
    // Extract worktree name from path like /repo/.git/worktrees/<name>\
    // or /repo/.bare/worktrees/<name> (bare repo + worktree layout).\
    // The path after the marker is the worktree identifier.\
    var worktreeID string\
    var found bool\
    for _, marker := range []string{".git/worktrees/", ".bare/worktrees/"} {\
        _, worktreeID, found = strings.Cut(gitdir, marker)\
        if found {\
            break\
        if _, worktreeID, found := strings.Cut(gitdir, marker); found {\
            return strings.TrimSuffix(worktreeID, "/"), true\
        }\
    }\
    if !found {\
        return "", fmt.Errorf("unexpected gitdir format (no worktrees): %s", gitdir)\
    }\
    // Remove trailing slashes if any\
    worktreeID = strings.TrimSuffix(worktreeID, "/")\
\
    return worktreeID, nil\
    return "", false\
}\
```\
\
Mcmd/entire/cli/paths/worktree.go+24/-11\
\
```\
7 unmodified lines\
\
8\
9\
10\
11\
12\
13\
14\
15\
8 unmodified lines\
\
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\
55 unmodified lines\
\
125\
126\
127\
128\
129\
130\
131\
132\
\
7 unmodified lines\
\
)\
\
func TestGetWorktreeID(t *testing.T) {\
    t.Parallel()\
\
    tests := []struct {\
        name       string\
        setupFunc  func(dir string) error\
8 unmodified lines\
\
            },\
            wantID: "",\
        },\
        {\
            name: "ordinary submodule relative gitdir",\
            setupFunc: func(dir string) error {\
                content := "gitdir: ../../.git/modules/deps/go-git\n"\
                return os.WriteFile(filepath.Join(dir, ".git"), []byte(content), 0o644)\
            },\
            wantID: "",\
        },\
        {\
            name: "ordinary submodule absolute gitdir",\
            setupFunc: func(dir string) error {\
                content := "gitdir: /repo/.git/modules/deps/go-git\n"\
                return os.WriteFile(filepath.Join(dir, ".git"), []byte(content), 0o644)\
            },\
            wantID: "",\
        },\
        {\
            name: "nested ordinary submodule gitdir",\
            setupFunc: func(dir string) error {\
                content := "gitdir: /repo/.git/modules/libs/go-git/modules/vendor/crypto\n"\
                return os.WriteFile(filepath.Join(dir, ".git"), []byte(content), 0o644)\
            },\
            wantID: "",\
        },\
        {\
            name: "linked worktree of submodule",\
            setupFunc: func(dir string) error {\
                content := "gitdir: /repo/.git/modules/deps/go-git/worktrees/sub-linked\n"\
                return os.WriteFile(filepath.Join(dir, ".git"), []byte(content), 0o644)\
            },\
            wantID: "sub-linked",\
        },\
        {\
            name: "ordinary submodule inside linked superproject worktree",\
            setupFunc: func(dir string) error {\
                content := "gitdir: /repo/.git/worktrees/super-linked/modules/deps/go-git\n"\
                return os.WriteFile(filepath.Join(dir, ".git"), []byte(content), 0o644)\
            },\
            wantID: "",\
        },\
        {\
            name: "linked worktree simple name",\
            setupFunc: func(dir string) error {\
55 unmodified lines\
\
    for _, tt := range tests {\
        t.Run(tt.name, func(t *testing.T) {\
            t.Parallel()\
\
            dir := t.TempDir()\
            if err := tt.setupFunc(dir); err != nil {\
                t.Fatalf("setup failed: %v", err)\
```\
\
Mcmd/entire/cli/paths/worktree\_test.go+44\
\
```\
9 unmodified lines\
\
10\
11\
12\
13\
13\
14\
15\
2 unmodified lines\
\
18\
19\
20\
22\
21\
22\
23\
53 unmodified lines\
\
77\
78\
79\
80\
81\
82\
83\
83\
84\
85\
86\
87\
88\
2 unmodified lines\
\
91\
92\
93\
92\
94\
95\
96\
97\
98\
28 unmodified lines\
\
127\
128\
129\
127\
128\
129\
130\
131\
132\
133\
130\
131\
132\
133\
134\
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\
135\
136\
137\
138\
180 unmodified lines\
\
319\
320\
321\
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\
322\
323\
324\
\
9 unmodified lines\
\
    "os/exec"\
    "path/filepath"\
    "strings"\
    "time"\
    "unicode"\
\
    agentpkg "github.com/entireio/cli/cmd/entire/cli/agent"\
2 unmodified lines\
\
    "github.com/entireio/cli/cmd/entire/cli/checkpoint"\
    "github.com/entireio/cli/cmd/entire/cli/checkpoint/id"\
    "github.com/entireio/cli/cmd/entire/cli/gitrepo"\
    "github.com/entireio/cli/cmd/entire/cli/jsonutil"\
    "github.com/entireio/cli/cmd/entire/cli/logging"\
    "github.com/entireio/cli/cmd/entire/cli/paths"\
    "github.com/entireio/cli/cmd/entire/cli/strategy"\
53 unmodified lines\
\
            external.DiscoverAndRegister(ctx)\
            w := cmd.OutOrStdout()\
            errW := cmd.ErrOrStderr()\
            // --list is a hidden deprecated bridge for external scripts that still\
            // invoke rewind --list. Same JSON bytes as checkpoint list --pending\
            // --json; remove together with the rewind command itself.\
            if listFlag {\
                return runRewindList(ctx, w)\
                fmt.Fprintln(errW, "note: 'rewind --list' is deprecated; use 'entire checkpoint list --pending --json'")\
                return runCheckpointPendingListJSON(ctx, w)\
            }\
            if toFlag != "" {\
                return runRewindToWithOptions(ctx, w, errW, toFlag, logsOnlyFlag, resetFlag)\
2 unmodified lines\
\
        },\
    }\
\
    cmd.Flags().BoolVar(&listFlag, "list", false, "List available rewind points (JSON output)")\
    cmd.Flags().BoolVar(&listFlag, "list", false, "List available rewind points (JSON output); deprecated, use checkpoint list --pending --json")\
    _ = cmd.Flags().MarkHidden("list") //nolint:errcheck // flag is defined above\
    cmd.Flags().StringVar(&toFlag, "to", "", "Rewind to specific commit ID (non-interactive)")\
    cmd.Flags().BoolVar(&logsOnlyFlag, "logs-only", false, "Only restore logs, don't modify working directory (for logs-only points)")\
    cmd.Flags().BoolVar(&resetFlag, "reset", false, "Reset branch to commit (destructive, for logs-only points)")\
28 unmodified lines\
\
    }\
\
    // Check if there are multiple sessions (to show session identifier)\
    sessionIDs := make(map[string]bool)\
    for _, p := range points {\
        if p.SessionID != "" {\
            sessionIDs[p.SessionID] = true\
        }\
    }\
    hasMultipleSessions := len(sessionIDs) > 1\
    multi := hasMultipleSessions(points)\
\
    // Build options for the select menu\
    options := make([]huh.Option[string], 0, len(points)+1)\
    for _, p := range points {\
        var label string\
        timestamp := p.Date.Format("2006-01-02 15:04")\
\
        // Build session identifier for display when multiple sessions exist\
        sessionLabel := ""\
        if hasMultipleSessions && p.SessionPrompt != "" {\
            // Show truncated prompt to identify the session\
            sessionLabel = fmt.Sprintf(" [%s]", sanitizeForTerminal(p.SessionPrompt))\
        }\
\
        switch {\
        case p.IsLogsOnly:\
            // Committed checkpoint - show commit sha (this is the real user commit)\
            shortID := p.ID\
            if len(shortID) >= 7 {\
                shortID = shortID[:7]\
            }\
            label = fmt.Sprintf("%s (%s) %s%s", shortID, timestamp, sanitizeForTerminal(p.Message), sessionLabel)\
        case p.IsTaskCheckpoint:\
            // Task checkpoint (uncommitted) - no sha shown\
            label = fmt.Sprintf("        (%s) [Task] %s%s", timestamp, sanitizeForTerminal(p.Message), sessionLabel)\
        default:\
            // Shadow checkpoint (uncommitted) - no sha shown (internal commit)\
            label = fmt.Sprintf("        (%s) %s%s", timestamp, sanitizeForTerminal(p.Message), sessionLabel)\
        }\
        options = append(options, huh.NewOption(label, p.ID))\
        options = append(options, huh.NewOption(rewindPointLabel(p, multi), p.ID))\
    }\
    options = append(options, huh.NewOption("Cancel", "cancel"))\
\
180 unmodified lines\
\
    return nil\
}\
\
func runRewindList(ctx context.Context, w io.Writer) error {\
    start := GetStrategy(ctx)\
\
    points, err := start.GetRewindPoints(ctx, 20)\
    if err != nil {\
        return fmt.Errorf("failed to find rewind points: %w", err)\
    }\
\
    // Output as JSON for programmatic use\
    type jsonPoint struct {\
        ID               string `json:"id"`\
        Message          string `json:"message"`\
        MetadataDir      string `json:"metadata_dir"`\
        Date             string `json:"date"`\
        IsTaskCheckpoint bool   `json:"is_task_checkpoint"`\
        ToolUseID        string `json:"tool_use_id,omitempty"`\
        IsLogsOnly       bool   `json:"is_logs_only"`\
        CondensationID   string `json:"condensation_id,omitempty"`\
        SessionID        string `json:"session_id,omitempty"`\
        SessionPrompt    string `json:"session_prompt,omitempty"`\
    }\
\
    output := make([]jsonPoint, len(points))\
    for i, p := range points {\
        output[i] = jsonPoint{\
            ID:               p.ID,\
            Message:          p.Message,\
            MetadataDir:      p.MetadataDir,\
            Date:             p.Date.Format(time.RFC3339),\
            IsTaskCheckpoint: p.IsTaskCheckpoint,\
            ToolUseID:        p.ToolUseID,\
            IsLogsOnly:       p.IsLogsOnly,\
            CondensationID:   p.CheckpointID.String(),\
            SessionID:        p.SessionID,\
            SessionPrompt:    p.SessionPrompt,\
        }\
    }\
\
    // Print as JSON\
    data, err := jsonutil.MarshalIndentWithNewline(output, "", "  ")\
    if err != nil {\
        return err //nolint:wrapcheck // already present in codebase\
    }\
    fmt.Fprintln(w, string(data))\
    return nil\
}\
\
func runRewindToWithOptions(ctx context.Context, w, errW io.Writer, commitID string, logsOnly bool, reset bool) error {\
    return runRewindToInternal(ctx, w, errW, commitID, logsOnly, reset)\
}\
```\
\
Mcmd/entire/cli/rewind.go+9/-84\
\
```\
74 unmodified lines\
\
75\
76\
77\
78\
79\
80\
81\
82\
83\
84\
85\
86\
87\
88\
89\
\
74 unmodified lines\
\
            metadataDir: ".entire",\
            want:        "",\
        },\
        {\
            // Containment is a fail-closed allow gate: it must stay case-SENSITIVE\
            // on every OS. A case variant names a different on-disk dir on a\
            // case-sensitive volume (which exists under GOOS=darwin), so folding it\
            // in would fail open. Must return "" regardless of platform.\
            name:        "case-variant of metadata dir fails closed on all OSes",\
            metadataDir: ".Entire/metadata/sess-123",\
            want:        "",\
        },\
    }\
\
    for _, tt := range tests {\
```\
\
Mcmd/entire/cli/rewind\_test.go+9\
\
```\
164 unmodified lines\
\
165\
166\
167\
168\
169\
170\
171\
\
164 unmodified lines\
\
    cmd.AddCommand(newTrailCmd())\
    cmd.AddCommand(newSendAnalyticsCmd())\
    cmd.AddCommand(newCurlBashPostInstallCmd())\
    cmd.AddCommand(newRefreshTrailEnablementCmd())\
\
    // Experimental command (developer-only visibility; setup/tune runners).\
    experimental.Register(cmd, newRunnerCmd()) // 'runner' (experimental)\
```\
\
Mcmd/entire/cli/root.go+1\
\
```\
275 unmodified lines\
\
276\
277\
278\
279\
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\
140 unmodified lines\
\
455\
456\
457\
458\
459\
460\
461\
462\
463\
464\
465\
466\
467\
468\
469\
470\
471\
472\
473\
474\
475\
\
275 unmodified lines\
\
    // Set from hook data when the agent provides it.\
    ModelName string `json:"model_name,omitempty"`\
\
    // Token usage tracking (accumulated across all checkpoints in this session)\
    // Token usage tracking (accumulated across all checkpoints in this session).\
    //\
    // DECISION: SubagentTokens is "latest snapshot wins", not summed. Subagent\
    // usage arrives as a cumulative-since-session-start total (each subagent\
    // transcript is re-read from line 0 every call), so accumulateTokenUsage\
    // replaces rather than adds it (see cmd/entire/cli/strategy). Tradeoff: if\
    // the main transcript resets or rotates mid-session (compaction writing a\
    // fresh file, or a resume that truncates), a subsequent snapshot can be\
    // SMALLER than a previous one, so this session-wide total regresses\
    // (undercounts) for the rest of the session. This is accepted: undercounting\
    // after a transcript reset is preferable to the multiplicative overcount the\
    // summing approach produced, and the alternative (a session-wide high-water\
    // mark) would mask genuine subagent-transcript cleanup. Checkpoint deltas do\
    // not share this exposure — CheckpointTokenUsage.SubagentTokens is derived as\
    // (this total - SubagentTokensBaseline) and floored at 0 by clampSubtract, so\
    // a shrunk snapshot yields 0, never a negative or stale delta.\
    TokenUsage *agent.TokenUsage `json:"token_usage,omitempty"`\
\
    // CheckpointTokenUsage tracks hook-provided token usage since the last condensation.\
    // This is checkpoint-scoped; TokenUsage remains the session-wide total.\
    CheckpointTokenUsage *agent.TokenUsage `json:"checkpoint_token_usage,omitempty"`\
\
    // SubagentTokensBaseline is a snapshot of TokenUsage.SubagentTokens captured\
    // at the last condensation reset. Subagent token usage is always re-read\
    // from the start of each subagent transcript (agent IDs are discovered from\
    // the full main transcript so subagents spawned before the checkpoint\
    // window are still found), so it arrives as a cumulative-since-session-start\
    // total rather than a per-checkpoint delta. This baseline lets\
    // CheckpointTokenUsage.SubagentTokens be rescoped to "since last\
    // condensation" via SubtractTokenUsage instead of re-adding the same\
    // cumulative total on every checkpoint.\
    SubagentTokensBaseline *agent.TokenUsage `json:"subagent_tokens_baseline,omitempty"`\
\
    // SkillEvents records explicit native skill signals observed during this session.\
    // Stored as sidecar metadata so consumers can collapse skill-related transcript events\
    // without mutating the raw agent transcript.\
140 unmodified lines\
\
    s.TranscriptLinesAtStart = 0\
}\
\
// RebaselineSubagentTokens snapshots the current cumulative subagent total\
// (TokenUsage.SubagentTokens) into SubagentTokensBaseline so the next checkpoint\
// window's CheckpointTokenUsage.SubagentTokens is rescoped to "since this\
// re-baseline" rather than re-reporting the full cumulative subagent total.\
//\
// The invariant is: every site that starts a fresh checkpoint window by clearing\
// CheckpointTokenUsage MUST also re-baseline. Callers: the condensation reset\
// helper (resetCheckpointWindow) and cross-repo session adoption, which likewise\
// opens a fresh target-local window. Sharing this here keeps the two in step.\
func (s *State) RebaselineSubagentTokens() {\
    if s.TokenUsage != nil {\
        s.SubagentTokensBaseline = s.TokenUsage.SubagentTokens\
    }\
}\
\
// RealignAttributionBase sets AttributionBaseCommit to newBase and clears any\
// bookkeeping whose meaning depends on attribution being diverged from the\
// shadow-branch base. Call this every time a code path intentionally brings\
```\
\
Mcmd/entire/cli/session/state.go+42/-1\
\
```\
479 unmodified lines\
\
480\
481\
482\
483\
484\
485\
486\
487\
488\
489\
490\
491\
492\
493\
\
479 unmodified lines\
\
    adopted.LastCheckpointID = id.EmptyCheckpointID\
    adopted.LastCheckpointCommitHash = ""\
    adopted.CheckpointTokenUsage = nil\
    // Re-baseline the subagent cumulative for the fresh target-local window. The\
    // cloned TokenUsage carries the SOURCE session's full cumulative subagent\
    // total; without re-baselining here, the first post-adopt checkpoint would\
    // subtract the source's (stale or nil) baseline and over-report — potentially\
    // the source session's entire subagent usage. Mirrors resetCheckpointWindow's\
    // baseline capture so the first adopted checkpoint only counts target-side\
    // subagent growth, consistent with the PromptWindowBase reset below.\
    adopted.RebaselineSubagentTokens()\
\
    adopted.FullyCondensed = false\
    adopted.UntrackedFilesAtStart = untrackedFiles\
```\
\
Mcmd/entire/cli/session\_adopt.go+8\
\
```\
12 unmodified lines\
\
13\
14\
15\
16\
17\
18\
19\
1082 unmodified lines\
\
1102\
1103\
1104\
1105\
1106\
1107\
1108\
1109\
1110\
1111\
1112\
1113\
1114\
1115\
1116\
1117\
1118\
1119\
1120\
1121\
1122\
1123\
1124\
1125\
1126\
1127\
1128\
1129\
1130\
1131\
1132\
1133\
1134\
1135\
1136\
1137\
1138\
1139\
1140\
1141\
1142\
1143\
1144\
1145\
1146\
1147\
1148\
1149\
1150\
1151\
1152\
1153\
1154\
1155\
1156\
1157\
1158\
1159\
1160\
1161\
1162\
1163\
1164\
1165\
1166\
1167\
\
12 unmodified lines\
\
    "time"\
\
    "github.com/entireio/cli/cmd/entire/cli/agent"\
    "github.com/entireio/cli/cmd/entire/cli/agent/types"\
    "github.com/entireio/cli/cmd/entire/cli/checkpoint/id"\
    "github.com/entireio/cli/cmd/entire/cli/internal/flock"\
    "github.com/entireio/cli/cmd/entire/cli/paths"\
1082 unmodified lines\
\
    }\
}\
\
// TestSessionAdopt_RebaselinesSubagentTokens pins finding 019f5ebf-dc42: cross-repo\
// adoption opens a fresh target-local checkpoint window (StepCount=0,\
// CheckpointTokenUsage=nil), but the cloned TokenUsage carries the SOURCE\
// session's full cumulative subagent total. If SubagentTokensBaseline is not\
// re-baselined to that cumulative, the first post-adopt checkpoint subtracts a\
// stale/nil baseline and over-reports the source session's subagent usage.\
func TestSessionAdopt_RebaselinesSubagentTokens(t *testing.T) {\
    for _, tc := range []struct {\
        name           string\
        sourceBaseline *agent.TokenUsage\
    }{\
        // Source never condensed: baseline is nil, so the first adopted\
        // checkpoint would report the entire cumulative subagent total.\
        {name: "never-condensed-source", sourceBaseline: nil},\
        // Source condensed at an earlier window: its baseline is stale relative\
        // to the current cumulative and must not carry into the target window.\
        {name: "previously-condensed-source", sourceBaseline: &agent.TokenUsage{InputTokens: 200, OutputTokens: 100, APICallCount: 2}},\
    } {\
        t.Run(tc.name, func(t *testing.T) {\
            targetRepo := setupAdoptRepo(t)\
            testutil.WriteFile(t, targetRepo, "feature.txt", "agent change\n")\
            t.Chdir(targetRepo)\
\
            adopted, _, err := buildAdoptedSessionState(context.Background(), &session.State{\
                SessionID:    "test-adopt-subagent-baseline-" + tc.name,\
                AgentType:    agent.AgentTypeClaudeCode,\
                StartedAt:    time.Now().Add(-5 * time.Minute),\
                Phase:        session.PhaseActive,\
                BaseCommit:   "source-head",\
                WorktreePath: "/source/repo",\
                TokenUsage: &agent.TokenUsage{\
                    InputTokens:    1000,\
                    OutputTokens:   500,\
                    APICallCount:   10,\
                    SubagentTokens: &agent.TokenUsage{InputTokens: 500, OutputTokens: 250, APICallCount: 5},\
                },\
                SubagentTokensBaseline: tc.sourceBaseline,\
            })\
            if err != nil {\
                t.Fatalf("buildAdoptedSessionState failed: %v", err)\
            }\
\
            if adopted.SubagentTokensBaseline == nil {\
                t.Fatal("adopted SubagentTokensBaseline = nil, want re-baselined to the cumulative subagent total")\
            }\
            if adopted.SubagentTokensBaseline.InputTokens != 500 || adopted.SubagentTokensBaseline.OutputTokens != 250 {\
                t.Fatalf("adopted SubagentTokensBaseline = %#v, want cumulative subagent total 500/250",\
                    adopted.SubagentTokensBaseline)\
            }\
\
            // The first post-adopt checkpoint delta (cumulative - baseline) must be\
            // zero: adoption should count only target-side subagent growth.\
            delta := types.SubtractTokenUsage(adopted.TokenUsage.SubagentTokens, adopted.SubagentTokensBaseline)\
            if delta.InputTokens != 0 || delta.OutputTokens != 0 || delta.APICallCount != 0 {\
                t.Fatalf("first post-adopt subagent delta = %#v, want zero", delta)\
            }\
        })\
    }\
}\
\
func TestSessionAdopt_PreservesReviewAndInvestigateMetadata(t *testing.T) {\
    for _, tc := range []struct {\
        name string\
```\
\
Mcmd/entire/cli/session\_adopt\_test.go+61\
\
```\
637 unmodified lines\
\
638\
639\
640\
641\
642\
643\
644\
645\
646\
647\
648\
649\
650\
\
637 unmodified lines\
\
    if err != nil {\
        return fmt.Errorf("marshal %s settings: %w", label, err)\
    }\
    // Ensure the parent directory exists, mirroring the struct save path\
    // (saveToFile). Without this, the raw save path fails in a repo that has\
    // never created .entire/ — e.g. a bare `entire disable` in a fresh repo,\
    // which resolves to a raw flip before any directory is created.\
    if err := os.MkdirAll(filepath.Dir(path), 0o750); err != nil {\
        return fmt.Errorf("creating %s settings directory: %w", label, err)\
    }\
    if err := jsonutil.WriteFileAtomic(path, data, 0o644); err != nil {\
        return fmt.Errorf("writing %s settings: %w", label, err)\
    }\
```\
\
Mcmd/entire/cli/settings/settings.go+7\
\
```\
1331 unmodified lines\
\
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\
1361\
1362\
1363\
1364\
1365\
1366\
1367\
1368\
1369\
1370\
1371\
1372\
1373\
1374\
1375\
1376\
\
1331 unmodified lines\
\
    }\
}\
\
// TestSaveProjectRaw_CreatesMissingParentDir verifies the raw save path creates\
// its parent directory, mirroring the struct save path (saveToFile). Without\
// this, a raw enabled-flag flip in a repo that has never created .entire/\
// (e.g. a bare `entire disable` in a fresh repo) hard-fails with "no such file\
// or directory". Regression test for the saveRaw MkdirAll fix.\
func TestSaveProjectRaw_CreatesMissingParentDir(t *testing.T) {\
    tmpDir := t.TempDir()\
    path := filepath.Join(tmpDir, ".entire", "settings.json")\
\
    raw := map[string]json.RawMessage{"enabled": json.RawMessage("false")}\
    if err := SaveProjectRaw(path, raw); err != nil {\
        t.Fatalf("SaveProjectRaw() into a missing .entire dir should succeed, got: %v", err)\
    }\
\
    data, err := os.ReadFile(path)\
    if err != nil {\
        t.Fatalf("settings file should have been created: %v", err)\
    }\
    if !strings.Contains(string(data), `"enabled": false`) {\
        t.Errorf("expected enabled:false, got: %s", data)\
    }\
}\
\
// TestSaveLocalRaw_CreatesMissingParentDir is the local-scope mirror of\
// TestSaveProjectRaw_CreatesMissingParentDir.\
func TestSaveLocalRaw_CreatesMissingParentDir(t *testing.T) {\
    tmpDir := t.TempDir()\
    path := filepath.Join(tmpDir, ".entire", "settings.local.json")\
\
    raw := map[string]json.RawMessage{"enabled": json.RawMessage("false")}\
    if err := SaveLocalRaw(path, raw); err != nil {\
        t.Fatalf("SaveLocalRaw() into a missing .entire dir should succeed, got: %v", err)\
    }\
\
    if _, err := os.ReadFile(path); err != nil {\
        t.Fatalf("local settings file should have been created: %v", err)\
    }\
}\
\
// 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\
```\
\
Mcmd/entire/cli/settings/settings\_test.go+39\
\
```\
1 unmodified line\
\
2\
3\
4\
5\
6\
7\
8\
1040 unmodified lines\
\
1049\
1050\
1051\
1052\
1053\
1054\
1055\
17 unmodified lines\
\
1073\
1074\
1075\
1076\
1077\
1078\
1079\
1080\
1081\
1082\
1083\
1084\
1085\
1086\
32 unmodified lines\
\
1119\
1120\
1121\
1122\
1123\
1124\
1125\
1126\
1127\
1128\
1129\
1130\
1131\
1132\
1133\
1134\
1135\
1136\
1137\
1138\
1139\
1140\
1117\
1141\
1142\
1143\
1144\
1145\
1146\
1147\
1124\
1148\
1149\
1150\
1151\
1152\
1153\
1154\
1155\
1156\
1157\
1158\
1159\
1160\
1161\
1162\
1163\
1164\
1165\
1166\
1167\
1168\
1169\
1170\
1171\
1172\
1173\
1174\
1175\
1176\
168 unmodified lines\
\
1345\
1346\
1347\
1299\
1300\
1301\
1348\
1349\
1350\
1351\
1352\
1303\
1304\
1305\
1306\
1307\
1308\
1309\
1310\
1353\
1354\
1355\
1356\
2 unmodified lines\
\
1359\
1360\
1361\
1362\
1363\
1364\
1365\
1366\
1367\
1368\
1369\
1370\
1371\
1372\
1373\
1374\
1375\
1376\
1377\
1378\
1320\
1321\
1322\
1379\
1380\
1381\
1382\
1383\
1384\
1385\
1325\
1326\
1327\
1386\
1387\
1388\
1389\
1331\
1390\
1391\
1392\
1393\
1335\
1336\
1337\
1394\
1395\
1396\
1397\
1398\
1399\
1400\
1401\
1402\
1403\
1404\
1405\
1406\
1407\
1408\
1409\
1410\
1411\
1412\
1413\
1414\
1415\
1416\
1417\
1418\
1419\
1420\
1421\
1422\
1423\
1424\
1425\
1426\
1427\
1428\
1429\
1430\
1431\
1432\
1433\
1434\
1435\
1436\
1437\
1438\
1439\
1440\
1441\
1442\
1443\
1444\
1445\
1446\
1447\
1448\
1449\
1450\
1451\
1452\
1453\
1454\
1455\
1456\
1457\
1458\
1459\
1460\
1461\
1462\
1463\
1464\
1465\
1343\
1466\
1467\
1345\
1468\
1469\
1470\
1471\
334 unmodified lines\
\
1806\
1807\
1808\
1686\
1687\
1809\
1810\
1811\
1812\
1813\
1814\
1815\
1689\
1690\
1816\
1817\
1692\
1818\
1819\
1820\
1821\
1822\
1823\
1824\
1825\
1826\
1827\
1828\
1829\
1830\
1831\
1832\
1833\
1834\
1835\
1836\
1694\
1837\
1838\
1839\
1697\
1840\
1841\
1842\
1843\
1844\
1702\
1845\
1846\
1847\
1705\
1848\
1849\
1850\
1708\
1851\
1852\
1853\
1854\
1 unmodified line\
\
1856\
1857\
1858\
1716\
1859\
1860\
1861\
1719\
1720\
1862\
1863\
1864\
1865\
1724\
1725\
1726\
1866\
1867\
1868\
1869\
1870\
1871\
1872\
1873\
1874\
1875\
1876\
1877\
1878\
1879\
1880\
1881\
1882\
1883\
1884\
1885\
1729\
1886\
1887\
1888\
1889\
\
1 unmodified line\
\
import (\
    "context"\
    "encoding/json"\
    "errors"\
    "fmt"\
    "io"\
1040 unmodified lines\
\
}\
\
func newDisableCmd() *cobra.Command {\
    var useLocalSettings bool\
    var useProjectSettings bool\
    var uninstall bool\
    var force bool\
17 unmodified lines\
\
            if uninstall {\
                return runUninstall(ctx, cmd.OutOrStdout(), cmd.ErrOrStderr(), force)\
            }\
            if err := validateSetupFlags(useLocalSettings, useProjectSettings); err != nil {\
                return err\
            }\
            return runDisable(ctx, cmd.OutOrStdout(), useProjectSettings)\
        },\
    }\
\
    cmd.Flags().BoolVar(&useLocalSettings, "local", false, "Update .entire/settings.local.json (the default) instead of .entire/settings.json")\
    cmd.Flags().BoolVar(&useProjectSettings, "project", false, "Update .entire/settings.json instead of .entire/settings.local.json")\
    cmd.Flags().BoolVar(&uninstall, "uninstall", false, "Completely remove Entire from this repository")\
    cmd.Flags().BoolVar(&force, "force", false, "Skip confirmation prompt (use with --uninstall)")\
32 unmodified lines\
\
        }\
    }\
\
    // Resolve the target scope first, then decide whether there is anything to\
    // do. Enable writes to the scope resolved by settingsTargetFile, which is\
    // also what strategy/checkpoint-backend updates above use. Without this, a\
    // plain `entire enable` (no --project/--local) resolved the strategy write\
    // to the existing project settings.json but wrote the enabled flag to\
    // settings.local.json, leaving the project file the user disabled still\
    // enabled=false.\
    targetFile, _ := settingsTargetFile(ctx, opts.UseLocalSettings, opts.UseProjectSettings)\
    useProject := targetFile == settings.EntireSettingsFile\
\
    // The merged view can report enabled while the resolved target file is\
    // itself still disabled — exactly the legacy split state a pre-fix binary\
    // left on disk (committed settings.json enabled:false masked by\
    // settings.local.json enabled:true, which wins in the merge). In that case\
    // the early "already enabled" return would never flip the target file, even\
    // with an explicit --project, so `enable` could not recover that split\
    // state. Only short-circuit when the merged view is enabled AND the target\
    // file is not itself explicitly disabled.\
    enabled, err := IsEnabled(ctx)\
    if err == nil && enabled {\
    if err == nil && enabled && !scopeExplicitlyDisabled(ctx, useProject) {\
        if !usedSetupFlow {\
            fmt.Fprintln(w, "Entire is already enabled.")\
        }\
        printEnabledStatus(ctx, w)\
        return nil\
    }\
    return runEnable(ctx, w, opts.UseProjectSettings)\
    return runEnable(ctx, w, useProject)\
}\
\
// scopeExplicitlyDisabled reports whether the settings file for the given scope\
// exists and carries an explicit "enabled": false. A missing file or a missing\
// "enabled" key returns false: those default to enabled, so there is nothing to\
// recover. Used to detect the legacy split state where the merged view is\
// enabled but the target file the user cares about is still disabled.\
func scopeExplicitlyDisabled(ctx context.Context, useProject bool) bool {\
    load := settings.LoadLocalRaw\
    if useProject {\
        load = settings.LoadProjectRaw\
    }\
    _, raw, _, err := load(ctx)\
    if err != nil {\
        return false\
    }\
    value, ok := raw["enabled"]\
    if !ok {\
        return false\
    }\
    var enabled bool\
    if err := json.Unmarshal(value, &enabled); err != nil {\
        return false\
    }\
    return !enabled\
}\
\
func runEnableInteractive(ctx context.Context, w io.Writer, agents []agent.Agent, opts EnableOptions) error {\
168 unmodified lines\
\
    fmt.Fprintln(w, "\nTo add more agents, run `entire agent add <name>`.")\
}\
\
// runEnable sets the enabled flag in settings.\
// Writes to the target file (local by default, project with --project),\
// and also updates the other file if it exists, so they can't get out of sync.\
// runEnable flips the enabled flag to true in the scope chosen by the caller\
// (see setEnabledFlag). Callers resolve the scope: runEnableOnConfiguredRepo\
// uses settingsTargetFile so a bare `entire enable` targets the committed\
// settings.json when present and can recover a repo disabled there.\
func runEnable(ctx context.Context, w io.Writer, useProjectSettings bool) error {\
    s, err := LoadEntireSettings(ctx)\
    if err != nil {\
        return fmt.Errorf("failed to load settings: %w", err)\
    }\
\
    s.Enabled = true\
\
    if err := saveEnabledState(ctx, s, useProjectSettings); err != nil {\
    if err := setEnabledFlag(ctx, true, useProjectSettings); err != nil {\
        return err\
    }\
\
2 unmodified lines\
\
    return nil\
}\
\
// runDisable flips the enabled flag to false in the resolved settings scope.\
//\
// Scope resolution is deliberately asymmetric with enable because\
// settings.local.json overrides settings.json in the merged view:\
//   - bare `entire disable` (and --local) writes settings.local.json — the\
//     minimal, always-effective way to silence Entire on one machine without\
//     editing committed team config;\
//   - --project writes the committed settings.json (and setEnabledFlag also\
//     syncs the local file if present, so a stale local override can't leave\
//     the repo enabled).\
//\
// This restores origin/main's default (bare disable -> local) and matches the\
// --project flag's help text. Enable, by contrast, must reach the committed\
// file to recover a project the user disabled there, so it resolves via\
// settingsTargetFile (see runEnableOnConfiguredRepo). --local is accepted for\
// symmetry with enable; for disable it is the same as the bare default.\
func runDisable(ctx context.Context, w io.Writer, useProjectSettings bool) error {\
    s, err := LoadEntireSettings(ctx)\
    if err != nil {\
        return fmt.Errorf("failed to load settings: %w", err)\
    targetFile := settings.EntireSettingsLocalFile\
    configDisplay := configDisplayLocal\
    if useProjectSettings {\
        targetFile = settings.EntireSettingsFile\
        configDisplay = configDisplayProject\
    }\
\
    s.Enabled = false\
\
    if err := saveEnabledState(ctx, s, useProjectSettings); err != nil {\
    if err := setEnabledFlag(ctx, false, targetFile == settings.EntireSettingsFile); err != nil {\
        return err\
    }\
\
    fmt.Fprintln(w, "Entire is now disabled.")\
    fmt.Fprintf(w, "Entire is now disabled (%s).\n", configDisplay)\
    return nil\
}\
\
// saveEnabledState writes settings to the target file and also updates the\
// other settings file if it exists, preventing local/project from getting\
// out of sync on the enabled field.\
// setEnabledFlag flips only the "enabled" key in the target scope's settings\
// file, and — when writing the project scope — also syncs that one key into\
// settings.local.json if it exists. The sync is one-directional (project ->\
// local) because settings.local.json overrides settings.json in the merged\
// view, so a stale local "enabled": false would otherwise keep the repo\
// disabled after a project-scope re-enable.\
//\
// This is the canonical explanation of the merged-vs-scoped write rule that the\
// whole enable/disable surface follows; other sites point here.\
//\
// The write path stays scoped to a single file's own raw JSON on purpose.\
// Enable/disable *read* current state through the LoadEntireSettings merged\
// view (e.g. IsEnabled), which flattens settings.local.json overrides\
// (local_dev, log_level, personal strategy_options/checkpoint_remote, ...) on\
// top of settings.json. Writing that merged struct back into one file would\
// leak a developer's local-only overrides into the shared, committed project\
// file whenever a write resolves to settings.json. setEnabledRaw\
// therefore edits only the "enabled" key in each file's own content; its\
// sibling saveEnabledState applies the same rule to a caller-provided,\
// already-target-scoped struct.\
func setEnabledFlag(ctx context.Context, enabled, useProjectSettings bool) error {\
    if useProjectSettings {\
        if err := setEnabledRaw(ctx, settings.LoadProjectRaw, settings.SaveProjectRaw, enabled); err != nil {\
            return fmt.Errorf("failed to save settings: %w", err)\
        }\
        // Also update local if it exists, so it doesn't override.\
        if localExists(ctx) {\
            if err := setEnabledRaw(ctx, settings.LoadLocalRaw, settings.SaveLocalRaw, enabled); err != nil {\
                return fmt.Errorf("failed to save local settings: %w", err)\
            }\
        }\
    } else {\
        if err := setEnabledRaw(ctx, settings.LoadLocalRaw, settings.SaveLocalRaw, enabled); err != nil {\
            return fmt.Errorf("failed to save local settings: %w", err)\
        }\
    }\
    return nil\
}\
\
// setEnabledRaw loads a settings file via load, sets its "enabled" key, and\
// writes it back via save, preserving every other key already in that file.\
func setEnabledRaw(\
    ctx context.Context,\
    load func(context.Context) (path string, raw map[string]json.RawMessage, exists bool, err error),\
    save func(path string, raw map[string]json.RawMessage) error,\
    enabled bool,\
) error {\
    path, raw, _, err := load(ctx)\
    if err != nil {\
        return err\
    }\
    value, err := json.Marshal(enabled)\
    if err != nil {\
        return fmt.Errorf("marshal enabled flag: %w", err)\
    }\
    raw["enabled"] = value\
    return save(path, raw)\
}\
\
// saveEnabledState writes the caller-provided, already-target-scoped struct s\
// to the target file, then applies the same one-directional project -> local\
// sync of the "enabled" key as setEnabledFlag (see that function for the full\
// merged-vs-scoped rationale). s must already be scoped to the target file's\
// own content: it is intentionally NOT written into the other scope, which\
// would overwrite that file's own fields (local_dev, log_level, personal\
// strategy_options, ...) — the same leak this rule prevents, in the other\
// direction.\
func saveEnabledState(ctx context.Context, s *EntireSettings, useProjectSettings bool) error {\
    if useProjectSettings {\
        if err := SaveEntireSettings(ctx, s); err != nil {\
            return fmt.Errorf("failed to save settings: %w", err)\
        }\
        // Also update local if it exists, so it doesn't override\
        // Also sync just the enabled key to local if it exists, so it doesn't override.\
        if localExists(ctx) {\
            if err := SaveEntireSettingsLocal(ctx, s); err != nil {\
            if err := setEnabledRaw(ctx, settings.LoadLocalRaw, settings.SaveLocalRaw, s.Enabled); err != nil {\
                return fmt.Errorf("failed to save local settings: %w", err)\
            }\
        }\
334 unmodified lines\
\
        return fmt.Errorf("failed to setup .entire directory: %w", err)\
    }\
\
    // Load existing settings to preserve other options (like strategy_options.push)\
    settings, err := LoadEntireSettings(ctx)\
    // Resolve the target file up front so the load below is scoped to that\
    // file's own content rather than the merged view (see setEnabledFlag for\
    // why: writing the merged struct back into a single scope leaks the other\
    // scope's fields into it).\
    targetFile, configDisplay := settingsTargetFile(ctx, opts.UseLocalSettings, opts.UseProjectSettings)\
    targetFileAbs, err := paths.AbsPath(ctx, targetFile)\
    if err != nil {\
        // If we can't load, start with defaults\
        settings = &EntireSettings{}\
        targetFileAbs = targetFile\
    }\
    settings.Enabled = true\
\
    // Load existing settings from the target file only, to preserve other\
    // options already set there (like strategy_options.push) without pulling\
    // in the other scope's overrides. The local var is named targetSettings so\
    // it does not shadow the settings package for the rest of the function.\
    //\
    // On a parse/validation failure we refuse rather than start from defaults:\
    // the previous behavior silently replaced a settings.json holding real\
    // content (strategy_options, log_level, and — under DisallowUnknownFields —\
    // any key written by a newer CLI) with a bare {"enabled": true}, destroying\
    // the user's config. A missing file is NOT an error here (LoadFromFile\
    // returns defaults for it), so first-time enable still works. This mirrors\
    // updateStrategyOptions, which already refuses on an unparseable target file.\
    targetSettings, err := settings.LoadFromFile(targetFileAbs)\
    if err != nil {\
        return fmt.Errorf("refusing to enable: %s could not be parsed (invalid JSON, or written by a newer entire version); fix or remove it, or upgrade the CLI, then retry: %w", configDisplay, err)\
    }\
    targetSettings.Enabled = true\
    if opts.LocalDev {\
        settings.LocalDev = true\
        targetSettings.LocalDev = true\
    }\
    if opts.AbsoluteGitHookPath {\
        settings.AbsoluteGitHookPath = true\
        targetSettings.AbsoluteGitHookPath = true\
    }\
\
    // Auto-enable external_agents setting if the agent is external.\
    if external.IsExternal(ag) {\
        settings.ExternalAgents = true\
        targetSettings.ExternalAgents = true\
    }\
\
    opts.applyStrategyOptions(settings)\
    opts.applyStrategyOptions(targetSettings)\
\
    // Apply an explicit --checkpoint-backend (no prompt on this non-interactive path).\
    if err := applyCheckpointBackendFlag(settings, opts.CheckpointBackend); err != nil {\
    if err := applyCheckpointBackendFlag(targetSettings, opts.CheckpointBackend); err != nil {\
        return err\
    }\
\
1 unmodified line\
\
    // Note: if telemetry is nil (not configured), it defaults to disabled\
    if !opts.Telemetry || os.Getenv("ENTIRE_TELEMETRY_OPTOUT") != "" {\
        f := false\
        settings.Telemetry = &f\
        targetSettings.Telemetry = &f\
    }\
\
    targetFile, configDisplay := settingsTargetFile(ctx, opts.UseLocalSettings, opts.UseProjectSettings)\
    if err := saveEnabledState(ctx, settings, targetFile == EntireSettingsFile); err != nil {\
    if err := saveEnabledState(ctx, targetSettings, targetFile == EntireSettingsFile); err != nil {\
        return fmt.Errorf("failed to save settings: %w", err)\
    }\
\
    // Use settings values (merged from existing config + flags) for hook installation\
    // This ensures re-running `entire enable --agent X` without flags preserves existing settings\
    if _, err := strategy.InstallGitHook(ctx, true, settings.LocalDev, settings.AbsoluteGitHookPath); err != nil {\
    // Hook installation decisions need the merged view across both settings\
    // files, not just the single scope we wrote to above: local_dev and\
    // absolute_git_hook_path may be set only in settings.local.json while\
    // this enable resolves to settings.json (or vice versa). Using the\
    // target-scoped struct here would silently drop that override when\
    // regenerating the git hook script. This mirrors runEnableInteractive,\
    // which uses the merged view for the same two fields; only the *write*\
    // path (saveEnabledState above) stays scoped to the target file (see\
    // setEnabledFlag for why).\
    mergedSettings, err := LoadEntireSettings(ctx)\
    if err != nil {\
        logging.Warn(ctx, "could not load merged settings for hook installation; proceeding with target-scoped settings only, so local overrides (e.g. local_dev, absolute_git_hook_path) may not be applied to the generated git hook", "error", err)\
        mergedSettings = targetSettings\
    }\
    hookLocalDev := mergedSettings.LocalDev || opts.LocalDev\
    hookAbsoluteGitHookPath := mergedSettings.AbsoluteGitHookPath || opts.AbsoluteGitHookPath\
\
    if _, err := strategy.InstallGitHook(ctx, true, hookLocalDev, hookAbsoluteGitHookPath); err != nil {\
        return fmt.Errorf("failed to install git hooks: %w", err)\
    }\
    strategy.CheckAndWarnHookManagers(ctx, w, settings.LocalDev, settings.AbsoluteGitHookPath)\
    strategy.CheckAndWarnHookManagers(ctx, w, hookLocalDev, hookAbsoluteGitHookPath)\
\
    if installedHooks == 0 {\
        msg := fmt.Sprintf("Hooks for %s already installed", ag.Description())\
```\
\
Mcmd/entire/cli/setup.go+199/-42\
\
```\
224 unmodified lines\
\
225\
226\
227\
228\
229\
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\
234\
235\
236\
237\
238\
239\
240\
323\
324\
325\
326\
327\
328\
246\
329\
330\
331\
249\
250\
332\
333\
334\
335\
336\
255\
337\
338\
339\
340\
1 unmodified line\
\
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\
266\
405\
406\
407\
408\
409\
410\
271\
272\
273\
274\
411\
412\
413\
414\
415\
416\
417\
418\
419\
277\
278\
420\
421\
422\
423\
424\
283\
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\
298\
299\
468\
469\
470\
471\
472\
473\
302\
474\
475\
476\
477\
306\
307\
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\
315\
512\
513\
514\
515\
516\
320\
517\
518\
322\
519\
520\
324\
325\
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\
676\
677\
678\
679\
680\
681\
682\
683\
684\
685\
686\
687\
688\
689\
690\
691\
692\
693\
694\
695\
696\
697\
698\
699\
700\
701\
702\
703\
704\
705\
706\
707\
708\
709\
710\
711\
712\
713\
714\
715\
716\
717\
718\
719\
720\
721\
722\
723\
724\
725\
726\
727\
728\
729\
730\
140 unmodified lines\
\
871\
872\
873\
472\
473\
474\
475\
874\
875\
876\
877\
878\
879\
880\
881\
882\
883\
884\
885\
3 unmodified lines\
\
889\
890\
891\
485\
892\
893\
894\
895\
2 unmodified lines\
\
898\
899\
900\
494\
901\
902\
903\
497\
904\
905\
906\
500\
907\
908\
909\
503\
910\
911\
912\
913\
914\
915\
509\
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\
958\
959\
960\
961\
962\
963\
964\
965\
966\
967\
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\
1026\
1027\
1028\
1029\
1030\
1031\
1032\
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\
1092\
1093\
1094\
1095\
1096\
1097\
1098\
1099\
1100\
1101\
1102\
1103\
1104\
1105\
1106\
1107\
1108\
1109\
1110\
1111\
1112\
1113\
1114\
1115\
1116\
1117\
1118\
1119\
1120\
1121\
1122\
1123\
1124\
1125\
652 unmodified lines\
\
1778\
1779\
1780\
1781\
1782\
1783\
1784\
1785\
1786\
1787\
1788\
1789\
1790\
1791\
1792\
1793\
1794\
1795\
1796\
1797\
1798\
1799\
1800\
1801\
1802\
1803\
1804\
1805\
1806\
1807\
1808\
1809\
1810\
1811\
1812\
\
224 unmodified lines\
\
    }\
}\
\
// TestRunEnable_ProjectFlag_ClearsLocalDisable verifies that `entire enable --project`\
// after `entire disable` (which writes to local) actually re-enables by updating both files.\
// TestRunEnableOnConfiguredRepo_RecoversLegacySplitState covers recovering\
// the split state a pre-fix binary left on disk — committed\
// settings.json enabled:false, settings.local.json enabled:true. The local\
// override wins in the merged view, so IsEnabled reports true; a bare early\
// return on the merged view would leave the committed project file disabled\
// forever, even with an explicit --project. runEnableOnConfiguredRepo must\
// detect that the target scope is itself disabled and flip it.\
func TestRunEnableOnConfiguredRepo_RecoversLegacySplitState(t *testing.T) {\
    setupTestRepo(t)\
    // Legacy split state.\
    writeSettings(t, testSettingsDisabled)\
    writeLocalSettings(t, `{"enabled": true}`)\
\
    // Sanity: the merged view already reports enabled (local override wins).\
    enabled, err := IsEnabled(context.Background())\
    if err != nil {\
        t.Fatalf("IsEnabled() error = %v", err)\
    }\
    if !enabled {\
        t.Fatal("precondition: merged view should report enabled (local override wins)")\
    }\
\
    cmd := newEnableCmd()\
    var buf bytes.Buffer\
    cmd.SetOut(&buf)\
    if err := runEnableOnConfiguredRepo(context.Background(), cmd, EnableOptions{UseProjectSettings: true}); err != nil {\
        t.Fatalf("runEnableOnConfiguredRepo(--project) error = %v", err)\
    }\
\
    // The committed project file must now be enabled — this split state could\
    // not recover before this fix.\
    projectS, err := settings.LoadFromFile(EntireSettingsFile)\
    if err != nil {\
        t.Fatalf("failed to load project settings: %v", err)\
    }\
    if !projectS.Enabled {\
        t.Error("committed settings.json should be enabled:true after enable --project recovered the split state")\
    }\
}\
\
// TestRunEnableOnConfiguredRepo_BareEnable_RecoversLegacySplitState verifies the\
// same recovery happens for a bare `entire enable` (no --project), which\
// resolves to the committed settings.json via settingsTargetFile.\
func TestRunEnableOnConfiguredRepo_BareEnable_RecoversLegacySplitState(t *testing.T) {\
    setupTestRepo(t)\
    writeSettings(t, testSettingsDisabled)\
    writeLocalSettings(t, `{"enabled": true}`)\
\
    cmd := newEnableCmd()\
    var buf bytes.Buffer\
    cmd.SetOut(&buf)\
    if err := runEnableOnConfiguredRepo(context.Background(), cmd, EnableOptions{}); err != nil {\
        t.Fatalf("runEnableOnConfiguredRepo() error = %v", err)\
    }\
\
    projectS, err := settings.LoadFromFile(EntireSettingsFile)\
    if err != nil {\
        t.Fatalf("failed to load project settings: %v", err)\
    }\
    if !projectS.Enabled {\
        t.Error("committed settings.json should be enabled:true after a bare enable recovered the split state")\
    }\
}\
\
// TestRunEnableOnConfiguredRepo_AlreadyEnabled_NoSplit verifies the early\
// return still fires (nothing to flip, "already enabled") when the merged view\
// AND the resolved target scope agree that Entire is enabled.\
func TestRunEnableOnConfiguredRepo_AlreadyEnabled_NoSplit(t *testing.T) {\
    setupTestRepo(t)\
    writeSettings(t, testSettingsEnabled)\
\
    cmd := newEnableCmd()\
    var buf bytes.Buffer\
    cmd.SetOut(&buf)\
    if err := runEnableOnConfiguredRepo(context.Background(), cmd, EnableOptions{}); err != nil {\
        t.Fatalf("runEnableOnConfiguredRepo() error = %v", err)\
    }\
    if !strings.Contains(buf.String(), "already enabled") {\
        t.Errorf("expected 'already enabled' output when nothing to recover, got: %s", buf.String())\
    }\
}\
\
// TestRunEnable_ProjectFlag_ClearsLocalDisable verifies that `entire enable\
// --project` clears a real local disable override. The precondition is seeded\
// directly (settings.local.json enabled:false with a local-only field) rather\
// than through runDisable, so the "local override wins and must be cleared"\
// scenario is genuinely exercised — the local-sync in setEnabledFlag's project\
// branch is what makes the re-enable stick.\
func TestRunEnable_ProjectFlag_ClearsLocalDisable(t *testing.T) {\
    setupTestDir(t)\
    writeSettings(t, testSettingsEnabled)\
    // A real local disable override with a local-only field to prove the sync\
    // touches only the enabled key.\
    writeLocalSettings(t, `{"enabled": false, "local_dev": true}`)\
\
    // Simulate `entire disable` (writes enabled:false to local)\
    var buf bytes.Buffer\
    if err := runDisable(context.Background(), &buf, false); err != nil {\
        t.Fatalf("runDisable() error = %v", err)\
    }\
\
    // Verify it's disabled\
    // Precondition: the local override wins, so the merged view is disabled.\
    enabled, err := IsEnabled(context.Background())\
    if err != nil {\
        t.Fatalf("IsEnabled() error = %v", err)\
    }\
    if enabled {\
        t.Fatal("Expected disabled after runDisable")\
        t.Fatal("precondition: local override should make the merged view disabled")\
    }\
\
    // Now re-enable with --project flag\
    buf.Reset()\
    var buf bytes.Buffer\
    if err := runEnable(context.Background(), &buf, true); err != nil {\
        t.Fatalf("runEnable(project=true) error = %v", err)\
    }\
\
    // Must actually be enabled — local override must not win\
    // Must actually be enabled — the local override must have been cleared.\
    enabled, err = IsEnabled(context.Background())\
    if err != nil {\
        t.Fatalf("IsEnabled() error = %v", err)\
1 unmodified line\
\
    if !enabled {\
        t.Error("Expected enabled after runEnable --project, but IsEnabled() returned false (local override not cleared)")\
    }\
\
    // The local file's enabled key was synced to true, and its local-only\
    // field survived.\
    localContent, err := os.ReadFile(EntireSettingsLocalFile)\
    if err != nil {\
        t.Fatalf("failed to read local settings: %v", err)\
    }\
    if !strings.Contains(string(localContent), `"enabled":true`) && !strings.Contains(string(localContent), `"enabled": true`) {\
        t.Errorf("local override should be synced to enabled:true, got: %s", localContent)\
    }\
    if !strings.Contains(string(localContent), "local_dev") {\
        t.Errorf("local-only field local_dev should be retained, got: %s", localContent)\
    }\
}\
\
// TestRunEnable_ProjectScope_ClearsExplicitLocalDisable seeds both files\
// disabled (committed settings.json enabled:false AND settings.local.json\
// enabled:false with local_dev) and asserts that a project-scope enable flips\
// both and retains the local-only field. This is the mutation-sensitive test\
// for setEnabledFlag's project-branch local sync: skipping the sync leaves the\
// local override at enabled:false, which would win and keep IsEnabled false.\
func TestRunEnable_ProjectScope_ClearsExplicitLocalDisable(t *testing.T) {\
    setupTestDir(t)\
    writeSettings(t, testSettingsDisabled)\
    writeLocalSettings(t, `{"enabled": false, "local_dev": true}`)\
\
    var buf bytes.Buffer\
    if err := runEnable(context.Background(), &buf, true); err != nil {\
        t.Fatalf("runEnable(project=true) error = %v", err)\
    }\
\
    enabled, err := IsEnabled(context.Background())\
    if err != nil {\
        t.Fatalf("IsEnabled() error = %v", err)\
    }\
    if !enabled {\
        t.Error("Expected enabled after runEnable --project (local override must be synced to enabled:true)")\
    }\
\
    projectContent, err := os.ReadFile(EntireSettingsFile)\
    if err != nil {\
        t.Fatalf("failed to read project settings: %v", err)\
    }\
    if !strings.Contains(string(projectContent), `"enabled":true`) && !strings.Contains(string(projectContent), `"enabled": true`) {\
        t.Errorf("committed project settings should be enabled:true, got: %s", projectContent)\
    }\
\
    localContent, err := os.ReadFile(EntireSettingsLocalFile)\
    if err != nil {\
        t.Fatalf("failed to read local settings: %v", err)\
    }\
    if !strings.Contains(string(localContent), `"enabled":true`) && !strings.Contains(string(localContent), `"enabled": true`) {\
        t.Errorf("local override should be synced to enabled:true, got: %s", localContent)\
    }\
    if !strings.Contains(string(localContent), "local_dev") {\
        t.Errorf("local-only field local_dev should be retained, got: %s", localContent)\
    }\
}\
\
// TestRunEnable_DefaultFlag_ClearsLocalDisable verifies that `entire enable`\
// (default, no --project) after `entire disable` actually re-enables.\
// (default/local scope) clears an explicitly-seeded local disable override.\
func TestRunEnable_DefaultFlag_ClearsLocalDisable(t *testing.T) {\
    setupTestDir(t)\
    writeSettings(t, testSettingsEnabled)\
    writeLocalSettings(t, `{"enabled": false, "local_dev": true}`)\
\
    // Simulate `entire disable` (writes enabled:false to local)\
    var buf bytes.Buffer\
    if err := runDisable(context.Background(), &buf, false); err != nil {\
        t.Fatalf("runDisable() error = %v", err)\
    // Precondition: local override wins → disabled.\
    enabled, err := IsEnabled(context.Background())\
    if err != nil {\
        t.Fatalf("IsEnabled() error = %v", err)\
    }\
    if enabled {\
        t.Fatal("precondition: local override should make the merged view disabled")\
    }\
\
    // Now re-enable with default (no --project)\
    buf.Reset()\
    var buf bytes.Buffer\
    if err := runEnable(context.Background(), &buf, false); err != nil {\
        t.Fatalf("runEnable(project=false) error = %v", err)\
    }\
\
    enabled, err := IsEnabled(context.Background())\
    enabled, err = IsEnabled(context.Background())\
    if err != nil {\
        t.Fatalf("IsEnabled() error = %v", err)\
    }\
    if !enabled {\
        t.Error("Expected enabled after runEnable, but IsEnabled() returned false")\
    }\
\
    localContent, err := os.ReadFile(EntireSettingsLocalFile)\
    if err != nil {\
        t.Fatalf("failed to read local settings: %v", err)\
    }\
    if !strings.Contains(string(localContent), "local_dev") {\
        t.Errorf("local-only field local_dev should be retained, got: %s", localContent)\
    }\
}\
\
// TestSetupAgentHooksNonInteractive_ClearsLocalDisable verifies that a\
// project-scope `enable --agent` clears a real local disable override. The\
// precondition is seeded directly (settings.local.json enabled:false) rather\
// than via runDisable, and the assertion checks the local override was actually\
// synced — otherwise "ClearsLocalDisable" would assert nothing.\
func TestSetupAgentHooksNonInteractive_ClearsLocalDisable(t *testing.T) {\
    setupTestRepo(t)\
    writeSettings(t, testSettingsEnabled)\
    writeLocalSettings(t, `{"enabled": false, "local_dev": true}`)\
    writeClaudeHooksFixture(t)\
\
    // Precondition: local override wins → disabled.\
    enabled, err := IsEnabled(context.Background())\
    if err != nil {\
        t.Fatalf("IsEnabled() error = %v", err)\
    }\
    if enabled {\
        t.Fatal("precondition: local override should make the merged view disabled")\
    }\
\
    ag, err := agent.Get(types.AgentName("claude-code"))\
    if err != nil {\
        t.Fatalf("agent.Get(claude-code) error = %v", err)\
    }\
\
    var buf bytes.Buffer\
    if err := runDisable(context.Background(), &buf, false); err != nil {\
        t.Fatalf("runDisable() error = %v", err)\
    // UseProjectSettings so the enable resolves to the committed file and its\
    // project branch syncs the local override.\
    if err := setupAgentHooksNonInteractive(context.Background(), &buf, ag, EnableOptions{UseProjectSettings: true}); err != nil {\
        t.Fatalf("setupAgentHooksNonInteractive() error = %v", err)\
    }\
\
    enabled, err := IsEnabled(context.Background())\
    enabled, err = IsEnabled(context.Background())\
    if err != nil {\
        t.Fatalf("IsEnabled() error = %v", err)\
    }\
    if enabled {\
        t.Fatal("expected disabled after runDisable")\
    if !enabled {\
        t.Fatal("expected enabled after setupAgentHooksNonInteractive (local override must be cleared)")\
    }\
\
    localContent, err := os.ReadFile(EntireSettingsLocalFile)\
    if err != nil {\
        t.Fatalf("failed to read local settings: %v", err)\
    }\
    if !strings.Contains(string(localContent), `"enabled":true`) && !strings.Contains(string(localContent), `"enabled": true`) {\
        t.Errorf("local override should be synced to enabled:true, got: %s", localContent)\
    }\
    if !strings.Contains(string(localContent), "local_dev") {\
        t.Errorf("local-only field local_dev should be retained, got: %s", localContent)\
    }\
}\
\
// TestSetupAgentHooksNonInteractive_DoesNotLeakLocalOverridesIntoProject:\
// `entire enable --agent <name>` on an already-configured repo used to load the\
// merged settings view (LoadEntireSettings) and write it back wholesale to the\
// project file via saveEnabledState, flattening settings.local.json-only\
// overrides (e.g. log_level) into the shared, committed settings.json — the\
// same leak fixed for the bare enable/disable path, just via a different\
// entry point (setupAgentHooksNonInteractive).\
func TestSetupAgentHooksNonInteractive_DoesNotLeakLocalOverridesIntoProject(t *testing.T) {\
    setupTestRepo(t)\
    writeSettings(t, testSettingsEnabled)\
    writeLocalSettings(t, `{"log_level": "debug"}`)\
    writeClaudeHooksFixture(t)\
\
    ag, err := agent.Get(types.AgentName("claude-code"))\
    if err != nil {\
        t.Fatalf("agent.Get(claude-code) error = %v", err)\
    }\
\
    buf.Reset()\
    var buf bytes.Buffer\
    if err := setupAgentHooksNonInteractive(context.Background(), &buf, ag, EnableOptions{}); err != nil {\
        t.Fatalf("setupAgentHooksNonInteractive() error = %v", err)\
    }\
\
    enabled, err = IsEnabled(context.Background())\
    projectS, err := settings.LoadFromFile(EntireSettingsFile)\
    if err != nil {\
        t.Fatalf("IsEnabled() error = %v", err)\
        t.Fatalf("failed to load project settings: %v", err)\
    }\
    if !enabled {\
        t.Fatal("expected enabled after setupAgentHooksNonInteractive")\
    if projectS.LogLevel != "" {\
        t.Errorf("local-only log_level leaked into project settings: %q", projectS.LogLevel)\
    }\
    if !projectS.Enabled {\
        t.Error("expected project settings to remain enabled")\
    }\
\
    localS, err := settings.LoadFromFile(EntireSettingsLocalFile)\
    if err != nil {\
        t.Fatalf("failed to load local settings: %v", err)\
    }\
    if localS.LogLevel != "debug" {\
        t.Errorf("expected local log_level to be preserved, got %q", localS.LogLevel)\
    }\
}\
\
// TestSetupAgentHooksNonInteractive_UsesMergedViewForHookInstall:\
// setupAgentHooksNonInteractive loads settings.LoadFromFile scoped to a single\
// file for building the settings struct it writes. If local_dev is set only in\
// settings.local.json while this enable resolves (via --project) to\
// settings.json, the local_dev override must still be honored when\
// installing/regenerating the git hook script — otherwise it's silently\
// dropped and the hook reverts to the plain "entire" cmd prefix instead of\
// the local-dev "./scripts/entire-dev" one. Write scoping (no leaking\
// local_dev into the committed project file) must still hold.\
func TestSetupAgentHooksNonInteractive_UsesMergedViewForHookInstall(t *testing.T) {\
    setupTestRepo(t)\
    writeSettings(t, testSettingsEnabled)\
    writeLocalSettings(t, `{"enabled": true, "local_dev": true}`)\
    writeClaudeHooksFixture(t)\
\
    ag, err := agent.Get(types.AgentName("claude-code"))\
    if err != nil {\
        t.Fatalf("agent.Get(claude-code) error = %v", err)\
    }\
\
    var buf bytes.Buffer\
    opts := EnableOptions{UseProjectSettings: true}\
    if err := setupAgentHooksNonInteractive(context.Background(), &buf, ag, opts); err != nil {\
        t.Fatalf("setupAgentHooksNonInteractive() error = %v", err)\
    }\
\
    // The git hook script must reflect the merged local_dev override, even\
    // though the write resolved to the project file.\
    hooksDir, err := strategy.GetHooksDir(context.Background())\
    if err != nil {\
        t.Fatalf("GetHooksDir() error = %v", err)\
    }\
    hookContent, err := os.ReadFile(filepath.Join(hooksDir, "post-commit"))\
    if err != nil {\
        t.Fatalf("failed to read post-commit hook: %v", err)\
    }\
    if !strings.Contains(string(hookContent), "./scripts/entire-dev") {\
        t.Errorf("expected hook to use local-dev cmd prefix from the merged view, got: %s", hookContent)\
    }\
\
    // The write path must still stay scoped: local_dev must not leak into\
    // the committed project settings.json.\
    projectS, err := settings.LoadFromFile(EntireSettingsFile)\
    if err != nil {\
        t.Fatalf("failed to load project settings: %v", err)\
    }\
    if projectS.LocalDev {\
        t.Error("local-only local_dev override leaked into project settings")\
    }\
    if !projectS.Enabled {\
        t.Error("expected project settings to remain enabled")\
    }\
}\
\
// TestSetupAgentHooksNonInteractive_UsesMergedAbsoluteHookPathForHookInstall is\
// the absolute_git_hook_path counterpart of the local_dev merged-view test:\
// with absolute_git_hook_path set only in settings.local.json while the enable\
// resolves (via --project) to settings.json, the generated hook must embed the\
// absolute binary path from the merged view — not fall back to the bare\
// "entire" prefix the target-scoped struct alone would yield. Guards against a\
// mutation reverting hookAbsoluteGitHookPath to the scoped struct.\
func TestSetupAgentHooksNonInteractive_UsesMergedAbsoluteHookPathForHookInstall(t *testing.T) {\
    setupTestRepo(t)\
    writeSettings(t, testSettingsEnabled)\
    // absolute_git_hook_path only in the local override; no local_dev, which\
    // would otherwise take precedence in hookCmdPrefix.\
    writeLocalSettings(t, `{"enabled": true, "absolute_git_hook_path": true}`)\
    writeClaudeHooksFixture(t)\
\
    ag, err := agent.Get(types.AgentName("claude-code"))\
    if err != nil {\
        t.Fatalf("agent.Get(claude-code) error = %v", err)\
    }\
\
    var buf bytes.Buffer\
    opts := EnableOptions{UseProjectSettings: true}\
    if err := setupAgentHooksNonInteractive(context.Background(), &buf, ag, opts); err != nil {\
        t.Fatalf("setupAgentHooksNonInteractive() error = %v", err)\
    }\
\
    // The hook must embed the resolved absolute executable path (what\
    // absolute_git_hook_path produces), proving the merged override was honored.\
    exe, err := os.Executable()\
    if err != nil {\
        t.Fatalf("os.Executable() error = %v", err)\
    }\
    resolved, err := filepath.EvalSymlinks(exe)\
    if err != nil {\
        t.Fatalf("EvalSymlinks() error = %v", err)\
    }\
\
    hooksDir, err := strategy.GetHooksDir(context.Background())\
    if err != nil {\
        t.Fatalf("GetHooksDir() error = %v", err)\
    }\
    hookContent, err := os.ReadFile(filepath.Join(hooksDir, "post-commit"))\
    if err != nil {\
        t.Fatalf("failed to read post-commit hook: %v", err)\
    }\
    if !strings.Contains(string(hookContent), resolved) {\
        t.Errorf("expected hook to embed absolute binary path %q from the merged view, got: %s", resolved, hookContent)\
    }\
\
    // The write path must still stay scoped: absolute_git_hook_path must not\
    // leak into the committed project settings.json.\
    projectS, err := settings.LoadFromFile(EntireSettingsFile)\
    if err != nil {\
        t.Fatalf("failed to load project settings: %v", err)\
    }\
    if projectS.AbsoluteGitHookPath {\
        t.Error("local-only absolute_git_hook_path override leaked into project settings")\
    }\
}\
\
// TestSetupAgentHooksNonInteractive_LocalTarget_DoesNotLeakProjectFieldsIntoLocal\
// covers the mirror-image direction: writing to settings.local.json (--local)\
// must not flatten project-only fields into the local file either.\
func TestSetupAgentHooksNonInteractive_LocalTarget_DoesNotLeakProjectFieldsIntoLocal(t *testing.T) {\
    setupTestRepo(t)\
    writeSettings(t, `{"enabled": true, "log_level": "warn"}`)\
    writeClaudeHooksFixture(t)\
\
    ag, err := agent.Get(types.AgentName("claude-code"))\
    if err != nil {\
        t.Fatalf("agent.Get(claude-code) error = %v", err)\
    }\
\
    var buf bytes.Buffer\
    opts := EnableOptions{UseLocalSettings: true}\
    if err := setupAgentHooksNonInteractive(context.Background(), &buf, ag, opts); err != nil {\
        t.Fatalf("setupAgentHooksNonInteractive() error = %v", err)\
    }\
\
    localS, err := settings.LoadFromFile(EntireSettingsLocalFile)\
    if err != nil {\
        t.Fatalf("failed to load local settings: %v", err)\
    }\
    if localS.LogLevel != "" {\
        t.Errorf("project-only log_level leaked into local settings: %q", localS.LogLevel)\
    }\
    if !localS.Enabled {\
        t.Error("expected local settings to be enabled")\
    }\
\
    projectS, err := settings.LoadFromFile(EntireSettingsFile)\
    if err != nil {\
        t.Fatalf("failed to load project settings: %v", err)\
    }\
    if projectS.LogLevel != "warn" {\
        t.Errorf("expected project log_level to be preserved, got %q", projectS.LogLevel)\
    }\
}\
\
// TestSetupAgentHooksNonInteractive_RefusesToClobberUnparseableSettings covers\
// the finding that `entire enable --agent` silently wiped a corrupt or\
// newer-versioned target settings file to defaults. settings.LoadFromFile\
// errors on invalid JSON AND on any unknown key (DisallowUnknownFields); the\
// old catch replaced the struct with defaults and wrote it back, so a\
// settings.json with strategy_options/log_level/one-unknown-key became exactly\
// {"enabled": true}. Now it refuses and leaves the file untouched.\
func TestSetupAgentHooksNonInteractive_RefusesToClobberUnparseableSettings(t *testing.T) {\
    setupTestRepo(t)\
    // A settings.json a newer CLI could write: valid JSON, real content, plus a\
    // key this build doesn't recognize (rejected by DisallowUnknownFields).\
    original := `{"enabled": false, "log_level": "debug", "totally_unknown_future_key": 42}`\
    writeSettings(t, original)\
    writeClaudeHooksFixture(t)\
\
    ag, err := agent.Get(types.AgentName("claude-code"))\
    if err != nil {\
        t.Fatalf("agent.Get(claude-code) error = %v", err)\
    }\
\
    var buf bytes.Buffer\
    if err := setupAgentHooksNonInteractive(context.Background(), &buf, ag, EnableOptions{}); err == nil {\
        t.Fatal("expected setupAgentHooksNonInteractive to refuse on an unparseable settings file, got nil error")\
    }\
\
    // The file must be left as-is, not wiped to {"enabled": true}.\
    got, err := os.ReadFile(EntireSettingsFile)\
    if err != nil {\
        t.Fatalf("failed to read project settings: %v", err)\
    }\
    if !strings.Contains(string(got), "totally_unknown_future_key") {\
        t.Errorf("unknown key must survive (file must not be clobbered), got: %s", got)\
    }\
    if !strings.Contains(string(got), "log_level") {\
        t.Errorf("log_level must survive (file must not be clobbered), got: %s", got)\
    }\
    if strings.Contains(string(got), `"enabled": true`) || strings.Contains(string(got), `"enabled":true`) {\
        t.Errorf("enabled must not have been flipped/rewritten, got: %s", got)\
    }\
}\
\
140 unmodified lines\
\
    }\
}\
\
// TestRunDisable_CreatesLocalSettingsWhenMissing verifies that running\
// `entire disable` without --project creates settings.local.json when it\
// doesn't exist, rather than writing to settings.json.\
func TestRunDisable_CreatesLocalSettingsWhenMissing(t *testing.T) {\
// TestRunDisable_BareCommand_WritesLocalOverrideWhenProjectOnly verifies that a\
// bare `entire disable`, on a repo that only has a committed settings.json (no\
// settings.local.json yet), writes the enabled:false override into\
// settings.local.json and leaves the committed settings.json untouched. Bare\
// disable is a personal, non-destructive silence: because local overrides\
// project in the merged view, it makes IsEnabled false without editing shared\
// team config. Restores origin/main behavior; regression test for the bare\
// disable scope-resolution finding.\
func TestRunDisable_BareCommand_WritesLocalOverrideWhenProjectOnly(t *testing.T) {\
    setupTestDir(t)\
    // Only create project settings (no local settings)\
    writeSettings(t, testSettingsEnabled)\
3 unmodified lines\
\
        t.Fatalf("runDisable() error = %v", err)\
    }\
\
    // Should be disabled\
    // Should be disabled (local override wins in the merged view).\
    enabled, err := IsEnabled(context.Background())\
    if err != nil {\
        t.Fatalf("IsEnabled(context.Background()) error = %v", err)\
2 unmodified lines\
\
        t.Error("Entire should be disabled after running disable command")\
    }\
\
    // Local settings file should be created with enabled:false\
    // The local override should be created with enabled:false.\
    localContent, err := os.ReadFile(EntireSettingsLocalFile)\
    if err != nil {\
        t.Fatalf("Local settings file should have been created: %v", err)\
        t.Fatalf("settings.local.json should have been created: %v", err)\
    }\
    if !strings.Contains(string(localContent), `"enabled":false`) && !strings.Contains(string(localContent), `"enabled": false`) {\
        t.Errorf("Local settings should have enabled:false, got: %s", localContent)\
        t.Errorf("local settings should have enabled:false, got: %s", localContent)\
    }\
\
    // Project settings should remain unchanged (still enabled)\
    // The committed project file must be left untouched (still enabled).\
    projectContent, err := os.ReadFile(EntireSettingsFile)\
    if err != nil {\
        t.Fatalf("Failed to read project settings: %v", err)\
    }\
    if !strings.Contains(string(projectContent), `"enabled":true`) && !strings.Contains(string(projectContent), `"enabled": true`) {\
        t.Errorf("Project settings should still have enabled:true, got: %s", projectContent)\
        t.Errorf("committed project settings should stay enabled:true after a bare disable, got: %s", projectContent)\
    }\
}\
\
// TestRunDisable_CreatesSettingsDirWhenMissing verifies that a bare `entire\
// disable` succeeds in a repo that has never created a .entire/ directory,\
// creating settings.local.json (with its parent dir) rather than hard-failing.\
// End-to-end regression test for the saveRaw MkdirAll fix.\
func TestRunDisable_CreatesSettingsDirWhenMissing(t *testing.T) {\
    setupTestDir(t)\
    // No .entire/ directory or settings files at all.\
\
    var stdout bytes.Buffer\
    if err := runDisable(context.Background(), &stdout, false); err != nil {\
        t.Fatalf("runDisable() in a repo with no .entire/ dir should succeed, got: %v", err)\
    }\
\
    enabled, err := IsEnabled(context.Background())\
    if err != nil {\
        t.Fatalf("IsEnabled(context.Background()) error = %v", err)\
    }\
    if enabled {\
        t.Error("Entire should be disabled after running disable command")\
    }\
\
    localContent, err := os.ReadFile(EntireSettingsLocalFile)\
    if err != nil {\
        t.Fatalf("settings.local.json should have been created: %v", err)\
    }\
    if !strings.Contains(string(localContent), `"enabled":false`) && !strings.Contains(string(localContent), `"enabled": false`) {\
        t.Errorf("local settings should have enabled:false, got: %s", localContent)\
    }\
}\
\
// TestRunDisable_BareCommand_WritesLocalWhenBothExist verifies that a bare\
// `entire disable`, when both settings.json and settings.local.json exist,\
// writes enabled:false into the local override only and leaves the committed\
// settings.json untouched (no field leakage between scopes). Regression test\
// for the bare disable scope-resolution finding.\
func TestRunDisable_BareCommand_WritesLocalWhenBothExist(t *testing.T) {\
    setupTestDir(t)\
    writeSettings(t, `{"enabled": true, "log_level": "warn"}`)\
    writeLocalSettings(t, `{"enabled": true, "local_dev": true}`)\
\
    var stdout bytes.Buffer\
    if err := runDisable(context.Background(), &stdout, false); err != nil {\
        t.Fatalf("runDisable() error = %v", err)\
    }\
\
    enabled, err := IsEnabled(context.Background())\
    if err != nil {\
        t.Fatalf("IsEnabled() error = %v", err)\
    }\
    if enabled {\
        t.Error("Entire should be disabled after running disable command")\
    }\
\
    // The committed project file must be untouched: still enabled, keeps its\
    // own fields, and never gains the local-only override.\
    projectContent, err := os.ReadFile(EntireSettingsFile)\
    if err != nil {\
        t.Fatalf("failed to read project settings: %v", err)\
    }\
    if !strings.Contains(string(projectContent), `"enabled":true`) && !strings.Contains(string(projectContent), `"enabled": true`) {\
        t.Errorf("committed project settings should stay enabled:true after a bare disable, got: %s", projectContent)\
    }\
    if !strings.Contains(string(projectContent), "log_level") {\
        t.Errorf("project settings should retain its own log_level field, got: %s", projectContent)\
    }\
    if strings.Contains(string(projectContent), "local_dev") {\
        t.Errorf("project settings must not gain local-only override local_dev, got: %s", projectContent)\
    }\
\
    // The local override carries the disable and keeps its own fields.\
    localContent, err := os.ReadFile(EntireSettingsLocalFile)\
    if err != nil {\
        t.Fatalf("failed to read local settings: %v", err)\
    }\
    if !strings.Contains(string(localContent), `"enabled":false`) && !strings.Contains(string(localContent), `"enabled": false`) {\
        t.Errorf("local settings should have enabled:false, got: %s", localContent)\
    }\
    if !strings.Contains(string(localContent), "local_dev") {\
        t.Errorf("local settings should retain its own local_dev field, got: %s", localContent)\
    }\
}\
\
// TestRunDisable_ProjectFlag_WritesCommittedFile verifies that `entire disable\
// --project` flips the committed settings.json and syncs the local override so\
// a stale local file can't leave the repo enabled.\
func TestRunDisable_ProjectFlag_WritesCommittedFile(t *testing.T) {\
    setupTestDir(t)\
    writeSettings(t, `{"enabled": true, "log_level": "warn"}`)\
    writeLocalSettings(t, `{"enabled": true, "local_dev": true}`)\
\
    var stdout bytes.Buffer\
    if err := runDisable(context.Background(), &stdout, true); err != nil {\
        t.Fatalf("runDisable(project=true) error = %v", err)\
    }\
\
    projectContent, err := os.ReadFile(EntireSettingsFile)\
    if err != nil {\
        t.Fatalf("failed to read project settings: %v", err)\
    }\
    if !strings.Contains(string(projectContent), `"enabled":false`) && !strings.Contains(string(projectContent), `"enabled": false`) {\
        t.Errorf("project settings should have enabled:false, got: %s", projectContent)\
    }\
    if strings.Contains(string(projectContent), "local_dev") {\
        t.Errorf("project settings must not leak local-only override local_dev, got: %s", projectContent)\
    }\
\
    localContent, err := os.ReadFile(EntireSettingsLocalFile)\
    if err != nil {\
        t.Fatalf("failed to read local settings: %v", err)\
    }\
    if !strings.Contains(string(localContent), `"enabled":false`) && !strings.Contains(string(localContent), `"enabled": false`) {\
        t.Errorf("local settings should be synced to enabled:false, got: %s", localContent)\
    }\
}\
\
// TestRunEnable_ProjectFlag_DoesNotLeakLocalOverrides verifies that\
// `entire enable --project` with a local-only override present (e.g.\
// local_dev, set via settings.local.json) does not write that override into\
// the shared, committed project settings.json — only the enabled flag should\
// change there (runEnable must not round-trip the merged settings view\
// through the project file).\
func TestRunEnable_ProjectFlag_DoesNotLeakLocalOverrides(t *testing.T) {\
    setupTestDir(t)\
    writeSettings(t, testSettingsDisabled)\
    writeLocalSettings(t, `{"enabled": true, "local_dev": true}`)\
\
    var buf bytes.Buffer\
    if err := runEnable(context.Background(), &buf, true); err != nil {\
        t.Fatalf("runEnable(project=true) error = %v", err)\
    }\
\
    // The merged view is correctly enabled.\
    enabled, err := IsEnabled(context.Background())\
    if err != nil {\
        t.Fatalf("IsEnabled() error = %v", err)\
    }\
    if !enabled {\
        t.Error("expected enabled after runEnable --project")\
    }\
\
    // The project file must be flipped to enabled, and must NOT gain the\
    // local-only override.\
    projectContent, err := os.ReadFile(EntireSettingsFile)\
    if err != nil {\
        t.Fatalf("failed to read project settings: %v", err)\
    }\
    if !strings.Contains(string(projectContent), `"enabled":true`) && !strings.Contains(string(projectContent), `"enabled": true`) {\
        t.Errorf("project settings should have enabled:true, got: %s", projectContent)\
    }\
    if strings.Contains(string(projectContent), "local_dev") {\
        t.Errorf("project settings must not leak local-only override local_dev, got: %s", projectContent)\
    }\
\
    // The local file's own override must be preserved untouched.\
    localContent, err := os.ReadFile(EntireSettingsLocalFile)\
    if err != nil {\
        t.Fatalf("failed to read local settings: %v", err)\
    }\
    if !strings.Contains(string(localContent), "local_dev") {\
        t.Errorf("local settings should still contain local_dev override, got: %s", localContent)\
    }\
}\
\
// TestRunEnable_LocalScope_PreservesLocalOnlyFields verifies that `entire\
// enable` (default, no --project) with an existing local-only override only\
// flips the enabled flag in settings.local.json and leaves the rest of that\
// file's own content (like local_dev) intact.\
func TestRunEnable_LocalScope_PreservesLocalOnlyFields(t *testing.T) {\
    setupTestDir(t)\
    writeSettings(t, testSettingsEnabled)\
    writeLocalSettings(t, `{"enabled": false, "local_dev": true}`)\
\
    var buf bytes.Buffer\
    if err := runEnable(context.Background(), &buf, false); err != nil {\
        t.Fatalf("runEnable(project=false) error = %v", err)\
    }\
\
    enabled, err := IsEnabled(context.Background())\
    if err != nil {\
        t.Fatalf("IsEnabled() error = %v", err)\
    }\
    if !enabled {\
        t.Error("expected enabled after runEnable")\
    }\
\
    localContent, err := os.ReadFile(EntireSettingsLocalFile)\
    if err != nil {\
        t.Fatalf("failed to read local settings: %v", err)\
    }\
    if !strings.Contains(string(localContent), `"enabled":true`) && !strings.Contains(string(localContent), `"enabled": true`) {\
        t.Errorf("local settings should have enabled:true, got: %s", localContent)\
    }\
    if !strings.Contains(string(localContent), "local_dev") {\
        t.Errorf("local settings should still contain local_dev override, got: %s", localContent)\
    }\
\
    // Project settings must be untouched by the local-scope write.\
    projectContent, err := os.ReadFile(EntireSettingsFile)\
    if err != nil {\
        t.Fatalf("failed to read project settings: %v", err)\
    }\
    if strings.Contains(string(projectContent), "local_dev") {\
        t.Errorf("project settings must not gain local-only override local_dev, got: %s", projectContent)\
    }\
}\
\
652 unmodified lines\
\
    }\
}\
\
// Regression: `entire enable --checkpoint-remote ...` (no --project)\
// on a repo disabled at the project level must re-enable the project\
// settings.json, not write the enabled flag to a shadow settings.local.json —\
// which left the file the user disabled still enabled=false.\
func TestEnableCmd_StrategyFlagsOnDisabledProjectRepo_EnablesProjectFile(t *testing.T) {\
    setupTestRepo(t)\
    writeSettings(t, testSettingsDisabled) // settings.json: {"enabled": false}\
    writeClaudeHooksFixture(t)\
\
    cmd := newEnableCmd()\
    var stdout, stderr bytes.Buffer\
    cmd.SetOut(&stdout)\
    cmd.SetErr(&stderr)\
    cmd.SetArgs([]string{"--checkpoint-remote", "github:org/repo", "--skip-push-sessions"})\
\
    if err := cmd.Execute(); err != nil {\
        t.Fatalf("enable error = %v\nstdout: %s\nstderr: %s", err, stdout.String(), stderr.String())\
    }\
\
    // The project file the user disabled must be enabled again.\
    projectS, err := settings.LoadFromFile(EntireSettingsFile)\
    if err != nil {\
        t.Fatalf("load project settings: %v", err)\
    }\
    if !projectS.Enabled {\
        t.Errorf("settings.json still enabled=false after enable; the enabled flag went to the wrong file")\
    }\
}\
\
// Tests for detectOrSelectAgent\
\
func TestDetectOrSelectAgent_AgentDetected(t *testing.T) {\
```\
\
Mcmd/entire/cli/setup\_test.go+683/-41\
\
```\
237 unmodified lines\
\
238\
239\
240\
241\
242\
241\
242\
243\
244\
245\
246\
247\
249\
248\
249\
250\
251\
\
237 unmodified lines\
\
    }\
\
    for _, file := range agent.AllProtectedFiles() {\
        cleanFile := filepath.Clean(filepath.FromSlash(file))\
        if cleanPath == cleanFile {\
        if paths.Equal(cleanPath, file) {\
            return true\
        }\
    }\
\
    for _, dir := range agent.AllProtectedDirs() {\
        cleanDir := filepath.Clean(filepath.FromSlash(dir))\
        if paths.IsSubpath(cleanDir, cleanPath) {\
        if paths.IsProtectedSubpath(cleanDir, cleanPath) {\
            return true\
        }\
    }\
```\
\
Mcmd/entire/cli/state.go+2/-3\
\
```\
351 unmodified lines\
\
352\
353\
354\
355\
355\
356\
357\
358\
\
351 unmodified lines\
\
// registered agent config directories.\
func isProtectedPath(relPath string) bool {\
    for _, dir := range protectedDirs() {\
        if paths.IsSubpath(dir, relPath) {\
        if paths.IsProtectedSubpath(dir, relPath) {\
            return true\
        }\
    }\
```\
\
Mcmd/entire/cli/strategy/common.go+1/-1\
\
```\
264 unmodified lines\
\
265\
266\
267\
268\
269\
270\
271\
268\
269\
270\
271\
272\
273\
274\
275\
460 unmodified lines\
\
736\
737\
738\
739\
740\
741\
742\
743\
744\
745\
746\
747\
748\
749\
750\
751\
752\
753\
754\
755\
756\
757\
758\
759\
760\
761\
762\
763\
764\
765\
766\
767\
768\
769\
770\
771\
468 unmodified lines\
\
1240\
1241\
1242\
1212\
1213\
1243\
1244\
1245\
1246\
111 unmodified lines\
\
1358\
1359\
1360\
1331\
1332\
1361\
1362\
1363\
1364\
\
264 unmodified lines\
\
    // Backfill session state token usage from the freshly-extracted transcript.\
    // Copilot CLI writes session.shutdown after the hooks return, so by condensation\
    // time we can recover the authoritative full-session total from the transcript\
    // while keeping checkpoint metadata scoped to CheckpointTranscriptStart.\
    if backfillUsage := sessionStateBackfillTokenUsage(ctx, ag, state.AgentType, sessionData.Transcript, sessionData.TokenUsage); backfillUsage != nil {\
        state.TokenUsage = backfillUsage\
    }\
    // while keeping checkpoint metadata scoped to CheckpointTranscriptStart. The\
    // recompute drops SubagentTokens (subagentsDir=""); the helper preserves the\
    // cumulative subagent total across the backfill so resetCheckpointWindow's\
    // baseline does not regress to nil (finding 019f5ebf-a57e).\
    applyBackfilledSessionTokenUsage(ctx, ag, state, sessionData.Transcript, sessionData.TokenUsage)\
\
    if !hasTokenUsageData(sessionData.TokenUsage) && hasTokenUsageData(state.CheckpointTokenUsage) {\
        sessionData.TokenUsage = accumulateTokenUsage(nil, state.CheckpointTokenUsage)\
460 unmodified lines\
\
    return hasTokenUsageData(usage.SubagentTokens)\
}\
\
// applyBackfilledSessionTokenUsage overwrites state.TokenUsage with the\
// transcript-recomputed session total (see sessionStateBackfillTokenUsage) when\
// one is available, preserving the cumulative subagent total across the backfill.\
//\
// The recompute runs with subagentsDir="" (see extractSessionData), so the\
// backfilled usage never carries SubagentTokens, whereas state.TokenUsage holds\
// the authoritative cumulative subagent total accumulated by SaveStep.\
// resetCheckpointWindow captures the next window's baseline from\
// state.TokenUsage.SubagentTokens after CondenseSession returns, so letting the\
// backfill drop it would make the baseline nil and the next checkpoint re-report\
// the full cumulative subagent total. The cumulative is folded onto a copy so it\
// is never mixed into checkpointUsage, which is the checkpoint-scoped value\
// written to metadata.\
func applyBackfilledSessionTokenUsage(ctx context.Context, ag agent.Agent, state *SessionState, transcript []byte, checkpointUsage *agent.TokenUsage) {\
    backfillUsage := sessionStateBackfillTokenUsage(ctx, ag, state.AgentType, transcript, checkpointUsage)\
    if backfillUsage == nil {\
        return\
    }\
    var priorSubagentTokens *agent.TokenUsage\
    if state.TokenUsage != nil {\
        priorSubagentTokens = state.TokenUsage.SubagentTokens\
    }\
    if backfillUsage.SubagentTokens == nil && priorSubagentTokens != nil {\
        preserved := *backfillUsage\
        preserved.SubagentTokens = priorSubagentTokens\
        backfillUsage = &preserved\
    }\
    state.TokenUsage = backfillUsage\
}\
\
// sessionStateBackfillTokenUsage returns the best session-level token usage to\
// persist in session state after condensation.\
func sessionStateBackfillTokenUsage(ctx context.Context, ag agent.Agent, agentType types.AgentType, transcript []byte, checkpointUsage *agent.TokenUsage) *agent.TokenUsage {\
468 unmodified lines\
\
            slog.Int("checkpoints_condensed", result.CheckpointsCount),\
        )\
\
        state.StepCount = 0\
        state.CheckpointTokenUsage = nil\
        resetCheckpointWindow(state)\
        state.CheckpointTranscriptStart = result.TotalTranscriptLines\
        state.CheckpointTranscriptSize = int64(len(result.Transcript))\
        state.Phase = session.PhaseIdle\
111 unmodified lines\
\
            return nil\
        }\
\
        state.StepCount = 0\
        state.CheckpointTokenUsage = nil\
        resetCheckpointWindow(state)\
        state.CheckpointTranscriptStart = result.TotalTranscriptLines\
        state.LastCheckpointID = checkpointID\
        state.LastCheckpointCommitHash = state.BaseCommit\
```\
\
Mcmd/entire/cli/strategy/manual\_commit\_condensation.go+37/-8\
\
```\
121 unmodified lines\
\
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\
182 unmodified lines\
\
338\
339\
340\
341\
342\
343\
344\
345\
346\
347\
348\
349\
350\
351\
352\
353\
354\
17 unmodified lines\
\
372\
373\
374\
336\
375\
376\
377\
378\
338\
379\
380\
381\
382\
383\
384\
385\
386\
387\
388\
389\
390\
391\
392\
393\
394\
395\
396\
397\
398\
399\
400\
401\
402\
\
121 unmodified lines\
\
        if step.TokenUsage != nil {\
            state.TokenUsage = accumulateTokenUsage(state.TokenUsage, step.TokenUsage)\
            state.CheckpointTokenUsage = accumulateTokenUsage(state.CheckpointTokenUsage, step.TokenUsage)\
            // step.TokenUsage.SubagentTokens is a cumulative-since-session-start\
            // snapshot (agent IDs are discovered from the full transcript and each\
            // subagent's own transcript is re-read from its start on every call —\
            // see CalculateTotalTokenUsage in the claudecode/factoryaidroid\
            // packages), not a per-step delta like the rest of TokenUsage.\
            // accumulateTokenUsage already replaces (rather than adds) the\
            // SubagentTokens field for that reason, so state.TokenUsage ends up\
            // correctly holding the latest cumulative total. CheckpointTokenUsage\
            // additionally needs rescoping to "since last condensation" by\
            // subtracting the baseline captured at the last reset, otherwise the\
            // full cumulative subagent total would be reported again at every\
            // checkpoint instead of just this checkpoint's share.\
            //\
            // Derive the checkpoint delta FRESH each call from the session-wide\
            // cumulative (state.TokenUsage.SubagentTokens) minus the baseline —\
            // do NOT mutate CheckpointTokenUsage.SubagentTokens in place. A later\
            // step in the same window can carry step.TokenUsage != nil but\
            // SubagentTokens == nil (the subagent transcript was cleaned up, so\
            // CalculateTotalTokenUsage returned APICallCount==0 and left it nil);\
            // accumulateTokenUsage then leaves CheckpointTokenUsage.SubagentTokens\
            // at its already-rescoped value, and re-subtracting the baseline from\
            // that would double-subtract and (via clampSubtract) shrink or zero a\
            // real subagent total. Recomputing from the session-wide cumulative\
            // is idempotent regardless of whether this step carried a snapshot.\
            if state.CheckpointTokenUsage != nil {\
                state.CheckpointTokenUsage.SubagentTokens = types.SubtractTokenUsage(\
                    state.TokenUsage.SubagentTokens, state.SubagentTokensBaseline)\
            }\
        }\
\
        if !branchExisted {\
182 unmodified lines\
\
// accumulateTokenUsage adds new token usage to existing accumulated usage.\
// If existing is nil, returns a copy of incoming. If incoming is nil, returns existing unchanged.\
//\
// SubagentTokens is handled differently from the other fields: main-agent\
// usage (InputTokens, OutputTokens, ...) arrives per step as a delta scoped to\
// that step's transcript slice, so it is correct to sum deltas across steps.\
// Subagent usage arrives as a cumulative-since-session-start snapshot instead\
// — CalculateTotalTokenUsage discovers agent IDs from the full transcript\
// (so a subagent spawned before the current checkpoint window is still\
// found) and re-reads each subagent transcript from its start on every call.\
// Summing that snapshot across steps would re-add a subagent's full usage on\
// every subsequent step after it was first discovered, so SubagentTokens is\
// replaced with the latest snapshot rather than added.\
func accumulateTokenUsage(existing, incoming *agent.TokenUsage) *agent.TokenUsage {\
    if incoming == nil {\
        return existing\
17 unmodified lines\
\
    existing.OutputTokens += incoming.OutputTokens\
    existing.APICallCount += incoming.APICallCount\
\
    // Accumulate subagent tokens if present\
    // Replace (not add) subagent tokens: incoming.SubagentTokens is already\
    // the cumulative total as of this step, so the latest snapshot supersedes\
    // whatever was recorded before rather than stacking on top of it.\
    if incoming.SubagentTokens != nil {\
        existing.SubagentTokens = accumulateTokenUsage(existing.SubagentTokens, incoming.SubagentTokens)\
        existing.SubagentTokens = incoming.SubagentTokens\
    }\
\
    return existing\
}\
\
// resetCheckpointWindow resets the per-checkpoint accumulation window after a\
// condensation reset. It zeroes the step count, clears the checkpoint-scoped\
// token usage, and snapshots the cumulative subagent total into\
// SubagentTokensBaseline so the next window's CheckpointTokenUsage.SubagentTokens\
// can be rescoped to "since this condensation" rather than re-reporting the full\
// cumulative subagent total (see accumulateTokenUsage and the SaveStep rescoping\
// in this file, plus SessionState.SubagentTokensBaseline). Shared by all three\
// condensation reset sites (CondenseSessionByID, CondenseAndMarkFullyCondensed,\
// condenseAndUpdateState) so the baseline capture cannot drift between them.\
func resetCheckpointWindow(state *SessionState) {\
    state.StepCount = 0\
    state.CheckpointTokenUsage = nil\
    state.RebaselineSubagentTokens()\
}\
\
// deleteShadowBranch deletes a shadow branch by name.\
// Returns nil if the branch doesn't exist (idempotent).\
// Uses git CLI instead of go-git's RemoveReference because go-git v5\
```\
\
Mcmd/entire/cli/strategy/manual\_commit\_git.go+58/-2\
\
```\
1406 unmodified lines\
\
1407\
1408\
1409\
1410\
1411\
1410\
1411\
1412\
1413\
\
1406 unmodified lines\
\
    newHead := head.Hash().String()\
    state.BaseCommit = newHead\
    state.RealignAttributionBase(newHead)\
    state.StepCount = 0\
    state.CheckpointTokenUsage = nil\
    resetCheckpointWindow(state)\
    state.CheckpointTranscriptStart = result.TotalTranscriptLines\
    state.CheckpointTranscriptSize = int64(len(result.Transcript))\
```\
\
Mcmd/entire/cli/strategy/manual\_commit\_hooks.go+1/-2\
\
```\
13 unmodified lines\
\
14\
15\
16\
17\
18\
19\
20\
28 unmodified lines\
\
49\
50\
51\
52\
53\
54\
55\
56\
57\
58\
59\
60\
61\
62\
63\
64\
65\
66\
67\
68\
291 unmodified lines\
\
360\
361\
362\
348\
363\
364\
365\
366\
367\
3 unmodified lines\
\
371\
372\
373\
374\
375\
376\
377\
378\
379\
380\
381\
382\
383\
384\
385\
386\
7 unmodified lines\
\
394\
395\
396\
397\
398\
399\
400\
401\
402\
\
13 unmodified lines\
\
    "github.com/go-git/go-git/v6/plumbing"\
\
    "github.com/entireio/cli/cmd/entire/cli/checkpoint"\
    checkpointremote "github.com/entireio/cli/cmd/entire/cli/checkpoint/remote"\
    "github.com/entireio/cli/cmd/entire/cli/logging"\
    "github.com/entireio/cli/cmd/entire/cli/settings"\
    "github.com/entireio/cli/perf"\
28 unmodified lines\
\
}\
\
func (s *ManualCommitStrategy) prePush(ctx context.Context, remote string, protectFirstUserBranch bool) error {\
    // This runs inside the user's `git push` pre-push hook. Every checkpoint\
    // git subprocess spawned here (metadata fetch, policy sync, checkpoint\
    // push and its recovery fetch) must fail fast rather than block on an\
    // interactive SSH passphrase prompt — there is no way to answer it here and\
    // it would hang the user's push. Foreground commands do not set this.\
    //\
    // BatchMode=yes suppresses passphrase/PIN prompts (including FIDO2\
    // verify-required PIN entry). Touch-only security keys still work because\
    // user-presence touch is not a terminal read. Users who need a PIN prompt\
    // in this path should load the key into ssh-agent, or set an explicit\
    // BatchMode=no via GIT_SSH_COMMAND / core.sshCommand (respected by the\
    // non-interactive SSH helper).\
    ctx = checkpointremote.WithNonInteractiveSSH(ctx)\
\
    // 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),\
291 unmodified lines\
\
    // Fast path: push all refs in one round-trip (fast-forward-only). If every\
    // ref was up to date or fast-forwarded, we're done.\
    if err := batchPushRefs(pushCtx, pushTarget, existing); err == nil {\
    batchErr := batchPushRefs(pushCtx, pushTarget, existing)\
    if batchErr == nil {\
        stop(" done")\
        if removeErr := queue.Remove(existing); removeErr != nil {\
            logging.Warn(ctx, "git-refs push: clear pushed refs from queue failed",\
3 unmodified lines\
\
    }\
    stop("")\
\
    // Non-interactive SSH auth failures cannot be fixed by per-ref\
    // fetch+replay. Surface the same actionable hint as the v1 doPushRef path\
    // (issue #1523) instead of only logging to .entire/logs/.\
    if nonInteractiveSSHAuthFailure(pushCtx, batchErr) {\
        fmt.Fprintf(os.Stderr, "[entire] Warning: couldn't push checkpoint refs: %v\n", batchErr)\
        printNonInteractiveSSHAuthHint()\
        printCheckpointRemoteHint(pushTarget)\
        return 0, batchErr\
    }\
\
    // At least one ref was rejected — typically a non-fast-forward divergence\
    // (the same checkpoint re-written on another machine). Retry per ref with\
    // fetch+replay recovery, and remove from the queue only the refs that land\
7 unmodified lines\
\
        if err := pushCheckpointRefWithRecovery(pushCtx, pushTarget, ref); err != nil {\
            logging.Warn(ctx, "git-refs push: checkpoint ref push/sync failed; left queued, not overwritten",\
                slog.String("ref", ref.String()), slog.String("error", err.Error()))\
            if nonInteractiveSSHAuthFailure(pushCtx, err) {\
                printNonInteractiveSSHAuthHint()\
            }\
            if firstErr == nil {\
                firstErr = err\
            }\
```\
\
Mcmd/entire/cli/strategy/manual\_commit\_push.go+30/-1\
\
```\
177 unmodified lines\
\
178\
179\
180\
181\
182\
183\
184\
185\
186\
187\
188\
189\
190\
191\
192\
193\
7 unmodified lines\
\
201\
202\
203\
204\
205\
206\
207\
208\
209\
6 unmodified lines\
\
216\
217\
218\
219\
220\
221\
222\
223\
224\
11 unmodified lines\
\
236\
237\
238\
239\
240\
241\
242\
243\
244\
245\
246\
247\
248\
4 unmodified lines\
\
253\
254\
255\
256\
257\
258\
259\
260\
261\
262\
263\
264\
265\
266\
267\
268\
269\
270\
271\
272\
\
177 unmodified lines\
\
        return nil\
    }\
\
    // Non-interactive SSH (pre-push BatchMode): auth failures cannot be fixed by\
    // fetch+rebase, and retrying would just reprint the same opaque error.\
    // Surface an actionable ssh-agent hint and skip recovery (issue #1523).\
    if nonInteractiveSSHAuthFailure(ctx, err) {\
        fmt.Fprintf(os.Stderr, "[entire] Warning: couldn't push %s: %v\n", refLabel, err)\
        printNonInteractiveSSHAuthHint()\
        printCheckpointRemoteHint(target)\
        return nil\
    }\
\
    // Push failed - likely non-fast-forward. Try to fetch and rebase.\
    // Spanned (with the network fetch as a child) so the trace distinguishes\
    // "the raw push is slow" from "we keep hitting contention and re-syncing".\
7 unmodified lines\
\
    if syncErr != nil {\
        stop("")\
        fmt.Fprintf(os.Stderr, "[entire] Warning: couldn't sync %s: %v\n", refLabel, syncErr)\
        if nonInteractiveSSHAuthFailure(ctx, syncErr) {\
            printNonInteractiveSSHAuthHint()\
        }\
        printCheckpointRemoteHint(target)\
        return nil // Don't fail the main push\
    }\
6 unmodified lines\
\
    if result, err := tryPushRefCommon(ctx, target, ref); err != nil {\
        stop("")\
        fmt.Fprintf(os.Stderr, "[entire] Warning: failed to push %s after sync: %v\n", refLabel, err)\
        if nonInteractiveSSHAuthFailure(ctx, err) {\
            printNonInteractiveSSHAuthHint()\
        }\
        printCheckpointRemoteHint(target)\
    } else {\
        finishPush(ctx, stop, result, target)\
11 unmodified lines\
\
    return ref.String()\
}\
\
// nonInteractiveSSHAuthFailure reports whether err is an SSH auth-shaped\
// failure under a BatchMode (non-interactive) context. Used to print the\
// actionable ssh-agent hint and skip useless recovery retries.\
func nonInteractiveSSHAuthFailure(ctx context.Context, err error) bool {\
    return err != nil && remote.IsNonInteractiveSSH(ctx) && remote.LooksLikeSSHAuthFailure(err.Error())\
}\
\
// printCheckpointRemoteHint prints a hint when a push to a checkpoint URL fails.\
// Only prints when the target is a URL (not the user's default remote).\
func printCheckpointRemoteHint(target string) {\
4 unmodified lines\
\
    fmt.Fprintln(os.Stderr, "[entire] Checkpoints are saved locally but not synced. Ensure you have access to the checkpoint remote.")\
}\
\
// sshAuthHintOnce ensures the ssh-agent hint prints at most once per process\
// (pre-push can push multiple refs).\
var sshAuthHintOnce sync.Once\
\
// printNonInteractiveSSHAuthHint tells the user how to unblock checkpoint pushes\
// that failed because SSH needed interactive auth under BatchMode (issue #1523).\
func printNonInteractiveSSHAuthHint() {\
    sshAuthHintOnce.Do(func() {\
        fmt.Fprintln(os.Stderr, "[entire] Checkpoint push skipped: SSH needs interactive auth (passphrase/PIN) and cannot prompt during git hooks.")\
        fmt.Fprintln(os.Stderr, "[entire] Load your key into ssh-agent (`ssh-add`), then push again. Checkpoints are saved locally until then.")\
        fmt.Fprintln(os.Stderr, "[entire] PIN-protected security keys: unlock/add them to the agent first. To allow prompts in this path, set GIT_SSH_COMMAND (or core.sshCommand) with an explicit BatchMode=no.")\
    })\
}\
\
// settingsHintOnce ensures the settings commit hint prints at most once per process.\
var settingsHintOnce sync.Once\
```\
\
Mcmd/entire/cli/strategy/push\_common.go+37\
\
```\
3 unmodified lines\
\
4\
5\
6\
7\
8\
9\
10\
2 unmodified lines\
\
13\
14\
15\
16\
17\
18\
19\
1649 unmodified lines\
\
1669\
1670\
1671\
1672\
1673\
1674\
1675\
1676\
1677\
1678\
1679\
1680\
1681\
1682\
1683\
1684\
1685\
1686\
1687\
1688\
1689\
1690\
1691\
1692\
1693\
1694\
1695\
1696\
1697\
1698\
1699\
1700\
1701\
1702\
1703\
\
3 unmodified lines\
\
    "bytes"\
    "context"\
    "errors"\
    "io"\
    "os"\
    "os/exec"\
    "path/filepath"\
2 unmodified lines\
\
    "testing"\
\
    "github.com/entireio/cli/cmd/entire/cli/checkpoint"\
    "github.com/entireio/cli/cmd/entire/cli/checkpoint/remote"\
    "github.com/entireio/cli/cmd/entire/cli/paths"\
    "github.com/entireio/cli/cmd/entire/cli/testutil"\
\
1649 unmodified lines\
\
        assert.NotContains(t, out, "git@github.com:org/repo.git")\
    })\
}\
\
func TestPrintNonInteractiveSSHAuthHint(t *testing.T) {\
    // Reset the once for this test process isolation: reassign the sync.Once.\
    sshAuthHintOnce = sync.Once{}\
\
    var buf bytes.Buffer\
    old := os.Stderr\
    r, w, err := os.Pipe()\
    require.NoError(t, err)\
    os.Stderr = w\
    printNonInteractiveSSHAuthHint()\
    printNonInteractiveSSHAuthHint() // second call must be a no-op\
    require.NoError(t, w.Close())\
    os.Stderr = old\
    _, copyErr := io.Copy(&buf, r)\
    require.NoError(t, copyErr)\
    out := buf.String()\
    assert.Contains(t, out, "ssh-add")\
    assert.Contains(t, out, "Checkpoint push skipped")\
    assert.Equal(t, 1, strings.Count(out, "Checkpoint push skipped"), "hint must print once")\
}\
\
func TestNonInteractiveSSHAuthFailure(t *testing.T) {\
    t.Parallel()\
    authErr := errors.New("permission denied (publickey)")\
    ctx := remote.WithNonInteractiveSSH(context.Background())\
    assert.True(t, nonInteractiveSSHAuthFailure(ctx, authErr))\
    assert.False(t, nonInteractiveSSHAuthFailure(context.Background(), authErr),\
        "interactive context must not treat auth errors as BatchMode hints")\
    assert.False(t, nonInteractiveSSHAuthFailure(ctx, errors.New("non-fast-forward")))\
    assert.False(t, nonInteractiveSSHAuthFailure(ctx, nil))\
}\
```\
\
Mcmd/entire/cli/strategy/push\_common\_test.go+34\
\
```\
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\
\
package strategy\
\
import (\
    "context"\
    "fmt"\
    "os"\
    "path/filepath"\
    "testing"\
    "time"\
\
    "github.com/entireio/cli/cmd/entire/cli/agent"\
    "github.com/entireio/cli/cmd/entire/cli/agent/types"\
    "github.com/entireio/cli/cmd/entire/cli/paths"\
    "github.com/entireio/cli/cmd/entire/cli/testutil"\
    "github.com/go-git/go-git/v6"\
    "github.com/go-git/go-git/v6/plumbing/object"\
    "github.com/stretchr/testify/require"\
)\
\
// TestAccumulateTokenUsage_SubagentTokensReplacedNotSummed is a focused unit\
// test on accumulateTokenUsage: CalculateTotalTokenUsage (claudecode and\
// factoryaidroid) discovers subagent IDs from the full transcript and re-reads\
// each subagent transcript from line 0 on every call, so incoming.SubagentTokens\
// is always a cumulative-since-session-start snapshot, not a per-step delta.\
// Summing that snapshot across steps (as accumulateTokenUsage does for the\
// main-agent fields) would re-add a subagent's full usage on every subsequent\
// step after it was first discovered. accumulateTokenUsage must replace\
// SubagentTokens with the latest snapshot instead.\
func TestAccumulateTokenUsage_SubagentTokensReplacedNotSummed(t *testing.T) {\
    subagentSnapshot := &agent.TokenUsage{InputTokens: 500, OutputTokens: 250, APICallCount: 5}\
\
    step1 := &agent.TokenUsage{InputTokens: 100, OutputTokens: 50, APICallCount: 1, SubagentTokens: subagentSnapshot}\
    existing := accumulateTokenUsage(nil, step1)\
    require.NotNil(t, existing.SubagentTokens)\
    require.Equal(t, 500, existing.SubagentTokens.InputTokens)\
    require.Equal(t, 250, existing.SubagentTokens.OutputTokens)\
\
    // Second step within the same checkpoint window: the subagent transcript\
    // hasn't changed, so CalculateTotalTokenUsage returns the SAME cumulative\
    // snapshot again. Main-agent fields are per-step deltas and should sum;\
    // SubagentTokens must NOT double.\
    step2 := &agent.TokenUsage{InputTokens: 100, OutputTokens: 50, APICallCount: 1, SubagentTokens: subagentSnapshot}\
    existing = accumulateTokenUsage(existing, step2)\
\
    require.Equal(t, 200, existing.InputTokens, "main-agent InputTokens should sum across steps")\
    require.Equal(t, 100, existing.OutputTokens, "main-agent OutputTokens should sum across steps")\
    require.NotNil(t, existing.SubagentTokens)\
    require.Equal(t, 500, existing.SubagentTokens.InputTokens, "SubagentTokens must be replaced, not summed")\
    require.Equal(t, 250, existing.SubagentTokens.OutputTokens, "SubagentTokens must be replaced, not summed")\
}\
\
// TestSaveStep_SubagentTokensNotDoubleCountedAcrossCheckpoints exercises the\
// real SaveStep path for both Claude Code and Factory AI Droid (the two\
// agents whose CalculateTotalTokenUsage implementations discover subagent IDs\
// from the full transcript per #329) and proves that a subagent discovered\
// before a checkpoint window is folded into that checkpoint's token usage\
// exactly once, not re-added on every subsequent checkpoint it remains\
// discoverable in.\
func TestSaveStep_SubagentTokensNotDoubleCountedAcrossCheckpoints(t *testing.T) {\
    agentTypes := []types.AgentType{agent.AgentTypeClaudeCode, agent.AgentTypeFactoryAIDroid}\
\
    for _, agentType := range agentTypes {\
        t.Run(string(agentType), func(t *testing.T) {\
            dir := t.TempDir()\
            testutil.InitRepo(t, dir)\
            repo, err := git.PlainOpen(dir)\
            require.NoError(t, err)\
\
            worktree, err := repo.Worktree()\
            require.NoError(t, err)\
            require.NoError(t, os.WriteFile(filepath.Join(dir, "test.txt"), []byte("v1"), 0o644))\
            _, err = worktree.Add("test.txt")\
            require.NoError(t, err)\
            _, err = worktree.Commit("Initial commit", &git.CommitOptions{\
                Author: &object.Signature{Name: "Test", Email: "test@test.com", When: time.Now()},\
            })\
            require.NoError(t, err)\
\
            t.Chdir(dir)\
            ctx := context.Background()\
            s := &ManualCommitStrategy{}\
            sessionID := "2026-07-10-subagent-dedup-" + string(agentType)\
\
            metadataDir := ".entire/metadata/" + sessionID\
            metadataDirAbs := filepath.Join(dir, metadataDir)\
            require.NoError(t, os.MkdirAll(metadataDirAbs, 0o755))\
            transcript := `{"type":"human","message":{"content":"test"}}` + "\n"\
            require.NoError(t, os.WriteFile(filepath.Join(metadataDirAbs, paths.TranscriptFileName), []byte(transcript), 0o644))\
\
            // Checkpoint 1, step 1: a subagent spawned before this checkpoint's\
            // window is discovered via the full-transcript scan (#329) and its\
            // cumulative usage as of now is 500/250 across 5 calls.\
            subagentAtCheckpoint1 := &agent.TokenUsage{InputTokens: 500, OutputTokens: 250, APICallCount: 5}\
            require.NoError(t, s.SaveStep(ctx, StepContext{\
                SessionID:      sessionID,\
                MetadataDir:    metadataDir,\
                MetadataDirAbs: metadataDirAbs,\
                ModifiedFiles:  []string{"test.txt"},\
                CommitMessage:  "checkpoint 1 step 1",\
                AuthorName:     "Test",\
                AuthorEmail:    "test@test.com",\
                AgentType:      agentType,\
                TokenUsage: &agent.TokenUsage{\
                    InputTokens: 100, OutputTokens: 50, APICallCount: 1,\
                    SubagentTokens: subagentAtCheckpoint1,\
                },\
            }))\
\
            // Checkpoint 1, step 2: same turn window, subagent transcript\
            // unchanged (CalculateTotalTokenUsage would return the identical\
            // cumulative snapshot again since it always re-reads from line 0).\
            // Change the working tree so SaveStep sees a real diff to save.\
            require.NoError(t, os.WriteFile(filepath.Join(dir, "test.txt"), []byte("v2"), 0o644))\
            require.NoError(t, s.SaveStep(ctx, StepContext{\
                SessionID:      sessionID,\
                MetadataDir:    metadataDir,\
                MetadataDirAbs: metadataDirAbs,\
                ModifiedFiles:  []string{"test.txt"},\
                CommitMessage:  "checkpoint 1 step 2",\
                AuthorName:     "Test",\
                AuthorEmail:    "test@test.com",\
                AgentType:      agentType,\
                TokenUsage: &agent.TokenUsage{\
                    InputTokens: 100, OutputTokens: 50, APICallCount: 1,\
                    SubagentTokens: subagentAtCheckpoint1,\
                },\
            }))\
\
            state, err := s.loadSessionState(ctx, sessionID)\
            require.NoError(t, err)\
            require.NotNil(t, state.CheckpointTokenUsage)\
            require.NotNil(t, state.CheckpointTokenUsage.SubagentTokens)\
            require.Equal(t, 500, state.CheckpointTokenUsage.SubagentTokens.InputTokens,\
                "subagent usage must be folded once per checkpoint window, not once per step")\
            require.Equal(t, 250, state.CheckpointTokenUsage.SubagentTokens.OutputTokens)\
            require.Equal(t, 200, state.CheckpointTokenUsage.InputTokens, "main-agent deltas still sum across steps")\
\
            // Simulate the condensation reset that happens between checkpoints:\
            // CheckpointTokenUsage is cleared and SubagentTokensBaseline snapshots\
            // the cumulative subagent total counted so far, so the next\
            // checkpoint's CheckpointTokenUsage.SubagentTokens is scoped to\
            // "since this reset" instead of the whole session again.\
            require.NoError(t, MutateSessionState(ctx, sessionID, func(st *SessionState) error {\
                st.StepCount = 0\
                st.CheckpointTokenUsage = nil\
                if st.TokenUsage != nil {\
                    st.SubagentTokensBaseline = st.TokenUsage.SubagentTokens\
                }\
                st.CheckpointTranscriptStart = 10\
                return nil\
            }))\
\
            // Checkpoint 2, step 1: the same subagent is still discoverable (its\
            // marker line is still in the full transcript) and has grown a bit\
            // more since checkpoint 1.\
            subagentAtCheckpoint2 := &agent.TokenUsage{InputTokens: 620, OutputTokens: 310, APICallCount: 6}\
            require.NoError(t, os.WriteFile(filepath.Join(dir, "test.txt"), []byte("v3"), 0o644))\
            require.NoError(t, s.SaveStep(ctx, StepContext{\
                SessionID:      sessionID,\
                MetadataDir:    metadataDir,\
                MetadataDirAbs: metadataDirAbs,\
                ModifiedFiles:  []string{"test.txt"},\
                CommitMessage:  "checkpoint 2 step 1",\
                AuthorName:     "Test",\
                AuthorEmail:    "test@test.com",\
                AgentType:      agentType,\
                TokenUsage: &agent.TokenUsage{\
                    InputTokens: 100, OutputTokens: 50, APICallCount: 1,\
                    SubagentTokens: subagentAtCheckpoint2,\
                },\
            }))\
\
            state2, err := s.loadSessionState(ctx, sessionID)\
            require.NoError(t, err)\
\
            // The session-wide total tracks the latest cumulative subagent\
            // snapshot directly (it is already cumulative) — not the sum of the\
            // checkpoint-1 and checkpoint-2 snapshots.\
            require.NotNil(t, state2.TokenUsage.SubagentTokens)\
            require.Equal(t, 620, state2.TokenUsage.SubagentTokens.InputTokens,\
                "session-wide subagent total must be the latest cumulative snapshot, not summed across checkpoints")\
            require.Equal(t, 310, state2.TokenUsage.SubagentTokens.OutputTokens)\
\
            // Checkpoint 2's own CheckpointTokenUsage.SubagentTokens must be\
            // rescoped to just what grew since the checkpoint-1 baseline\
            // (620-500, 310-250), not the full cumulative total again.\
            require.NotNil(t, state2.CheckpointTokenUsage)\
            require.NotNil(t, state2.CheckpointTokenUsage.SubagentTokens)\
            require.Equal(t, 120, state2.CheckpointTokenUsage.SubagentTokens.InputTokens,\
                "checkpoint 2's subagent delta must exclude what was already counted in checkpoint 1")\
            require.Equal(t, 60, state2.CheckpointTokenUsage.SubagentTokens.OutputTokens)\
        })\
    }\
}\
\
// TestSaveStep_SubagentBaselineNotDoubleSubtractedWhenLaterStepDropsSubagent\
// pins the double-subtraction bug: within a single checkpoint window, once a\
// step has set CheckpointTokenUsage.SubagentTokens (rescoped by subtracting the\
// baseline), a LATER step whose TokenUsage is non-nil but carries no\
// SubagentTokens (subagent transcript cleaned up, so CalculateTotalTokenUsage\
// returns APICallCount==0 and leaves SubagentTokens nil) must not cause the\
// baseline to be subtracted a second time. accumulateTokenUsage only REPLACES\
// SubagentTokens when the incoming snapshot is non-nil, so a nil-subagent step\
// leaves CheckpointTokenUsage.SubagentTokens at its already-rescoped value; a\
// per-step re-subtraction would shrink (and via clampSubtract zero) a real\
// subagent total. The checkpoint delta must be derived FRESH each call from the\
// session-wide cumulative snapshot minus the baseline instead.\
func TestSaveStep_SubagentBaselineNotDoubleSubtractedWhenLaterStepDropsSubagent(t *testing.T) {\
    agentTypes := []types.AgentType{agent.AgentTypeClaudeCode, agent.AgentTypeFactoryAIDroid}\
\
    for _, agentType := range agentTypes {\
        t.Run(string(agentType), func(t *testing.T) {\
            dir := t.TempDir()\
            testutil.InitRepo(t, dir)\
            repo, err := git.PlainOpen(dir)\
            require.NoError(t, err)\
\
            worktree, err := repo.Worktree()\
            require.NoError(t, err)\
            require.NoError(t, os.WriteFile(filepath.Join(dir, "test.txt"), []byte("v1"), 0o644))\
            _, err = worktree.Add("test.txt")\
            require.NoError(t, err)\
            _, err = worktree.Commit("Initial commit", &git.CommitOptions{\
                Author: &object.Signature{Name: "Test", Email: "test@test.com", When: time.Now()},\
            })\
            require.NoError(t, err)\
\
            t.Chdir(dir)\
            ctx := context.Background()\
            s := &ManualCommitStrategy{}\
            sessionID := "2026-07-13-subagent-nodouble-" + string(agentType)\
\
            metadataDir := ".entire/metadata/" + sessionID\
            metadataDirAbs := filepath.Join(dir, metadataDir)\
            require.NoError(t, os.MkdirAll(metadataDirAbs, 0o755))\
            transcript := `{"type":"human","message":{"content":"test"}}` + "\n"\
            require.NoError(t, os.WriteFile(filepath.Join(metadataDirAbs, paths.TranscriptFileName), []byte(transcript), 0o644))\
\
            // Checkpoint 1: a subagent is discovered with cumulative usage 500/250.\
            require.NoError(t, s.SaveStep(ctx, StepContext{\
                SessionID:      sessionID,\
                MetadataDir:    metadataDir,\
                MetadataDirAbs: metadataDirAbs,\
                ModifiedFiles:  []string{"test.txt"},\
                CommitMessage:  "checkpoint 1",\
                AuthorName:     "Test",\
                AuthorEmail:    "test@test.com",\
                AgentType:      agentType,\
                TokenUsage: &agent.TokenUsage{\
                    InputTokens: 100, OutputTokens: 50, APICallCount: 1,\
                    SubagentTokens: &agent.TokenUsage{InputTokens: 500, OutputTokens: 250, APICallCount: 5},\
                },\
            }))\
\
            // Condensation reset: baseline snapshots the cumulative subagent total.\
            require.NoError(t, MutateSessionState(ctx, sessionID, func(st *SessionState) error {\
                st.StepCount = 0\
                st.CheckpointTokenUsage = nil\
                if st.TokenUsage != nil {\
                    st.SubagentTokensBaseline = st.TokenUsage.SubagentTokens\
                }\
                st.CheckpointTranscriptStart = 10\
                return nil\
            }))\
\
            // Checkpoint 2, step 1: the subagent has grown to 620/310. The\
            // checkpoint delta must be 620-500 / 310-250 = 120 / 60.\
            require.NoError(t, os.WriteFile(filepath.Join(dir, "test.txt"), []byte("v2"), 0o644))\
            require.NoError(t, s.SaveStep(ctx, StepContext{\
                SessionID:      sessionID,\
                MetadataDir:    metadataDir,\
                MetadataDirAbs: metadataDirAbs,\
                ModifiedFiles:  []string{"test.txt"},\
                CommitMessage:  "checkpoint 2 step 1",\
                AuthorName:     "Test",\
                AuthorEmail:    "test@test.com",\
                AgentType:      agentType,\
                TokenUsage: &agent.TokenUsage{\
                    InputTokens: 100, OutputTokens: 50, APICallCount: 1,\
                    SubagentTokens: &agent.TokenUsage{InputTokens: 620, OutputTokens: 310, APICallCount: 6},\
                },\
            }))\
\
            // Checkpoint 2, step 2: same window, but this step's TokenUsage carries\
            // NO SubagentTokens (the subagent transcript was cleaned up, so\
            // CalculateTotalTokenUsage found APICallCount==0 and left SubagentTokens\
            // nil). accumulateTokenUsage will not replace SubagentTokens, so it stays\
            // at the checkpoint-1-baseline-subtracted 120/60 — and the baseline must\
            // NOT be subtracted again.\
            require.NoError(t, os.WriteFile(filepath.Join(dir, "test.txt"), []byte("v3"), 0o644))\
            require.NoError(t, s.SaveStep(ctx, StepContext{\
                SessionID:      sessionID,\
                MetadataDir:    metadataDir,\
                MetadataDirAbs: metadataDirAbs,\
                ModifiedFiles:  []string{"test.txt"},\
                CommitMessage:  "checkpoint 2 step 2",\
                AuthorName:     "Test",\
                AuthorEmail:    "test@test.com",\
                AgentType:      agentType,\
                TokenUsage: &agent.TokenUsage{\
                    InputTokens: 100, OutputTokens: 50, APICallCount: 1,\
                    // SubagentTokens intentionally nil.\
                },\
            }))\
\
            state, err := s.loadSessionState(ctx, sessionID)\
            require.NoError(t, err)\
\
            // Session-wide total keeps the latest cumulative snapshot (620/310):\
            // the nil-subagent step must not clobber or shrink it.\
            require.NotNil(t, state.TokenUsage.SubagentTokens)\
            require.Equal(t, 620, state.TokenUsage.SubagentTokens.InputTokens,\
                "session-wide subagent total must retain the latest cumulative snapshot")\
            require.Equal(t, 310, state.TokenUsage.SubagentTokens.OutputTokens)\
\
            // Checkpoint delta must remain the checkpoint-1-baseline-subtracted\
            // 120/60, NOT 620-500-500 clamped to 0. This is the regression.\
            require.NotNil(t, state.CheckpointTokenUsage)\
            require.NotNil(t, state.CheckpointTokenUsage.SubagentTokens)\
            require.Equal(t, 120, state.CheckpointTokenUsage.SubagentTokens.InputTokens,\
                "baseline must be subtracted once, not re-subtracted on a later nil-subagent step")\
            require.Equal(t, 60, state.CheckpointTokenUsage.SubagentTokens.OutputTokens)\
            // Main-agent deltas still sum across all three steps in the window.\
            require.Equal(t, 200, state.CheckpointTokenUsage.InputTokens,\
                "main-agent deltas sum across checkpoint-2 steps")\
        })\
    }\
}\
\
// TestSaveStep_CheckpointSubagentAlwaysDerivedFromSessionCumulative walks the\
// finding-1 edge matrix in one window after a baseline reset: a nil-subagent\
// first step, a step that grows the subagent, then repeated nil-subagent steps.\
// After every step the checkpoint subagent total must equal the session-wide\
// cumulative minus the baseline (idempotent), never drifting from repeated\
// subtraction. The strategy-layer accounting is agent-agnostic, so one agent\
// exercises it.\
func TestSaveStep_CheckpointSubagentAlwaysDerivedFromSessionCumulative(t *testing.T) {\
    dir := t.TempDir()\
    testutil.InitRepo(t, dir)\
    repo, err := git.PlainOpen(dir)\
    require.NoError(t, err)\
\
    worktree, err := repo.Worktree()\
    require.NoError(t, err)\
    require.NoError(t, os.WriteFile(filepath.Join(dir, "test.txt"), []byte("v0"), 0o644))\
    _, err = worktree.Add("test.txt")\
    require.NoError(t, err)\
    _, err = worktree.Commit("Initial commit", &git.CommitOptions{\
        Author: &object.Signature{Name: "Test", Email: "test@test.com", When: time.Now()},\
    })\
    require.NoError(t, err)\
\
    t.Chdir(dir)\
    ctx := context.Background()\
    s := &ManualCommitStrategy{}\
    sessionID := "2026-07-13-subagent-edgematrix"\
\
    metadataDir := ".entire/metadata/" + sessionID\
    metadataDirAbs := filepath.Join(dir, metadataDir)\
    require.NoError(t, os.MkdirAll(metadataDirAbs, 0o755))\
    require.NoError(t, os.WriteFile(filepath.Join(metadataDirAbs, paths.TranscriptFileName),\
        []byte(`{"type":"human","message":{"content":"test"}}`+"\n"), 0o644))\
\
    rev := 0\
    save := func(sub *agent.TokenUsage) {\
        rev++\
        require.NoError(t, os.WriteFile(filepath.Join(dir, "test.txt"), []byte(fmt.Sprintf("rev%d", rev)), 0o644))\
        require.NoError(t, s.SaveStep(ctx, StepContext{\
            SessionID:      sessionID,\
            MetadataDir:    metadataDir,\
            MetadataDirAbs: metadataDirAbs,\
            ModifiedFiles:  []string{"test.txt"},\
            CommitMessage:  fmt.Sprintf("step %d", rev),\
            AuthorName:     "Test",\
            AuthorEmail:    "test@test.com",\
            AgentType:      agent.AgentTypeClaudeCode,\
            TokenUsage:     &agent.TokenUsage{InputTokens: 10, APICallCount: 1, SubagentTokens: sub},\
        }))\
    }\
    // checkpointSubIn returns the current checkpoint subagent InputTokens (0 when nil).\
    checkpointSubIn := func() int {\
        st, loadErr := s.loadSessionState(ctx, sessionID)\
        require.NoError(t, loadErr)\
        if st.CheckpointTokenUsage == nil || st.CheckpointTokenUsage.SubagentTokens == nil {\
            return 0\
        }\
        return st.CheckpointTokenUsage.SubagentTokens.InputTokens\
    }\
\
    // Establish a baseline of 400 via a first window + reset.\
    save(&agent.TokenUsage{InputTokens: 400, APICallCount: 4})\
    require.NoError(t, MutateSessionState(ctx, sessionID, func(st *SessionState) error {\
        st.StepCount = 0\
        st.CheckpointTokenUsage = nil\
        st.SubagentTokensBaseline = st.TokenUsage.SubagentTokens // 400\
        st.CheckpointTranscriptStart = 5\
        return nil\
    }))\
\
    // Edge: nil first step of the window — cumulative stays 400, delta 0.\
    save(nil)\
    require.Equal(t, 0, checkpointSubIn(), "nil first step: delta is cumulative(400)-baseline(400)=0")\
\
    // Growth step — cumulative 550, delta 150.\
    save(&agent.TokenUsage{InputTokens: 550, APICallCount: 5})\
    require.Equal(t, 150, checkpointSubIn(), "growth step: delta is 550-400")\
\
    // Repeated nil steps must NOT shrink the delta (idempotent derive-fresh).\
    save(nil)\
    require.Equal(t, 150, checkpointSubIn(), "nil step must not re-subtract baseline")\
    save(nil)\
    require.Equal(t, 150, checkpointSubIn(), "second nil step must not re-subtract baseline")\
}\
\
// TestCondenseSessionByID_CapturesSubagentBaselineViaRealResetPath drives a REAL\
// condensation (CondenseSessionByID) rather than hand-simulating the reset, so\
// the production baseline-snapshot code in resetCheckpointWindow — shared by the\
// three condensation reset sites — is exercised where it actually lives. It then\
// runs a follow-up checkpoint to prove the baseline captured by the real path is\
// used to rescope the next checkpoint's subagent delta.\
func TestCondenseSessionByID_CapturesSubagentBaselineViaRealResetPath(t *testing.T) {\
    dir := t.TempDir()\
    testutil.InitRepo(t, dir)\
    repo, err := git.PlainOpen(dir)\
    require.NoError(t, err)\
\
    worktree, err := repo.Worktree()\
    require.NoError(t, err)\
    require.NoError(t, os.WriteFile(filepath.Join(dir, "test.txt"), []byte("v1"), 0o644))\
    _, err = worktree.Add("test.txt")\
    require.NoError(t, err)\
    _, err = worktree.Commit("Initial commit", &git.CommitOptions{\
        Author: &object.Signature{Name: "Test", Email: "test@test.com", When: time.Now()},\
    })\
    require.NoError(t, err)\
\
    t.Chdir(dir)\
    ctx := context.Background()\
    s := &ManualCommitStrategy{}\
    sessionID := "2026-07-13-subagent-realreset"\
\
    metadataDir := ".entire/metadata/" + sessionID\
    metadataDirAbs := filepath.Join(dir, metadataDir)\
    require.NoError(t, os.MkdirAll(metadataDirAbs, 0o755))\
    // The assistant line carries real usage data (message.id + usage). Real\
    // Claude Code transcripts always do, which makes sessionStateBackfillTokenUsage\
    // fire during condensation (its InputTokens > 0 branch) and overwrite\
    // state.TokenUsage with the transcript-recomputed value — which is computed\
    // with subagentsDir="" and therefore drops SubagentTokens. This is what makes\
    // this test guard the REAL condensation path: without preserving the\
    // cumulative subagent total across the backfill, resetCheckpointWindow would\
    // snapshot a nil baseline and the next checkpoint would re-report the full\
    // cumulative subagent total (finding 019f5ebf-a57e).\
    transcript := `{"type":"human","message":{"content":"do the thing"}}\
{"type":"assistant","uuid":"a1","message":{"id":"m1","usage":{"input_tokens":300,"output_tokens":150}}}\
`\
    require.NoError(t, os.WriteFile(filepath.Join(metadataDirAbs, paths.TranscriptFileName), []byte(transcript), 0o644))\
\
    // Checkpoint 1: subagent discovered with cumulative usage 500/250.\
    require.NoError(t, os.WriteFile(filepath.Join(dir, "test.txt"), []byte("v2"), 0o644))\
    require.NoError(t, s.SaveStep(ctx, StepContext{\
        SessionID:      sessionID,\
        MetadataDir:    metadataDir,\
        MetadataDirAbs: metadataDirAbs,\
        ModifiedFiles:  []string{"test.txt"},\
        CommitMessage:  "checkpoint 1",\
        AuthorName:     "Test",\
        AuthorEmail:    "test@test.com",\
        AgentType:      agent.AgentTypeClaudeCode,\
        TokenUsage: &agent.TokenUsage{\
            InputTokens: 100, OutputTokens: 50, APICallCount: 1,\
            SubagentTokens: &agent.TokenUsage{InputTokens: 500, OutputTokens: 250, APICallCount: 5},\
        },\
    }))\
\
    // Drive the REAL condensation reset path (not a hand-simulated one). This\
    // executes resetCheckpointWindow inside CondenseSessionByID.\
    require.NoError(t, s.CondenseSessionByID(ctx, sessionID))\
\
    state, err := s.loadSessionState(ctx, sessionID)\
    require.NoError(t, err)\
    require.Equal(t, 0, state.StepCount, "real condensation must reset StepCount")\
    require.Nil(t, state.CheckpointTokenUsage, "real condensation must clear CheckpointTokenUsage")\
    require.NotNil(t, state.SubagentTokensBaseline,\
        "real condensation must snapshot the subagent baseline")\
    require.Equal(t, 500, state.SubagentTokensBaseline.InputTokens,\
        "baseline must capture the cumulative subagent total at condensation")\
    require.Equal(t, 250, state.SubagentTokensBaseline.OutputTokens)\
\
    // Checkpoint 2 after the real reset: the subagent grew to 620/310. Its\
    // checkpoint delta must be rescoped against the real-path baseline (120/60).\
    require.NoError(t, os.WriteFile(filepath.Join(dir, "test.txt"), []byte("v3"), 0o644))\
    require.NoError(t, s.SaveStep(ctx, StepContext{\
        SessionID:      sessionID,\
        MetadataDir:    metadataDir,\
        MetadataDirAbs: metadataDirAbs,\
        ModifiedFiles:  []string{"test.txt"},\
        CommitMessage:  "checkpoint 2",\
        AuthorName:     "Test",\
        AuthorEmail:    "test@test.com",\
        AgentType:      agent.AgentTypeClaudeCode,\
        TokenUsage: &agent.TokenUsage{\
            InputTokens: 100, OutputTokens: 50, APICallCount: 1,\
            SubagentTokens: &agent.TokenUsage{InputTokens: 620, OutputTokens: 310, APICallCount: 6},\
        },\
    }))\
\
    state2, err := s.loadSessionState(ctx, sessionID)\
    require.NoError(t, err)\
    require.NotNil(t, state2.CheckpointTokenUsage)\
    require.NotNil(t, state2.CheckpointTokenUsage.SubagentTokens)\
    require.Equal(t, 120, state2.CheckpointTokenUsage.SubagentTokens.InputTokens,\
        "checkpoint delta must be rescoped against the real-path baseline")\
    require.Equal(t, 60, state2.CheckpointTokenUsage.SubagentTokens.OutputTokens)\
}\
```\
\
Acmd/entire/cli/strategy/subagent\_token\_dedup\_test.go+515\
\
```\
9 unmodified lines\
\
10\
11\
12\
13\
14\
15\
16\
69 unmodified lines\
\
86\
87\
88\
89\
90\
91\
92\
93\
94\
95\
96\
97\
98\
\
9 unmodified lines\
\
    "time"\
\
    "github.com/denisbrodbeck/machineid"\
    "github.com/entireio/cli/cmd/entire/cli/execx"\
    "github.com/posthog/posthog-go"\
    "github.com/spf13/cobra"\
    "github.com/spf13/pflag"\
69 unmodified lines\
\
    }\
}\
\
// spawnDetachedAnalytics sends the payload from a detached `entire\
// __send_analytics` child so the network call never blocks the CLI. The empty\
// dir keeps the child out of the parent's working directory.\
func spawnDetachedAnalytics(payloadJSON string) {\
    execx.SpawnDetached("", "__send_analytics", payloadJSON)\
}\
\
// TrackCommandDetached tracks a command execution by spawning a detached subprocess.\
// This returns immediately without blocking the CLI.\
func TrackCommandDetached(cmd *cobra.Command, agent string, isEntireEnabled bool, version string) {\
```\
\
Mcmd/entire/cli/telemetry/detached.go+8\
\
```\
1\
2\
3\
4\
5\
6\
7\
8\
9\
10\
11\
\
//go:build !unix && !windows\
\
package telemetry\
\
// spawnDetachedAnalytics is a no-op on non-Unix platforms.\
// Windows support for detached processes would require different syscall flags\
// (CREATE_NEW_PROCESS_GROUP, DETACHED_PROCESS), but telemetry is best-effort\
// so we simply skip it on unsupported platforms.\
func spawnDetachedAnalytics(string) {\
    // No-op: detached subprocess spawning not implemented for this platform\
}\
```\
\
Dcmd/entire/cli/telemetry/detached\_other.go-11\
\
```\
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\
\
//go:build unix\
\
package telemetry\
\
import (\
    "context"\
    "io"\
    "os"\
    "os/exec"\
    "syscall"\
)\
\
// spawnDetachedAnalytics spawns a detached subprocess to send analytics.\
// On Unix, this uses process group detachment so the subprocess continues\
// after the parent exits.\
func spawnDetachedAnalytics(payloadJSON string) {\
    executable, err := os.Executable()\
    if err != nil {\
        return\
    }\
\
    cmd := exec.CommandContext(context.Background(), executable, "__send_analytics", payloadJSON)\
\
    // Detach from parent process group so subprocess survives parent exit\
    cmd.SysProcAttr = &syscall.SysProcAttr{\
        Setpgid: true,\
    }\
\
    // Don't hold the working directory\
    cmd.Dir = "/"\
\
    // Inherit environment (may be needed for network config)\
    cmd.Env = os.Environ()\
\
    // Discard stdout/stderr to prevent output leaking to parent's terminal\
    cmd.Stdout = io.Discard\
    cmd.Stderr = io.Discard\
\
    // Start the process (non-blocking)\
    if err := cmd.Start(); err != nil {\
        return\
    }\
\
    // Release the process so it can run independently\
    //nolint:errcheck // Best effort - process should continue regardless\
    _ = cmd.Process.Release()\
}\
```\
\
Dcmd/entire/cli/telemetry/detached\_unix.go-47\
\
```\
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\
\
//go:build windows\
\
package telemetry\
\
import (\
    "context"\
    "io"\
    "os"\
    "os/exec"\
    "syscall"\
\
    "golang.org/x/sys/windows"\
)\
\
// spawnDetachedAnalytics spawns a detached subprocess to send analytics.\
// On Windows, this uses CREATE_NEW_PROCESS_GROUP | DETACHED_PROCESS flags\
// so the subprocess continues after the parent exits.\
func spawnDetachedAnalytics(payloadJSON string) {\
    executable, err := os.Executable()\
    if err != nil {\
        return\
    }\
\
    cmd := exec.CommandContext(context.Background(), executable, "__send_analytics", payloadJSON)\
\
    // Detach from parent console so subprocess survives parent exit.\
    // CREATE_NEW_PROCESS_GROUP: own Ctrl+C group (prevents signal propagation).\
    // DETACHED_PROCESS: fully detach from parent's console.\
    cmd.SysProcAttr = &syscall.SysProcAttr{\
        CreationFlags: windows.CREATE_NEW_PROCESS_GROUP | windows.DETACHED_PROCESS,\
    }\
\
    // Use temp dir since "/" doesn't exist on Windows\
    cmd.Dir = os.TempDir()\
\
    // Inherit environment (may be needed for network config)\
    cmd.Env = os.Environ()\
\
    // Discard stdout/stderr to prevent output leaking to parent's terminal\
    cmd.Stdout = io.Discard\
    cmd.Stderr = io.Discard\
\
    // Start the process (non-blocking)\
    if err := cmd.Start(); err != nil {\
        return\
    }\
\
    // Release the process so it can run independently\
    //nolint:errcheck // Best effort - process should continue regardless\
    _ = cmd.Process.Release()\
}\
```\
\
Dcmd/entire/cli/telemetry/detached\_windows.go-51\
\
```\
17 unmodified lines\
\
18\
19\
20\
21\
21\
22\
23\
24\
\
17 unmodified lines\
\
    "github.com/go-git/go-git/v6/plumbing/object"\
)\
\
// RewindPoint mirrors the rewind --list JSON output.\
// RewindPoint mirrors the `checkpoint list --pending --json` JSON output.\
type RewindPoint struct {\
    ID               string    `json:"id"`\
    Message          string    `json:"message"`\
```\
\
Mcmd/entire/cli/testutil/testutil.go+1/-1\
\
```\
12 unmodified lines\
\
13\
14\
15\
16\
17\
18\
19\
20\
21\
22\
23\
24\
25\
26\
27\
28\
29\
206 unmodified lines\
\
236\
237\
238\
239\
240\
241\
242\
243\
244\
245\
246\
247\
248\
249\
250\
1 unmodified line\
\
252\
253\
254\
241\
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\
243\
282\
283\
284\
245\
246\
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\
\
12 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/execx"\
    "github.com/entireio/cli/cmd/entire/cli/gitremote"\
    "github.com/entireio/cli/cmd/entire/cli/internal/flock"\
    "github.com/entireio/cli/cmd/entire/cli/jsonutil"\
    "github.com/entireio/cli/cmd/entire/cli/logging"\
    "github.com/entireio/cli/cmd/entire/cli/paths"\
    "github.com/entireio/cli/cmd/entire/cli/session"\
    "github.com/entireio/cli/cmd/entire/cli/settings"\
    "github.com/entireio/cli/cmd/entire/cli/validation"\
\
    "github.com/spf13/cobra"\
)\
\
const (\
206 unmodified lines\
\
    return nil\
}\
\
// refreshTrailsEnabledCacheIfStaleForScope refreshes the trails-enablement\
// cache when it's unknown/expired for scope. Callers on hot, latency-sensitive\
// paths (SessionStart) must not block on this: resolving the API token and\
// dialing TrailsEnabled can stall for seconds when the host is slow or\
// unreachable (VPN, firewall, offline). Instead of doing that\
// network work inline, hand it off to a detached `__refresh_trail_enablement`\
// subprocess and return immediately; a later SessionStart will observe the\
// freshly written cache once the subprocess completes. The "not supported"\
// case is answered locally (no network) since it's free.\
func refreshTrailsEnabledCacheIfStaleForScope(ctx context.Context, scope trailEnablementScope) error {\
    if cachedTrailsEnablementForScope(ctx, scope, time.Now()) != trailEnablementCacheUnknown {\
        return nil\
1 unmodified line\
\
    if !scope.Supported {\
        return saveTrailsEnabledForScope(ctx, scope, false, time.Now())\
    }\
    client, err := NewAuthenticatedAPIClient(ctx, false)\
    spawnDetachedTrailEnablementRefresh(ctx)\
    return nil\
}\
\
// trailRefreshAPIClient is the authenticated-client seam used by\
// runTrailEnablementRefresh, swapped in tests so they can force the\
// refreshTrailsEnabledCacheForScope error branch (e.g. a broken API host)\
// without a real login context. Production code always uses\
// NewAuthenticatedAPIClient.\
var trailRefreshAPIClient = NewAuthenticatedAPIClient\
\
// runTrailEnablementRefresh performs the actual (potentially slow) network\
// refresh. It is invoked from the detached `__refresh_trail_enablement`\
// subprocess spawned by refreshTrailsEnabledCacheIfStaleForScope, never\
// synchronously from a hook path.\
func runTrailEnablementRefresh(ctx context.Context) error {\
    ctx, cancel := context.WithTimeout(ctx, trailEnablementRefreshTimeout)\
    defer cancel()\
\
    // This runs detached with stdout/stderr discarded, so log at debug to the\
    // repo's .entire/logs/entire.log (initialized by newRefreshTrailEnablementCmd).\
    // Without this, an unreachable/failing host would leave the background\
    // refresh silently failing with no diagnostic trail.\
    logCtx := logging.WithComponent(ctx, "trail-refresh")\
\
    scope, err := currentTrailEnablementScope(ctx)\
    if err != nil {\
        return err\
        logging.Debug(logCtx, "trails enablement refresh skipped: scope unresolved", "error", err.Error())\
        return nil\
    }\
    _, err = refreshTrailsEnabledCacheForScope(ctx, client, scope)\
    return err\
    // Another process (e.g. a fast-following SessionStart, or a concurrent\
    // refresh already in flight) may have populated the cache first.\
    if cachedTrailsEnablementForScope(ctx, scope, time.Now()) != trailEnablementCacheUnknown {\
        return nil\
    }\
    if !scope.Supported {\
        if err := saveTrailsEnabledForScope(ctx, scope, false, time.Now()); err != nil {\
            logging.Debug(logCtx, "trails enablement refresh failed to save unsupported scope", "error", err.Error())\
        }\
        return nil\
    }\
    client, err := trailRefreshAPIClient(ctx, false)\
    if err != nil {\
        logging.Debug(logCtx, "trails enablement refresh skipped: authenticated client unavailable", "error", err.Error())\
        return nil\
    }\
    // Best-effort: this runs from the detached __refresh_trail_enablement\
    // subprocess (stdout/stderr discarded, see newRefreshTrailEnablementCmd),\
    // so a transient network/API failure here must not surface as a non-zero\
    // process exit — there's no one watching it and no user-visible benefit,\
    // only a spurious failure signal. The failure is still diagnosable via the\
    // debug log above.\
    if _, err := refreshTrailsEnabledCacheForScope(ctx, client, scope); err != nil {\
        logging.Debug(logCtx, "trails enablement refresh failed", "error", err.Error())\
        return nil\
    }\
    logging.Debug(logCtx, "trails enablement refresh completed", "enabled_repo_key", scope.RepoKey)\
    return nil\
}\
\
// trailRefreshSpawn is the process-spawn seam used by\
// spawnDetachedTrailEnablementRefresh. Swapped in tests so they can assert\
// SessionStart never blocks on it without forking a real subprocess (a real\
// `go test` binary doesn't understand `__refresh_trail_enablement` as an\
// argument). Production code always uses spawnDetachedTrailRefreshProcess.\
var trailRefreshSpawn = spawnDetachedTrailRefreshProcess\
\
// spawnDetachedTrailRefreshProcess starts `entire __refresh_trail_enablement`\
// as a detached child so the trails-enablement network refresh can't add\
// latency to the SessionStart hook that spawned it. The child runs from the\
// worktree root because the refresh resolves the origin remote and\
// git-common-dir for cache storage from its working directory.\
func spawnDetachedTrailRefreshProcess(worktreeRoot string) {\
    execx.SpawnDetached(worktreeRoot, "__refresh_trail_enablement")\
}\
\
// trailRefreshSpawnThrottle bounds how often SessionStart forks a detached\
// refresh child for a given repo. When the API host is unreachable the refresh\
// never writes the cache, so cachedTrailsEnablementForScope stays unknown and\
// the hourly TTL never starts — without this guard every SessionStart (and\
// every concurrent worktree) would fork a fresh child that re-opens the repo,\
// re-resolves auth, and re-dials the dead host. Tying the window to the child's\
// own timeout collapses a burst of hooks to roughly one child per window while\
// still retrying promptly once the host recovers.\
const trailRefreshSpawnThrottle = trailEnablementRefreshTimeout\
\
// spawnDetachedTrailEnablementRefresh starts a detached child process that\
// runs runTrailEnablementRefresh in the background. Best-effort: if the\
// worktree root can't be resolved or the subprocess can't be spawned, the\
// cache simply stays unknown and the next SessionStart tries again. A recent\
// spawn for the same repo short-circuits so a burst of hooks doesn't fork a\
// herd of redundant refresh children (see trailRefreshRecentlySpawned).\
func spawnDetachedTrailEnablementRefresh(ctx context.Context) {\
    worktreeRoot, err := paths.WorktreeRoot(ctx)\
    if err != nil {\
        return\
    }\
    if commonDir, err := session.GetGitCommonDir(ctx); err == nil &&\
        trailRefreshRecentlySpawned(commonDir, time.Now()) {\
        return\
    }\
    trailRefreshSpawn(worktreeRoot)\
}\
\
// trailRefreshRecentlySpawned reports whether a detached refresh was spawned for\
// this repo within trailRefreshSpawnThrottle and, when it wasn't, records now as\
// the most recent spawn. The read-and-record is serialized with a flock keyed to\
// the shared git-common-dir (so every worktree of the repo agrees), collapsing a\
// burst of concurrent SessionStart hooks to a single child rather than one per\
// hook. Best-effort: any error resolving, locking, or writing the marker falls\
// through to spawning — never worse than before this guard existed.\
func trailRefreshRecentlySpawned(commonDir string, now time.Time) bool {\
    dir := filepath.Join(commonDir, "entire")\
    // Create the directory before acquiring the lock: flock.Acquire opens the\
    // lock file, which fails if its parent doesn't exist yet (mirrors\
    // ModifyClonePreferences, which MkdirAlls before locking).\
    if err := os.MkdirAll(dir, 0o750); err != nil {\
        return false\
    }\
    markerPath := filepath.Join(dir, "trail-refresh-spawn")\
    release, err := flock.Acquire(markerPath + ".lock")\
    if err != nil {\
        return false\
    }\
    defer release()\
\
    if data, readErr := os.ReadFile(markerPath); readErr == nil { //nolint:gosec // markerPath is derived from the trusted git-common-dir, not user input\
        if last, parseErr := time.Parse(time.RFC3339Nano, strings.TrimSpace(string(data))); parseErr == nil &&\
            now.After(last) && now.Sub(last) < trailRefreshSpawnThrottle {\
            return true\
        }\
    }\
    //nolint:errcheck // best-effort marker; a failed write just means the next hook re-spawns\
    _ = os.WriteFile(markerPath, []byte(now.UTC().Format(time.RFC3339Nano)), 0o600)\
    return false\
}\
\
// newRefreshTrailEnablementCmd creates the hidden command that performs the\
// (potentially slow) trails-enablement network refresh out of band. It is\
// invoked by spawnDetachedTrailEnablementRefresh from a detached subprocess\
// and should not be called directly.\
func newRefreshTrailEnablementCmd() *cobra.Command {\
    return &cobra.Command{\
        Use:    "__refresh_trail_enablement",\
        Hidden: true,\
        Args:   cobra.NoArgs,\
        RunE: func(cmd *cobra.Command, _ []string) error {\
            ctx := cmd.Context()\
            // Detached child with discarded stdout/stderr: initialize file\
            // logging so a failing background refresh (e.g. an unreachable\
            // host) is diagnosable in .entire/logs/entire.log rather than\
            // vanishing. Guard on WorktreeRoot first — matching resume/rewind/\
            // reset/explain — so a child whose worktree was removed or relocated\
            // between spawn and exec (or a manual invocation outside a repo)\
            // doesn't create a stray .entire/logs/ in an arbitrary directory;\
            // logging.Init falls back to cwd when WorktreeRoot fails.\
            if _, err := paths.WorktreeRoot(ctx); err == nil {\
                logging.SetLogLevelGetter(GetLogLevel)\
                if err := logging.Init(ctx, ""); err == nil {\
                    defer logging.Close()\
                }\
            }\
            return runTrailEnablementRefresh(ctx)\
        },\
    }\
}\
\
func refreshTrailsEnabledCache(ctx context.Context, client *api.Client) (bool, error) {\
```\
\
Mcmd/entire/cli/trail\_context\_cache.go+177/-4\
\
```\
21 unmodified lines\
\
22\
23\
24\
25\
25\
26\
27\
28\
36 unmodified lines\
\
65\
66\
67\
68\
68\
69\
70\
71\
71\
72\
73\
74\
\
21 unmodified lines\
\
    return p\
}\
\
// RewindPoint represents a single entry from `entire rewind --list`.\
// RewindPoint represents a single entry from `entire checkpoint list --pending --json`.\
type RewindPoint struct {\
    ID               string `json:"id"`\
    Message          string `json:"message"`\
36 unmodified lines\
\
    return run(t, dir, "clean", "--force")\
}\
\
// RewindList runs `entire checkpoint rewind --list` and parses the JSON output.\
// RewindList runs `entire checkpoint list --pending --json` and parses the JSON output.\
func RewindList(t *testing.T, dir string) []RewindPoint {\
    t.Helper()\
    out := runStdout(t, dir, "checkpoint", "rewind", "--list")\
    out := runStdout(t, dir, "checkpoint", "list", "--pending", "--json")\
\
    var points []RewindPoint\
    if err := json.Unmarshal([]byte(out), &points); err != nil {\
```\
\
Me2e/entire/entire.go+3/-3\
\
```\
25 unmodified lines\
\
26\
27\
28\
29\
29\
30\
31\
32\
\
25 unmodified lines\
\
    github.com/muesli/termenv v0.16.0\
    github.com/ogen-go/ogen v1.23.0\
    github.com/oklog/ulid/v2 v2.1.1\
    github.com/posthog/posthog-go v1.17.5\
    github.com/posthog/posthog-go v1.18.0\
    github.com/sergi/go-diff v1.4.0\
    github.com/spf13/cobra v1.10.2\
    github.com/spf13/pflag v1.0.10\
```\
\
Mgo.mod+1/-1\
\
```\
244 unmodified lines\
\
245\
246\
247\
248\
249\
248\
249\
250\
251\
252\
\
244 unmodified lines\
\
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=\
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\
github.com/posthog/posthog-go v1.17.5 h1:mrLiAdyiQpl8Yeyg23iShyAztJMaUrYzsBjKeH52Aak=\
github.com/posthog/posthog-go v1.17.5/go.mod h1://M430hNH3e8CDv4i8SJesb26816Mpa6GIZaiP4pNQU=\
github.com/posthog/posthog-go v1.18.0 h1:gCkHzRjGR0WFype95mVvCXn4AioGHdy9fdrmxxFIRls=\
github.com/posthog/posthog-go v1.18.0/go.mod h1://M430hNH3e8CDv4i8SJesb26816Mpa6GIZaiP4pNQU=\
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=\
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=\
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=\
```\
\
Mgo.sum+2/-2\
\
```\
143 unmodified lines\
\
144\
145\
146\
147\
147\
148\
149\
150\
106 unmodified lines\
\
257\
258\
259\
260\
260\
261\
262\
263\
\
143 unmodified lines\
\
# Check rewind points\
echo -e "${BLUE}=== Step 9: Check rewind points ===${NC}"\
entire rewind --list || true\
entire checkpoint list --pending --json || true\
\
# Show session state (includes PromptAttributions for debugging)\
echo ""\
106 unmodified lines\
\
# Show rewind points summary\
echo ""\
echo -e "${BLUE}=== Step 15: Rewind points summary ===${NC}"\
entire rewind --list | jq -r '.[] | "  \(.id[0:8])... - \(.message[0:60])"' 2>/dev/null || echo "  (no rewind points)"\
entire checkpoint list --pending --json | jq -r '.[] | "  \(.id[0:8])... - \(.message[0:60])"' 2>/dev/null || echo "  (no rewind points)"\
\
# Final summary\
echo ""\
```\
\
Mscripts/test-attribution-e2e.sh+2/-2