delivery

package
v0.1.3 Latest Latest
Warning

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

Go to latest
Published: Aug 30, 2026 License: AGPL-3.0 Imports: 28 Imported by: 0

Documentation

Overview

Package delivery runs delivery commands against pinned git contexts.

Package delivery runs delivery commands against pinned git contexts.

Package delivery implements host-owned pull-request publication for workflow runs. The GitHub CLI adapter uses fixed argv only. It never passes values through a shell.

PRTitlePolicy loads and applies the OPTIONAL workspace PR-title policy (.mivia/policy/pr-title.toml) before a delivery PR is created. The loader mirrors the commit-message policy pattern in policy.go: an absent file validates nothing, and every config defect is a permanent RefusalError.

Validation failures are a different class of error. A title or summary that violates the policy is REPAIRABLE: the agent can change the metadata and retry, so Validate returns PRMetadataError, never RefusalError. The caller routes a PRMetadataError back to the agent for a fix and routes a RefusalError to a settled delivery_failed.

Stacking delivery: reserved chunk-mode inputs (pr_base, stack_part) and the actual-diff-size gate. The controller and driver inject pr_base and stack_part on chunk-mode runs; delivery honors them when present and rejects invalid values with a repairable PRMetadataError, so the delivery repair loop receives a delivery hint naming the problem and fixes it before the next attempt. An over-limit delivered diff is a repairable DiffSizeError (deliberately NOT a PRMetadataError: metadata edits cannot shrink a diff, so repair routing must send it to a step that edits the worktree). Absent inputs leave single-PR delivery unchanged.

stacking_plan.go: the durable state layer of the workflow stack driver - chunk-plan parsing, stable chunk admission keys, topological chunk ordering, task-ledger seeding, and the admission-input builders for chunk and integration runs.

It is the shared implementation behind two drive surfaces:

  • internal/cli's `mivia workflow run`/`mivia stack drive` driver (the operator path), which keeps its own drive loop and merge machinery;
  • internal/workflows/localengine's in-process engine (the agent-tools path), which drives a parked multi-chunk plan run automatically after its controller settles (drive-before-delivery).

Every decision here is derived from durable state only - the task ledger, the run ledger, and git merge state - never from driver memory, so either surface can resume the other's stack (D8, plan v2.1 §5a).

Index

Constants

View Source
const (
	InputPRBase    = "pr_base"
	InputStackPart = "stack_part"
	// InputStackMode is the run's stacking mode input ("chunk", "single",
	// "plan", "decompose_continue"). Delivery reads it to key per-chunk
	// behavior: the hard diff-size gate measures chunk deliveries only.
	InputStackMode     = "stack_mode"
	StackModeSingle    = "single"
	InputDeferredFiles = "deferred_files"
)

Reserved stacking input names (plan D3). The controller and driver inject these on chunk-mode runs; delivery honors pr_base and stack_part when present.

View Source
const (
	StatusPlanned     = "planned"
	StatusQueued      = "queued"
	StatusBlocked     = "blocked"
	StatusRunning     = "running"
	StatusImplemented = "implemented"
	StatusReviewed    = "reviewed"
	StatusPublished   = "published"
	StatusMerged      = "merged"
	StatusReopened    = "reopened"
	StatusFailed      = "failed"
	StatusSkipped     = "skipped"
	// StatusCanceled marks a chunk that a drive gave up on because a chunk
	// it depends on failed terminally. It is TERMINAL: no drive pass may
	// re-admit it, re-open it, or mark it merged. A canceled chunk was
	// never implemented, so its content is absent from the stack.
	StatusCanceled = "canceled"
)

Stack task statuses (stacking vocabulary; D8: statuses are opaque strings owned by the consumer; the engine only makes transitions durable).

View Source
const DecomposeStepID = "decompose"

DecomposeStepID is the engine-synthesized decompose step (compiler s2 contract). The driver uses it to read the plan-mode run's chunk plan.

View Source
const DefaultMaxCommitMessageBytes = 1048576

DefaultMaxCommitMessageBytes is the default limit for rendered commit messages. Zero or negative values in the workflow TOML are replaced by this default.

View Source
const DefaultMaxDeliveryRepairs = 5

DefaultMaxDeliveryRepairs is the default budget for the delivery -> repair -> success -> delivery cycle when the workflow does not configure delivery.max_repairs. It bounds how many times a delivery rejection may route back into the workflow's repair step before the run settles terminal (delivery_failed). The ceiling is deliberately higher than the original hard-coded 3: a gate that needs a couple of repair iterations (for example a config/code drift like a dialect the base does not yet implement) is common, while the run's duration cap still bounds the total spend.

View Source
const DefaultMaxRenderedBytes = MaxTemplateBytes

DefaultMaxRenderedBytes bounds one rendered prompt context.

View Source
const DefaultMaxTitleBytes = 65536

DefaultMaxTitleBytes is the default limit for rendered pull-request titles. Zero or negative values in the workflow TOML are replaced by this default.

View Source
const DefaultPRTitlePolicyPath = workspace.Namespace + "/policy/pr-title.toml"

DefaultPRTitlePolicyPath is the OPTIONAL workspace policy file consulted before a delivery PR is created. It is only read when present: a workspace that configures nothing is unaffected.

View Source
const DeliveryRepairStepID = "wf-delivery"

DeliveryRepairStepID is the synthetic step id of the cli repair path for delivery failures. The cli declares its own unexported constant of the same value; this package re-declares it so the ledger queries below never import the cli package.

View Source
const InputChunkPlan = "chunk_plan"

InputChunkPlan is the reserved admission input carrying the chunk's own decompose plan entry (JSON: id, title, files, ...). Only chunk-mode runs of a stacking workflow have it.

View Source
const IntegrationChunkID = "integration"

