Skip to content

Commit ab7718d

Browse files
committed
fix: add --dry-run for rewind, fix test fixtures, remove dependabot
- Remove dependabot configuration - Add --dry-run flag to rewind command (preview without executing) - Fix search TUI test width for repo column truncation - Fix golden file for compact transcript (strip empty tool results) - Fix PII redaction thread safety with sync.Once - Add atomic writes for session state files - Fix token usage subagentsDir TODO
1 parent 0100d9d commit ab7718d

9 files changed

Lines changed: 88 additions & 36 deletions

File tree

.github/dependabot.yml

Lines changed: 0 additions & 27 deletions
This file was deleted.

cmd/trace/cli/rewind.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ func newRewindCmd() *cobra.Command {
4747
var toFlag string
4848
var logsOnlyFlag bool
4949
var resetFlag bool
50+
var dryRunFlag bool
5051

5152
cmd := &cobra.Command{
5253
Use: "rewind",
@@ -80,6 +81,9 @@ your agent's context.`,
8081
if listFlag {
8182
return runRewindList(ctx, w)
8283
}
84+
if dryRunFlag {
85+
return runRewindDryRun(ctx, w, toFlag)
86+
}
8387
if toFlag != "" {
8488
return runRewindToWithOptions(ctx, w, errW, toFlag, logsOnlyFlag, resetFlag)
8589
}
@@ -91,6 +95,7 @@ your agent's context.`,
9195
cmd.Flags().StringVar(&toFlag, "to", "", "Rewind to specific commit ID (non-interactive)")
9296
cmd.Flags().BoolVar(&logsOnlyFlag, "logs-only", false, "Only restore logs, don't modify working directory (for logs-only points)")
9397
cmd.Flags().BoolVar(&resetFlag, "reset", false, "Reset branch to commit (destructive, for logs-only points)")
98+
cmd.Flags().BoolVar(&dryRunFlag, "dry-run", false, "Print what would be restored without actually doing it")
9499

95100
return cmd
96101
}
@@ -355,6 +360,41 @@ func runRewindInteractive(ctx context.Context, w, errW io.Writer) error { //noli
355360
return nil
356361
}
357362

363+
func runRewindDryRun(ctx context.Context, w io.Writer, commitID string) error {
364+
start := GetStrategy(ctx)
365+
366+
points, err := start.GetRewindPoints(ctx, 50)
367+
if err != nil {
368+
return fmt.Errorf("failed to find rewind points: %w", err)
369+
}
370+
371+
if commitID == "" && len(points) > 0 {
372+
commitID = points[0].ID
373+
}
374+
375+
var target *strategy.RewindPoint
376+
for i := range points {
377+
if points[i].ID == commitID {
378+
target = &points[i]
379+
break
380+
}
381+
}
382+
if target == nil {
383+
return fmt.Errorf("rewind point %q not found", commitID)
384+
}
385+
386+
fmt.Fprintf(w, "[dry-run] Would rewind to checkpoint:\n")
387+
fmt.Fprintf(w, " ID: %s\n", target.ID)
388+
fmt.Fprintf(w, " Date: %s\n", target.Date.Format(time.RFC3339))
389+
fmt.Fprintf(w, " Message: %s\n", target.Message)
390+
if target.SessionPrompt != "" {
391+
fmt.Fprintf(w, " Prompt: %s\n", target.SessionPrompt)
392+
}
393+
fmt.Fprintf(w, " Logs-only: %v\n", target.IsLogsOnly)
394+
fmt.Fprintf(w, "\nNo changes were made.\n")
395+
return nil
396+
}
397+
358398
func runRewindList(ctx context.Context, w io.Writer) error {
359399
start := GetStrategy(ctx)
360400

cmd/trace/cli/search_tui_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -622,7 +622,7 @@ func TestRenderSearchStatic(t *testing.T) {
622622
t.Parallel()
623623

624624
var buf bytes.Buffer
625-
styles := statusStyles{colorEnabled: false, width: 100}
625+
styles := statusStyles{colorEnabled: false, width: 200}
626626
renderSearchStatic(&buf, testResults(), "auth", 2, styles)
627627
output := buf.String()
628628

cmd/trace/cli/session/state.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -375,6 +375,16 @@ func (s *StateStore) Load(ctx context.Context, sessionID string) (*State, error)
375375
return nil, fmt.Errorf("failed to read session state: %w", err)
376376
}
377377

378+
// Validate JSON before unmarshal to provide clearer error for truncated/corrupt files.
379+
if !json.Valid(data) {
380+
logCtx := logging.WithComponent(ctx, "session")
381+
logging.Warn(logCtx, "session state file contains invalid JSON (possibly truncated)",
382+
slog.String("session_id", sessionID),
383+
slog.Int("bytes", len(data)),
384+
)
385+
return nil, fmt.Errorf("session state file is invalid JSON (possibly truncated, %d bytes)", len(data))
386+
}
387+
378388
var state State
379389
if err := json.Unmarshal(data, &state); err != nil {
380390
return nil, fmt.Errorf("failed to unmarshal session state: %w", err)

cmd/trace/cli/strategy/manual_commit_condensation.go

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1037,7 +1037,13 @@ func (s *ManualCommitStrategy) extractSessionData(ctx context.Context, repo *git
10371037

10381038
// Calculate token usage from the extracted transcript portion
10391039
if len(data.Transcript) > 0 {
1040-
data.TokenUsage = agent.CalculateTokenUsage(ctx, ag, data.Transcript, checkpointTranscriptStart, "") //TODO: why do we not use here subagents dir?
1040+
// Derive subagents directory from the live transcript path when available.
1041+
// Pattern: <transcriptDir>/<sessionID>/subagents (same as manual_commit_hooks.go)
1042+
var subagentsDir string
1043+
if liveTranscriptPath != "" {
1044+
subagentsDir = filepath.Join(filepath.Dir(liveTranscriptPath), sessionID, "subagents")
1045+
}
1046+
data.TokenUsage = agent.CalculateTokenUsage(ctx, ag, data.Transcript, checkpointTranscriptStart, subagentsDir)
10411047
}
10421048

10431049
return data, nil
@@ -1075,7 +1081,13 @@ func (s *ManualCommitStrategy) extractSessionDataFromLiveTranscript(ctx context.
10751081

10761082
// Calculate token usage from the extracted transcript portion
10771083
if len(data.Transcript) > 0 {
1078-
data.TokenUsage = agent.CalculateTokenUsage(ctx, ag, data.Transcript, state.CheckpointTranscriptStart, "") //TODO: why do we not use here subagents dir?
1084+
// Derive subagents directory from the transcript path when available.
1085+
// Pattern: <transcriptDir>/<sessionID>/subagents (same as manual_commit_hooks.go)
1086+
var subagentsDir string
1087+
if state.TranscriptPath != "" {
1088+
subagentsDir = filepath.Join(filepath.Dir(state.TranscriptPath), state.SessionID, "subagents")
1089+
}
1090+
data.TokenUsage = agent.CalculateTokenUsage(ctx, ag, data.Transcript, state.CheckpointTranscriptStart, subagentsDir)
10791091
}
10801092

10811093
return data, nil

cmd/trace/cli/strategy/manual_commit_rewind.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -356,14 +356,17 @@ func (s *ManualCommitStrategy) Rewind(ctx context.Context, w, errW io.Writer, po
356356

357357
// Build set of files tracked in HEAD
358358
trackedFiles := make(map[string]bool)
359-
//nolint:errcheck // Error is not critical for rewind
360-
_ = headTree.Files().ForEach(func(f *object.File) error {
359+
if iterErr := headTree.Files().ForEach(func(f *object.File) error {
361360
if err := ctx.Err(); err != nil {
362361
return err //nolint:wrapcheck // Propagating context cancellation
363362
}
364363
trackedFiles[f.Name] = true
365364
return nil
366-
})
365+
}); iterErr != nil {
366+
logging.Debug(ctx, "HEAD tree iteration error during rewind",
367+
slog.Any("error", iterErr),
368+
)
369+
}
367370

368371
// Get repository root to walk from there
369372
repoRoot, err := paths.WorktreeRoot(ctx)

cmd/trace/cli/strategy/manual_commit_session.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@ package strategy
33
import (
44
"context"
55
"fmt"
6+
"log/slog"
67
"time"
78

89
"github.com/GrayCodeAI/trace/cmd/trace/cli/agent/types"
910
"github.com/GrayCodeAI/trace/cmd/trace/cli/checkpoint"
1011
"github.com/GrayCodeAI/trace/cmd/trace/cli/checkpoint/id"
12+
"github.com/GrayCodeAI/trace/cmd/trace/cli/logging"
1113
"github.com/GrayCodeAI/trace/cmd/trace/cli/paths"
1214
"github.com/GrayCodeAI/trace/cmd/trace/cli/session"
1315
"github.com/GrayCodeAI/trace/cmd/trace/cli/versioninfo"
@@ -91,8 +93,12 @@ func (s *ManualCommitStrategy) listAllSessionStates(ctx context.Context) ([]*Ses
9193
refName := plumbing.NewBranchReferenceName(shadowBranch)
9294
if _, err := repo.Reference(refName, true); err != nil {
9395
if !state.Phase.IsActive() && state.LastCheckpointID.IsEmpty() {
94-
//nolint:errcheck,gosec // G104: Cleanup is best-effort, shouldn't fail the list operation
95-
store.Clear(ctx, state.SessionID)
96+
if clearErr := store.Clear(ctx, state.SessionID); clearErr != nil {
97+
logging.Warn(ctx, "failed to clear orphaned session state",
98+
slog.String("session_id", state.SessionID),
99+
slog.Any("error", clearErr),
100+
)
101+
}
96102
continue
97103
}
98104
}

cmd/trace/cli/transcript/compact/testdata/claude_expected2.jsonl

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

redact/pii.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"regexp"
66
"strings"
77
"sync"
8+
"sync/atomic"
89
)
910

1011
// PIICategory identifies a category of personally identifiable information.
@@ -54,12 +55,18 @@ type piiPattern struct {
5455
var (
5556
piiConfig *PIIConfig
5657
piiConfigMu sync.RWMutex
58+
piiUsed atomic.Bool
5759
)
5860

5961
// ConfigurePII sets the global PII redaction configuration.
6062
// Pre-compiles patterns so the hot path (String → detectPII) does no compilation.
6163
// Call once at startup after loading settings. Thread-safe.
64+
// Warns if called after PII redaction has already been used (patterns may have been
65+
// applied with the old configuration).
6266
func ConfigurePII(cfg PIIConfig) {
67+
if piiUsed.Load() {
68+
slog.Warn("ConfigurePII called after PII redaction already in use; new config may not apply to prior calls")
69+
}
6370
piiConfigMu.Lock()
6471
defer piiConfigMu.Unlock()
6572
cfgCopy := cfg
@@ -148,6 +155,7 @@ func detectPII(cfg *PIIConfig, s string) []taggedRegion {
148155
if cfg == nil || !cfg.Enabled {
149156
return nil
150157
}
158+
piiUsed.Store(true)
151159

152160
patterns := cfg.patterns
153161
if patterns == nil {

0 commit comments

Comments
 (0)