handoff

package
v0.8.0 Latest Latest
Warning

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

Go to latest
Published: Oct 1, 2026 License: MIT Imports: 10 Imported by: 0

Documentation

Overview

Package handoff decides whether work can safely move to another machine.

It answers a different question than status or doctor. Those report how healthy a repository is; handoff reports whether anything in it exists only on this machine. Uncommitted files, unpushed commits, and stash entries are all invisible to every other device and to every agent, so leaving them behind is how work gets lost or duplicated.

Assess is a pure function over the results of a bulk status scan, so the classification is testable without touching git.

Blockers are split into two kinds. Auto-fixable ones (uncommitted work, unpushed commits) are exactly what "handoff end" resolves by committing and pushing. The rest — conflicts, an interrupted rebase or merge, a detached HEAD, a stash, a repository with no remote — need a decision that only the person at this machine can make, and no automation should guess at.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Paths

func Paths(repos []RepoAssessment) []string

Paths returns the absolute paths of the repositories in a set, in scan order.

Types

type Assessment

type Assessment struct {
	Verdict Verdict `json:"verdict"`
	// Repositories holds every scanned repository, ready ones included, so
	// callers can render a complete picture without a second scan.
	Repositories []RepoAssessment `json:"repositories"`
	TotalScanned int              `json:"total_scanned"`
}

Assessment is the verdict across a scanned directory.

func Assess

func Assess(results []repository.RepositoryStatusResult) *Assessment

Assess classifies the results of a bulk status scan into a handoff verdict.

func (*Assessment) Blocked

func (a *Assessment) Blocked() []RepoAssessment

Blocked returns the repositories that "handoff end" cannot resolve.

func (*Assessment) NotReady

func (a *Assessment) NotReady() []RepoAssessment

NotReady returns the repositories with at least one blocker, preserving the order they were scanned in.

func (*Assessment) ReasonCounts

func (a *Assessment) ReasonCounts() map[Reason]int

ReasonCounts tallies blockers by reason across all repositories.

type Blocker

type Blocker struct {
	Reason Reason `json:"reason"`
	Detail string `json:"detail"`
	// AutoFixable reports whether "handoff end" clears this blocker on its own.
	// It is false whenever clearing it needs a decision only the person at this
	// machine can make.
	AutoFixable bool `json:"auto_fixable"`
}

Blocker is one reason work in a repository would not survive the move.

func FirstHardBlocker

func FirstHardBlocker(r RepoAssessment) (Blocker, bool)

FirstHardBlocker returns the blocker that disqualifies a repository from an automatic checkpoint, in the order the blockers were recorded.

func FirstStartBlocker

func FirstStartBlocker(r RepoAssessment) (Blocker, bool)

FirstStartBlocker returns the blocker that disqualifies a repository from an automatic rebase on arrival.

type Finding

type Finding struct {
	Kind   FindingKind `json:"kind"`
	File   string      `json:"file"`
	Detail string      `json:"detail"`
}

Finding is one file the guard refuses to commit unattended.

func Guard

func Guard(ctx context.Context, exec *gitcmd.Executor, repoPath string) ([]Finding, error)

Guard reports everything in repoPath that an automatic commit would sweep up but a person would not have staged deliberately.

This is what separates an explicit checkpoint command from a background auto-commit loop: the sweep still happens, but never silently.

type FindingKind

type FindingKind string

FindingKind classifies why a file should not be swept into a checkpoint commit without someone looking at it first.

const (
	// FindingSecret marks a file that looks like it holds a credential.
	FindingSecret FindingKind = "secret"
	// FindingLargeFile marks a file too big to be source.
	FindingLargeFile FindingKind = "large-file"
	// FindingArtifact marks generated output that .gitignore does not cover.
	FindingArtifact FindingKind = "artifact"
)

type Plan

type Plan struct {
	// Checkpoint holds the repositories that will be committed and pushed.
	Checkpoint []RepoAssessment `json:"checkpoint"`
	// Skipped holds the repositories with a blocker no automatic step can clear.
	Skipped []RepoAssessment `json:"skipped,omitempty"`
}

Plan is the split of a scanned workspace into what "handoff end" will move and what it must leave for a person.

func PlanCheckpoint

func PlanCheckpoint(a *Assessment) Plan

PlanCheckpoint decides, per repository, whether "handoff end" may act.

A repository qualifies when it has work that a commit and a push would move and nothing that makes those steps unsafe. A stash does not disqualify it: the stash is untouched either way, and the committed work still deserves to reach the remote.

func (Plan) Empty

func (p Plan) Empty() bool

Empty reports whether there is nothing for "handoff end" to do.

type Reason

type Reason string

Reason identifies why a repository is not ready to be left behind.

const (
	// ReasonUncommitted marks changes that exist only in the working tree.
	ReasonUncommitted Reason = "uncommitted"
	// ReasonUnpushed marks commits that exist only in the local repository.
	ReasonUnpushed Reason = "unpushed"
	// ReasonStashed marks stash entries, which are never transferred by git.
	ReasonStashed Reason = "stashed"
	// ReasonStranded marks a stash old enough to have outlived the task that
	// created it. It is the same blocker as ReasonStashed with a worse prognosis:
	// nobody is coming back for it on their own.
	ReasonStranded Reason = "stranded"
	// ReasonConflict marks unresolved merge conflicts.
	ReasonConflict Reason = "conflict"
	// ReasonInProgress marks an interrupted rebase or merge.
	ReasonInProgress Reason = "in-progress"
	// ReasonDetached marks a detached HEAD, where new commits belong to no branch.
	ReasonDetached Reason = "detached-head"
	// ReasonNoRemote marks a repository with nowhere to push.
	ReasonNoRemote Reason = "no-remote"
	// ReasonNoUpstream marks a branch that has no upstream to push to yet.
	ReasonNoUpstream Reason = "no-upstream"
	// ReasonError marks a repository whose state could not be read.
	ReasonError Reason = "error"
)

type RepoAssessment

type RepoAssessment struct {
	Path         string    `json:"path"`
	RelativePath string    `json:"relative_path"`
	Branch       string    `json:"branch,omitempty"`
	Blockers     []Blocker `json:"blockers,omitempty"`
}

RepoAssessment is the verdict for a single repository.

func (RepoAssessment) AutoFixable

func (r RepoAssessment) AutoFixable() bool

AutoFixable reports whether "handoff end" would clear every blocker here.

func (RepoAssessment) Ready

func (r RepoAssessment) Ready() bool

Ready reports whether nothing in this repository exists only locally.

type StartPlan

type StartPlan struct {
	// Update holds the repositories that will be pulled with a rebase.
	Update []RepoAssessment `json:"update"`
	// Skipped holds the repositories a rebase would endanger.
	Skipped []RepoAssessment `json:"skipped,omitempty"`
}

StartPlan is the split of a scanned workspace into what "handoff start" will bring up to date and what it must leave for a person.

func PlanStart

func PlanStart(a *Assessment) StartPlan

PlanStart decides, per repository, whether "handoff start" may rebase it.

Unpushed commits do not disqualify a repository: replaying them onto the updated remote branch is the point of arriving with a rebase. A stash does not either, since a rebase never touches one.

type Verdict

type Verdict string

Verdict summarizes an assessment across all repositories.

const (
	// VerdictReady means nothing exists only on this machine.
	VerdictReady Verdict = "ready"
	// VerdictFixable means "handoff end" would make everything ready.
	VerdictFixable Verdict = "fixable"
	// VerdictBlocked means at least one repository needs a decision made here.
	VerdictBlocked Verdict = "blocked"
)

Jump to

Keyboard shortcuts

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