IntegrationChunkID is the fixed chunk id of the final full-suite run.

View Source
const MaxChunkAttempts = 3

MaxChunkAttempts bounds reopen retries of a failed chunk run (plan F6 engine default). Past the bound a chunk is marked failed and the stack halts.

View Source
const MaxDeliveryRepairs = DefaultMaxDeliveryRepairs

MaxDeliveryRepairs is the DEFAULT budget for the delivery -> repair -> success -> delivery cycle, used when the workflow does not configure delivery.max_repairs. It bounds how many times a delivery rejection may route back into the workflow's repair step. A rejection the named repair step cannot actually fix would otherwise cycle until the step cap or the 24h run deadline is spent, and a run that repairs at the last minute is destroyed rather than delivered. The ceiling is higher than the original hard-coded 3 so a drift that needs a couple of repair iterations (for example a config/code mismatch like a reasoning dialect the base does not yet implement) can converge; the run's duration cap still bounds the total spend.

View Source
const MaxTemplateBytes = 32768

MaxTemplateBytes is the maximum allowed size for a single template file.

View Source
const MaxTitleRunes = 256

MaxTitleRunes is GitHub's hard ceiling for pull-request titles, counted in characters (runes) rather than bytes. GitHub rejects titles longer than 256 characters, so this is the effective ceiling for every rendered title even when MaxTitleBytes is configured higher: RenderTitle enforces BOTH limits (MaxTitleBytes as the byte cap, MaxTitleRunes as the character cap) and the stricter of the two wins.

View Source
const PlanSchema = "chunk-plan-v1"

PlanSchema is the schema name recorded on the stack's plan artifact.

Variables

AdmissiblePreStatuses are the statuses a drive pass selects a chunk from. Drive admission guards (TransitionTaskCAS) use the same list as their compare-and-swap precondition, so the two never drift apart: a status an admission pass would offer is always one the admission CAS accepts.

TerminalStatuses are the statuses no drive pass may move a chunk out of. Every enumeration of "leave this task alone" must derive from this list, or a terminal task is resurrected by the next pass: a canceled dependent whose run row is failed fell to the reopen path and was re-admitted, because the reconciler's short-circuit named only merged/failed/skipped (audit finding R3, 2026-08-17).

Functions

func AdmissionKey

func AdmissionKey(stackID, chunkID string) (string, error)

AdmissionKey derives the stable run invocation key for a chunk run (plan F15): re-admission after a restart resolves to the SAME run, never a duplicate. The key is <stack-id>:<chunk-id> (plan D3, §5a step 3).

func AdmitDeliveryTarget

func AdmitDeliveryTarget(ctx context.Context, git GitRunner, gc GitContext, base, worktreeBaseCommit string) (originURL, targetOriginCommit string, err error)

AdmitDeliveryTarget verifies a fresh delivery-required run's target branch admits, and pins the value delivery-time rewrite detection must compare against later.

The repository must have an origin remote, and the delivery target (base) must exist on that remote and CONTAIN (as an ancestor) the commit the run's worktree started from - the worktree's own source branch does NOT need to equal the target by name. This replaces an older check that required the target to sit at the EXACT SAME commit as the worktree base, which only ever admitted a run started at the target's then-tip.

The containment test fetches the target from the ADMITTED origin URL (never a possibly-stale local refs/heads/<base>), so a target that has advanced beyond what this checkout last saw still admits. When the fetched tip does NOT contain the worktree base, one fallback is tried: the LOCAL refs/heads/<base>, for the ordinary case where the operator committed to the target locally but not yet pushed it (see TestAdmitDeliveryTargetLocalAheadOfOriginAccepted). The fallback accepts only a local ref strictly AHEAD of the fetched origin tip; a DIVERGED local ref (a local rebase, or a target rewritten on origin while this clone was stale) is refused, because the recorded pin would then be unrelated to the worktree base and delivery-time rewrite detection would compare the pin against the same rewritten history it came from.

The returned targetOriginCommit is always the FETCHED origin tip (never the local fallback ref); callers record it as OriginBaseCommit, the pin verifyRemoteBaseAncestry compares a later fetch against.

func AllChunksMerged

func AllChunksMerged(chunks []ChunkPlan, merged map[string]bool) bool

AllChunksMerged reports whether every chunk in the plan is merged.

func ChunkPartIndex

func ChunkPartIndex(chunkID string, order []string) (int, error)

ChunkPartIndex returns the 0-based position of a chunk in dependency order, for the canonical "k/N" stack_part. An id absent from order is an error, not position 0: silently treating an unknown chunk as "first" would mislabel its stack_part and, once cross-wave chunk ids exist, mask a real bug (an id order was built without).

func ChunkRunInputs

func ChunkRunInputs(planInputs map[string]string, chunkID, prBase, stackPart string, plan *ChunkPlan, siblingFiles []string) (map[string]any, map[string]string)

ChunkRunInputs builds the admission inputs and snapshot for one chunk-mode run: the plan run's declared inputs replayed (D3) plus the engine's reserved stack inputs, which win on any name collision. The integration run uses the same shape with an empty stack_part and a nil plan entry. When the chunk's decompose plan entry is given, it rides along as chunk_plan JSON: without it the implement agent sees only the FULL task text and a bare chunk ID, and (live finding, smoke-stack-3chunk-v3) implements the whole task instead of its slice.

func ChunkRunNoDiff

ChunkRunNoDiff reports whether a run settled succeeded with a confirmed no_diff delivery outcome: the intended diff was empty, no PR was created, and the chunk is therefore complete. This requires POSITIVE evidence - an actual "no_diff" delivery record - not merely the absence of pushed evidence: a ListDeliveries read failure or a not-yet-recorded delivery also produce zero pushed records, and misreading either as "confirmed no_diff" durably marks a chunk merged with no PR ever created, silently dropping its content (an adversarial audit found this exact regression). A record that reached pushed/succeeded with a commit SHA always wins over a stale no_diff record from an earlier attempt on the same run.

