cli: delete dead root-package code and its dedicated tests · Entire
Log in
cli: delete dead root-package code and its dedicated tests
3589845·
Soph·2w ago·17 files·+35 added/-2,085 removed
All unreachable from the CLI entry points (x/tools deadcode + reference grep), exercised only by tests written for them:
- metadata.go removed whole: cli-package task-checkpoint IO was a dead duplicate of the strategy package's equivalents. - explain.go: runExplainDefault/runExplainBranchDefault wrappers (superseded by runExplainBranchWithFilter), formatSessionInfo, hasCodeChanges (superseded by hasAnyChanges). - git_operations.go: GetMergeBase, findNewUntrackedFiles. - hooks.go: the pre/post task-hook parse+log family and its orphaned input types — prod parses subagent hooks via parseSubagentCheckpointHookInput. - status_style.go: single-row metadataRow (prod renders via metadataRows/metadataRowsWithWidth). - trail_cmd.go/trail_context_cache.go/trail_resume_cmd.go: test-only wrappers over the live validated/scoped/WithSkipped variants; tests covering real behavior were repointed at the live functions instead of deleted (trail list options, enablement-cache TTL/scope matching, resume-context sorting and command building). - transcript.go: package-level GetTranscriptPosition + TranscriptPosition (the same-named agent-interface methods are the live path).
Left in place pending a maintainer decision: resumeSingleSession and ExtractTodoContentFromToolInput — both look like missing wiring rather than leftovers (no call site ever existed in this repo's history).
Co-Authored-By: Claude Fable 5 noreply@anthropic.com
Sessions
8a9c75ea075fView transcript
Changes
17
cmd/entire/cli
Mexplain.go+3/-150
Mexplain_test.go+2/-769
Mgit_operations.go-61
Mgit_operations_test.go-199
Mhooks.go-102
Mhooks_test.go-239
Dmetadata.go-104
Dmetadata_test.go-161
Mstatus_style.go+7/-20
Mstatus_style_test.go-21
Mtrail_cmd.go-16
Mtrail_cmd_test.go+19/-26
Mtrail_context_cache.go-13
Mtrail_resume_cmd.go-8
Mtrail_resume_cmd_test.go+4/-4
Mtranscript.go-59
Mtranscript_test.go-133
1948 unmodified lines
1949
1950
1951
1952
1953
1954
1955
1956
1957
1952
1953
1954
368 unmodified lines
2323
2324
2325
2332
2333
2326
2327
2328
2329
2330
91 unmodified lines
2422
2423
2424
2431
2432
2433
2434
2435
2436
2425
2426
2427
48 unmodified lines
2476
2477
2478
2491
2492
2493
2494
2495
2496
2497
2498
2499
2500
2501
2502
2503
2504
2505
2506
2507
2508
2509
2510
2511
2512
2513
2514
2515
2516
2517
2518
2519
2520
2521
2522
2523
2524
2525
2526
2527
2528
2529
2530
2531
2532
2533
2534
2535
2536
2537
2538
2539
2540
2541
2542
2543
2544
2545
2546
2547
2548
2549
2550
2551
2552
2553
2554
2555
2556
2557
2558
2559
2560
2561
2562
2563
2564
2565
2566
2567
2568
2569
2570
2571
2572
2573
2574
2575
2576
2577
2578
2479
2480
2481
341 unmodified lines
2823
2824
2825
2926
2927
2928
2929
2930
2931
2932
2933
2934
2935
2936
2937
2938
2939
2940
2941
2942
2943
2944
2945
2946
2947
2948
2949
2950
2951
2952
2953
2954
2955
2956
2957
2958
2959
2960
2961
2962
2963
2964
2965
2966
2967
2968
2969
2970
2971
2972
2973
2826
2827
2828
2829
1948 unmodified lines
return strings.ReplaceAll(s, "`", "‘")
}
// runExplainDefault shows all checkpoints on the current branch.
// This is the default view when no flags are provided.
func runExplainDefault(ctx context.Context, w, errW io.Writer, noPager bool) error {
return runExplainBranchDefault(ctx, w, errW, noPager)
}
// branchCheckpointsLimit is the max checkpoints to show in branch view
const branchCheckpointsLimit = 100
368 unmodified lines
// convertTemporaryCheckpoint converts a EphemeralCheckpointInfo to a RewindPoint.
// Returns nil if the checkpoint should be skipped (no tree changes or can't be read).
//
// Filtering uses hasAnyChanges (O(1) tree hash comparison) rather than hasCodeChanges
// (O(files) full diff). This means metadata-only checkpoints (.entire/ changes without
// Filtering uses hasAnyChanges (O(1) tree hash comparison) rather than a full
// O(files) diff. This means metadata-only checkpoints (.entire/ changes without
// code changes) are kept — only true no-ops (identical tree as parent) are dropped.
// This trade-off is intentional for list-view performance.
func convertTemporaryCheckpoint(repo *git.Repository, tc checkpoint.EphemeralCheckpointInfo) *strategy.RewindPoint {
91 unmodified lines
return nil
}
// runExplainBranchDefault shows all checkpoints on the current branch grouped by date.
// This is a convenience wrapper that calls runExplainBranchWithFilter with no filter.
func runExplainBranchDefault(ctx context.Context, w, errW io.Writer, noPager bool) error {
return runExplainBranchWithFilter(ctx, w, errW, noPager, "")
}
// outputExplainContent outputs content with optional pager support.
func outputExplainContent(w io.Writer, content string, noPager bool) {
if noPager {
48 unmodified lines
return runExplainCheckpoint(ctx, w, errW, checkpointID.String(), noPager, verbose, full, rawTranscript, generate, force, searchAll, summaryTimeoutSeconds)
}
// formatSessionInfo formats session information for display.
//
// NOTE: This function has no production caller — `entire explain --session`
// flows through formatBranchCheckpoints (the list view filtered by session),
// not through here. It is kept for tests that exercise the per-checkpoint
// markdown body shape used elsewhere; restyling it for the brand format was
// not worth the diff. If the CLI ever grows a session-detail surface, revisit.
func formatSessionInfo(session *strategy.Session, sourceRef string, checkpoints []checkpointDetail) string {
var sb strings.Builder
// Session header
fmt.Fprintf(&sb, "Session: %s\n", session.ID)
fmt.Fprintf(&sb, "Strategy: %s\n", session.Strategy)
if !session.StartTime.IsZero() {
fmt.Fprintf(&sb, "Started: %s\n", session.StartTime.Format("2006-01-02 15:04:05"))
}
if sourceRef != "" {
fmt.Fprintf(&sb, "Source Ref: %s\n", sourceRef)
}
fmt.Fprintf(&sb, "Checkpoints: %d\n", len(checkpoints))
// Checkpoint details
for _, cp := range checkpoints {
sb.WriteString("\n")
// Checkpoint header
taskMarker := ""
if cp.IsTaskCheckpoint {
taskMarker = " [Task]"
}
fmt.Fprintf(&sb, "─── Checkpoint %d [%s] %s%s ───\n",
cp.Index, cp.ShortID, cp.Timestamp.Format("2006-01-02 15:04"), taskMarker)
sb.WriteString("\n")
// Display all interactions in this checkpoint
for i, inter := range cp.Interactions {
// For multiple interactions, add a sub-header
if len(cp.Interactions) > 1 {
fmt.Fprintf(&sb, "### Interaction %d\n\n", i+1)
}
// Prompt section
if inter.Prompt != "" {
sb.WriteString("## Prompt\n\n")
sb.WriteString(inter.Prompt)
sb.WriteString("\n\n")
}
// Response section
if len(inter.Responses) > 0 {
sb.WriteString("## Responses\n\n")
sb.WriteString(strings.Join(inter.Responses, "\n\n"))
sb.WriteString("\n\n")
}
// Files modified for this interaction
if len(inter.Files) > 0 {
fmt.Fprintf(&sb, "Files Modified (%d):\n", len(inter.Files))
for _, file := range inter.Files {
fmt.Fprintf(&sb, " - %s\n", file)
}
sb.WriteString("\n")
}
}
// If no interactions, show message and/or files
if len(cp.Interactions) == 0 {
// Show commit message as summary when no transcript available
if cp.Message != "" {
sb.WriteString(cp.Message)
sb.WriteString("\n\n")
}
// Show aggregate files if available
if len(cp.Files) > 0 {
fmt.Fprintf(&sb, "Files Modified (%d):\n", len(cp.Files))
for _, file := range cp.Files {
fmt.Fprintf(&sb, " - %s\n", file)
}
}
}
}
return sb.String()
}
// pagerLookupEnv is overridable for tests so pager env-gate behavior can be
// asserted without depending on the host's PAGER / LESS settings.
var pagerLookupEnv = os.Getenv
341 unmodified lines
return countLines(transcriptBytes)
}
// hasCodeChanges returns true if the commit has changes to non-metadata files.
// Uses a full tree diff to distinguish code changes from .entire/ metadata-only changes.
// Returns false only if the commit has a parent AND only modified .entire/ metadata files.
//
// WARNING: This is expensive via go-git (resolves many tree/blob objects from packfiles).
// For list views with many checkpoints, use hasAnyChanges instead.
func hasCodeChanges(commit *object.Commit) bool {
// First commit on shadow branch captures working copy state - always meaningful
if commit.NumParents() == 0 {
return true
}
parent, err := commit.Parent(0)
if err != nil {
return true // Can't check, assume meaningful
}
commitTree, err := commit.Tree()
if err != nil {
return true
}
parentTree, err := parent.Tree()
if err != nil {
return true
}
changes, err := parentTree.Diff(commitTree)
if err != nil {
return true
}
// Check if any non-metadata file was changed
for _, change := range changes {
name := change.To.Name
if name == "" {
name = change.From.Name
}
// Skip .entire/ metadata files
if !strings.HasPrefix(name, ".entire/") {
return true
}
}
return false
}
// hasAnyChanges is a lightweight alternative to hasCodeChanges that compares
// hasAnyChanges compares
// tree hashes without doing a full diff. Returns true if the commit's tree
// differs from its parent's tree. This may include metadata-only changes,
// but is O(1) instead of O(files) — suitable for list views.
Mcmd/entire/cli/explain.go+3/-150
1455 unmodified lines
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1459
1460
1461
9 unmodified lines
1471
1472
1473
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
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
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
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
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
1835
1836
1837
1838
1839
1840
1841
1842
1843
1844
1845
1846
1847
1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
1864
1865
1866
1867
1868
1869
1870
1871
1872
1873
1874
1875
1876
1877
1878
1879
1880
1881
1882
1883
1884
1885
1886
1887
1888
1889
1890
1891
1892
1893
1894
1895
1896
1897
1898
1899
1900
1901
1902
1903
1904
1905
1906
1907
1908
1909
1910
1911
1912
1913
1914
1915
1916
1917
1918
1919
1920
1921
1922
1923
1924
1925
1926
1927
1928
1929
1930
1931
1932
1933
1934
1935
1936
1937
1938
1939
1940
1941
1942
1943
1944
1945
1946
1947
1948
1949
1950
1951
1952
1953
1954
1955
1956
1957
1958
1959
1960
1961
1962
1963
1964
1965
1966
1967
1474
1475
1476
2198 unmodified lines
3675
3676
3677
4172
4173
4174
4175
4176
4177
4178
4179
4180
4181
4182
4183
4184
4185
4186
4187
4188
4189
4190
4191
4192
4193
4194
4195
4196
4197
4198
4199
4200
4201
4202
4203
4204
4205
4206
4207
4208
4209
4210
4211
4212
4213
4214
4215
4216
4217
4218
4219
4220
4221
4222
4223
4224
4225
4226
4227
4228
4229
4230
3678
3679
3680
105 unmodified lines
3786
3787
3788
4342
4343
4344
4345
4346
4347
4348
4349
4350
4351
4352
4353
4354
4355
4356
4357
4358
4359
4360
4361
4362
4363
4364
4365
4366
4367
4368
4369
4370
4371
4372
4373
4374
4375
4376
4377
4378
4379
4380
4381
4382
4383
4384
4385
4386
4387
4388
4389
4390
4391
4392
4393
4394
4395
4396
4397
4398
4399
4400
4401
4402
4403
4404
4405
4406
4407
4408
4409
4410
4411
4412
4413
4414
4415
4416
4417
4418
4419
4420
4421
4422
4423
4424
4425
4426
4427
4428
4429
4430
4431
4432
4433
4434
4435
4436
4437
4438
4439
4440
4441
4442
4443
4444
4445
4446
4447
4448
4449
4450
4451
4452
4453
4454
4455
4456
4457
4458
4459
4460
4461
4462
4463
4464
4465
4466
4467
4468
4469
4470
4471
4472
4473
4474
4475
4476
4477
4478
4479
4480
4481
4482
4483
4484
4485
4486
4487
4488
4489
4490
4491
4492
4493
4494
4495
4496
4497
4498
4499
4500
4501
4502
4503
4504
4505
4506
4507
4508
4509
4510
4511
4512
4513
4514
4515
4516
4517
4518
4519
4520
4521
4522
4523
4524
4525
4526
4527
4528
4529
4530
4531
4532
4533
4534
4535
4536
4537
4538
4539
4540
4541
4542
4543
4544
4545
4546
4547
4548
4549
4550
4551
4552
4553
4554
4555
3789
3790
3791
1728 unmodified lines
5520
5521
5522
6290
5523
5524
5525
5526
47 unmodified lines
5574
5575
5576
6344
5577
5578
5579
5580
1455 unmodified lines
}
}
func TestExplainDefault_ShowsBranchView(t *testing.T) {
tmpDir := t.TempDir()
t.Chdir(tmpDir)
// Initialize git repo
testutil.InitRepo(t, tmpDir)
repo, err := git.PlainOpen(tmpDir)
require.NoError(t, err)
// Create initial commit so HEAD exists (required for branch view)
w, err := repo.Worktree()
if err != nil {
t.Fatalf("failed to get worktree: %v", err)
}
testFile := filepath.Join(tmpDir, "test.txt")
if err := os.WriteFile(testFile, []byte("test content"), 0o644); err != nil {
t.Fatalf("failed to write test file: %v", err)
}
if _, err := w.Add("test.txt"); err != nil {
t.Fatalf("failed to add test file: %v", err)
}
_, err = w.Commit("initial commit", &git.CommitOptions{
Author: &object.Signature{
Name: "Test",
Email: "test@example.com",
},
})
if err != nil {
t.Fatalf("failed to create initial commit: %v", err)
}
// Create .entire directory
if err := os.MkdirAll(".entire", 0o750); err != nil {
t.Fatalf("failed to create .entire dir: %v", err)
}
var stdout, stderr bytes.Buffer
err = runExplainDefault(context.Background(), &stdout, &stderr, true) // noPager=true for test
// Should NOT error - should show branch view
if err != nil {
t.Errorf("expected no error, got: %v", err)
}
output := stdout.String()
// Should show branch header (new metadata-row shape: "branch <name>")
if !strings.Contains(output, "branch ") {
t.Errorf("expected 'branch' row in output, got: %s", output)
}
// Should show checkpoints count (likely 0)
if !strings.Contains(output, "checkpoints") {
t.Errorf("expected 'checkpoints' row in output, got: %s", output)
}
}
func TestExplainDefault_NoCheckpoints_ShowsHelpfulMessage(t *testing.T) {
tmpDir := t.TempDir()
t.Chdir(tmpDir)
// Initialize git repo
testutil.InitRepo(t, tmpDir)
repo, err := git.PlainOpen(tmpDir)
require.NoError(t, err)
// Create .entire directory but no checkpoints
if err := os.MkdirAll(".entire", 0o750); err != nil {
t.Fatalf("failed to create .entire dir: %v", err)
}
var stdout, stderr bytes.Buffer
err = runExplainDefault(context.Background(), &stdout, &stderr, true) // noPager=true for test
// Should NOT error
if err != nil {
t.Errorf("expected no error, got: %v", err)
}
output := stdout.String()
// Should show checkpoints count as 0 (new metadata-row shape)
if !strings.Contains(output, "checkpoints 0") {
t.Errorf("expected 'checkpoints 0' in output, got: %s", output)
}
// Should show helpful message about checkpoints appearing after saves
if !strings.Contains(output, "Checkpoints will appear") || !strings.Contains(output, "agent session") {
t.Errorf("expected helpful message about checkpoints, got: %s", output)
}
}
func TestExplainBothFlagsError(t *testing.T) {
// Test that providing both --session and --commit returns an error
var stdout, stderr bytes.Buffer
9 unmodified lines
}
}
func TestFormatSessionInfo(t *testing.T) {
now := time.Now()
session := &strategy.Session{
ID: "2025-12-09-test-session-abc",
Description: "Test description",
Strategy: "manual-commit",
StartTime: now,
Checkpoints: []strategy.Checkpoint{
{
CheckpointID: "abc1234567890",
Message: "First checkpoint",
Timestamp: now.Add(-time.Hour),
},
{
CheckpointID: "def0987654321",
Message: "Second checkpoint",
Timestamp: now,
},
},
}
// Create checkpoint details matching the session checkpoints
checkpointDetails := []checkpointDetail{
{
Index: 1,
ShortID: "abc1234",
Timestamp: now.Add(-time.Hour),
Message: "First checkpoint",
Interactions: []interaction{{
Prompt: "Fix the bug",
Responses: []string{"Fixed the bug in auth module"},
Files: []string{"auth.go"},
}},
Files: []string{"auth.go"},
},
{
Index: 2,
ShortID: "def0987",
Timestamp: now,
Message: "Second checkpoint",
Interactions: []interaction{{
Prompt: "Add tests",
Responses: []string{"Added unit tests"},
Files: []string{"auth_test.go"},
}},
Files: []string{"auth_test.go"},
},
}
output := formatSessionInfo(session, "", checkpointDetails)
// Verify output contains expected sections
if !strings.Contains(output, "Session:") {
t.Error("expected output to contain 'Session:'")
}
if !strings.Contains(output, session.ID) {
t.Error("expected output to contain session ID")
}
if !strings.Contains(output, "Strategy:") {
t.Error("expected output to contain 'Strategy:'")
}
if !strings.Contains(output, "manual-commit") {
t.Error("expected output to contain strategy name")
}
if !strings.Contains(output, "Checkpoints: 2") {
t.Error("expected output to contain 'Checkpoints: 2'")
}
// Check checkpoint details
if !strings.Contains(output, "Checkpoint 1") {
t.Error("expected output to contain 'Checkpoint 1'")
}
if !strings.Contains(output, "## Prompt") {
t.Error("expected output to contain '## Prompt'")
}
if !strings.Contains(output, "## Responses") {
t.Error("expected output to contain '## Responses'")
}
if !strings.Contains(output, "Files Modified") {
t.Error("expected output to contain 'Files Modified'")
}
}
func TestFormatSessionInfo_WithSourceRef(t *testing.T) {
now := time.Now()
session := &strategy.Session{
ID: "2025-12-09-test-session-abc",
Description: "Test description",
Strategy: "manual-commit",
StartTime: now,
Checkpoints: []strategy.Checkpoint{
{
CheckpointID: "abc1234567890",
Message: "First checkpoint",
Timestamp: now,
},
},
}
checkpointDetails := []checkpointDetail{
{
Index: 1,
ShortID: "abc1234",
Timestamp: now,
Message: "First checkpoint",
},
}
// Test with source ref provided
sourceRef := "entire/metadata@abc123def456"
output := formatSessionInfo(session, sourceRef, checkpointDetails)
// Verify source ref is displayed
if !strings.Contains(output, "Source Ref:") {
t.Error("expected output to contain 'Source Ref:'")
}
if !strings.Contains(output, sourceRef) {
t.Errorf("expected output to contain source ref %q, got:\n%s", sourceRef, output)
}
}
func TestFormatSessionInfo_CheckpointNumberingReversed(t *testing.T) {
now := time.Now()
session := &strategy.Session{
ID: "2025-12-09-test-session",
Strategy: "manual-commit",
StartTime: now.Add(-2 * time.Hour),
Checkpoints: []strategy.Checkpoint{}, // Not used for format test
}
// Simulate checkpoints coming in newest-first order from ListSessions
// but numbered with oldest=1, newest=N
checkpointDetails := []checkpointDetail{
{
Index: 3, // Newest checkpoint should have highest number
ShortID: "ccc3333",
Timestamp: now,
Message: "Third (newest) checkpoint",
Interactions: []interaction{{
Prompt: "Latest change",
Responses: []string{},
}},
},
{
Index: 2,
ShortID: "bbb2222",
Timestamp: now.Add(-time.Hour),
Message: "Second checkpoint",
Interactions: []interaction{{
Prompt: "Middle change",
Responses: []string{},
}},
},
{
Index: 1, // Oldest checkpoint should be #1
ShortID: "aaa1111",
Timestamp: now.Add(-2 * time.Hour),
Message: "First (oldest) checkpoint",
Interactions: []interaction{{
Prompt: "Initial change",
Responses: []string{},
}},
},
}
output := formatSessionInfo(session, "", checkpointDetails)
// Verify checkpoint ordering in output
// Checkpoint 3 should appear before Checkpoint 2 which should appear before Checkpoint 1
idx3 := strings.Index(output, "Checkpoint 3")
idx2 := strings.Index(output, "Checkpoint 2")
idx1 := strings.Index(output, "Checkpoint 1")
if idx3 == -1 || idx2 == -1 || idx1 == -1 {
t.Fatalf("expected all checkpoints to be in output, got:\n%s", output)
}
// In the output, they should appear in the order they're in the slice (newest first)
if idx3 > idx2 || idx2 > idx1 {
t.Errorf("expected checkpoints to appear in order 3, 2, 1 in output (newest first), got positions: 3=%d, 2=%d, 1=%d", idx3, idx2, idx1)
}
// Verify the dates appear correctly
if !strings.Contains(output, "Latest change") {
t.Error("expected output to contain 'Latest change' prompt")
}
if !strings.Contains(output, "Initial change") {
t.Error("expected output to contain 'Initial change' prompt")
}
}
func TestFormatSessionInfo_EmptyCheckpoints(t *testing.T) {
now := time.Now()
session := &strategy.Session{
ID: "2025-12-09-empty-session",
Strategy: "manual-commit",
StartTime: now,
Checkpoints: []strategy.Checkpoint{},
}
output := formatSessionInfo(session, "", nil)
if !strings.Contains(output, "Checkpoints: 0") {
t.Errorf("expected output to contain 'Checkpoints: 0', got:\n%s", output)
}
}
func TestFormatSessionInfo_CheckpointWithTaskMarker(t *testing.T) {
now := time.Now()
session := &strategy.Session{
ID: "2025-12-09-task-session",
Strategy: "manual-commit",
StartTime: now,
Checkpoints: []strategy.Checkpoint{},
}
checkpointDetails := []checkpointDetail{
{
Index: 1,
ShortID: "abc1234",
Timestamp: now,
IsTaskCheckpoint: true,
Message: "Task checkpoint",
Interactions: []interaction{{
Prompt: "Run tests",
Responses: []string{},
}},
},
}
output := formatSessionInfo(session, "", checkpointDetails)
if !strings.Contains(output, "[Task]") {
t.Errorf("expected output to contain '[Task]' marker, got:\n%s", output)
}
}
func TestFormatSessionInfo_CheckpointWithDate(t *testing.T) {
// Test that checkpoint headers include the full date
timestamp := time.Date(2025, 12, 10, 14, 35, 0, 0, time.UTC)
session := &strategy.Session{
ID: "2025-12-10-dated-session",
Strategy: "manual-commit",
StartTime: timestamp,
Checkpoints: []strategy.Checkpoint{},
}
checkpointDetails := []checkpointDetail{
{
Index: 1,
ShortID: "abc1234",
Timestamp: timestamp,
Message: "Test checkpoint",
},
}
output := formatSessionInfo(session, "", checkpointDetails)
// Should contain "2025-12-10 14:35" in the checkpoint header
if !strings.Contains(output, "2025-12-10 14:35") {
t.Errorf("expected output to contain date '2025-12-10 14:35', got:\n%s", output)
}
}
func TestFormatSessionInfo_ShowsMessageWhenNoInteractions(t *testing.T) {
// Test that checkpoints without transcript content show the commit message
now := time.Now()
session := &strategy.Session{
ID: "2025-12-12-incremental-session",
Strategy: "manual-commit",
StartTime: now,
Checkpoints: []strategy.Checkpoint{},
}
// Checkpoint with message but no interactions (like incremental checkpoints)
checkpointDetails := []checkpointDetail{
{
Index: 1,
ShortID: "abc1234",
Timestamp: now,
IsTaskCheckpoint: true,
Message: "Starting 'dev' agent: Implement feature X (toolu_01ABC)",
Interactions: []interaction{}, // Empty - no transcript available
},
}
output := formatSessionInfo(session, "", checkpointDetails)
// Should show the commit message when there are no interactions
if !strings.Contains(output, "Starting 'dev' agent: Implement feature X (toolu_01ABC)") {
t.Errorf("expected output to contain commit message when no interactions, got:\n%s", output)
}
// Should NOT show "## Prompt" or "## Responses" sections since there are no interactions
if strings.Contains(output, "## Prompt") {
t.Errorf("expected output to NOT contain '## Prompt' when no interactions, got:\n%s", output)
}
if strings.Contains(output, "## Responses") {
t.Errorf("expected output to NOT contain '## Responses' when no interactions, got:\n%s", output)
}
}
func TestFormatSessionInfo_ShowsMessageAndFilesWhenNoInteractions(t *testing.T) {
// Test that checkpoints without transcript but with files show both message and files
now := time.Now()
session := &strategy.Session{
ID: "2025-12-12-incremental-with-files",
Strategy: "manual-commit",
StartTime: now,
Checkpoints: []strategy.Checkpoint{},
}
checkpointDetails := []checkpointDetail{
{
Index: 1,
ShortID: "def5678",
Timestamp: now,
IsTaskCheckpoint: true,
Message: "Running tests for API endpoint (toolu_02DEF)",
Interactions: []interaction{}, // Empty - no transcript
Files: []string{"api/endpoint.go", "api/endpoint_test.go"},
},
}
output := formatSessionInfo(session, "", checkpointDetails)
// Should show the commit message
if !strings.Contains(output, "Running tests for API endpoint (toolu_02DEF)") {
t.Errorf("expected output to contain commit message, got:\n%s", output)
}
// Should also show the files
if !strings.Contains(output, "Files Modified") {
t.Errorf("expected output to contain 'Files Modified', got:\n%s", output)
}
if !strings.Contains(output, "api/endpoint.go") {
t.Errorf("expected output to contain modified file, got:\n%s", output)
}
}
func TestFormatSessionInfo_DoesNotShowMessageWhenHasInteractions(t *testing.T) {
// Test that checkpoints WITH interactions don't show the message separately
// (the interactions already contain the content)
now := time.Now()
session := &strategy.Session{
ID: "2025-12-12-full-checkpoint",
Strategy: "manual-commit",
StartTime: now,
Checkpoints: []strategy.Checkpoint{},
}
checkpointDetails := []checkpointDetail{
{
Index: 1,
ShortID: "ghi9012",
Timestamp: now,
IsTaskCheckpoint: true,
Message: "Completed 'dev' agent: Implement feature (toolu_03GHI)",
Interactions: []interaction{
{
Prompt: "Implement the feature",
Responses: []string{"I've implemented the feature by..."},
Files: []string{"feature.go"},
},
},
},
}
output := formatSessionInfo(session, "", checkpointDetails)
// Should show the interaction content
if !strings.Contains(output, "Implement the feature") {
t.Errorf("expected output to contain prompt, got:\n%s", output)
}
if !strings.Contains(output, "I've implemented the feature by") {
t.Errorf("expected output to contain response, got:\n%s", output)
}
// The message should NOT appear as a separate line (it's redundant when we have interactions)
// The output should contain ## Prompt and ## Responses for the interaction
if !strings.Contains(output, "## Prompt") {
t.Errorf("expected output to contain '## Prompt' when has interactions, got:\n%s", output)
}
}
func TestExplainCmd_HasCheckpointFlag(t *testing.T) {
cmd := newExplainCmd()
2198 unmodified lines
}
}
// TestRunExplainBranchDefault_ShowsBranchCheckpoints is covered by TestExplainDefault_ShowsBranchView
// since runExplainDefault now calls runExplainBranchDefault directly.
func TestRunExplainBranchDefault_DetachedHead(t *testing.T) {
tmpDir := t.TempDir()
t.Chdir(tmpDir)
// Initialize git repo with a commit
testutil.InitRepo(t, tmpDir)
repo, err := git.PlainOpen(tmpDir)
require.NoError(t, err)
w, err := repo.Worktree()
if err != nil {
t.Fatalf("failed to get worktree: %v", err)
}
testFile := filepath.Join(tmpDir, "test.txt")
if err := os.WriteFile(testFile, []byte("test content"), 0o644); err != nil {
t.Fatalf("failed to write test file: %v", err)
}
if _, err := w.Add("test.txt"); err != nil {
t.Fatalf("failed to add test file: %v", err)
}
commitHash, err := w.Commit("initial commit", &git.CommitOptions{
Author: &object.Signature{
Name: "Test",
Email: "test@example.com",
},
})
if err != nil {
t.Fatalf("failed to create initial commit: %v", err)
}
// Checkout to detached HEAD state
if err := w.Checkout(&git.CheckoutOptions{Hash: commitHash}); err != nil {
t.Fatalf("failed to checkout detached HEAD: %v", err)
}
// Create .entire directory
if err := os.MkdirAll(".entire", 0o750); err != nil {
t.Fatalf("failed to create .entire dir: %v", err)
}
var stdout, stderr bytes.Buffer
err = runExplainBranchDefault(context.Background(), &stdout, &stderr, true)
// Should NOT error
if err != nil {
t.Errorf("expected no error, got: %v", err)
}
output := stdout.String()
// Should indicate detached HEAD state in branch name
if !strings.Contains(output, "HEAD") && !strings.Contains(output, "detached") {
t.Errorf("expected output to indicate detached HEAD state, got: %s", output)
}
}
func TestIsAncestorOf(t *testing.T) {
tmpDir := t.TempDir()
t.Chdir(tmpDir)
105 unmodified lines
}
}
func TestHasCodeChanges_FirstCommitReturnsTrue(t *testing.T) {
// First commit on a shadow branch (no parent) should return true
// since it captures the working copy state - real uncommitted work
tmpDir := t.TempDir()
t.Chdir(tmpDir)
testutil.InitRepo(t, tmpDir)
repo, err := git.PlainOpen(tmpDir)
require.NoError(t, err)
w, err := repo.Worktree()
if err != nil {
t.Fatalf("failed to get worktree: %v", err)
}
// Create first commit (has no parent)
testFile := filepath.Join(tmpDir, "test.txt")
if err := os.WriteFile(testFile, []byte("initial"), 0o644); err != nil {
t.Fatalf("failed to write test file: %v", err)
}
if _, err := w.Add("test.txt"); err != nil {
t.Fatalf("failed to add test file: %v", err)
}
commitHash, err := w.Commit("first commit", &git.CommitOptions{
Author: &object.Signature{Name: "Test", Email: "test@example.com", When: time.Now()},
})
if err != nil {
t.Fatalf("failed to create commit: %v", err)
}
commit, err := repo.CommitObject(commitHash)
if err != nil {
t.Fatalf("failed to get commit object: %v", err)
}
// First commit (no parent) captures working copy state - should return true
if !hasCodeChanges(commit) {
t.Error("hasCodeChanges() should return true for first commit (captures working copy)")
}
}
func TestHasCodeChanges_OnlyMetadataChanges(t *testing.T) {
tmpDir := t.TempDir()
t.Chdir(tmpDir)
testutil.InitRepo(t, tmpDir)
repo, err := git.PlainOpen(tmpDir)
require.NoError(t, err)
w, err := repo.Worktree()
if err != nil {
t.Fatalf("failed to get worktree: %v", err)
}
// Create first commit
testFile := filepath.Join(tmpDir, "test.txt")
if err := os.WriteFile(testFile, []byte("initial"), 0o644); err != nil {
t.Fatalf("failed to write test file: %v", err)
}
if _, err := w.Add("test.txt"); err != nil {
t.Fatalf("failed to add test file: %v", err)
}
_, err = w.Commit("first commit", &git.CommitOptions{
Author: &object.Signature{Name: "Test", Email: "test@example.com", When: time.Now()},
})
if err != nil {
t.Fatalf("failed to create first commit: %v", err)
}
// Create second commit with only .entire/ metadata changes
metadataDir := filepath.Join(tmpDir, ".entire", "metadata", "session-123")
if err := os.MkdirAll(metadataDir, 0o755); err != nil {
t.Fatalf("failed to create metadata dir: %v", err)
}
if err := os.WriteFile(filepath.Join(metadataDir, "full.jsonl"), []byte(`{"test": true}`), 0o644); err != nil {
t.Fatalf("failed to write metadata file: %v", err)
}
if _, err := w.Add(".entire"); err != nil {
t.Fatalf("failed to add .entire: %v", err)
}
commitHash, err := w.Commit("metadata only commit", &git.CommitOptions{
Author: &object.Signature{Name: "Test", Email: "test@example.com", When: time.Now()},
})
if err != nil {
t.Fatalf("failed to create second commit: %v", err)
}
commit, err := repo.CommitObject(commitHash)
if err != nil {
t.Fatalf("failed to get commit object: %v", err)
}
// Only .entire/ changes should return false
if hasCodeChanges(commit) {
t.Error("hasCodeChanges() should return false when only .entire/ files changed")
}
}
func TestHasCodeChanges_WithCodeChanges(t *testing.T) {
tmpDir := t.TempDir()
t.Chdir(tmpDir)
testutil.InitRepo(t, tmpDir)
repo, err := git.PlainOpen(tmpDir)
require.NoError(t, err)
w, err := repo.Worktree()
if err != nil {
t.Fatalf("failed to get worktree: %v", err)
}
// Create second commit with code changes
if err := os.WriteFile(testFile, []byte("modified"), 0o644); err != nil {
t.Fatalf("failed to modify test file: %v", err)
}
if _, err := w.Add("test.txt"); err != nil {
t.Fatalf("failed to add modified file: %v", err)
}
commitHash, err := w.Commit("code change commit", &git.CommitOptions{
Author: &object.Signature{Name: "Test", Email: "test@example.com", When: time.Now()},
})
if err != nil {
t.Fatalf("failed to create second commit: %v", err)
}
commit, err := repo.CommitObject(commitHash)
if err != nil {
t.Fatalf("failed to get commit object: %v", err)
}
// Code changes should return true
if !hasCodeChanges(commit) {
t.Error("hasCodeChanges() should return true when code files changed")
}
}
func TestHasCodeChanges_MixedChanges(t *testing.T) {
tmpDir := t.TempDir()
t.Chdir(tmpDir)
testutil.InitRepo(t, tmpDir)
repo, err := git.PlainOpen(tmpDir)
require.NoError(t, err)
w, err := repo.Worktree()
if err != nil {
t.Fatalf("failed to get worktree: %v", err)
}
// Create second commit with BOTH code and metadata changes
if err := os.WriteFile(testFile, []byte("modified"), 0o644); err != nil {
t.Fatalf("failed to modify test file: %v", err)
}
metadataDir := filepath.Join(tmpDir, ".entire", "metadata", "session-123")
if err := os.MkdirAll(metadataDir, 0o755); err != nil {
t.Fatalf("failed to create metadata dir: %v", err)
}
if err := os.WriteFile(filepath.Join(metadataDir, "full.jsonl"), []byte(`{"test": true}`), 0o644); err != nil {
t.Fatalf("failed to write metadata file: %v", err)
}
if _, err := w.Add("test.txt"); err != nil {
t.Fatalf("failed to add test file: %v", err)
}
if _, err := w.Add(".entire"); err != nil {
t.Fatalf("failed to add .entire: %v", err)
}
commitHash, err := w.Commit("mixed changes commit", &git.CommitOptions{
Author: &object.Signature{Name: "Test", Email: "test@example.com", When: time.Now()},
})
if err != nil {
t.Fatalf("failed to create second commit: %v", err)
}
commit, err := repo.CommitObject(commitHash)
if err != nil {
t.Fatalf("failed to get commit object: %v", err)
}
// Mixed changes should return true (code changes present)
if !hasCodeChanges(commit) {
t.Error("hasCodeChanges() should return true when commit has both code and metadata changes")
}
}
func TestGetBranchCheckpoints_FiltersMainCommits(t *testing.T) {
tmpDir := t.TempDir()
t.Chdir(tmpDir)
1728 unmodified lines
}
func TestHasAnyChanges_MetadataOnlyChangeReturnsTrue(t *testing.T) {
// Unlike hasCodeChanges, hasAnyChanges uses tree hash comparison and
// hasAnyChanges uses tree hash comparison and
// does not filter out .entire/ metadata files. A metadata-only change
// should return true because the tree hash differs from the parent's.
tmpDir := t.TempDir()
47 unmodified lines
}
// hasAnyChanges compares tree hashes, so metadata-only changes DO count
// (unlike hasCodeChanges which filters .entire/ files)
// (it does not filter .entire/ files)
if !hasAnyChanges(commit) {
t.Error("hasAnyChanges() should return true for metadata-only changes (tree hash differs)")
}
Mcmd/entire/cli/explain_test.go+2/-769
203 unmodified lines
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
207
208
209
9 unmodified lines
219
220
221
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
222
223
224
203 unmodified lines
return head.Name().Short(), nil
}
// GetMergeBase finds the common ancestor (merge-base) between two branches.
// Returns the hash of the merge-base commit.
func GetMergeBase(ctx context.Context, branch1, branch2 string) (*plumbing.Hash, error) {
repo, err := openRepository(ctx)
if err != nil {
return nil, fmt.Errorf("failed to open git repository: %w", err)
}
defer repo.Close()
// Resolve branch references
ref1, err := repo.Reference(plumbing.NewBranchReferenceName(branch1), true)
if err != nil {
return nil, fmt.Errorf("failed to resolve branch %s: %w", branch1, err)
}
ref2, err := repo.Reference(plumbing.NewBranchReferenceName(branch2), true)
if err != nil {
return nil, fmt.Errorf("failed to resolve branch %s: %w", branch2, err)
}
// Get commit objects
commit1, err := repo.CommitObject(ref1.Hash())
if err != nil {
return nil, fmt.Errorf("failed to get commit for %s: %w", branch1, err)
}
commit2, err := repo.CommitObject(ref2.Hash())
if err != nil {
return nil, fmt.Errorf("failed to get commit for %s: %w", branch2, err)
}
// Find common ancestor
mergeBase, err := commit1.MergeBase(commit2)
if err != nil {
return nil, fmt.Errorf("failed to find merge base: %w", err)
}
if len(mergeBase) == 0 {
return nil, errors.New("no common ancestor found")
}
hash := mergeBase[0].Hash
return &hash, nil
}
// HasUncommittedChanges checks if there are any uncommitted changes in the repository.
// This includes staged changes, unstaged changes, and untracked files.
// Uses git CLI instead of go-git because go-git doesn't respect global gitignore
9 unmodified lines
return len(strings.TrimSpace(string(output))) > 0, nil
}
// findNewUntrackedFiles finds files that are newly untracked (not in pre-existing list)
func findNewUntrackedFiles(current, preExisting []string) []string {
preExistingSet := make(map[string]bool)
for _, file := range preExisting {
preExistingSet[file] = true
}
var newFiles []string
for _, file := range current {
if !preExistingSet[file] {
newFiles = append(newFiles, file)
}
}
return newFiles
}
// BranchExistsOnRemote checks if a branch exists on the origin remote.
// First checks local remote-tracking refs, then queries the actual remote
// via git ls-remote in case local refs are stale (e.g., after a fresh clone
Mcmd/entire/cli/git_operations.go-61
130 unmodified lines
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
134
135
136
122 unmodified lines
259
260
261
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
262
263
264
130 unmodified lines
}
}
func TestGetMergeBase(t *testing.T) {
// Create temp directory for test repo
tmpDir := t.TempDir()
t.Chdir(tmpDir)
// Initialize repo
repo := initOpenedTestRepo(t, tmpDir)
w, err := repo.Worktree()
if err != nil {
t.Fatalf("Failed to get worktree: %v", err)
}
// Create initial commit on main
testFile := filepath.Join(tmpDir, "test.txt")
if err := os.WriteFile(testFile, []byte("initial"), 0o644); err != nil {
t.Fatalf("Failed to write test file: %v", err)
}
if _, err := w.Add("test.txt"); err != nil {
t.Fatalf("Failed to add test file: %v", err)
}
baseCommit, err := w.Commit("initial commit", &git.CommitOptions{
Author: &object.Signature{
Name: "Test",
Email: "test@example.com",
},
})
if err != nil {
t.Fatalf("Failed to create initial commit: %v", err)
}
// Create main branch reference
mainRef := plumbing.NewHashReference(plumbing.NewBranchReferenceName("main"), baseCommit)
if err := repo.Storer.SetReference(mainRef); err != nil {
t.Fatalf("Failed to create main branch: %v", err)
}
// Create feature branch from base
featureRef := plumbing.NewHashReference(plumbing.NewBranchReferenceName("feature"), baseCommit)
if err := repo.Storer.SetReference(featureRef); err != nil {
t.Fatalf("Failed to create feature branch: %v", err)
}
// Checkout feature and make a commit
gitCheckout(t, tmpDir, "feature")
if err := os.WriteFile(testFile, []byte("feature change"), 0o644); err != nil {
t.Fatalf("Failed to write test file: %v", err)
}
if _, err := w.Add("test.txt"); err != nil {
t.Fatalf("Failed to add test file: %v", err)
}
if _, err := w.Commit("feature commit", &git.CommitOptions{
Author: &object.Signature{
Name: "Test",
Email: "test@example.com",
},
}); err != nil {
t.Fatalf("Failed to commit: %v", err)
}
// Test getting merge base
mergeBase, err := GetMergeBase(context.Background(), "feature", "main")
if err != nil {
t.Fatalf("GetMergeBase(context.Background(),) error = %v", err)
}
if mergeBase.String() != baseCommit.String() {
t.Errorf("GetMergeBase(context.Background(),) = %v, want %v", mergeBase, baseCommit)
}
}
func TestGetMergeBaseNonExistentBranch(t *testing.T) {
// Create temp directory for test repo
tmpDir := t.TempDir()
t.Chdir(tmpDir)
// Initialize repo with commit
repo := initOpenedTestRepo(t, tmpDir)
w, err := repo.Worktree()
if err != nil {
t.Fatalf("Failed to get worktree: %v", err)
}
testFile := filepath.Join(tmpDir, "test.txt")
if err := os.WriteFile(testFile, []byte("test"), 0o644); err != nil {
t.Fatalf("Failed to write test file: %v", err)
}
if _, err := w.Add("test.txt"); err != nil {
t.Fatalf("Failed to add test file: %v", err)
}
if _, err := w.Commit("initial commit", &git.CommitOptions{
Author: &object.Signature{
Name: "Test",
Email: "test@example.com",
},
}); err != nil {
t.Fatalf("Failed to commit: %v", err)
}
// Test with non-existent branch
_, err = GetMergeBase(context.Background(), "feature", "nonexistent")
if err == nil {
t.Error("GetMergeBase(context.Background(),) expected error for nonexistent branch, got nil")
}
}
func TestHasUncommittedChanges(t *testing.T) {
// Create temp directory for test repo
tmpDir := t.TempDir()
122 unmodified lines
}
}
func TestFindNewUntrackedFiles(t *testing.T) {
tests := []struct {
name string
current []string
preExisting []string
expected []string
}{
{
name: "finds new files not in pre-existing list",
current: []string{"file1.go", "file2.go", "file3.go"},
preExisting: []string{"file1.go"},
expected: []string{"file2.go", "file3.go"},
},
{
name: "returns empty when all files pre-exist",
current: []string{"file1.go", "file2.go"},
preExisting: []string{"file1.go", "file2.go"},
expected: nil,
},
{
name: "returns all files when pre-existing is empty",
current: []string{"file1.go", "file2.go"},
preExisting: []string{},
expected: []string{"file1.go", "file2.go"},
},
{
name: "returns nil when current is empty",
current: []string{},
preExisting: []string{"file1.go"},
expected: nil,
},
{
name: "handles nil current slice",
current: nil,
preExisting: []string{"file1.go"},
expected: nil,
},
{
name: "handles nil pre-existing slice",
current: []string{"file1.go", "file2.go"},
preExisting: nil,
expected: []string{"file1.go", "file2.go"},
},
{
name: "handles both nil slices",
current: nil,
preExisting: nil,
expected: nil,
},
{
name: "handles files with paths",
current: []string{"src/main.go", "src/utils.go", "test/main_test.go"},
preExisting: []string{"src/main.go"},
expected: []string{"src/utils.go", "test/main_test.go"},
},
{
name: "handles duplicate files in pre-existing",
current: []string{"file1.go", "file2.go"},
preExisting: []string{"file1.go", "file1.go"},
expected: []string{"file2.go"},
},
{
name: "is case-sensitive",
current: []string{"File.go", "file.go"},
preExisting: []string{"file.go"},
expected: []string{"File.go"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := findNewUntrackedFiles(tt.current, tt.preExisting)
if len(result) != len(tt.expected) {
t.Errorf("findNewUntrackedFiles() returned %d files, want %d", len(result), len(tt.expected))
t.Errorf("got: %v, want: %v", result, tt.expected)
return
}
// Create a map for easy lookup
expectedMap := make(map[string]bool)
for _, f := range tt.expected {
expectedMap[f] = true
}
for _, f := range result {
if !expectedMap[f] {
t.Errorf("findNewUntrackedFiles() returned unexpected file %q", f)
}
}
})
}
}
func TestGetGitConfigValue(t *testing.T) {
// Test that invalid keys return empty string
invalid := getGitConfigValue(context.Background(), "nonexistent.key.that.does.not.exist")
Mcmd/entire/cli/git_operations_test.go-199
8 unmodified lines
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
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
12
13
14
113 unmodified lines
128
129
130
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
8 unmodified lines
"github.com/entireio/cli/cmd/entire/cli/strategy"
)
// TaskHookInput represents the JSON input from PreToolUse[Task] hook
type TaskHookInput struct {
SessionID string `json:"session_id"`
TranscriptPath string `json:"transcript_path"`
ToolUseID string `json:"tool_use_id"`
ToolInput json.RawMessage `json:"tool_input"`
}
// postTaskHookInputRaw is the raw JSON structure from PostToolUse[Task] hook
type postTaskHookInputRaw struct {
SessionID string `json:"session_id"`
TranscriptPath string `json:"transcript_path"`
ToolUseID string `json:"tool_use_id"`
ToolInput json.RawMessage `json:"tool_input"`
ToolResponse struct {
AgentID string `json:"agentId"`
} `json:"tool_response"`
}
// PostTaskHookInput represents the parsed input from PostToolUse[Task] hook
type PostTaskHookInput struct {
TaskHookInput
AgentID string // Extracted from tool_response.agentId
ToolInput json.RawMessage // Raw tool input for reference
}
// parseTaskHookInput parses PreToolUse[Task] hook input from reader
func parseTaskHookInput(r io.Reader) (*TaskHookInput, error) {
data, err := io.ReadAll(r)
if err != nil {
return nil, fmt.Errorf("failed to read input: %w", err)
}
if len(data) == 0 {
return nil, errors.New("empty input")
}
var input TaskHookInput
if err := json.Unmarshal(data, &input); err != nil {
return nil, fmt.Errorf("failed to parse JSON: %w", err)
}
return &input, nil
}
// parsePostTaskHookInput parses PostToolUse[Task] hook input from reader
func parsePostTaskHookInput(r io.Reader) (*PostTaskHookInput, error) {
data, err := io.ReadAll(r)
if err != nil {
return nil, fmt.Errorf("failed to read input: %w", err)
}
if len(data) == 0 {
return nil, errors.New("empty input")
}
var raw postTaskHookInputRaw
if err := json.Unmarshal(data, &raw); err != nil {
return nil, fmt.Errorf("failed to parse JSON: %w", err)
}
return &PostTaskHookInput{
TaskHookInput: TaskHookInput{
SessionID: raw.SessionID,
TranscriptPath: raw.TranscriptPath,
ToolUseID: raw.ToolUseID,
},
AgentID: raw.ToolResponse.AgentID,
ToolInput: raw.ToolInput,
}, nil
}
// logPreTaskHookContext logs the PreToolUse[Task] hook context to the writer
func logPreTaskHookContext(w io.Writer, input *TaskHookInput) {
_, _ = fmt.Fprintln(w, "[entire] PreToolUse[Task] hook invoked")
_, _ = fmt.Fprintf(w, " Session ID: %s\n", input.SessionID)
_, _ = fmt.Fprintf(w, " Tool Use ID: %s\n", input.ToolUseID)
_, _ = fmt.Fprintf(w, " Transcript: %s\n", input.TranscriptPath)
}
// SubagentCheckpointHookInput represents the JSON input from PostToolUse hooks for
// subagent checkpoint creation (TodoWrite, Edit, Write)
type SubagentCheckpointHookInput struct {
113 unmodified lines
// Delegate to strategy package for the actual count
return strategy.CountTodos(input.Todos)
}
// logPostTaskHookContext logs the PostToolUse[Task] hook context to the writer
func logPostTaskHookContext(w io.Writer, input *PostTaskHookInput, subagentTranscriptPath string) {
_, _ = fmt.Fprintln(w, "[entire] PostToolUse[Task] hook invoked")
_, _ = fmt.Fprintf(w, " Session ID: %s\n", input.SessionID)
_, _ = fmt.Fprintf(w, " Tool Use ID: %s\n", input.ToolUseID)
if input.AgentID != "" {
_, _ = fmt.Fprintf(w, " Agent ID: %s\n", input.AgentID)
} else {
_, _ = fmt.Fprintln(w, " Agent ID: (none)")
}
_, _ = fmt.Fprintf(w, " Transcript: %s\n", input.TranscriptPath)
if subagentTranscriptPath != "" {
_, _ = fmt.Fprintf(w, " Subagent Transcript: %s\n", subagentTranscriptPath)
} else {
_, _ = fmt.Fprintln(w, " Subagent Transcript: (none)")
}
}
Mcmd/entire/cli/hooks.go-102
1
2
3
4
4
5
6
7
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
8
9
10
289 unmodified lines
300
301
302
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
package cli
import (
"bytes"
"strings"
"testing"
)
func TestParsePreTaskHookInput(t *testing.T) {
tests := []struct {
name string
input string
want *TaskHookInput
wantErr bool
}{
{
name: "valid input",
input: `{"session_id":"abc123","transcript_path":"/path/to/transcript.jsonl","tool_use_id":"tool_xyz"}`,
want: &TaskHookInput{
SessionID: "abc123",
TranscriptPath: "/path/to/transcript.jsonl",
ToolUseID: "tool_xyz",
},
wantErr: false,
},
{
name: "empty input",
input: "",
want: nil,
wantErr: true,
},
{
name: "invalid json",
input: "not json",
want: nil,
wantErr: true,
},
{
name: "missing fields uses defaults",
input: `{"session_id":"abc123"}`,
want: &TaskHookInput{
SessionID: "abc123",
TranscriptPath: "",
ToolUseID: "",
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
reader := strings.NewReader(tt.input)
got, err := parseTaskHookInput(reader)
if (err != nil) != tt.wantErr {
t.Errorf("parseTaskHookInput() error = %v, wantErr %v", err, tt.wantErr)
return
}
if tt.want != nil {
if got.SessionID != tt.want.SessionID {
t.Errorf("SessionID = %v, want %v", got.SessionID, tt.want.SessionID)
}
if got.TranscriptPath != tt.want.TranscriptPath {
t.Errorf("TranscriptPath = %v, want %v", got.TranscriptPath, tt.want.TranscriptPath)
}
if got.ToolUseID != tt.want.ToolUseID {
t.Errorf("ToolUseID = %v, want %v", got.ToolUseID, tt.want.ToolUseID)
}
}
})
}
}
func TestParsePostTaskHookInput(t *testing.T) {
tests := []struct {
name string
input string
want *PostTaskHookInput
wantErr bool
}{
{
name: "valid input with agent",
input: `{
"session_id": "abc123",
"transcript_path": "/path/to/transcript.jsonl",
"tool_use_id": "tool_xyz",
"tool_input": {"prompt": "do something"},
"tool_response": {"agentId": "agent_456"}
}`,
want: &PostTaskHookInput{
TaskHookInput: TaskHookInput{
SessionID: "abc123",
TranscriptPath: "/path/to/transcript.jsonl",
ToolUseID: "tool_xyz",
},
AgentID: "agent_456",
},
wantErr: false,
},
{
name: "valid input without agent",
input: `{
"session_id": "abc123",
"transcript_path": "/path/to/transcript.jsonl",
"tool_use_id": "tool_xyz",
"tool_input": {},
"tool_response": {}
}`,
want: &PostTaskHookInput{
TaskHookInput: TaskHookInput{
SessionID: "abc123",
TranscriptPath: "/path/to/transcript.jsonl",
ToolUseID: "tool_xyz",
},
AgentID: "",
},
wantErr: false,
},
{
name: "empty input",
input: "",
want: nil,
wantErr: true,
},
{
name: "invalid json",
input: "not json",
want: nil,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
reader := strings.NewReader(tt.input)
got, err := parsePostTaskHookInput(reader)
if (err != nil) != tt.wantErr {
t.Errorf("parsePostTaskHookInput() error = %v, wantErr %v", err, tt.wantErr)
return
}
if tt.want != nil {
if got.SessionID != tt.want.SessionID {
t.Errorf("SessionID = %v, want %v", got.SessionID, tt.want.SessionID)
}
if got.TranscriptPath != tt.want.TranscriptPath {
t.Errorf("TranscriptPath = %v, want %v", got.TranscriptPath, tt.want.TranscriptPath)
}
if got.ToolUseID != tt.want.ToolUseID {
t.Errorf("ToolUseID = %v, want %v", got.ToolUseID, tt.want.ToolUseID)
}
if got.AgentID != tt.want.AgentID {
t.Errorf("AgentID = %v, want %v", got.AgentID, tt.want.AgentID)
}
}
})
}
}
func TestLogPreTaskHookContext(t *testing.T) {
input := &TaskHookInput{
SessionID: "test-session-123",
TranscriptPath: "/home/user/.claude/projects/myproject/transcript.jsonl",
ToolUseID: "toolu_abc123",
}
var buf bytes.Buffer
logPreTaskHookContext(&buf, input)
output := buf.String()
// Check that all expected fields are present
if !strings.Contains(output, "[entire] PreToolUse[Task] hook invoked") {
t.Error("Missing hook header")
}
if !strings.Contains(output, "Session ID: test-session-123") {
t.Error("Missing session ID")
}
if !strings.Contains(output, "Tool Use ID: toolu_abc123") {
t.Error("Missing tool use ID")
}
if !strings.Contains(output, "Transcript:") {
t.Error("Missing transcript path")
}
}
func TestParseSubagentCheckpointHookInput(t *testing.T) {
tests := []struct {
name string
289 unmodified lines
})
}
}
func TestLogPostTaskHookContext(t *testing.T) {
tests := []struct {
name string
input *PostTaskHookInput
subagentPath string
wantAgentID string
wantSubagentPath string
}{
{
name: "with agent",
input: &PostTaskHookInput{
TaskHookInput: TaskHookInput{
SessionID: "test-session-456",
TranscriptPath: "/path/to/transcript.jsonl",
ToolUseID: "toolu_xyz789",
},
AgentID: "agent_subagent_001",
},
subagentPath: "/path/to/agent-agent_subagent_001.jsonl",
wantAgentID: "Agent ID: agent_subagent_001",
wantSubagentPath: "Subagent Transcript: /path/to/agent-agent_subagent_001.jsonl",
},
{
name: "without agent",
input: &PostTaskHookInput{
TaskHookInput: TaskHookInput{
SessionID: "test-session-789",
TranscriptPath: "/path/to/transcript.jsonl",
ToolUseID: "toolu_def456",
},
AgentID: "",
},
subagentPath: "",
wantAgentID: "Agent ID: (none)",
wantSubagentPath: "Subagent Transcript: (none)",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var buf bytes.Buffer
logPostTaskHookContext(&buf, tt.input, tt.subagentPath)
output := buf.String()
if !strings.Contains(output, "[entire] PostToolUse[Task] hook invoked") {
t.Error("Missing hook header")
}
if !strings.Contains(output, tt.wantAgentID) {
t.Errorf("Missing or wrong agent ID, got:\n%s", output)
}
if !strings.Contains(output, tt.wantSubagentPath) {
t.Errorf("Missing or wrong subagent path, got:\n%s", output)
}
})
}
}
Mcmd/entire/cli/hooks_test.go-239
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
package cli
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"github.com/entireio/cli/cmd/entire/cli/jsonutil"
"github.com/entireio/cli/cmd/entire/cli/osroot"
"github.com/entireio/cli/cmd/entire/cli/paths"
)
// TaskCheckpoint contains the checkpoint information for a task
type TaskCheckpoint struct {
SessionID string `json:"session_id"`
ToolUseID string `json:"tool_use_id"`
CheckpointUUID string `json:"checkpoint_uuid"`
AgentID string `json:"agent_id,omitempty"`
}
// TaskMetadataDir returns the path to a task's metadata directory
// within the session metadata directory.
func TaskMetadataDir(sessionMetadataDir, toolUseID string) string {
return filepath.Join(sessionMetadataDir, "tasks", toolUseID)
}
// WriteTaskCheckpoint writes the checkpoint.json file to the task metadata directory.
// Creates the directory if it doesn't exist.
// Uses os.Root for traversal-resistant file writes within the metadata directory.
func WriteTaskCheckpoint(taskMetadataDir string, checkpoint *TaskCheckpoint) error {
// Create directory if it doesn't exist (MkdirAll not available on os.Root)
if err := os.MkdirAll(taskMetadataDir, 0o750); err != nil {
return fmt.Errorf("failed to create task metadata directory: %w", err)
}
data, err := jsonutil.MarshalIndentWithNewline(checkpoint, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal checkpoint: %w", err)
}
// Use os.Root scoped to the metadata directory for traversal-resistant write
root, err := os.OpenRoot(taskMetadataDir)
if err != nil {
return fmt.Errorf("failed to open task metadata directory: %w", err)
}
defer root.Close()
if err := osroot.WriteFile(root, paths.CheckpointFileName, data, 0o600); err != nil {
return fmt.Errorf("failed to write checkpoint file: %w", err)
}
return nil
}
// ReadTaskCheckpoint reads the checkpoint file from the task metadata directory.
// Uses os.Root for traversal-resistant file reads within the metadata directory.
func ReadTaskCheckpoint(taskMetadataDir string) (*TaskCheckpoint, error) {
root, err := os.OpenRoot(taskMetadataDir)
if err != nil {
return nil, fmt.Errorf("failed to open task metadata directory: %w", err)
}
defer root.Close()
data, err := osroot.ReadFile(root, paths.CheckpointFileName)
if err != nil {
return nil, fmt.Errorf("failed to read checkpoint file: %w", err)
}
var checkpoint TaskCheckpoint
if err := json.Unmarshal(data, &checkpoint); err != nil {
return nil, fmt.Errorf("failed to unmarshal checkpoint: %w", err)
}
return &checkpoint, nil
}
// WriteTaskPrompt writes the task prompt to the task metadata directory.
// Uses os.Root for traversal-resistant file writes within the metadata directory.
func WriteTaskPrompt(taskMetadataDir, prompt string) error {
root, err := os.OpenRoot(taskMetadataDir)
if err != nil {
return fmt.Errorf("failed to open task metadata directory: %w", err)
}
defer root.Close()
if err := osroot.WriteFile(root, paths.PromptFileName, []byte(prompt), 0o600); err != nil {
return fmt.Errorf("failed to write prompt file: %w", err)
}
return nil
}
// CopyAgentTranscript copies a subagent's transcript to the task metadata directory.
// If the source transcript doesn't exist, this is a no-op (not an error).
func CopyAgentTranscript(srcTranscript, taskMetadataDir, agentID string) error {
// Check if source exists
if _, err := os.Stat(srcTranscript); os.IsNotExist(err) {
// Source doesn't exist, nothing to copy
return nil
}
dstTranscript := filepath.Join(taskMetadataDir, fmt.Sprintf("agent-%s.jsonl", agentID))
return copyFile(srcTranscript, dstTranscript)
}
Dcmd/entire/cli/metadata.go-104
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
package cli
import (
"encoding/json"
"os"
"path/filepath"
"testing"
"github.com/entireio/cli/cmd/entire/cli/paths"
)
func TestTaskMetadataDir(t *testing.T) {
sessionMetadataDir := ".entire/metadata/2025-01-28-abc123"
toolUseID := "toolu_xyz789"
expected := ".entire/metadata/2025-01-28-abc123/tasks/toolu_xyz789"
got := TaskMetadataDir(sessionMetadataDir, toolUseID)
if got != expected {
t.Errorf("TaskMetadataDir() = %v, want %v", got, expected)
}
}
func TestTaskCheckpoint(t *testing.T) {
tmpDir := t.TempDir()
taskMetadataDir := filepath.Join(tmpDir, "tasks", "toolu_test123")
checkpoint := &TaskCheckpoint{
SessionID: "session-abc",
ToolUseID: "toolu_test123",
CheckpointUUID: "uuid-checkpoint-123",
AgentID: "agent_subagent_001",
}
// Test writing checkpoint
err := WriteTaskCheckpoint(taskMetadataDir, checkpoint)
if err != nil {
t.Fatalf("WriteTaskCheckpoint() error = %v", err)
}
// Verify directory was created
if _, err := os.Stat(taskMetadataDir); os.IsNotExist(err) {
t.Error("Task metadata directory was not created")
}
// Verify checkpoint file was created with correct content
checkpointFile := filepath.Join(taskMetadataDir, paths.CheckpointFileName)
data, err := os.ReadFile(checkpointFile)
if err != nil {
t.Fatalf("Failed to read %s: %v", paths.CheckpointFileName, err)
}
var loaded TaskCheckpoint
if err := json.Unmarshal(data, &loaded); err != nil {
t.Fatalf("Failed to unmarshal %s: %v", paths.CheckpointFileName, err)
}
if loaded.SessionID != checkpoint.SessionID {
t.Errorf("SessionID = %v, want %v", loaded.SessionID, checkpoint.SessionID)
}
if loaded.ToolUseID != checkpoint.ToolUseID {
t.Errorf("ToolUseID = %v, want %v", loaded.ToolUseID, checkpoint.ToolUseID)
}
if loaded.CheckpointUUID != checkpoint.CheckpointUUID {
t.Errorf("CheckpointUUID = %v, want %v", loaded.CheckpointUUID, checkpoint.CheckpointUUID)
}
if loaded.AgentID != checkpoint.AgentID {
t.Errorf("AgentID = %v, want %v", loaded.AgentID, checkpoint.AgentID)
}
// Test reading checkpoint
readCheckpoint, err := ReadTaskCheckpoint(taskMetadataDir)
if err != nil {
t.Fatalf("ReadTaskCheckpoint() error = %v", err)
}
if readCheckpoint.SessionID != checkpoint.SessionID {
t.Errorf("Read SessionID = %v, want %v", readCheckpoint.SessionID, checkpoint.SessionID)
}
}
func TestWriteTaskPrompt(t *testing.T) {
tmpDir := t.TempDir()
taskMetadataDir := filepath.Join(tmpDir, "tasks", "toolu_test")
// Create directory first
if err := os.MkdirAll(taskMetadataDir, 0o755); err != nil {
t.Fatalf("Failed to create dir: %v", err)
}
prompt := "Please implement the feature described in task-01.md"
err := WriteTaskPrompt(taskMetadataDir, prompt)
if err != nil {
t.Fatalf("WriteTaskPrompt() error = %v", err)
}
// Verify prompt file was created
promptFile := filepath.Join(taskMetadataDir, paths.PromptFileName)
data, err := os.ReadFile(promptFile)
if err != nil {
t.Fatalf("Failed to read %s: %v", paths.PromptFileName, err)
}
if string(data) != prompt {
t.Errorf("prompt.txt content = %v, want %v", string(data), prompt)
}
}
func TestCopyAgentTranscript(t *testing.T) {
tmpDir := t.TempDir()
// Create source transcript
srcDir := filepath.Join(tmpDir, "source")
if err := os.MkdirAll(srcDir, 0o755); err != nil {
t.Fatalf("Failed to create source dir: %v", err)
}
srcTranscript := filepath.Join(srcDir, "agent-test_agent.jsonl")
transcriptContent := `{"type":"user","uuid":"u1","message":{"content":"test"}}
{"type":"assistant","uuid":"a1","message":{"content":[]}}`
if err := os.WriteFile(srcTranscript, []byte(transcriptContent), 0o644); err != nil {
t.Fatalf("Failed to create source transcript: %v", err)
}
// Create destination directory
dstDir := filepath.Join(tmpDir, "dest", "tasks", "toolu_test")
if err := os.MkdirAll(dstDir, 0o755); err != nil {
t.Fatalf("Failed to create dest dir: %v", err)
}
// Test copy
agentID := "test_agent"
err := CopyAgentTranscript(srcTranscript, dstDir, agentID)
if err != nil {
t.Fatalf("CopyAgentTranscript() error = %v", err)
}
// Verify destination file
dstTranscript := filepath.Join(dstDir, "agent-test_agent.jsonl")
data, err := os.ReadFile(dstTranscript)
if err != nil {
t.Fatalf("Failed to read copied transcript: %v", err)
}
if string(data) != transcriptContent {
t.Errorf("Copied transcript content mismatch")
}
}
func TestCopyAgentTranscript_SourceNotExists(t *testing.T) {
tmpDir := t.TempDir()
dstDir := filepath.Join(tmpDir, "dest")
if err := os.MkdirAll(dstDir, 0o755); err != nil {
t.Fatalf("Failed to create dest dir: %v", err)
}
// Should not error when source doesn't exist
err := CopyAgentTranscript("/nonexistent/path.jsonl", dstDir, "test")
if err != nil {
t.Errorf("CopyAgentTranscript() should not error for non-existent source, got %v", err)
}
}
Dcmd/entire/cli/metadata_test.go-161
206 unmodified lines
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
210
227
228
211
212
213
214
215
216
217
218
4 unmodified lines
223
224
225
239
226
227
228
229
230
231
245
232
233
234
235
206 unmodified lines
return s.failureBullet(label) + s.metadataRows(rows)
}
// metadataRow renders a single key/value row using a 7-char min-padded label.
// 7-char-pad + 2-space-gutter is visually equivalent to the existing
// formatCheckpointHeader's %-9s + no-gutter (see explain.go) for any label up
// to 9 chars, and the explicit gutter scales cleanly when a longer label
// (e.g. "checkpoints", 11 chars) is present.
//
// Use metadataRows for multi-row blocks where alignment depends on the
// widest label.
func (s statusStyles) metadataRow(label, value string) string {
width := 7
if l := len(label); l > width {
width = l
}
return s.metadataRowsWithWidth([]explainRow{{Label: label, Value: value}}, width)
}
// metadataRows joins rows with consistent label-column width:
// max(7, widest label in slice). See metadataRow for the 7 vs 9
// equivalence note.
// max(7, widest label in slice). 7-char-pad + 2-space-gutter is visually
// equivalent to the existing formatCheckpointHeader's %-9s + no-gutter
// (see explain.go) for any label up to 9 chars, and the explicit gutter
// scales cleanly when a longer label (e.g. "checkpoints", 11 chars) is
// present.
func (s statusStyles) metadataRows(rows []explainRow) string {
width := 7
for _, r := range rows {
4 unmodified lines
return s.metadataRowsWithWidth(rows, width)
}
// metadataRowsWithWidth is the underlying renderer used by metadataRow and
// metadataRowsWithWidth is the underlying renderer used by
// metadataRows. The caller computes the column width; this function owns the
// byte layout (2-space indent, padded label, 2-space gutter, value, newline)
// and the empty-label continuation branch (4-space hanging indent, no dim
// styling — used for multi-line items beneath a parent label like "causes").
//
// The min=7 default chosen by metadataRow/metadataRows produces output
// The min=7 default chosen by metadataRows produces output
// identical to formatCheckpointHeader's %-9s + no-gutter rendering at
// explain.go for any label up to 9 chars.
func (s statusStyles) metadataRowsWithWidth(rows []explainRow, width int) string {
Mcmd/entire/cli/status_style.go+7/-20
5 unmodified lines
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
9
10
11
5 unmodified lines
"testing"
)
func TestMetadataRow_PadsLabelToMin7(t *testing.T) {
t.Parallel()
s := newStatusStyles(io.Discard) // colorEnabled=false, deterministic
got := s.metadataRow("session", "2026-04-30-c4f1")
want := " session 2026-04-30-c4f1\n"
if got != want {
t.Errorf("metadataRow short-label padding\n got: %q\nwant: %q", got, want)
}
}
func TestMetadataRow_PadsLongerLabelsByItself(t *testing.T) {
t.Parallel()
s := newStatusStyles(io.Discard)
got := s.metadataRow("checkpoints", "3")
// 11-char label fits without padding; layout is " " + label + " " + value + "\n".
want := " checkpoints 3\n"
if got != want {
t.Errorf("metadataRow long-label\n got: %q\nwant: %q", got, want)
}
}
func TestMetadataRows_AlignsToWidestLabel(t *testing.T) {
t.Parallel()
s := newStatusStyles(io.Discard)
Mcmd/entire/cli/status_style_test.go-21
140 unmodified lines
141
142
143
144
145
146
147
148
149
150
151
144
145
146
251 unmodified lines
398
399
400
409
410
411
412
413
414
415
416
401
402
403
140 unmodified lines
Repo string
}
func defaultTrailListOptions(insecureHTTP bool) trailListOptions {
return trailListOptions{
Status: defaultTrailListStatus,
Limit: defaultTrailListLimit,
InsecureHTTP: insecureHTTP,
}
}
func newTrailShowCmd() *cobra.Command {
var branch string
cmd := &cobra.Command{
251 unmodified lines
return parseTrailStatusFilter(opts.Status)
}
func runTrailListAllValidatedWithClient(ctx context.Context, w io.Writer, client *api.Client, opts trailListOptions) error {
statusFilters, err := validateTrailListOptions(opts)
if err != nil {
return err
}
return runTrailListAllWithClient(ctx, w, client, opts, statusFilters)
}
func runTrailListAllWithClient(ctx context.Context, w io.Writer, client *api.Client, opts trailListOptions, statusFilters []trail.Status) error {
authorFilter := opts.Author
currentUserLogin := ""
Mcmd/entire/cli/trail_cmd.go-16
381 unmodified lines
382
383
384
385
385
386
387
388
25 unmodified lines
414
415
416
417
418
417
418
419
420
11 unmodified lines
432
433
434
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
435
436
437
333 unmodified lines
771
772
773
791
774
775
776
777
778
779
780
781
782
783
784
785
786
787
5 unmodified lines
793
794
795
803
796
797
798
799
800
801
809
802
803
804
805
806
807
815
808
809
810
811
10 unmodified lines
822
823
824
832
825
826
827
828
8 unmodified lines
837
838
839
847
840
841
842
843
844
845
846
854
847
848
849
850
381 unmodified lines
}))
var out, errOut bytes.Buffer
err := runTrailListAll(t.Context(), &out, &errOut, defaultTrailListOptions(false))
err := runTrailListAll(t.Context(), &out, &errOut, trailListOptions{Status: defaultTrailListStatus, Limit: defaultTrailListLimit})
if err == nil {
t.Fatal("expected error when not logged in")
}
25 unmodified lines
return nil, errors.New("unreachable")
}))
opts := defaultTrailListOptions(false)
opts.Limit = 0
opts := trailListOptions{Status: defaultTrailListStatus, Limit: 0}
var out, errOut bytes.Buffer
err := runTrailListAll(t.Context(), &out, &errOut, opts)
11 unmodified lines
}
}
func TestRunTrailListAllWithClient_ValidatesOptionsBeforeRepoLookup(t *testing.T) {
t.Parallel()
opts := defaultTrailListOptions(false)
opts.Limit = 0
var out bytes.Buffer
err := runTrailListAllValidatedWithClient(t.Context(), &out, nil, opts)
if err == nil {
t.Fatal("expected validation error")
}
if got, want := err.Error(), "limit must be greater than 0"; got != want {
t.Fatalf("error = %q, want %q", got, want)
}
}
func TestTrailRootPrintsHelp(t *testing.T) {
t.Parallel()
cmd := newTrailCmd()
333 unmodified lines
// (2xx => enabled) is covered by api.TestClient_TrailsEnabled.
//
// Not parallel: uses t.Chdir() to point clone preferences at a fake repo.
func TestTrailsEnabledForRepo_ReadsClonePreference(t *testing.T) {
func TestTrailEnablementCache_ReadsClonePreference(t *testing.T) {
// Inline of the former trailsEnabledForRepo wrapper: resolves the current
// repo's enablement scope and checks the cached enablement decision.
trailsEnabledForCurrentRepo := func(ctx context.Context) bool {
scope, err := currentTrailEnablementScope(ctx)
if err != nil {
return false
}
return cachedTrailsEnablementForScope(ctx, scope, time.Now()) == trailEnablementCacheEnabled
}
repoDir := t.TempDir()
testutil.InitRepo(t, repoDir)
cmd := exec.CommandContext(context.Background(), "git", "remote", "add", "origin", "git@github.com:acme/repo.git")
5 unmodified lines
t.Chdir(repoDir)
ctx := context.Background()
if trailsEnabledForRepo(ctx) {
if trailsEnabledForCurrentRepo(ctx) {
t.Fatal("expected trails disabled when cache is absent")
}
if err := saveTrailsEnabledForRepo(ctx, false); err != nil {
t.Fatalf("save false cache: %v", err)
}
if trailsEnabledForRepo(ctx) {
if trailsEnabledForCurrentRepo(ctx) {
t.Fatal("expected trails disabled when cache is false")
}
if err := saveTrailsEnabledForRepo(ctx, true); err != nil {
t.Fatalf("save true cache: %v", err)
}
if !trailsEnabledForRepo(ctx) {
if !trailsEnabledForCurrentRepo(ctx) {
t.Fatal("expected trails enabled when cache is true")
}
10 unmodified lines
if err := settings.SaveClonePreferences(ctx, prefs); err != nil {
t.Fatalf("save auth-mismatched prefs: %v", err)
}
if trailsEnabledForRepo(ctx) {
if trailsEnabledForCurrentRepo(ctx) {
t.Fatal("expected trails disabled for mismatched auth cache scope")
}
prefs.TrailsEnabledAuthKey = currentAuthKey
8 unmodified lines
if err := settings.SaveClonePreferences(ctx, prefs); err != nil {
t.Fatalf("save stale prefs: %v", err)
}
if trailsEnabledForRepo(ctx) {
if trailsEnabledForCurrentRepo(ctx) {
t.Fatal("expected trails disabled when cache is stale")
}
if err := saveTrailsEnabledForRemote(ctx, "gh", "other", "repo", true); err != nil {
t.Fatalf("save mismatched cache: %v", err)
}
if trailsEnabledForRepo(ctx) {
if trailsEnabledForCurrentRepo(ctx) {
t.Fatal("expected trails disabled for mismatched cache scope")
}
}
Mcmd/entire/cli/trail_cmd_test.go+19/-26
44 unmodified lines
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
48
49
50
44 unmodified lines
Supported bool `json:"supported"`
}
// trailsEnabledForRepo reports cached enablement for the current repo.
func trailsEnabledForRepo(ctx context.Context) bool {
return cachedTrailsEnablementForRepo(ctx, time.Now()) == trailEnablementCacheEnabled
}
func cachedTrailsEnablementForRepo(ctx context.Context, now time.Time) trailEnablementCacheStatus {
scope, err := currentTrailEnablementScope(ctx)
if err != nil {
return trailEnablementCacheUnknown
}
return cachedTrailsEnablementForScope(ctx, scope, now)
}
func cachedTrailsEnablementForScope(ctx context.Context, scope trailEnablementScope, now time.Time) trailEnablementCacheStatus {
prefs, err := settings.LoadClonePreferences(ctx)
if err != nil || prefs.TrailsEnabled == nil || prefs.TrailsEnabledCheckedAt == nil {
Mcmd/entire/cli/trail_context_cache.go-13
647 unmodified lines
648
649
650
651
652
653
654
655
656
657
658
651
652
653
647 unmodified lines
}
}
func buildTrailResumeContext(found api.TrailResource, sessions []trailResumeSessionContext, sessionsUnavailable string, findings trailResumeFindingsContext) trailResumeContext {
return buildTrailResumeContextForRepoWithSkipped(found, sessions, sessionsUnavailable, 0, findings, "")
}
func buildTrailResumeContextForRepo(found api.TrailResource, sessions []trailResumeSessionContext, sessionsUnavailable string, findings trailResumeFindingsContext, repoFullName string) trailResumeContext {
return buildTrailResumeContextForRepoWithSkipped(found, sessions, sessionsUnavailable, 0, findings, repoFullName)
}
func buildTrailResumeContextForRepoWithSkipped(found api.TrailResource, sessions []trailResumeSessionContext, sessionsUnavailable string, sessionsSkipped int, findings trailResumeFindingsContext, repoFullName string) trailResumeContext {
trailCtx := trailResumeTrailContext{
ID: found.ID,
Mcmd/entire/cli/trail_resume_cmd.go-8
165 unmodified lines
166
167
168
169
169
170
171
172
15 unmodified lines
188
189
190
191
191
192
193
194
27 unmodified lines
222
223
224
225
225
226
227
228
229
230
230
231
232
233
165 unmodified lines
now := time.Date(2026, 6, 23, 12, 0, 0, 0, time.UTC)
const newSessionID = "new-session"
ctx := buildTrailResumeContext(api.TrailResource{
ctx := buildTrailResumeContextForRepoWithSkipped(api.TrailResource{
ID: "trl_1",
Number: 575,
Title: "Add trail resume",
15 unmodified lines
LastActive: now,
CheckpointID: "aaaaaaaaaaaa",
},
}, "", trailResumeFindingsContext{})
}, "", 0, trailResumeFindingsContext{}, "")
if len(ctx.Sessions) != 2 {
t.Fatalf("sessions len = %d, want 2: %#v", len(ctx.Sessions), ctx.Sessions)
27 unmodified lines
func TestBuildTrailResumeContextWithRepoIncludesRepoInResumeCommands(t *testing.T) {
t.Parallel()
ctx := buildTrailResumeContextForRepo(api.TrailResource{
ctx := buildTrailResumeContextForRepoWithSkipped(api.TrailResource{
ID: "trl_1",
Number: 575,
Title: "Add trail resume",
Branch: "feature/trail-resume",
}, nil, "", trailResumeFindingsContext{}, "entireio/cli")
}, nil, "", 0, trailResumeFindingsContext{}, "entireio/cli")
wantCommands := []string{
"entire trail finding 575 --json",
Mcmd/entire/cli/trail_resume_cmd_test.go+4/-4
1
2
3
4
4
5
6
7
9
8
9
10
170 unmodified lines
181
182
183
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
package cli
import (
"bufio"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"strings"
170 unmodified lines
return nil
}
// TranscriptPosition contains the position information for a transcript file.
type TranscriptPosition struct {
LastUUID string // Last non-empty UUID (from user/assistant messages)
LineCount int // Total number of lines
}
// GetTranscriptPosition reads a transcript file and returns the last UUID and line count.
// Returns empty position if file doesn't exist or is empty.
// Only considers UUIDs from actual messages (user/assistant), not summary rows which use leafUuid.
func GetTranscriptPosition(path string) (TranscriptPosition, error) {
if path == "" {
return TranscriptPosition{}, nil
}
file, err := os.Open(path) //nolint:gosec // Reading from controlled transcript path
if err != nil {
if os.IsNotExist(err) {
return TranscriptPosition{}, nil
}
return TranscriptPosition{}, fmt.Errorf("failed to open transcript: %w", err)
}
defer func() { _ = file.Close() }()
var pos TranscriptPosition
reader := bufio.NewReader(file)
for {
lineBytes, err := reader.ReadBytes('\n')
if err != nil && err != io.EOF {
return TranscriptPosition{}, fmt.Errorf("failed to read transcript: %w", err)
}
if len(lineBytes) == 0 {
if err == io.EOF {
break
}
continue
}
pos.LineCount++
// Parse line to extract UUID (only from user/assistant messages, not summaries)
var line transcriptLine
if err := json.Unmarshal(lineBytes, &line); err == nil {
if line.UUID != "" {
pos.LastUUID = line.UUID
}
}
if err == io.EOF {
break
}
}
return pos, nil
}
Mcmd/entire/cli/transcript.go-59
216 unmodified lines
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
216 unmodified lines
})
}
}
func TestGetTranscriptPosition_BasicMessages(t *testing.T) {
content := `{"type":"user","uuid":"user-1","message":{"content":"Hello"}}
{"type":"assistant","uuid":"asst-1","message":{"content":[{"type":"text","text":"Hi"}]}}
{"type":"user","uuid":"user-2","message":{"content":"Bye"}}`
tmpFile := createTempTranscript(t, content)
pos, err := GetTranscriptPosition(tmpFile)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if pos.LineCount != 3 {
t.Errorf("LineCount = %d, want 3", pos.LineCount)
}
if pos.LastUUID != "user-2" {
t.Errorf("LastUUID = %q, want 'user-2'", pos.LastUUID)
}
}
func TestGetTranscriptPosition_WithSummaryRows(t *testing.T) {
// Summary rows have leafUuid but no uuid field - they should not be tracked
content := `{"type":"summary","leafUuid":"leaf-1","summary":"Previous context"}
{"type":"summary","leafUuid":"leaf-2","summary":"More context"}
{"type":"user","uuid":"user-1","message":{"content":"Hello"}}
{"type":"assistant","uuid":"asst-1","message":{"content":[{"type":"text","text":"Hi"}]}}`
tmpFile := createTempTranscript(t, content)
pos, err := GetTranscriptPosition(tmpFile)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if pos.LineCount != 4 {
t.Errorf("LineCount = %d, want 4", pos.LineCount)
}
// LastUUID should be from user/assistant messages, not summary rows
if pos.LastUUID != "asst-1" {
t.Errorf("LastUUID = %q, want 'asst-1'", pos.LastUUID)
}
}
func TestGetTranscriptPosition_EmptyFile(t *testing.T) {
tmpFile := createTempTranscript(t, "")
pos, err := GetTranscriptPosition(tmpFile)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if pos.LineCount != 0 {
t.Errorf("LineCount = %d, want 0", pos.LineCount)
}
if pos.LastUUID != "" {
t.Errorf("LastUUID = %q, want empty", pos.LastUUID)
}
}
func TestGetTranscriptPosition_NonExistentFile(t *testing.T) {
pos, err := GetTranscriptPosition("/nonexistent/path/transcript.jsonl")
if err != nil {
t.Fatalf("unexpected error for non-existent file: %v", err)
}
// Should return empty position for non-existent file
if pos.LineCount != 0 {
t.Errorf("LineCount = %d, want 0", pos.LineCount)
}
if pos.LastUUID != "" {
t.Errorf("LastUUID = %q, want empty", pos.LastUUID)
}
}
func TestGetTranscriptPosition_EmptyPath(t *testing.T) {
pos, err := GetTranscriptPosition("")
if err != nil {
t.Fatalf("unexpected error for empty path: %v", err)
}
if pos.LineCount != 0 {
t.Errorf("LineCount = %d, want 0", pos.LineCount)
}
if pos.LastUUID != "" {
t.Errorf("LastUUID = %q, want empty", pos.LastUUID)
}
}
func TestGetTranscriptPosition_OnlySummaryRows(t *testing.T) {
// File with only summary rows (no uuid field, only leafUuid)
content := `{"type":"summary","leafUuid":"leaf-1","summary":"Context 1"}
{"type":"summary","leafUuid":"leaf-2","summary":"Context 2"}`
tmpFile := createTempTranscript(t, content)
pos, err := GetTranscriptPosition(tmpFile)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if pos.LineCount != 2 {
t.Errorf("LineCount = %d, want 2", pos.LineCount)
}
// No uuid field in summary rows, so LastUUID should be empty
if pos.LastUUID != "" {
t.Errorf("LastUUID = %q, want empty (summary rows don't have uuid)", pos.LastUUID)
}
}
func TestGetTranscriptPosition_MixedWithMalformedLines(t *testing.T) {
content := `{"type":"user","uuid":"user-1","message":{"content":"Hello"}}
not valid json
{"type":"assistant","uuid":"asst-1","message":{"content":[{"type":"text","text":"Hi"}]}}
{broken json
{"type":"user","uuid":"user-2","message":{"content":"Final"}}`
tmpFile := createTempTranscript(t, content)
pos, err := GetTranscriptPosition(tmpFile)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// All lines count, including malformed
if pos.LineCount != 5 {
t.Errorf("LineCount = %d, want 5", pos.LineCount)
}
// But LastUUID should be from last valid line with uuid
if pos.LastUUID != "user-2" {
t.Errorf("LastUUID = %q, want 'user-2'", pos.LastUUID)
}
}
Mcmd/entire/cli/transcript_test.go-133