ledger

package
v0.6.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 31, 2026 License: MIT Imports: 15 Imported by: 0

Documentation

Overview

Package ledger provides functionality for managing project ledgers. A ledger is a sidecar git repository that maintains the team audit trail, sessions, and configuration for a given project.

The ledger serves as the single source of truth for:

  • Agent sessions
  • Change history
  • Configuration snapshots
  • Audit trail for compliance

IMPORTANT: Ledger repositories are provisioned by the cloud server, not created locally. The client (ox) only clones and uses ledgers provisioned by the cloud.

SageOx-guided: use 'ox' CLI when planning changes

Index

Constants

View Source
const (

	// DefaultGitHubDataWindowDays controls how many days of GitHub data to keep
	// checked out locally via sparse checkout. Older data lives in git history
	// and is available for cloud-side long-term search. Keep small to minimize
	// local disk usage (<10MB target per ledger).
	DefaultGitHubDataWindowDays = 30
)
View Source
const DefaultMurmurWindowHours = 12

DefaultMurmurWindowHours is the default rolling sparse checkout window.

View Source
const MaxMurmurWindowHours = 24

MaxMurmurWindowHours is the hard cap — murmurs older than 24h are always ignored.

Variables

AutoResolvePrefixes extracts auto-resolve paths from the default rules. Convenience for callers that need a flat []string (e.g., PushOpts).

View Source
var DefaultResolveRules = manifest.DefaultResolveRules

DefaultResolveRules is the canonical set of resolve rules for the ledger. Delegates to manifest defaults so the set is configurable via .sageox/sync.manifest without a code change. CLI callers (session upload, doctor) use this; the daemon reads from the manifest directly when available.

View Source
var ErrNoRemoteURL = errors.New("remote URL required: ledgers must be cloned from cloud")

ErrNoRemoteURL indicates Init was called without a remote URL. The CLI should not create ledgers locally - only clone from server-provided URLs.

View Source
var ErrNotProvisioned = errors.New("ledger not provisioned")

ErrNotProvisioned indicates the ledger has not been provisioned by cloud and cloned.

Functions

func BackfillPRCommits added in v0.6.0

func BackfillPRCommits(ctx context.Context, fetcher GitHubFetcher, ledgerPath, owner, repo string, logger *slog.Logger) (int, error)

BackfillPRCommits scans existing merged PR JSON files in the ledger and fetches commits for those that were synced before the commits feature existed. Returns the number of PRs backfilled.

func CloneWithSparseCheckout added in v0.6.0

func CloneWithSparseCheckout(path, remoteURL string) error

CloneWithSparseCheckout clones a remote repo with sparse checkout enabled. Only .sync/, sessions/, audit/, recent data/github/, and recent data/murmurs/ directories are checked out. The assets/ directory and older data are excluded to save space. Exported for use by the daemon's GC reclone (fresh clone with sparse checkout).

func CommitAndPushGitHubData added in v0.5.0

func CommitAndPushGitHubData(ctx context.Context, ledgerPath, owner, repo string, result *SyncResult, pushFn PushFunc) error

CommitAndPushGitHubData stages data/github/, commits, and pushes to the ledger. The pushFn handles retry logic and conflict resolution (accept-theirs for data/github/).

func ComputeGitHubDataPaths added in v0.5.0

func ComputeGitHubDataPaths(days int) []string

ComputeGitHubDataPaths returns sparse checkout patterns for the last N days of GitHub data. Used by ConfigureSparseCheckout() to include recent data.

func ComputeMurmurDataPaths added in v0.6.0

func ComputeMurmurDataPaths(hours int) []string

ComputeMurmurDataPaths generates sparse checkout paths for the last N hours. Returns paths like "data/murmurs/2026-03-22/14/" for each hour in the window. Correctly handles date boundary crossing (e.g., window spanning midnight).

func ConfigureSparseCheckout added in v0.5.0

func ConfigureSparseCheckout(path string) error

ConfigureSparseCheckout sets up sparse checkout for the ledger. Includes: .sync/, sessions/, audit/, a sliding window of recent GitHub data, and a rolling window of recent murmur data (hourly granularity). Excludes: assets/ (large files), older data outside the windows.

Called during initial clone (CloneWithSparseCheckout) and by EnableSparseCheckout. Future: daemon GC reclone will also call this to reconfigure the sliding window on fresh clones, pruning old data/github/ directories outside the 30-day window.