func CloneInputs

func CloneInputs(inputs map[string]string) map[string]string

CloneInputs returns a shallow copy of inputs, safe for a delivery.Request to own and mutate (checkChunkDiffSize writes InputDeferredFiles into it in place - see stacking.go). Every delivery.Request construction site should pass a clone, never the run snapshot's own Inputs map, so that mutation never leaks into cached ledger state.

func CountUnshippedCommits

func CountUnshippedCommits(ctx context.Context, git GitRunner, gc GitContext, deliveredCommit string) (int, error)

CountUnshippedCommits counts the commits on the current worktree HEAD after deliveredCommit (git rev-list --count deliveredCommit..HEAD): the trailing commits a diff-size repair left on the branch after committing the review-sized slice as deliveredCommit (spec-auto-split-oversized-prs.md §5.2-5.3). Zero means no trailing commits: the repair produced exactly one delivered slice and nothing else. Shared by the delivery engine (to record DeliveryRecord.StackRemainingCommits after a successful re-delivery) and by the driver's follow-up chunk admission (§5.3), so both count identically.

func DecomposedChunks

func DecomposedChunks(ctx context.Context, repo workflowledger.Repository, runID string) (chunks int, ok bool)

DecomposedChunks reports whether runID is the plan run of a multi-chunk stack: the LATEST succeeded decompose step attempt that produced an output parses as mode=multi with at least one chunk (same selection rule as LoadStackPlanOutput and the controller's latestOutputAttempt; a later succeeded attempt with no output must not shadow the recorded plan). ok=false covers every other case (not a stacking plan run, single/no_bug, a malformed decompose output, or a lookup failure) - callers must treat a lookup failure as "not applicable", never as a refusal or a false "undriven" diagnostic.

func DeferredBranchName

func DeferredBranchName(branch string) string

DeferredBranchName derives the local branch name a split delivery (freshDeliveryCommitSplit) saves its deferred commit under, deterministic from the chunk's own delivery branch so no extra ledger field is needed to find it afterward: the driver (internal/cli) computes the same name to look it up and push it as a follow-up PR after this chunk's delivery succeeds. resumeDeliveryCommitSplit re-creates the same branch from the recorded DeferredFiles when a split attempt crashed after C1, so the name is also the resume contract between the delivery engine and the driver.

func DeliveryKey

func DeliveryKey(runID, workflowDigest string) string

DeliveryKey derives the stable idempotency key for one run's delivery from admitted data only: sha256(runID + NUL + workflowDigest), hex-encoded, prefixed wfdel:.

func EffectiveBase

func EffectiveBase(wf *definition.CompiledWorkflow, inputSnapshot map[string]string) string

EffectiveBase returns the delivery base a fresh admission must guard: a valid pr_base input overrides the workflow's declared delivery base (the same override delivery honors at publish time, resolveStackingInputs), so the admission origin-containment check keys off the branch the run will actually deliver to. An absent, empty, or invalid pr_base never fails admission — the declared base is used instead. inputSnapshot may be nil; a nil or non-delivery workflow yields the declared base or "".

func FormatOutcome

func FormatOutcome(r Result, err error) string

FormatOutcome renders the human-readable CLI summary of one delivery attempt. A successful or no_diff result is described from r; a refusal is permanent; any other error is a transient attempt failure that can be retried.

func HasDeferredFollowUp

func HasDeferredFollowUp(ctx context.Context, repo ledger.Repository, runID string) bool

HasDeferredFollowUp reports whether runID's most recent succeeded delivery record left a pending deferred commit (DeliveryRecord.StackRemainingCommits > 0).

func IntegrationRunInputs

func IntegrationRunInputs(planInputs map[string]string, prBase string) (map[string]any, map[string]string)

IntegrationRunInputs builds the admission inputs for the final full-suite integration run: it replays the plan run's declared inputs and admits as stack_mode=single (running the workflow's own plan+implement steps inline), never stack_mode=chunk. chunk_plan's chunk/pr_base/stack_part are deliberately absent: stack_mode=chunk REQUIRES stack_part present (validateStackingReservedInputs), and the integration run has none - a bug an adversarial audit found: chunkRunInputs forced stack_mode=chunk here with an always-empty stack_part, so every stack's integration run failed admission the moment every chunk merged.

func IsAncestryUnverifiable

func IsAncestryUnverifiable(err error) bool

IsAncestryUnverifiable reports whether err is an AncestryUnverifiableError (possibly wrapped).

func IsCommitMessageRejection

func IsCommitMessageRejection(cause error) bool

IsCommitMessageRejection reports whether cause is a git commit failure whose output names the workspace commit-msg hook (git runs that hook after the message is written, so its diagnostics appear in the commit error text). The rejected artifact is the commit MESSAGE, not the worktree: the subject is the agent's pr_title and the body is rendered from the workflow's commit_message_template, so the repair is a structured-output edit, not a worktree edit. Requiring the git failure marker keeps the classifier off any text that merely mentions a commit-msg hook without a failed commit.

func IsDiffSizeError

func IsDiffSizeError(err error) bool

IsDiffSizeError reports whether err is a DiffSizeError (possibly wrapped).

func IsPRMetadataError

func IsPRMetadataError(err error) bool

IsPRMetadataError reports whether err is a PRMetadataError (possibly wrapped).

func IsPermanentMergeError

func IsPermanentMergeError(err error) bool

IsPermanentMergeError reports whether err from MergePullRequest represents a failure that retrying will never fix: the PR is closed, auth is broken, a branch was deleted, or a merge conflict exists. Retriable conditions (pending CI, review requirements, transient gh errors) return false so the caller can keep polling.

func IsRefusal

