Skip to content
4 changes: 4 additions & 0 deletions cmd/entire/cli/attach.go
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,10 @@ func runAttach(ctx context.Context, w io.Writer, sessionID string, agentName typ
return nil
}

if err := ensureCommittedCheckpointWritePolicy(ctx, repo); err != nil {
return err
}

// Resolve agent and transcript path.
ag, transcriptPath, err := resolveAgentAndTranscript(logCtx, w, sessionID, agentName, existingState)
if err != nil {
Expand Down
34 changes: 34 additions & 0 deletions cmd/entire/cli/attach_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"github.com/entireio/cli/cmd/entire/cli/agent/types"
cpkg "github.com/entireio/cli/cmd/entire/cli/checkpoint"
"github.com/entireio/cli/cmd/entire/cli/checkpoint/id"
"github.com/entireio/cli/cmd/entire/cli/checkpointpolicy"
"github.com/entireio/cli/cmd/entire/cli/paths"
cliReview "github.com/entireio/cli/cmd/entire/cli/review"
"github.com/entireio/cli/cmd/entire/cli/session"
Expand Down Expand Up @@ -68,6 +69,39 @@ func TestAttach_TranscriptNotFound(t *testing.T) {
}
}

func TestAttachRejectsUnsupportedCheckpointWritePolicy(t *testing.T) {
setupAttachTestRepo(t)

repoRoot := mustGetwd(t)
repo, err := git.PlainOpen(repoRoot)
if err != nil {
t.Fatal(err)
}
if _, err := checkpointpolicy.WriteLocal(context.Background(), repo, plumbing.ZeroHash, checkpointpolicy.Policy{
CheckpointVersion: "refs-v1",
CheckpointMinVersion: "branch-v1",
}); err != nil {
t.Fatal(err)
}

sessionID := "test-attach-policy-unsupported"
setupClaudeTranscript(t, sessionID, `{"type":"user","message":{"role":"user","content":"create a file"},"uuid":"uuid-1"}
{"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"Done"}]},"uuid":"uuid-2"}
`)

var out bytes.Buffer
err = runAttach(context.Background(), &out, sessionID, agent.AgentNameClaudeCode, attachOptions{Force: true})
if err == nil {
t.Fatal("expected unsupported checkpoint policy error")
}
if !strings.Contains(err.Error(), `checkpoint_version "refs-v1"`) {
t.Fatalf("error = %v, want checkpoint policy version", err)
}
if _, refErr := repo.Reference(plumbing.NewBranchReferenceName(paths.MetadataBranchName), true); refErr == nil {
t.Fatal("metadata branch exists after rejected attach")
}
}