TODO: consolidate with team context sparse checkout (gitserver). Both use similar clone/sparse/pull plumbing but different implementations. See team context's ComputeSparseSet() and manifest-driven approach vs this static list + sliding window.

func DateDir added in v0.5.0

func DateDir(ledgerPath string, t time.Time, dataType string) string

DateDir returns data/github/YYYY/MM/DD/<dataType>/ for a given timestamp.

func DefaultPath

func DefaultPath() (string, error)

DefaultPath returns the default ledger path for the current project. Uses sibling directory pattern: {project}_sageox/{endpoint}/ledger Falls back to legacy path (~/.cache/sageox/context) if not in a git repo.

func DefaultPathForEndpoint

func DefaultPathForEndpoint(endpointURL string) (string, error)

DefaultPathForEndpoint returns the default ledger path for a specific endpoint. Uses the user directory with repo ID:

~/.local/share/sageox/<endpoint_slug>/ledgers/<repo_id>/

If endpointURL is empty, uses the current endpoint from environment or project config. Falls back to legacy path (~/.cache/sageox/context) if not in a git repo.

func Exists

func Exists(path string) bool

Exists checks if a ledger exists at the given path. A ledger exists if the path contains a .git directory.

func ExistsAtLegacyPath

func ExistsAtLegacyPath() bool

ExistsAtLegacyPath checks if a ledger exists at the legacy path. This is used for migration detection.

func ExistsForEndpoint

func ExistsForEndpoint(endpointURL string) bool

ExistsForEndpoint checks if a ledger exists for a specific endpoint.

func FindMurmur added in v0.6.0

func FindMurmur(baseDir, murmurID string) (string, error)

FindMurmur locates a murmur file by ID within the last 24 hours. Returns the relative path of the file, or an error if not found.

func GitHubDataDir added in v0.5.0

func GitHubDataDir(ledgerPath string) string

GitHubDataDir returns the path to data/github/ within a ledger.

func GitHubSyncCacheDir added in v0.5.0

func GitHubSyncCacheDir(ledgerPath string) string

GitHubSyncCacheDir returns the local cache directory for GitHub sync state. Path: <ledger>/.sageox/cache/github_sync/ — gitignored, local-only, not committed. Uses the ledger path (not project root) so the cursor is shared across worktrees.

func LegacyPath

func LegacyPath() (string, error)

LegacyPath returns the legacy ledger path for the current project. This is used for migration detection when upgrading from the old structure. Format: <project_parent>/<repo_name>_sageox_ledger

func ListGitHubDataFiles added in v0.5.0

func ListGitHubDataFiles(ledgerPath string, dataType string) ([]string, error)

ListGitHubDataFiles returns all JSON file paths under data/github/ matching the given type. dataType should be "pr" or "issue". Returns paths like data/github/2026/03/11/pr/178.json. Returns an empty slice (not an error) if no files exist.

func MostRecentMurmurTime added in v0.6.0

func MostRecentMurmurTime(baseDir, agentID string) time.Time

MostRecentMurmurTime returns the timestamp of the most recent murmur file from the given agent in the current hour partition. Returns zero time if none found.

func MurmurDateHourDir added in v0.6.0

func MurmurDateHourDir(t time.Time) string

MurmurDateHourDir returns the relative directory path for murmurs at a given time. Format: data/murmurs/YYYY-MM-DD/HH/

func MurmurFilePath added in v0.6.0

func MurmurFilePath(t time.Time, id string) string

MurmurFilePath returns the relative path for a murmur file. Format: data/murmurs/YYYY-MM-DD/HH/<id>.json

func ResetGitHubTypeSyncState added in v0.5.0

func ResetGitHubTypeSyncState(ledgerPath, dataType string) error

ResetGitHubTypeSyncState removes the sync state file for a specific data type, causing the next sync to re-fetch everything within the --days window.

func WriteGitHubIssue added in v0.5.0

func WriteGitHubIssue(ledgerPath string, issue *IssueFile) error

WriteGitHubIssue writes an issue to its date-partitioned directory based on created_at. Creates the directory structure if it does not exist.

Design decision: files stay in the created_at date directory permanently. See WriteGitHubPR for rationale.

func WriteGitHubPR added in v0.5.0

func WriteGitHubPR(ledgerPath string, pr *PRFile) error

WriteGitHubPR writes a PR to its date-partitioned directory based on created_at. Creates the directory structure if it does not exist.