func IsRefusal(err error) bool

IsRefusal reports whether err is a RefusalError (possibly wrapped).

func IsTransportFault

func IsTransportFault(err error) bool

IsTransportFault reports whether err is a git/gh transport fault: a wrapped net/syscall error of the connection-death kinds, or an error whose text carries one of the transport fault phrases. It never matches a bare context deadline or cancellation.

func LatestFailureText

func LatestFailureText(ctx context.Context, repo ledger.Repository, runID string) (string, error)

LatestFailureText returns the stored failure text of the latest wf-delivery repair attempt that carries an error ref, or "" when none exists. The attempt with the highest AttemptNo wins; a later attempt in event order wins a tie. Only a storage or load failure is an error.

func LoadStackPlanOutput

func LoadStackPlanOutput(ctx context.Context, repo workflowledger.Repository, stackID string) ([]byte, error)

LoadStackPlanOutput reads the succeeded decompose step output of a plan-mode run from the run ledger (F1/F8: the plan is a run output). When the plan run's decompose step ran more than once, the LATEST succeeded attempt that produced an output is authoritative.

func LoadTemplates

func LoadTemplates(baseDir string) (map[string]string, error)

LoadTemplates reads all .md files from the given directory. Returns a map of basename -> content, or an error if the directory is invalid. Path traversal is rejected: all resolved paths must remain under baseDir.

func MeasureChunkDiffSize

func MeasureChunkDiffSize(ctx context.Context, git GitRunner, gc GitContext, baseCommit string, hard int, excludePaths []string) (int, error)

MeasureChunkDiffSize measures the actual added+deleted line count of the staged worktree diff vs base, using the same staging and numstat rules the delivery gate applies (--find-renames, --ignore-all-space, untracked files included via git add -A). hard <= 0 means the gate is off and 0 is returned without touching git. excludePaths, when non-empty, excludes those workspace-relative paths from the measured diff via a pathspec exclusion (spec-auto-split-oversized-prs.md §5.2: a repair's deferred_files are committed separately and must not count against the delivered diff's own size). It is shared by the delivery gate and the controller's post-implement fail-fast gate so both measure identically; the controller's gate always passes nil (deferred_files is a repair-time decision, unknown that early).

func MergePullRequest

func MergePullRequest(ctx context.Context, repo, number string, draft bool) error

MergePullRequest merges the PR identified by number in repo (owner/repo). draft=true marks the PR ready for review first. It returns nil once the merge command succeeded (the PR is merged now, or enqueued on a merge queue); a non-nil error means the PR is not mergeable yet and the caller should retry later.

func MergedSet

func MergedSet(byID map[string]workflowledger.Task) map[string]bool

MergedSet returns the set of chunk ids whose tasks are merged.

func PRBase

func PRBase(wf *definition.CompiledWorkflow) (string, error)

PRBase returns the delivery base branch the chunk PRs branch from: the workflow's delivery policy base (delivery honors pr_base, S4).

func ParseDeferredFiles

func ParseDeferredFiles(raw string) ([]string, error)

ParseDeferredFiles decodes the InputDeferredFiles reserved input: a JSON-encoded array of workspace-relative paths, or "" (no deferral). A present-but-malformed value is a repairable PRMetadataError (the engine's own read of the repair step's output was well-formed JSON matching the schema; a malformed value here means something upstream corrupted it, which is worth surfacing as a repair-loop-visible failure rather than silently ignoring the split decision).

func ParseOwnerRepo

func ParseOwnerRepo(url string) (string, error)

ParseOwnerRepo normalizes a git remote URL to owner/repo. It supports the https, scp-like git@, and ssh:// forms. The host must be github.com (case-insensitive) and the path must be exactly owner/repo with an optional trailing .git.

func PlanInputs

func PlanInputs(ctx context.Context, repo workflowledger.Repository, stackID string) (map[string]string, error)

PlanInputs reads the plan run's admitted snapshot and returns the workflow-declared inputs the chunks were decomposed from, so chunk runs can replay them (D3: chunk runs replay the plan run's inputs). The plan run's own RunID IS the stack id; it was never admitted with a "<stack>:<chunk>" key, so it is read directly by RunID.

func ProbePRTool

func ProbePRTool(provider string) error

ProbePRTool reports whether the provider's PR tool is usable before a run starts.

Delivery is the last step of a long workflow, so a missing tool is only discovered after every gate has passed and the whole run is spent. The probe moves that discovery to admission.

It is deliberately OFFLINE. `gh --version` touches no network and proves the binary exists and executes. An auth probe was considered and rejected: it would put a network call in the admission path of every delivery run, and it still could not prove the token is valid at delivery time, which may be hours later. Delivery keeps its own authoritative checks.

func Render

func Render(source string, inputs, evidence map[string]any, maxBindingBytes, maxRenderedBytes int) (string, error)

Render expands only explicit input and evidence bindings. It does not read files, execute commands, or inspect an agent transcript.

A successful render is always valid UTF-8: the template source and every string binding value must be valid UTF-8 (refused otherwise), and non-string binding values are JSON-encoded and the encoded bytes validated as UTF-8 (refused otherwise), because a value implementing json.Marshaler can carry invalid bytes that json.Marshal copies verbatim. Rendered output is bounded by maxRenderedBytes and each encoded binding by maxBindingBytes; zero or negative limits select the package defaults.

func ReopenForRepair

func ReopenForRepair(ctx context.Context, repo ledger.Repository, runID, repairStep string, maxRepairs int, cause error, stdout io.Writer) error

ReopenForRepair returns a run whose delivery failed to the step the workflow names in delivery.on_failure (or the PR-metadata/diff-size variants; delivery.RepairTarget is the single classifier both the CLI and the local engine use). maxRepairs bounds the cycle; <=0 selects MaxDeliveryRepairs.

Delivery runs after the success terminal, outside the step graph, so a repairable delivery failure (commit hook rejection is the common case) used to have no route back into the workflow and just waited for a person.

The re-entry writes the delivery attempt and its TERMINAL failure outcome with a route to the repair step in ONE durable event, so the attempt is never observable non-terminal — a crash before the write leaves nothing durable changed (run returns to delivery via reconcile); a crash after leaves it already terminal with the repair route. Either way is recoverable. The ledger derives the active step from the last attempt's route, so the run continues at the repair step on next resume. Failure evidence (RepairHint: what to repair, whether a commit is involved) is stored content-addressed and referenced by the attempt, so the repair agent reads why delivery failed instead of guessing.

Nothing here knows what the failure was or which step repairs it — the workflow author names the step, so the mechanism stays generic.

func RepairHint

func RepairHint(cause error) string

RepairHint renders the harness guidance a repair agent needs to fix a delivery rejection: a short "what to repair" line derived from the failure class, then the raw rejection text. It is project- and language-agnostic: it never names a repository's tests, files, tools, or gate names, so it is safe to ship in the binary and render for any workspace.

The hint is the deterministic evidence a delivery re-entry step sees via delivery.failure. Without a class-specific lead the agent has to guess what to repair from a wall of hook output; with it, the agent is told up front whether the failure is a gate rejection of the change, a PR-metadata defect, an over-limit diff, or a permanent host refusal - and, when a commit is involved, that the host commits the repaired worktree before the next delivery attempt.

func RepairTarget

func RepairTarget(err error, p Policy) string

RepairTarget returns the delivery repair step the policy names for a failure class. Diff-size rejections route to OnDiffSizeFailure (falling back to OnFailure), PR-metadata rejections to OnPRMetadataFailure (falling back to OnFailure), and every other repairable rejection to OnFailure. An AncestryUnverifiableError (git itself could not complete the base-ancestry check - a missing or corrupt object) yields an empty result unconditionally: no agent can repair a git object failure, so the run stays delivery_pending with a recorded cause and a later attempt retries. An empty result otherwise means the workflow declares no repair route for the class (the run holds for a person). It is the single classifier shared by the CLI and the local engine, so a delivery rejection routes to the same step on both paths.

func ResolveLatestChangeSummary

func ResolveLatestChangeSummary(ctx context.Context, repo ledger.Repository, runID string) (map[string]any, error)

ResolveLatestChangeSummary returns the change-summary object of the latest attempt whose output is a JSON object carrying a non-empty string "pr_title" key, or nil when none exists. "Latest" is GLOBAL event order: the attempt that most recently recorded a change summary wins. Per-step AttemptNo is NOT comparable across steps - a repair step that re-ran after an implement step always has a lower AttemptNo than implement's later attempt even though it happened after it - so the attempt with the latest StartedAt wins, and a later attempt in event order wins a tie (including all-zero synthetic fixtures). Only schema-validated outputs ever carry an OutputRef, so the presence of pr_title marks a change summary. Only a storage or load failure is an error; an output that is not valid JSON is skipped.

func RunHeadCommit

RunHeadCommit returns the pushed commit SHA for a chunk run, if any. The commit is the durable evidence the merge oracle uses to verify that the base branch contains the change.

func RunPushed

RunPushed reports durable pushed evidence for a chunk run: any of its delivery records reached pushed/succeeded with a commit SHA. A record in that state is only written after the branch was actually pushed to origin (the deliverer writes pushed after the push, succeeded after the PR is created). Without this evidence a missing remote ref means "never pushed", not "merged" - a delivery_pending run's PR may never have been created.

func Scope

func Scope(stackID string) workflowledger.Scope

Scope binds every stack task to the plan run that produced the chunk plan, so queries never cross stacks (D8 scope binding).

func SeedStackLedger

func SeedStackLedger(ctx context.Context, ledger *workflowledger.Store, stackID string, chunks []ChunkPlan) error

SeedStackLedger records the plan artifact and the chunk tasks (D8). Re-entry is idempotent: existing tasks are left untouched (their durable status wins - a re-drive after a partial completion, a continuation-wave seed, or the recovery sweep must never overwrite or re-create them), and only missing tasks are created. A lost race against a concurrent seed is also a no-op (the other writer won).

func SentenceCount

func SentenceCount(s string) int

SentenceCount counts the sentence boundaries in s. The rule is deterministic: a sentence boundary is a terminator ('.', '!', '?') followed by whitespace and an uppercase letter, or followed by end of text. The function counts boundaries on the trimmed text. Empty text has zero sentences. The rule handles abbreviations and version numbers correctly: 'e.g.', 'v1.2.3', and 'U.S.' do not split a sentence because their terminators are not followed by whitespace and an uppercase letter.

func SiblingFiles

func SiblingFiles(chunks map[string]*ChunkPlan, chunkID string) []string

SiblingFiles returns the union of the declared files of every chunk except the named one, sorted for a deterministic input digest. The union covers the chunks known at admission; later decompose waves are not visible to already-admitted chunk runs, which keep the directory heuristic inside the engine.

func StatusIsAdmissiblePre

func StatusIsAdmissiblePre(status string) bool

StatusIsAdmissiblePre reports whether status is one a drive pass treats as "not yet admitted."

func StatusIsTerminal

func StatusIsTerminal(status string) bool

StatusIsTerminal reports whether status is one a drive pass must leave alone.

func StoreDeliveryFailureText

func StoreDeliveryFailureText(ctx context.Context, repo ledger.Repository, cause error) string

StoreDeliveryFailureText puts the harness repair hint (RepairHint) in content-addressed storage and returns its ref. Fail-soft: an empty ref costs the repair agent its evidence, but must not stop the re-entry.

func TaskMap

func TaskMap(ctx context.Context, ledger *workflowledger.Store, stackID string) (map[string]workflowledger.Task, error)

TaskMap loads every stack task by id for a drive pass.

func TaskReady

func TaskReady(t workflowledger.Task, merged map[string]bool) bool

TaskReady reports whether a task's dependencies are all merged.

func TopologicalOrder

func TopologicalOrder(chunks []ChunkPlan) ([]string, error)

TopologicalOrder returns chunk ids in admission order (dependencies first) using Kahn's algorithm. Unknown, duplicated, or cyclic dependencies are errors: a stack must never admit a chunk before the chunks it depends on, and a duplicated id must never emit a duplicated admission order. Without the duplicate check, two chunks sharing a zero-indegree id both land in the ready queue, the loop emits the id twice, len(order) == len(chunks) still holds, and the driver silently gets a wrong order: duplicated wave entries, inflated "k/N" stack_part labels (ChunkPartIndex returns the first occurrence), and admission of the same chunk twice. The deterministic chunk-plan gate rejects duplicate ids per wave, but a continuation wave can reuse an id from an earlier wave (loadAllStackChunks concatenates the waves), so the shared ordering must fail closed on its own.

func ValidatePRBase

func ValidatePRBase(name string) error

ValidatePRBase validates a pr_base value (a git branch name): the allowed characters are A-Za-z0-9._/-, the value is at most 100 characters, does not start with '-' (a git option-injection guard), and does not contain '..' (a ref-traversal guard). Every violation is a repairable PRMetadataError naming the problem, so the repair loop can fix the input.

func ValidateReferences

func ValidateReferences(loaded map[string]string, stepTemplates []string) []string

ValidateReferences checks that all template references in stepTemplates exist in the loaded templates map. Returns a list of missing template names.

func VerifyGitDir

func VerifyGitDir(ctx context.Context, mainRoot, worktreeName, worktreeDir string) (string, error)

VerifyGitDir validates the worktree's .git file and returns the real git directory. It refuses a missing, symlinked, or misdirected .git file.

Types

type AncestryUnverifiableError

type AncestryUnverifiableError struct {
	Reason string
	// contains filtered or unexported fields
}

AncestryUnverifiableError marks a delivery-time base-ancestry check that git itself could not complete (exit other than 0/1: a missing or corrupt object). It is a recoverable condition, not a rewrite verdict and not a repairable change defect: RepairTarget yields no step for it, so the run stays delivery_pending with a recorded cause and a later attempt retries. The struct wraps the underlying git failure (Unwrap) so errors.Is and errors.As keep traversing the cause chain - a wrapped connect fault, for example, still classifies as transient on the delivery settle paths.

func (*AncestryUnverifiableError) Error

func (e *AncestryUnverifiableError) Error() string

Error implements error.

func (*AncestryUnverifiableError) Unwrap

func (e *AncestryUnverifiableError) Unwrap() error

Unwrap returns the underlying git failure so the cause chain is preserved.

type ChunkPlan

type ChunkPlan struct {
	ID           string   `json:"id"`
	Title        string   `json:"title"`
	Files        []string `json:"files"`
	EstDiffLines int      `json:"est_diff_lines"`
	Tests        bool     `json:"tests"`
	DependsOn    []string `json:"depends_on"`
}

ChunkPlan is one entry of a decompose chunk-plan output.

func ParseStackPlanOutput

func ParseStackPlanOutput(raw []byte) (mode string, chunks []ChunkPlan, hasMore bool, remainingScope string, err error)

ParseStackPlanOutput decodes a decompose step output into the stack mode, its chunk list, and whether decompose declared more scope than this wave planned (§12.1 incremental decompose). stack_mode=single and no_bug are valid and mean there is nothing to stack; malformed output is an error (fail closed). hasMore/remainingScope are always zero-valued for single/ no_bug modes, matching decompose.md's contract that incremental planning only applies to multi mode.

type DiffSizeError

type DiffSizeError struct{ Reason string }

DiffSizeError marks a REPAIRABLE delivery rejection whose cause is a chunk diff that exceeds the stacking hard limit. It is deliberately NOT a PRMetadataError: a metadata step cannot shrink a diff, so delivery repair routing (delivery.RepairTarget) sends it to the workflow's diff-size repair step, which edits the worktree. A RefusalError is permanent; a DiffSizeError returns the run to the agent for a diff-size fix.

func (*DiffSizeError) Error

func (e *DiffSizeError) Error() string

Error implements error.

type GitContext

type GitContext struct {
	Dir    string // working tree directory
	GitDir string // real git directory (GIT_DIR)
}

GitContext pins one git repository context for delivery commands.

type GitHubCLI

type GitHubCLI struct{}

GitHubCLI drives the operator's gh binary with fixed argv and --repo.

func (GitHubCLI) Create

func (GitHubCLI) Create(ctx context.Context, repo string, in PRInput) (PRRef, error)

Create opens a pull request with the fixed input values. Title and body use the --title= and --body= equals forms, so values that start with '-' stay safe as single argv elements. The PR URL is parsed from stdout (gh pr create prints it on success); --json is not used because older gh versions do not support it on pr create. The created PR's base commit is read back from the REST API so the caller can verify the base still contains the admitted commit (delivery recovery, AR-7).

func (GitHubCLI) FindByHead

func (GitHubCLI) FindByHead(ctx context.Context, repo, headBranch string) (*PRRef, error)

FindByHead lists open PRs whose head branch matches and returns the first PR whose head repository belongs to the target repository's owner. A fork PR with the same branch name must never be reused as this delivery's PR. It returns (nil, nil) when no matching open PR exists.

func (GitHubCLI) IsMerged

func (GitHubCLI) IsMerged(ctx context.Context, repo, headBranch string) (bool, error)

IsMerged reports whether a pull request for headBranch has been merged. It queries all PR states, so squash/rebase merges are detected even when the original commit is no longer an ancestor of the base branch.

type GitRunner

type GitRunner interface {
	Run(ctx context.Context, gc GitContext, args ...string) (string, error)
}

GitRunner executes fixed-argv git commands with a pinned environment.

type PRClient

type PRClient interface {
	FindByHead(ctx context.Context, repo, headBranch string) (*PRRef, error)
	Create(ctx context.Context, repo string, in PRInput) (PRRef, error)
	// IsMerged reports whether a pull request for headBranch has been merged
	// on the remote host. A missing PR or a non-merged state returns false.
	IsMerged(ctx context.Context, repo, headBranch string) (bool, error)
}

PRClient is the remote PR boundary. Implementations are host-owned.

type PRInput

type PRInput struct {
	Base  string
	Head  string
	Title string
	Body  string
	Draft bool
}

PRInput is the fixed set of values for PR creation. Values come from host-rendered templates; they are passed as single argv elements, never through a shell.

type PRMetadataError

type PRMetadataError struct{ Reason string }

PRMetadataError marks a REPAIRABLE metadata defect in a PR title or summary. The agent can fix the metadata and retry, so it is never a RefusalError. A RefusalError is permanent; a PRMetadataError returns the run to the agent for a metadata fix.

func (*PRMetadataError) Error

func (e *PRMetadataError) Error() string

Error implements error.

type PRRef

type PRRef struct {
	RemoteID   string
	URL        string
	Title      string
	Draft      bool
	BaseRefOID string // the PR's current base commit (gh baseRefOid)
}

PRRef identifies one remote pull request.

func EnsureFollowUpPublished

func EnsureFollowUpPublished(ctx context.Context, git GitRunner, pr PRClient, worktreeRoot string, repo ledger.Repository, run ledger.RunSnapshot, label string, stdout func(string)) (branch, sha string, ref PRRef, published bool, err error)

