diff --git a/CLAUDE.md b/CLAUDE.md index 01d7bb37a4..43d0796f94 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -442,6 +442,7 @@ The manual-commit strategy (`manual_commit*.go`) does not modify the active bran - **Shadow branch migration** - if user does stash/pull/rebase (HEAD changes without commit), shadow branch is automatically moved to new base commit - **Orphaned branch cleanup** - if a shadow branch exists without a corresponding session state file, it is automatically reset when a new session starts - PrePush hook can push `entire/checkpoints/v1` branch alongside user pushes +- **OPF (OpenAI Privacy Filter) runs at pre-push, not post-commit**: when `redaction.openai_privacy_filter.enabled` is true, the PrePush hook re-redacts unpushed `entire/checkpoints/v1` commits with the OPF 8th layer, builds new commits carrying an `Entire-OPF-Applied: true` trailer, and atomically updates the local v1 ref before pushing. Per-commit condensation stays on the fast 7-layer pipeline. See `strategy/manual_commit_opf_rewrite.go` and `docs/security-and-privacy.md` for the full flow, including divergence detection, bootstrap caps, and CAS-on-conflict semantics. - Safe to use on main/master since it never modifies commit history #### Key Files @@ -450,6 +451,7 @@ The manual-commit strategy (`manual_commit*.go`) does not modify the active bran - `common.go` - Helpers for metadata extraction, tree building, rewind validation, `ListCheckpoints()` - `session.go` - Session/checkpoint data structures - `push_common.go` - PrePush logic for pushing `entire/checkpoints/v1` branch +- `manual_commit_opf_rewrite.go` - Pre-push OPF re-redaction: walks unpushed v1 commits, runs OPF over their blobs, rebuilds commits with `Entire-OPF-Applied: true` trailer, CAS-updates the local ref. Sentinel error types (use `errors.As`): `V1DivergedError`, `BootstrapTooLargeError`, `V1RefMovedError`, `OPFRuntimeFailedError`. - `manual_commit.go` - Manual-commit strategy main implementation - `manual_commit_types.go` - Type definitions: `SessionState`, `CheckpointInfo`, `CondenseResult` - `manual_commit_session.go` - Session state management (load/save/list session states) diff --git a/cmd/entire/cli/checkpoint/checkpoint.go b/cmd/entire/cli/checkpoint/checkpoint.go index 35f3aaf073..0e1b59cb1a 100644 --- a/cmd/entire/cli/checkpoint/checkpoint.go +++ b/cmd/entire/cli/checkpoint/checkpoint.go @@ -222,7 +222,9 @@ type WriteCommittedOptions struct { // Must be pre-redacted (via redact.JSONLBytes or redact.AlreadyRedacted for trusted sources). Transcript redact.RedactedBytes - // Prompts contains user prompts from the session + // Prompts contains the raw user prompts from the session. Run through + // redactedJoinedPrompts before persisting — the writer does this + // inside writeSessionToSubdirectory. Prompts []string // FilesTouched are files modified during the session @@ -353,7 +355,8 @@ type UpdateCommittedOptions struct { // Must be pre-redacted (via redact.JSONLBytes or redact.AlreadyRedacted for trusted sources). Transcript redact.RedactedBytes - // Prompts contains all user prompts (replaces existing) + // Prompts contains the raw user prompts (replaces existing). + // See WriteCommittedOptions.Prompts. Prompts []string // Agent identifies the agent type (needed for transcript chunking) diff --git a/cmd/entire/cli/checkpoint/checkpoint_test.go b/cmd/entire/cli/checkpoint/checkpoint_test.go index da249af176..329084e4bc 100644 --- a/cmd/entire/cli/checkpoint/checkpoint_test.go +++ b/cmd/entire/cli/checkpoint/checkpoint_test.go @@ -81,7 +81,7 @@ func TestCopyMetadataDir_SkipsSymlinks(t *testing.T) { store := NewGitStore(repo) entries := make(map[string]object.TreeEntry) - err = store.copyMetadataDir(metadataDir, "checkpoint/", entries) + err = store.copyMetadataDir(context.Background(), metadataDir, "checkpoint/", entries) if err != nil { t.Fatalf("copyMetadataDir failed: %v", err) } @@ -3406,7 +3406,7 @@ func TestCopyMetadataDir_RedactsSecrets(t *testing.T) { store := NewGitStore(repo) entries := make(map[string]object.TreeEntry) - if err := store.copyMetadataDir(metadataDir, "cp/", entries); err != nil { + if err := store.copyMetadataDir(context.Background(), metadataDir, "cp/", entries); err != nil { t.Fatalf("copyMetadataDir() error = %v", err) } @@ -4362,3 +4362,42 @@ func TestCheckpointSummary_HasReview(t *testing.T) { t.Errorf(`expected zero-value summary to omit "has_review" key, got %s`, string(bZero)) } } + +// TestRedactBlobBytes_JSONMetadata pins the .json branch of RedactBlobBytes: +// checkpoint metadata files (metadata.json) carry free-form fields like +// Summary.Intent and ReviewPrompt that previously bypassed redaction because +// the dispatcher only matched .jsonl. The PR 1236 fix extended the JSON-aware +// branch to .json. We assert via a low-entropy AWS-key shaped secret (catches +// the 7-layer pipeline) so the test stays deterministic without the OPF binary. +func TestRedactBlobBytes_JSONMetadata(t *testing.T) { + t.Parallel() + + meta := CommittedMetadata{ + Kind: "agent_review", + ReviewPrompt: "credential leak: key=AKIAYRWQG5EJLPZLBYNP", + Summary: &Summary{ + Intent: "leak: key=AKIAYRWQG5EJLPZLBYNP", + }, + } + b, err := json.Marshal(meta) + if err != nil { + t.Fatalf("marshal: %v", err) + } + + got := RedactBlobBytes(context.Background(), b, "metadata.json", false) + if strings.Contains(string(got), "AKIAYRWQG5EJLPZLBYNP") { + t.Errorf("expected AWS key redacted in metadata.json blob, got %s", string(got)) + } + if !strings.Contains(string(got), "REDACTED") { + t.Errorf("expected REDACTED placeholder in metadata.json blob, got %s", string(got)) + } + // JSON structure must survive — Kind is not redactable content, so it + // should round-trip through the JSON-aware redactor. + var roundTripped map[string]any + if err := json.Unmarshal(got, &roundTripped); err != nil { + t.Errorf("redacted .json blob must remain valid JSON, got parse err %v (content: %s)", err, string(got)) + } + if roundTripped["kind"] != "agent_review" { + t.Errorf(`expected "kind":"agent_review" preserved after redaction, got %v`, roundTripped["kind"]) + } +} diff --git a/cmd/entire/cli/checkpoint/committed.go b/cmd/entire/cli/checkpoint/committed.go index 69a2f767ee..414812bf78 100644 --- a/cmd/entire/cli/checkpoint/committed.go +++ b/cmd/entire/cli/checkpoint/committed.go @@ -354,7 +354,7 @@ func (s *GitStore) writeStandardCheckpointEntries(ctx context.Context, opts Writ // Copy additional metadata files from directory if specified (to session subdirectory) if opts.MetadataDir != "" { - if err := s.copyMetadataDir(opts.MetadataDir, sessionPath, entries); err != nil { + if err := s.copyMetadataDir(ctx, opts.MetadataDir, sessionPath, entries); err != nil { return fmt.Errorf("failed to copy metadata directory: %w", err) } } @@ -417,9 +417,10 @@ func (s *GitStore) writeSessionToSubdirectory(ctx context.Context, opts WriteCom filePaths.ContentHash = "/" + sessionPath + paths.ContentHashFileName } - // Write prompts + // Write prompts via the 7-layer pipeline. OPF runs only in the + // pre-push rewrite path (manual_commit_opf_rewrite.go). if len(opts.Prompts) > 0 { - promptContent := redact.String(JoinPrompts(opts.Prompts)) + promptContent := redactedJoinedPrompts(opts.Prompts) blobHash, err := CreateBlobFromContent(s.repo, []byte(promptContent)) if err != nil { return filePaths, err @@ -1400,9 +1401,9 @@ func (s *GitStore) UpdateCommitted(ctx context.Context, opts UpdateCommittedOpti } } - // Replace prompts (apply redaction as safety net) + // Replace prompts with 7-layer-redacted content. if len(opts.Prompts) > 0 { - promptContent := redact.String(JoinPrompts(opts.Prompts)) + promptContent := redactedJoinedPrompts(opts.Prompts) blobHash, err := CreateBlobFromContent(s.repo, []byte(promptContent)) if err != nil { return fmt.Errorf("failed to create prompt blob: %w", err) @@ -1682,7 +1683,7 @@ func CreateBlobFromContent(repo *git.Repository, content []byte) (plumbing.Hash, // copyMetadataDir copies all files from a directory to the checkpoint path. // Used to include additional metadata files like task checkpoints, subagent transcripts, etc. -func (s *GitStore) copyMetadataDir(metadataDir, basePath string, entries map[string]object.TreeEntry) error { +func (s *GitStore) copyMetadataDir(ctx context.Context, metadataDir, basePath string, entries map[string]object.TreeEntry) error { err := filepath.Walk(metadataDir, func(path string, info os.FileInfo, err error) error { if err != nil { return err @@ -1721,7 +1722,13 @@ func (s *GitStore) copyMetadataDir(metadataDir, basePath string, entries map[str return fmt.Errorf("path traversal detected: %s", relPath) } - // Create blob from file with secrets redaction + // Create blob from file with 7-layer secrets redaction. + // Post-commit emits 7-layer-only blobs; the pre-push rewrite + // (strategy/manual_commit_opf_rewrite.go) walks the resulting + // tree, re-redacts these blobs with OPF when enabled, and + // rewrites entire/checkpoints/v1 into 8-layer commits before + // they leave the local machine. + _ = ctx // ctx not needed by the 7-layer path; kept on caller signature for future use blobHash, mode, err := createRedactedBlobFromFile(s.repo, path, relPath) if err != nil { return fmt.Errorf("failed to create blob for %s: %w", path, err) @@ -1743,8 +1750,13 @@ func (s *GitStore) copyMetadataDir(metadataDir, basePath string, entries map[str return nil } -// createRedactedBlobFromFile reads a file, applies secrets redaction, and creates a git blob. -// JSONL files get JSONL-aware redaction; all other files get plain string redaction. +// createRedactedBlobFromFile reads a file, applies the 7-layer redaction +// pipeline, and creates a git blob. Used by committed-checkpoint writes +// at post-commit time. The OpenAI Privacy Filter is intentionally NOT +// run here — OPF lives in the pre-push rewrite path +// (strategy/manual_commit_opf_rewrite.go), which re-redacts the 7-layer +// blobs into 8-layer commits before they leave the local machine. +// JSONL files get JSONL-aware redaction; all other files get plain byte redaction. func createRedactedBlobFromFile(repo *git.Repository, filePath, treePath string) (plumbing.Hash, filemode.FileMode, error) { info, err := os.Stat(filePath) if err != nil { @@ -1772,16 +1784,7 @@ func createRedactedBlobFromFile(repo *git.Repository, filePath, treePath string) return hash, mode, nil } - if strings.HasSuffix(treePath, ".jsonl") { - redacted, jsonlErr := redact.JSONLBytes(content) - if jsonlErr != nil { - content = redact.Bytes(content) - } else { - content = redacted.Bytes() - } - } else { - content = redact.Bytes(content) - } + content = RedactBlobBytes(context.Background(), content, treePath, false) hash, err := CreateBlobFromContent(repo, content) if err != nil { @@ -1790,6 +1793,44 @@ func createRedactedBlobFromFile(repo *git.Repository, filePath, treePath string) return hash, mode, nil } +// RedactBlobBytes redacts a single blob's content given its tree path. +// JSON-shaped files (.jsonl or .json) get JSON-aware redaction (falling +// back to plain bytes on parse failure so regex/credential layers +// still apply); other files get plain byte redaction. When +// usePrivacyFilter is true the full 8-layer pipeline (including OPF) +// runs; otherwise the 7-layer pipeline. +// +// .json is handled alongside .jsonl because checkpoint metadata files +// (metadata.json, per-session metadata.json) carry free-form fields +// like Summary.Intent / Summary.Outcome / ReviewPrompt that can +// contain PII the regex layers miss. The JSON-aware redactor extracts +// string leaves and applies OPF only to those, preserving the JSON +// structure. +// +// Post-commit condensation uses false (fast path). The pre-push rewrite +// (strategy/manual_commit_opf_rewrite.go) uses true. +func RedactBlobBytes(ctx context.Context, content []byte, treePath string, usePrivacyFilter bool) []byte { + if strings.HasSuffix(treePath, ".jsonl") || strings.HasSuffix(treePath, ".json") { + var ( + redacted redact.RedactedBytes + err error + ) + if usePrivacyFilter { + redacted, err = redact.JSONLBytesWithPrivacyFilter(ctx, content) + } else { + redacted, err = redact.JSONLBytes(content) + } + if err == nil { + return redacted.Bytes() + } + // JSONL parse failed — fall through to plain bytes. + } + if usePrivacyFilter { + return redact.BytesWithPrivacyFilter(ctx, content) + } + return redact.Bytes(content) +} + // GetGitAuthorFromRepo retrieves the git user.name and user.email, // checking both the repository-local config and the global ~/.gitconfig. func GetGitAuthorFromRepo(repo *git.Repository) (name, email string) { diff --git a/cmd/entire/cli/checkpoint/committed_opf_trailer_test.go b/cmd/entire/cli/checkpoint/committed_opf_trailer_test.go new file mode 100644 index 0000000000..b731455374 --- /dev/null +++ b/cmd/entire/cli/checkpoint/committed_opf_trailer_test.go @@ -0,0 +1,70 @@ +package checkpoint + +import ( + "context" + "os" + "path/filepath" + "testing" + + "github.com/entireio/cli/cmd/entire/cli/checkpoint/id" + "github.com/entireio/cli/cmd/entire/cli/testutil" + "github.com/entireio/cli/cmd/entire/cli/trailers" + "github.com/entireio/cli/redact" + "github.com/go-git/go-git/v6" + "github.com/go-git/go-git/v6/plumbing" + "github.com/go-git/go-git/v6/plumbing/object" + "github.com/stretchr/testify/require" +) + +// TestWriteCommitted_DoesNotEmitOPFAppliedTrailer is the regression guard +// for the architectural promise: standard post-commit condensation writes +// 7-layer-only blobs and MUST NOT mark them with the Entire-OPF-Applied +// trailer. The trailer is emitted exclusively by the pre-push rewrite +// path; if a future change accidentally added it to the standard writer, +// the pre-push rewrite would skip those commits (HasOPFApplied true → +// reparent-only, no actual OPF run) and ship 7-layer content as if it +// were 8-layer. This test pins down that contract. +func TestWriteCommitted_DoesNotEmitOPFAppliedTrailer(t *testing.T) { + t.Parallel() + + tempDir := t.TempDir() + testutil.InitRepo(t, tempDir) + repo, err := git.PlainOpen(tempDir) + require.NoError(t, err) + + wt, err := repo.Worktree() + require.NoError(t, err) + readmeFile := filepath.Join(tempDir, "README.md") + require.NoError(t, os.WriteFile(readmeFile, []byte("# Test"), 0o644)) + _, err = wt.Add("README.md") + require.NoError(t, err) + _, err = wt.Commit("Initial commit", &git.CommitOptions{ + Author: &object.Signature{Name: "Test", Email: "test@test.com"}, + }) + require.NoError(t, err) + + store := NewGitStore(repo) + cpID := id.MustCheckpointID("a1b2c3d4e5f6") + + err = store.WriteCommitted(context.Background(), WriteCommittedOptions{ + CheckpointID: cpID, + SessionID: "regression-no-opf-trailer", + Strategy: "manual-commit", + Transcript: redact.AlreadyRedacted([]byte(`{"role":"user","content":"hello"}` + "\n")), + AuthorName: "Test", + AuthorEmail: "test@test.com", + }) + require.NoError(t, err) + + // Read the latest commit message on entire/checkpoints/v1 and assert + // HasOPFApplied is false. We resolve via the ref then walk back the + // single commit the writer just produced. + ref, err := repo.Reference(plumbing.NewBranchReferenceName("entire/checkpoints/v1"), true) + require.NoError(t, err, "writer should have created entire/checkpoints/v1") + commit, err := repo.CommitObject(ref.Hash()) + require.NoError(t, err) + + if trailers.HasOPFApplied(commit.Message) { + t.Errorf("standard WriteCommitted emitted Entire-OPF-Applied trailer; commit message:\n%s", commit.Message) + } +} diff --git a/cmd/entire/cli/checkpoint/prompts.go b/cmd/entire/cli/checkpoint/prompts.go index fc8d26d358..6df096ce3f 100644 --- a/cmd/entire/cli/checkpoint/prompts.go +++ b/cmd/entire/cli/checkpoint/prompts.go @@ -1,6 +1,10 @@ package checkpoint -import "strings" +import ( + "strings" + + "github.com/entireio/cli/redact" +) // PromptSeparator is the canonical separator used in prompt.txt when multiple // prompts are stored in a single file. @@ -23,3 +27,10 @@ func SplitPromptContent(content string) []string { } return prompts } + +// redactedJoinedPrompts joins prompts and runs the 7-layer redaction +// pipeline. OPF runs exclusively in the pre-push rewrite (not here), +// so the writer's hot path stays predictable. +func redactedJoinedPrompts(prompts []string) string { + return redact.String(strings.Join(prompts, PromptSeparator)) +} diff --git a/cmd/entire/cli/checkpoint/prompts_test.go b/cmd/entire/cli/checkpoint/prompts_test.go index 4b1119625c..93f4f31792 100644 --- a/cmd/entire/cli/checkpoint/prompts_test.go +++ b/cmd/entire/cli/checkpoint/prompts_test.go @@ -14,7 +14,6 @@ func TestJoinAndSplitPrompts_RoundTrip(t *testing.T) { "first line\nwith newline", "second prompt", } - joined := JoinPrompts(original) split := SplitPromptContent(joined) @@ -24,6 +23,15 @@ func TestJoinAndSplitPrompts_RoundTrip(t *testing.T) { func TestSplitPromptContent_EmptyContent(t *testing.T) { t.Parallel() - assert.Nil(t, SplitPromptContent("")) } + +// TestRedactedJoinedPrompts_AppliesSafetyNet verifies the helper joins +// prompts with the canonical separator and runs them through the 7-layer +// pipeline. OPF runs only in the pre-push rewrite path, never here. +func TestRedactedJoinedPrompts_AppliesSafetyNet(t *testing.T) { + t.Parallel() + got := redactedJoinedPrompts([]string{"hello", "world"}) + assert.NotEmpty(t, got) + assert.Contains(t, got, PromptSeparator) +} diff --git a/cmd/entire/cli/checkpoint/v2_committed.go b/cmd/entire/cli/checkpoint/v2_committed.go index 35edf2273a..b68f13d373 100644 --- a/cmd/entire/cli/checkpoint/v2_committed.go +++ b/cmd/entire/cli/checkpoint/v2_committed.go @@ -169,7 +169,7 @@ func (s *V2GitStore) buildFreshMainBatchGroupTree(ctx context.Context, cpID id.C for sessionIndex, opts := range groupOpts { sessionPath := fmt.Sprintf("%s%d/", basePath, sessionIndex) - filePaths, err := s.writeMainSessionToSubdirectory(opts, sessionPath, entries) + filePaths, err := s.writeMainSessionToSubdirectory(ctx, opts, sessionPath, entries) if err != nil { return plumbing.ZeroHash, err } @@ -283,7 +283,7 @@ func (s *V2GitStore) buildMainBatchGroupTree(ctx context.Context, rootTreeHash p } sessionPath := fmt.Sprintf("%s%d/", basePath, sessionIndex) - filePaths, err := s.writeMainSessionToSubdirectory(opts, sessionPath, entries) + filePaths, err := s.writeMainSessionToSubdirectory(ctx, opts, sessionPath, entries) if err != nil { return plumbing.ZeroHash, err } @@ -609,7 +609,7 @@ func (s *V2GitStore) updateCommittedMain(ctx context.Context, opts UpdateCommitt sessionPath := fmt.Sprintf("%s%d/", basePath, sessionIndex) if len(opts.Prompts) > 0 { - promptContent := redact.String(JoinPrompts(opts.Prompts)) + promptContent := redactedJoinedPrompts(opts.Prompts) blobHash, err := CreateBlobFromContent(s.repo, []byte(promptContent)) if err != nil { return 0, fmt.Errorf("failed to create prompt blob: %w", err) @@ -865,7 +865,7 @@ func (s *V2GitStore) writeMainCheckpointEntries(ctx context.Context, opts WriteC // Write session files (metadata and prompts — no transcript or content hash) sessionPath := fmt.Sprintf("%s%d/", basePath, sessionIndex) - sessionFilePaths, err := s.writeMainSessionToSubdirectory(opts, sessionPath, entries) + sessionFilePaths, err := s.writeMainSessionToSubdirectory(ctx, opts, sessionPath, entries) if err != nil { return 0, err } @@ -891,7 +891,7 @@ func (s *V2GitStore) writeMainCheckpointEntries(ctx context.Context, opts WriteC // and compact transcript to a session subdirectory (0/, 1/, 2/, … indexed by // session order within the checkpoint). The raw transcript (raw_transcript) and its // content hash (raw_transcript_hash.txt) go to /full/current, not here. -func (s *V2GitStore) writeMainSessionToSubdirectory(opts WriteCommittedOptions, sessionPath string, entries map[string]object.TreeEntry) (SessionFilePaths, error) { +func (s *V2GitStore) writeMainSessionToSubdirectory(_ context.Context, opts WriteCommittedOptions, sessionPath string, entries map[string]object.TreeEntry) (SessionFilePaths, error) { filePaths := SessionFilePaths{} // Clear existing entries at this session path @@ -903,7 +903,7 @@ func (s *V2GitStore) writeMainSessionToSubdirectory(opts WriteCommittedOptions, // Write prompts if len(opts.Prompts) > 0 { - promptContent := redact.String(JoinPrompts(opts.Prompts)) + promptContent := redactedJoinedPrompts(opts.Prompts) blobHash, err := CreateBlobFromContent(s.repo, []byte(promptContent)) if err != nil { return filePaths, err diff --git a/cmd/entire/cli/hooks_git_cmd.go b/cmd/entire/cli/hooks_git_cmd.go index 27ef45f281..f8729b94c3 100644 --- a/cmd/entire/cli/hooks_git_cmd.go +++ b/cmd/entire/cli/hooks_git_cmd.go @@ -2,6 +2,7 @@ package cli import ( "context" + "fmt" "log/slog" "time" @@ -231,6 +232,13 @@ func newHooksGitPrePushCmd() *cobra.Command { Use: "pre-push ", Short: "Handle pre-push git hook", Args: cobra.ExactArgs(1), + // SilenceUsage/Errors so non-zero exits from privacy-critical + // failures (OPF rewrite errors) print only the error message, + // not cobra's usage banner. The error message itself already + // includes user guidance (see ErrV1Diverged / ErrBootstrapTooLarge / + // ErrV1RefMoved in strategy/manual_commit_opf_rewrite.go). + SilenceUsage: true, + SilenceErrors: false, RunE: func(cmd *cobra.Command, args []string) error { if gitHooksDisabled { return nil @@ -245,7 +253,20 @@ func newHooksGitPrePushCmd() *cobra.Command { hookErr := g.strategy.PrePush(g.ctx, remote) g.logCompleted(hookErr) - return nil + // Propagate the error so the hook script exits non-zero and + // git push aborts the entire batch. PrePush itself only + // returns errors for privacy-critical failures (OPF rewrite — + // e.g., V1DivergedError, BootstrapTooLargeError, + // V1RefMovedError, OPFRuntimeFailedError); transient + // checkpoint-push failures are logged and swallowed before + // reaching this point. See strategy/manual_commit_push.go + // for the contract. We wrap with a short "pre-push:" prefix + // so the user sees the source of the abort without losing + // the underlying type (errors.As still finds the sentinels). + if hookErr == nil { + return nil + } + return fmt.Errorf("pre-push: %w", hookErr) }, } } diff --git a/cmd/entire/cli/review/manifest_test.go b/cmd/entire/cli/review/manifest_test.go index b66697eb55..31a15880b2 100644 --- a/cmd/entire/cli/review/manifest_test.go +++ b/cmd/entire/cli/review/manifest_test.go @@ -23,7 +23,13 @@ const manifestTokenTestAgentType agenttypes.AgentType = "Review Token Test" func TestHydrateReviewSummaryTokensFromStates_PopulatesTokensFromSessionState(t *testing.T) { t.Parallel() - started := time.Date(2026, 5, 8, 10, 0, 0, 0, time.UTC) + // Time-relative so this test doesn't go stale: session.StateStore.Load + // auto-deletes sessions whose StartedAt is older than 7 days + // (StaleSessionThreshold), and a hardcoded fixed date silently starts + // failing once the calendar clock crosses that threshold. Use "an hour + // ago" so we exercise the 5-second jitter check inside + // matchReviewSessionState while staying well inside the staleness window. + started := time.Now().UTC().Add(-time.Hour) summary := reviewtypes.RunSummary{ StartedAt: started, AgentRuns: []reviewtypes.AgentRun{ @@ -67,7 +73,13 @@ func TestHydrateReviewSummaryTokensFromStates_FallsBackToTranscript(t *testing.T return manifestTokenTestAgent{}, nil } - started := time.Date(2026, 5, 8, 10, 0, 0, 0, time.UTC) + // Time-relative so this test doesn't go stale: session.StateStore.Load + // auto-deletes sessions whose StartedAt is older than 7 days + // (StaleSessionThreshold), and a hardcoded fixed date silently starts + // failing once the calendar clock crosses that threshold. Use "an hour + // ago" so we exercise the 5-second jitter check inside + // matchReviewSessionState while staying well inside the staleness window. + started := time.Now().UTC().Add(-time.Hour) tmp := t.TempDir() transcriptPath := filepath.Join(tmp, "review.jsonl") transcript := "review transcript\n" @@ -112,7 +124,13 @@ func TestReviewSummaryTokenEnricher_LoadsCurrentSessionState(t *testing.T) { if err != nil { t.Fatalf("NewStateStore: %v", err) } - started := time.Date(2026, 5, 8, 10, 0, 0, 0, time.UTC) + // Time-relative so this test doesn't go stale: session.StateStore.Load + // auto-deletes sessions whose StartedAt is older than 7 days + // (StaleSessionThreshold), and a hardcoded fixed date silently starts + // failing once the calendar clock crosses that threshold. Use "an hour + // ago" so we exercise the 5-second jitter check inside + // matchReviewSessionState while staying well inside the staleness window. + started := time.Now().UTC().Add(-time.Hour) if err := store.Save(ctx, &session.State{ SessionID: "codex-session-token", Kind: session.KindAgentReview, diff --git a/cmd/entire/cli/settings/settings.go b/cmd/entire/cli/settings/settings.go index cd1513df11..8a4b28631a 100644 --- a/cmd/entire/cli/settings/settings.go +++ b/cmd/entire/cli/settings/settings.go @@ -20,6 +20,7 @@ import ( "github.com/entireio/cli/cmd/entire/cli/logging" "github.com/entireio/cli/cmd/entire/cli/paths" "github.com/entireio/cli/cmd/entire/cli/session" + "github.com/entireio/cli/redact" ) const ( @@ -190,6 +191,10 @@ type RedactionSettings struct { // "[REDACTED_