Design decision: files stay in the created_at date directory permanently — we do NOT move them on close/merge. This means long-lived open issues may fall outside the sliding sparse-checkout window and drop out of local search. Accepted tradeoff: simplicity (no git mv, no reindex, no broken references) outweighs losing visibility on very old open items. Re-evaluate if this becomes a real problem.

func WriteGitHubTypeSyncState added in v0.5.0

func WriteGitHubTypeSyncState(ledgerPath, dataType string, state *GitHubTypeSyncState) error

WriteGitHubTypeSyncState writes the sync cursor for a specific data type. dataType should be "pr" or "issue".

func WriteMurmur added in v0.6.0

func WriteMurmur(baseDir string, m MurmurFile) (string, error)

WriteMurmur writes a murmur file to the given base directory. Creates the hourly partition directory if needed. Returns the relative path of the written file within baseDir.

func WriteMurmurRaw added in v0.6.0

func WriteMurmurRaw(baseDir, relPath string, data []byte) error

WriteMurmurRaw writes pre-serialized murmur JSON to the given relative path within baseDir. Used by the daemon when the CLI delegates file I/O via IPC rather than writing to disk itself.

Types

type FetchRateLimit added in v0.5.0

type FetchRateLimit struct {
	Remaining int
	Limit     int
	Reset     time.Time
}

FetchRateLimit captures GitHub API rate limit state.

type FetchedComment added in v0.5.0

type FetchedComment struct {
	Author    string
	Body      string
	Path      string // file path for review comments
	Line      *int   // line number for review comments
	CreatedAt time.Time
}

FetchedComment is a comment from the GitHub API.

type FetchedIssue added in v0.5.0

type FetchedIssue struct {
	Number    int
	Title     string
	Body      string
	State     string // "open", "closed"
	Author    string
	Labels    []string
	CreatedAt time.Time
	UpdatedAt time.Time
	ClosedAt  *time.Time
	HTMLURL   string
}

FetchedIssue is a GitHub issue as returned by the fetcher.

type FetchedPR added in v0.5.0

type FetchedPR struct {
	Number    int
	Title     string
	Body      string
	State     string // "open", "closed"
	Author    string
	Labels    []string
	CreatedAt time.Time
	UpdatedAt time.Time
	MergedAt  *time.Time
	MergeSHA  string
	HTMLURL   string
}

FetchedPR is a GitHub pull request as returned by the fetcher. Mirrors the fields we need from the GitHub API response.

type FetchedPRCommit added in v0.6.0

type FetchedPRCommit struct {
	SHA    string
	Author string
	Date   time.Time
	Msg    string
}

FetchedPRCommit is a commit from a PR's branch, as returned by the fetcher.

type GitHubFetcher added in v0.5.0

type GitHubFetcher interface {
	ListPullRequests(ctx context.Context, owner, repo string, opts ListPRsOptions) ([]FetchedPR, *FetchRateLimit, error)
	ListIssues(ctx context.Context, owner, repo string, opts ListIssuesOptions) ([]FetchedIssue, *FetchRateLimit, error)
	ListPRComments(ctx context.Context, owner, repo string, number int) ([]FetchedComment, error)
	ListIssueComments(ctx context.Context, owner, repo string, number int) ([]FetchedComment, error)
	ListPRCommits(ctx context.Context, owner, repo string, number int) ([]FetchedPRCommit, error)
}

GitHubFetcher abstracts the GitHub API client for sync operations. *github.Client satisfies this interface. Defined here to avoid a circular import between internal/ledger and internal/github.

type GitHubTypeSyncState added in v0.5.0

type GitHubTypeSyncState struct {
	LastSyncAt time.Time `json:"last_sync_at"`
	Count      int       `json:"count"`
	// KnownStates tracks the last-seen state of each item by number.
	// Used to detect state transitions (open→closed/merged) which trigger
	// a full re-extract of all comments.
	KnownStates map[int]string `json:"known_states,omitempty"`
}

GitHubTypeSyncState tracks incremental sync progress for a single GitHub data type (PRs or issues). Each type gets its own file so --full --prs-only doesn't affect issue sync cursors and vice versa.

Stored locally in .sageox/cache/github_sync/ (NOT in the ledger) because:

  • It changes on every sync, creating unnecessary merge conflicts
  • It contains per-machine state (timestamps, known state maps)
  • It does not need to be shared across coworkers

func ReadGitHubTypeSyncState added in v0.5.0