EnsureFollowUpPublished pushes run's deferred branch (left by freshDeliveryCommitSplit, when checkChunkDiffSize split an oversized diff) and opens a follow-up PR stacked on the delivered branch, if delivery left one pending (HasDeferredFollowUp). It has no ledger/stack registration dependency beyond reading the deferred file list - it is still safe and cheap to call unconditionally after every successful delivery, from every completion path. Idempotent: FindByHead reuses an existing PR instead of creating a second one, so multiple callers publishing the SAME run's follow-up (the generic post-delivery call, and the stack driver's own later pass) never double-publish. published=false with a nil error means nothing was deferred - the normal case for every non-split delivery.

type PRTitlePolicy

type PRTitlePolicy struct {
	Title   TitleRule   `toml:"title"`
	Summary SummaryRule `toml:"summary"`
}

PRTitlePolicy is the parsed shape of the OPTIONAL workspace PR-title policy.

func LoadPRTitlePolicy

func LoadPRTitlePolicy(workspaceRoot, policyPath string) (*PRTitlePolicy, error)

LoadPRTitlePolicy reads the OPTIONAL workspace PR-title policy, when present. policyPath is the workflow-declared pr_title_policy path relative to workspaceRoot; an empty policyPath selects the default DefaultPRTitlePolicyPath. The asymmetry is deliberate and must be kept: a missing DEFAULT policy returns (nil, nil) — an unconfigured workspace validates nothing (legacy behavior) — but a caller-declared EXPLICIT custom policy path that does not exist is a config error, not an absent policy, so it is a permanent RefusalError naming the declared file (declared config that is missing is a config error). A workflow that declares the DEFAULT path explicitly has opted in just the same: an explicit declaration, even when it equals the default, is a declared file. Any other read error, malformed TOML, or strict decode error is a permanent RefusalError too: each is a config condition the workspace must fix before delivery can ever pass.

func (*PRTitlePolicy) Validate

func (p *PRTitlePolicy) Validate(title, summary string) error

Validate checks title and summary against the policy rules in a fixed order: (a) a policy exists, so the title is non-empty; (b) the pattern matches; (c) the captured scope is in the scope list; (d) title rune bounds; (e) summary presence and rune bounds; (f) sentence bounds. It is deterministic and returns *PRMetadataError only, never RefusalError. Every hint names the violated rule, the rule value, the received value, and the field to fix (pr_title or pr_summary). Title and summary values are redacted before they are embedded in a hint.

type Policy

type Policy struct {
	Kind                  string
	Mode                  string
	Provider              string
	Base                  string
	TitleTemplate         string
	CommitMessageTemplate string
	MaxTitleBytes         int
	MaxCommitMessageBytes int
	// OnFailure names the step the run returns to when delivery fails for a
	// reason an agent can repair. Empty means the run holds for a person.
	OnFailure string
	// PRTitlePolicyPath is the workflow-relative path of the project PR-title
	// policy. Empty selects the default .mivia/policy/pr-title.toml.
	PRTitlePolicyPath string
	// OnPRMetadataFailure names the step that repairs PR-metadata failures.
	// Empty defaults to OnFailure.
	OnPRMetadataFailure string
	// OnDiffSizeFailure names the step that repairs an over-limit delivered
	// diff (a DiffSizeError). Empty defaults to OnFailure, which keeps
	// pre-existing stacking workflows on their declared generic repair step.
	OnDiffSizeFailure string
	// MaxRepairs bounds the delivery repair cycle for this run. Zero or a
	// negative value selects DefaultMaxDeliveryRepairs; the workflow TOML's
	// delivery.max_repairs sets it per workflow.
	MaxRepairs int
	// StackingHardLines is the resolved hard per-chunk diff-size limit
	// (added+deleted lines) when the workflow has a resolved stacking
	// configuration (CompiledWorkflow.Stacking). Zero means no stacking
	// config: delivery runs single-PR behavior with no size gate.
	StackingHardLines int
	// SplitDeferred mirrors StackingConfig.SplitDeferred (§5.2-5.3): when
	// true and a chunk's delivered diff exceeds StackingHardLines,
	// checkChunkDiffSize computes a host-side deterministic split instead of
	// returning a DiffSizeError. Opt-in (default false).
	SplitDeferred bool
}

Policy is the snapshotted delivery policy of one workflow run. It is derived from the admitted compiled workflow (snapshot DefinitionTOML), never from a re-read of a changed file.

func FromCompiled

func FromCompiled(wf *definition.CompiledWorkflow) (Policy, bool)

FromCompiled returns the delivery policy of a compiled workflow and whether publication is required (kind=pull_request and mode in draft|ready).

func (Policy) RenderCommitMessage

func (p Policy) RenderCommitMessage(inputs map[string]string) (string, error)

RenderCommitMessage renders commit_message_template against the admitted inputs. This is BODY content only (trailers, "Delivers: ..." context) - the commit SUBJECT line is always the agent's own pr_title (see buildCommitMessage in deliver_stage.go), never this template, so the workspace commit-message policy's subject rules are enforced against something the agent can actually edit. If the rendered result exceeds MaxCommitMessageBytes, it is truncated at a byte boundary with "..." appended.

func (Policy) RenderTitle

func (p Policy) RenderTitle(inputs map[string]string) (string, error)

RenderTitle renders title_template against the admitted inputs.

Two ceilings apply and the stricter of the two wins:

  • MaxTitleBytes (byte cap, default DefaultMaxTitleBytes): truncation at a word boundary when a space exists, rune-safe otherwise.
  • MaxTitleRunes (GitHub's hard 256-character limit, rune count): GitHub rejects titles longer than 256 characters, so a rendered title is never longer than MaxTitleRunes runes, regardless of MaxTitleBytes.

func (Policy) Validate

func (p Policy) Validate() error

Validate rejects unsupported policy shapes. Returns an error suitable for a permanent delivery refusal.

func (Policy) ValidateCommitSubject

func (p Policy) ValidateCommitSubject(workspaceRoot, subject string) error

ValidateCommitSubject validates ONE commit subject line (the agent's own pr_title, already resolved and PR-title-policy-validated by validatePRMetadata - see deliver.go) against the OPTIONAL workspace commit-message policy file (.mivia/policy/commit-message.json) under workspaceRoot, when present. An absent file validates nothing. A non-conforming subject is a repairable PRMetadataError: the agent controls pr_title, so the SAME repair hint that already tells it to fix pr_title for the PR-title policy also fixes this - there is exactly one subject line and exactly one field the agent edits to change it. An unreadable or malformed policy file is a permanent RefusalError: that is a workspace configuration defect, not something any agent edit can fix.

type RealGit

type RealGit struct{}

RealGit implements GitRunner with exec.CommandContext("git", args...), no shell.

func (RealGit) Run

func (RealGit) Run(ctx context.Context, gc GitContext, args ...string) (string, error)

Run executes git with args in the pinned context. It returns the combined stdout and stderr. On failure it returns the output and a wrapped error that includes the git stderr.

type RefusalError

type RefusalError struct{ Reason string }

RefusalError marks a permanent, non-retryable delivery refusal. The run may CAS to delivery_failed. Transient failures are plain errors and leave the run delivery_pending for a retry.

func (*RefusalError) Error

func (e *RefusalError) Error() string

Error implements error.

type Request

type Request struct {
	RunID          string
	WorkflowDigest string
	Policy         Policy
	Inputs         map[string]string
	BaseCommit     string
	Branch         string
	GitCtx         GitContext
	OriginURL      string
	// Stage is an optional observability callback. Deliver invokes it once
	// per numbered delivery stage with a stable stage name ("guard",
	// "eligibility", "no_diff", "commit", "push", "pr", "success",
	// "failed") and a free-form detail. It is nil-safe: a nil Stage is a
	// silent no-op. The CLI prints these lines to stderr. The session engine
	// publishes them to the events bus when a bus is wired.
	Stage func(stage, detail string)
}

Request is the host-verified delivery invocation for one run.

type Result

type Result struct {
	Mode, BaseRef, HeadRef, CommitSHA, Provider, RemoteID, URL, Status, DiffRef string
}

Result is the durable outcome of a delivery attempt.

func Deliver

func Deliver(ctx context.Context, repo ledger.Repository, git GitRunner, pr PRClient, req Request) (Result, error)

Deliver performs one delivery attempt for a delivery_pending run. Refusals (RefusalError) are permanent and are only returned BEFORE this attempt writes any delivery record: eligibility failures and retry-path verification failures never touch the existing record, so a refusal can never destroy the prior attempt's CommitSHA/TreeSHA resume data. Failures once the attempt is in flight (after the stage record) are plain errors with the record marked failed, so the run stays delivery_pending for a retry.

type SummaryRule

type SummaryRule struct {
	Required     bool `toml:"required"`
	MinChars     int  `toml:"min_chars"`
	MaxChars     int  `toml:"max_chars"`
	MinSentences int  `toml:"min_sentences"`
	MaxSentences int  `toml:"max_sentences"`
}

SummaryRule holds the summary-side rules of the PR-title policy. A zero or negative numeric bound is unset and means UNLIMITED. Required defaults to false when absent.

type TitleRule

type TitleRule struct {
	Pattern  string   `toml:"pattern"`
	MinChars int      `toml:"min_chars"`
	MaxChars int      `toml:"max_chars"`
	Scopes   []string `toml:"scopes"`
}

TitleRule holds the title-side rules of the PR-title policy. A zero or negative numeric bound is unset and means UNLIMITED. An empty Pattern and an empty Scopes list disable those rules.

Jump to

Keyboard shortcuts

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