donecheck

package
v0.0.6 Latest Latest
Warning

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

Go to latest
Published: Oct 16, 2026 License: MIT Imports: 20 Imported by: 0

Documentation

Overview

Package donecheck is the git half of "done means committed" (Task 20370): what did one attempt at a task change in its repository and leave behind uncommitted — or commit without pushing?

Why the symptom is the signal

Task 20349 taught the orchestrator to hand a turn back when the agent's final message says it is waiting on its own work ("the suite is still running; I'll commit once it reports"). That is phrase matching, and new wordings keep slipping past it: Task 20368 ended "The only thing still running is the adversarial review agent. I'll address what it finds, re-run -race on the touched packages, then commit and push." — nothing matched, the task was recorded done, and 24 changed paths sat uncommitted for the next task to trip over. Nor can the background-work check see the cause: Claude Code's background subagents run inside the CLI process, not as processes of their own.

What every one of those turns has in common is not a phrase but a state: the working tree still holds the task's changes. So the check reads the state.

Baseline and blame

A repository is rarely clean when a task starts. An operator's own edits, a stranded tree from an earlier run, generated files nobody ignored — none of it is this task's, and blaming it would send every task back for work it never touched. So Take records, as an attempt starts, every path `git status` reports and a fingerprint of each: its status line (which carries the index entry) and its working-tree content. Check then blames exactly the paths that are dirty now and were either clean then or have changed since.

The exception is the task's own earlier attempt. An attempt that is aborted for uncommitted work leaves that work in the tree, by design: nothing here reverts or stashes an agent's changes. The next attempt's baseline would then count them as already there and let them through. A Carry, written when an attempt is blamed and read when the next one starts, keeps them the task's.

Pushed

With Pushed, the commits the attempt made on HEAD that HEAD's upstream lacks are reported too: commits not reachable from the upstream and not from the HEAD the attempt started at. Commits that were already local when it started are not its to publish. A branch with no upstream — a task worktree, a detached HEAD — is noted rather than failed, because there is nowhere to push to; and a push the review gate is holding counts as pending, since the gate publishes it once the reviewer approves.

What this package will not do

It never writes to the repository: git runs with optional locks off, so even `git status` does not refresh the index behind the agent's back. It never runs the repository's filesystem monitor. Everything it reads is bounded, because the repository is in the agent's reach: a status listing past maxStatusBytes is cut and noted, large files are fingerprinted by size and modification time rather than read, and a directory walk stops at maxDirEntries.

Index

Constants

View Source
const (

	// CarryMaxAge is how long a carry is honoured. A task re-run weeks later
	// starts from whatever the tree holds then.
	CarryMaxAge = 7 * 24 * time.Hour
)

Bounds on a carry, which lives in a directory the agent can write to.

View Source
const (
	// MaxListed bounds how many paths and commits a report names. The counts
	// beside them are exact.
	MaxListed = 40
)

Bounds on what one baseline or check reads.

Variables

View Source
var ErrNotRepository = errors.New("donecheck: not a git working tree")

ErrNotRepository reports that the directory is not inside a git working tree, so there is nothing to check.

Functions

func ClearCarry

func ClearCarry(path string) error

ClearCarry removes the carry at path, if there is one.

func PruneCarries

func PruneCarries(dir string, cutoff time.Time, dryRun bool) (int, int64, error)

PruneCarries removes the carry files in dir last written before cutoff, and reports how many it removed and their size. A carry older than CarryMaxAge is ignored anyway; this keeps the ones a task never came back for from accumulating. Nothing but carry files is touched, and a symbolic link is never followed.

func WriteCarry

func WriteCarry(path string, c *Carry) error

WriteCarry writes c to path, atomically, creating its directory.

Types

type Baseline

type Baseline struct {
	// Dir is the directory checked: the task's working directory. Only paths
	// under it are considered, and its .cloop/ never is.
	Dir string
	// Top is the repository's top-level directory, and Prefix is Dir relative
	// to it ("" when Dir is the top level, else "sub/dir/").
	Top    string
	Prefix string
	// Head is the commit HEAD named when the attempt started — or, for a
	// retry, when the first attempt the carry describes started. "" before
	// the first commit.
	Head string
	// Taken is when the baseline was recorded.
	Taken time.Time
	// contains filtered or unexported fields
}