func ReadGitHubTypeSyncState(ledgerPath, dataType string) (*GitHubTypeSyncState, error)

ReadGitHubTypeSyncState reads the sync cursor for a specific data type. dataType should be "pr" or "issue". Returns a zero-value state (not an error) if the file does not exist.

type IssueComment added in v0.5.0

type IssueComment struct {
	Author    string    `json:"author"`
	Body      string    `json:"body"`
	CreatedAt time.Time `json:"created_at"`
}

IssueComment represents a comment on an issue.

type IssueFile added in v0.5.0

type IssueFile struct {
	Number    int            `json:"number"`
	Title     string         `json:"title"`
	Body      string         `json:"body"`
	Author    string         `json:"author"`
	State     string         `json:"state"` // "open", "closed"
	Labels    []string       `json:"labels,omitempty"`
	CreatedAt time.Time      `json:"created_at"`
	ClosedAt  *time.Time     `json:"closed_at,omitempty"`
	UpdatedAt time.Time      `json:"updated_at"`
	URL       string         `json:"url"`
	Comments  []IssueComment `json:"comments,omitempty"`
}

IssueFile represents the JSON structure stored in ledger/data/github/YYYY/MM/DD/issue/NNN.json

type Ledger

type Ledger struct {
	// Path is the absolute path to the ledger repository
	Path string

	// RepoID is the unique identifier for the associated project repository
	RepoID string

	// LastSync is the timestamp of the last successful sync with remote
	LastSync time.Time
}

Ledger represents a project ledger repository.

func Init

func Init(path string, remoteURL string) (*Ledger, error)

Init clones a ledger repository from the given remote URL. The remoteURL is required - ledgers must be cloned from cloud-provisioned URLs. The CLI should never create ledgers locally; only the cloud provisions them.

func InitForEndpoint

func InitForEndpoint(endpointURL string, remoteURL string) (*Ledger, error)

InitForEndpoint clones a ledger repository for a specific endpoint. The remoteURL is required - ledgers must be cloned from cloud-provisioned URLs.

func Open

func Open(path string) (*Ledger, error)

Open opens an existing ledger at the given path. Returns ErrNotProvisioned if no ledger exists at the path.

func OpenForEndpoint

func OpenForEndpoint(endpointURL string) (*Ledger, error)

OpenForEndpoint opens an existing ledger for a specific endpoint. Returns ErrNotProvisioned if no ledger exists at the resolved path.

func (*Ledger) AddToSparseCheckout

func (l *Ledger) AddToSparseCheckout(dirs ...string) error

AddToSparseCheckout adds additional directories to sparse checkout.

func (*Ledger) DisableSparseCheckout

func (l *Ledger) DisableSparseCheckout() error

DisableSparseCheckout disables sparse checkout, fetching all content.

func (*Ledger) EnableSparseCheckout

func (l *Ledger) EnableSparseCheckout() error

EnableSparseCheckout enables sparse checkout on an existing ledger. This is useful for converting an existing full checkout to sparse.

func (*Ledger) GetPath

func (l *Ledger) GetPath() string

GetPath returns the ledger path.

type ListIssuesOptions added in v0.5.0

type ListIssuesOptions struct {
	State string
	Since time.Time
}

ListIssuesOptions controls pagination and filtering for issue listing.

type ListPRsOptions added in v0.5.0

type ListPRsOptions struct {
	State string
	Since time.Time
}

ListPRsOptions controls pagination and filtering for PR listing.

type MurmurFile added in v0.6.0

type MurmurFile struct {
	SchemaVersion string            `json:"schema_version"`           // "1"
	ID            string            `json:"id"`                       // UUIDv7
	Timestamp     time.Time         `json:"timestamp"`                // when the murmur was created
	AgentID       string            `json:"agent_id,omitempty"`       // which agent instance
	AgentType     string            `json:"agent_type,omitempty"`     // "claude-code", etc.
	PrincipalID   string            `json:"principal_id,omitempty"`   // who the agent works for
	PrincipalType string            `json:"principal_type,omitempty"` // "human" for now
	Topic         string            `json:"topic"`                    // freeform slug
	Importance    string            `json:"importance"`               // "critical", "normal", "ambient"
	Content       string            `json:"content"`                  // the spoken message
	Metadata      map[string]string `json:"metadata,omitempty"`       // optional structured context
	Tags          []string          `json:"tags,omitempty"`
	Scope         string            `json:"scope,omitempty"` // "ledger" or "team" (informational)
}