func TestAttach_Success(t *testing.T) {
setupAttachTestRepo(t)

Expand Down
51 changes: 51 additions & 0 deletions cmd/entire/cli/checkpoint_policy_warning.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package cli

import (
"context"
"fmt"
"io"

"github.com/entireio/cli/cmd/entire/cli/checkpointpolicy"
"github.com/entireio/cli/cmd/entire/cli/gitrepo"
"github.com/entireio/cli/cmd/entire/cli/versioncheck"
"github.com/spf13/cobra"
)

func ShouldCheckCheckpointPolicyWarning(cmd *cobra.Command) bool {
if cmd == nil {
return false
}
for c := cmd; c != nil; c = c.Parent() {
if isCheckpointPolicyWarningExcludedCommand(c.Name()) {
return false
}
}
return true
}

func isCheckpointPolicyWarningExcludedCommand(name string) bool {
switch name {
case "hooks", "__send_analytics", "curl-bash-post-install":
return true
default:
return false
}
}

func WarnCheckpointPolicyIfNeeded(ctx context.Context, w io.Writer, currentVersion string) {
repo, err := gitrepo.OpenCurrent(ctx)
if err != nil {
return
}
defer repo.Close()

state, err := checkpointpolicy.ReadLocal(ctx, repo)
if err != nil {
return
}
if !checkpointpolicy.RequiresUpgrade(state.Policy) && !checkpointpolicy.UnsupportedWrite(state.Policy) {
return
}

fmt.Fprint(w, checkpointpolicy.UpgradeWarning(versioncheck.UpdateCommandForCurrentBinary(currentVersion)))
}
54 changes: 54 additions & 0 deletions cmd/entire/cli/checkpoint_policy_warning_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package cli

import (
"bytes"
"context"
"testing"

"github.com/entireio/cli/cmd/entire/cli/checkpointpolicy"
"github.com/go-git/go-git/v6"
"github.com/go-git/go-git/v6/plumbing"
"github.com/spf13/cobra"
"github.com/stretchr/testify/require"
)

func TestWarnCheckpointPolicyIfNeeded(t *testing.T) {
_, _ = setupCheckpointPolicyRepo(t)
repo, err := git.PlainOpen(".")
require.NoError(t, err)
t.Cleanup(func() {
_ = repo.Close()
})
_, err = checkpointpolicy.WriteLocal(t.Context(), repo, plumbing.ZeroHash, checkpointpolicy.Policy{
CheckpointVersion: "refs-v1",
CheckpointMinVersion: "refs-v1",
})
require.NoError(t, err)

var buf bytes.Buffer
WarnCheckpointPolicyIfNeeded(context.Background(), &buf, "1.0.0")

require.Contains(t, buf.String(), "requires checkpoint support newer than this Entire CLI")
}

func TestShouldCheckCheckpointPolicyWarning(t *testing.T) {
root := &cobra.Command{Use: "entire"}
visible := &cobra.Command{Use: "status"}
root.AddCommand(visible)

hooks := &cobra.Command{Use: "hooks", Hidden: true}
gitHook := &cobra.Command{Use: "git"}
hooks.AddCommand(gitHook)
root.AddCommand(hooks)

hiddenAlias := &cobra.Command{Use: "explain", Hidden: true}
root.AddCommand(hiddenAlias)

sendAnalytics := &cobra.Command{Use: "__send_analytics", Hidden: true}
root.AddCommand(sendAnalytics)

require.True(t, ShouldCheckCheckpointPolicyWarning(visible))
require.True(t, ShouldCheckCheckpointPolicyWarning(hiddenAlias))
require.False(t, ShouldCheckCheckpointPolicyWarning(gitHook))
require.False(t, ShouldCheckCheckpointPolicyWarning(sendAnalytics))
}
26 changes: 26 additions & 0 deletions cmd/entire/cli/checkpoint_policy_write.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package cli

import (
"context"
"fmt"

"github.com/entireio/cli/cmd/entire/cli/checkpointpolicy"
"github.com/entireio/cli/cmd/entire/cli/versioncheck"
"github.com/entireio/cli/cmd/entire/cli/versioninfo"
"github.com/go-git/go-git/v6"
)

func ensureCommittedCheckpointWritePolicy(ctx context.Context, repo *git.Repository) error {
state, err := checkpointpolicy.ReadLocal(ctx, repo)
if err != nil {
return fmt.Errorf("read checkpoint policy: %w", err)
}
if !checkpointpolicy.UnsupportedWrite(state.Policy) {
return nil
}
return fmt.Errorf(
"checkpoint policy requires checkpoint_version %q, which this Entire CLI cannot write; upgrade Entire and rerun the command: %s",
state.Policy.CheckpointVersion,
versioncheck.UpdateCommandForCurrentBinary(versioninfo.Version),
)
}
22 changes: 22 additions & 0 deletions cmd/entire/cli/checkpointpolicy/policy.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,25 @@ func ValidatePolicy(policy Policy) error {

return nil
}

func RequiresUpgrade(policy Policy) bool {
policy = Normalize(policy)
minVersion, err := ParseFormat(policy.CheckpointMinVersion)
if err != nil {
return true
}
return !CanRead(minVersion)
}

func UnsupportedWrite(policy Policy) bool {
policy = Normalize(policy)
version, err := ParseFormat(policy.CheckpointVersion)
if err != nil {
return true
}
return !CanWrite(version)
}

func UpgradeWarning(updateCommand string) string {
return fmt.Sprintf("[entire] This repository requires checkpoint support newer than this Entire CLI.\n[entire] Upgrade Entire, then rerun the command:\n[entire] %s\n", updateCommand)
}
47 changes: 47 additions & 0 deletions cmd/entire/cli/checkpointpolicy/warning_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package checkpointpolicy_test

import (
"testing"

"github.com/entireio/cli/cmd/entire/cli/checkpoint"
"github.com/entireio/cli/cmd/entire/cli/checkpointpolicy"
"github.com/stretchr/testify/require"
)

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

require.False(t, checkpointpolicy.RequiresUpgrade(checkpointpolicy.DefaultPolicy()))
require.True(t, checkpointpolicy.RequiresUpgrade(checkpointpolicy.Policy{
CheckpointVersion: checkpoint.CheckpointVersionBranchV1,
CheckpointMinVersion: "refs-v1",
}))
require.True(t, checkpointpolicy.RequiresUpgrade(checkpointpolicy.Policy{
CheckpointVersion: checkpoint.CheckpointVersionBranchV1,
CheckpointMinVersion: "invalid",
}))
}

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

require.False(t, checkpointpolicy.UnsupportedWrite(checkpointpolicy.DefaultPolicy()))
require.True(t, checkpointpolicy.UnsupportedWrite(checkpointpolicy.Policy{
CheckpointVersion: "refs-v1",
CheckpointMinVersion: checkpoint.CheckpointVersionBranchV1,
}))
require.True(t, checkpointpolicy.UnsupportedWrite(checkpointpolicy.Policy{
CheckpointVersion: "invalid",
CheckpointMinVersion: checkpoint.CheckpointVersionBranchV1,
}))
}

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

got := checkpointpolicy.UpgradeWarning("brew upgrade entire")

require.Contains(t, got, "[entire] This repository requires checkpoint support newer than this Entire CLI.")
require.Contains(t, got, "[entire] Upgrade Entire, then rerun the command:")
require.Contains(t, got, "[entire] brew upgrade entire")
}
7 changes: 5 additions & 2 deletions cmd/entire/cli/explain.go
Original file line number Diff line number Diff line change
Expand Up @@ -687,7 +687,7 @@ func runExplainCheckpointWithLookup(ctx context.Context, w, errW io.Writer, chec
if openErr != nil {
return fmt.Errorf("open checkpoint store: %w", openErr)
}
if err := generateCheckpointSummary(ctx, w, errW, writeStores.Persistent, fullCheckpointID, summary, content, force, summaryTimeoutSeconds); err != nil {
if err := generateCheckpointSummary(ctx, w, errW, lookup.repo, writeStores.Persistent, fullCheckpointID, summary, content, force, summaryTimeoutSeconds); err != nil {
return err
}
// Reload to get the updated summary.
Expand Down Expand Up @@ -899,7 +899,7 @@ func newExplainCheckpointLookup(ctx context.Context) (*explainCheckpointLookup,
// summaryTimeoutSeconds is the per-invocation --summary-timeout-seconds flag
// value (0 = unset). Effective precedence for the deadline: flag > settings >
// package default. See resolveSummaryTimeout for the resolution.
func generateCheckpointSummary(ctx context.Context, w, errW io.Writer, store checkpoint.Writer, checkpointID id.CheckpointID, cpSummary *checkpoint.CheckpointSummary, content *checkpoint.SessionContent, force bool, summaryTimeoutSeconds int) error {
func generateCheckpointSummary(ctx context.Context, w, errW io.Writer, repo *git.Repository, store checkpoint.Writer, checkpointID id.CheckpointID, cpSummary *checkpoint.CheckpointSummary, content *checkpoint.SessionContent, force bool, summaryTimeoutSeconds int) error {
// Check if summary already exists
if content.Metadata.Summary != nil && !force {
return renderExplainFailure(errW, "Summary already exists", []explainRow{
Expand All @@ -922,6 +922,9 @@ func generateCheckpointSummary(ctx context.Context, w, errW io.Writer, store che
{Label: "id", Value: checkpointID.String()},
}, fmt.Errorf("checkpoint %s has no transcript content for this checkpoint (scoped)", checkpointID))
}
if err := ensureCommittedCheckpointWritePolicy(ctx, repo); err != nil {
return err
}
provider, err := resolveCheckpointSummaryProvider(ctx, w)
if err != nil {
return fmt.Errorf("failed to resolve summary provider: %w", err)
Expand Down
Loading
Loading