Baseline is a repository as an attempt found it.

func Take

func Take(ctx context.Context, dir string, carry *Carry) (*Baseline, error)

Take records the repository containing dir as an attempt at a task starts. carry, when not nil, is what an earlier aborted attempt at the same task left on record; see Carry. It returns ErrNotRepository when dir is not inside a git working tree.

func (*Baseline) Next

func (b *Baseline) Next(taskID int, rep *Report) *Carry

Next is the carry an attempt blamed with rep leaves for the next attempt at the task: the paths blamed now — which include those an earlier attempt was blamed for and that are still not committed — and the HEAD the first blamed attempt started at, which b already holds when it was taken from a carry.

type Carry

type Carry struct {
	Format int `json:"format"`
	TaskID int `json:"task_id"`
	// Top is the repository the paths are relative to. A carry for another
	// repository is ignored.
	Top string `json:"top"`
	// Paths are the paths blamed so far, relative to Top.
	Paths []string `json:"paths,omitempty"`
	// Head is the HEAD the first blamed attempt started at.
	Head string `json:"head,omitempty"`
	// At is when it was last written.
	At time.Time `json:"at"`
}

Carry is what an attempt that was blamed for outstanding work leaves on record for the next attempt at the same task: the paths it left uncommitted, and the HEAD the first such attempt started at. Without it the next attempt's baseline would find that work already in the tree, count it as somebody else's, and let it through.

It is not a security boundary. The agent can write to the directory it lives in, and a forged one can only make the check stricter on the agent's own task: more paths it has to commit, more of its branch's commits it has to push.

func ReadCarry

func ReadCarry(path string, taskID int) (*Carry, error)

ReadCarry reads the carry at path. It returns nil and no error when there is none, or when the one there is too old or belongs to another task; an error means one is there that cannot be used.

type Change

type Change struct {
	// Path is relative to the checked directory, with a trailing slash for a
	// directory.
	Path string
	// Code is the two-letter status `git status --short` would print:
	// "??" untracked, " M" modified, "A " added, " D" deleted, "UU" conflicted.
	Code string
}

Change is one path the attempt left uncommitted.

type Commit

type Commit struct {
	SHA     string
	Subject string
}

Commit is one commit the attempt made that its upstream lacks.

func (Commit) Short

func (c Commit) Short() string

Short is the abbreviated commit name.

type Options

type Options struct {
	// Pushed also requires the attempt's commits on HEAD's upstream.
	Pushed bool
	// Held are the commits of pushes the review gate is holding. Commits
	// they contain are pending, not missing: the gate publishes them after
	// the reviewer approves. Anything that is not a full commit name is
	// ignored — the list comes from a file the agent can write to.
	Held []string
	// NoPush, when set, says why pushing is not expected here; the push is
	// then not checked and the reason is noted instead.
	NoPush string
}

Options says what a check requires beyond a committed tree.

type Report

type Report struct {
	// Uncommitted lists up to MaxListed of the paths the attempt changed and
	// left uncommitted; UncommittedCount counts all of them.
	Uncommitted      []Change
	UncommittedCount int
	// Unpushed lists up to MaxListed of the commits the attempt made that are
	// on HEAD but not on its upstream, and that no held push covers;
	// UnpushedCount counts all of them.
	Unpushed      []Commit
	UnpushedCount int
	// Pending counts the attempt's commits that a held push covers.
	Pending int
	// Branch is the checked-out branch ("" when HEAD is detached) and
	// Upstream its upstream ("origin/main"), when the push was checked.
	Branch   string
	Upstream string
	// Notes say what could not be checked, and why.
	Notes []string
	// contains filtered or unexported fields
}

Report is what an attempt has left outstanding.

func Check

func Check(ctx context.Context, b *Baseline, opts Options) (*Report, error)

Check reports what the attempt has left outstanding since b was taken.

func (*Report) Outstanding

func (r *Report) Outstanding() bool

Outstanding reports whether the attempt left anything that "done" requires to be finished.

func (*Report) Summary

func (r *Report) Summary() string

Summary says in one line what is outstanding, naming the first few paths and the upstream: "3 uncommitted paths (a.go, b.go, c.md) and 1 commit not on origin/main".

Jump to

Keyboard shortcuts

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