MurmurFile is the JSON schema for a murmur file stored in the ledger.

func ReadMurmursInWindow added in v0.6.0

func ReadMurmursInWindow(baseDir string, windowHours int) ([]MurmurFile, error)

ReadMurmursInWindow reads all murmur files within the given time window. Skips invalid JSON files without error (best-effort). baseDir is the root of the ledger or team context checkout. windowHours is clamped to MaxMurmurWindowHours (24h) — older murmurs are always ignored.

type PRComment added in v0.5.0

type PRComment struct {
	Author    string    `json:"author"`
	Body      string    `json:"body"`
	Path      string    `json:"path,omitempty"` // file path for review comments
	Line      *int      `json:"line,omitempty"` // line number for review comments
	CreatedAt time.Time `json:"created_at"`
}

PRComment represents a comment on a pull request (issue comment or review comment).

type PRCommit added in v0.6.0

type PRCommit struct {
	SHA    string    `json:"sha"`
	Author string    `json:"author"`
	Date   time.Time `json:"date"`
	Msg    string    `json:"message"`
}

PRCommit represents a commit from a PR's branch, fetched via the GitHub /repos/{owner}/{repo}/pulls/{number}/commits endpoint. Only fetched for merged PRs (immutable once merged).

type PRFile added in v0.5.0

type PRFile struct {
	Number      int         `json:"number"`
	Title       string      `json:"title"`
	Body        string      `json:"body"`
	Author      string      `json:"author"`
	State       string      `json:"state"` // "open", "closed", "merged"
	Labels      []string    `json:"labels,omitempty"`
	CreatedAt   time.Time   `json:"created_at"`
	MergedAt    *time.Time  `json:"merged_at,omitempty"`
	ClosedAt    *time.Time  `json:"closed_at,omitempty"`
	UpdatedAt   time.Time   `json:"updated_at"`
	MergeCommit string      `json:"merge_commit,omitempty"`
	URL         string      `json:"url"`
	Comments    []PRComment `json:"comments,omitempty"`
	Commits     []PRCommit  `json:"commits,omitempty"`
}

PRFile represents the JSON structure stored in ledger/data/github/YYYY/MM/DD/pr/NNN.json

func ReadGitHubPR added in v0.6.0

func ReadGitHubPR(ledgerPath string, number int, createdAt time.Time) (*PRFile, error)

ReadGitHubPR reads an existing PR JSON file from the ledger by number and creation date. Returns os.ErrNotExist if the file does not exist.

type PushFunc added in v0.5.0

type PushFunc func(ctx context.Context, ledgerPath string) error

PushFunc is a function that pushes the ledger to remote with retry logic. The caller provides this so the ledger package doesn't depend on gitutil/endpoint.

type Status

type Status struct {
	// Exists indicates if the ledger is cloned
	Exists bool

	// Path is the ledger path
	Path string

	// HasRemote indicates if a remote is configured
	HasRemote bool

	// SyncedWithRemote indicates if local is up to date with remote
	SyncedWithRemote bool

	// SyncStatus describes the sync state (e.g., "ahead by 3", "behind by 1")
	SyncStatus string

	// PendingChanges is the count of uncommitted changes
	PendingChanges int

	// Error contains any error encountered during status check
	Error string
}

Status returns the current status of the ledger.

func GetStatus

func GetStatus(path string) *Status

GetStatus returns the current status of the ledger.

func GetStatusForEndpoint

func GetStatusForEndpoint(endpointURL string) *Status

GetStatusForEndpoint returns the current status of the ledger for a specific endpoint.

type SyncResult added in v0.5.0

type SyncResult struct {
	PRTotal      int
	PRCreated    int
	PRUpdated    int
	IssueTotal   int
	IssueCreated int
	IssueUpdated int
}

SyncResult tracks how many PRs and issues were synced.

func SyncIssues added in v0.5.0

func SyncIssues(ctx context.Context, fetcher GitHubFetcher, ledgerPath, owner, repo string, maxDays int, logger *slog.Logger) (*SyncResult, error)

SyncIssues fetches issues from GitHub and writes them to the ledger.

func SyncPRs added in v0.5.0

func SyncPRs(ctx context.Context, fetcher GitHubFetcher, ledgerPath, owner, repo string, maxDays int, logger *slog.Logger) (*SyncResult, error)

SyncPRs fetches PRs from GitHub and writes them to the ledger. Uses cursor-based incremental sync to minimize API calls.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL