provider

package
v0.0.0-...-0879ac6 Latest Latest
Warning

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

Go to latest
Published: Aug 25, 2026 License: MIT Imports: 35 Imported by: 0

Documentation

Index

Constants

View Source
const (
	DefaultGetDeadline      = 15 * time.Second
	DefaultListDeadline     = 30 * time.Second
	DefaultMutateDeadline   = 20 * time.Second
	DefaultCommentDeadline  = 15 * time.Second
	DefaultReadbackDeadline = 15 * time.Second
)

Default per-operation deadlines. Chosen so a hung board (observed 30–90s stalls) fails closed well before a worker lane parks indefinitely, while still allowing slow-but-alive pagination and readback.

View Source
const (
	KaneoGraphDeadlineThreshold = 64
	KaneoGraphMinDeadline       = 2 * time.Minute
)
View Source
const (
	CapabilityFileName = ".herd/lease-capability.json"
	AckFileName        = ".herd/lease-ack.json"
	HandbackNoteName   = ".herd/LEASE-HANDBACK.md"
	HandbackDoneName   = ".herd/lease-handback.done"
	HandbackIntentName = ".herd/lease-handback.intent"
	SessionFileName    = ".herd/lease-session.json"
)
View Source
const (
	KaneoLargeBoardThreshold          = 500
	DefaultKaneoLargeBoardConcurrency = 64
	MaxKaneoGraphConcurrency          = 128
	KaneoGraphBatchSize               = 256
)

Large-board Kaneo snapshots need more parallelism because Kaneo exposes no project-level relation endpoint. Keep the increase provider-specific and capped: ordinary list deadlines remain unchanged and a large board cannot turn into an unbounded request storm.

View Source
const (
	StatusToDo       = "to-do"
	StatusInProgress = "in-progress"
	StatusInReview   = "in-review"
	StatusDone       = "done"
	StatusPlanned    = "planned"
	StatusArchived   = "archived"
	StatusUnknown    = "unknown"
)

Canonical task lifecycle statuses at the adapter boundary.

View Source
const (
	DefaultReadMaxAttempts = 3
	DefaultReadBaseBackoff = 50 * time.Millisecond
	DefaultReadMaxBackoff  = 400 * time.Millisecond
)

Default read-retry policy. Mutations must never use RetryRead — ambiguous writes go through ReconcileStatus instead of blind re-apply.

View Source
const CommentOpTagPrefix = "[herd-op:"

CommentOpTag is the live-board marker binding a comment body to one opID. Exactly-once comment identity: two ops with the same body remain distinct.

View Source
const DefaultBulkRelationConcurrency = 16

DefaultBulkRelationConcurrency bounds concurrent per-task relation fetches for ordinary O(board) project graph snapshots (measured ~4s for 164 tasks @16). Kaneo uses a larger, still bounded pool for genuinely large boards; see the Kaneo-specific constants below.

View Source
const DefaultFenceBusyTimeout = 3 * time.Second
View Source
const DefaultMaxListPages = 50

DefaultMaxListPages is the safety ceiling for multi-page listing loops. Hitting this cap without observing an empty page is a hard error.

View Source
const EnvKaneoRelationsCLI = "HERD_KANEO_RELATIONS_CLI"

EnvKaneoRelationsCLI is an explicit local-development escape hatch for boards whose HTTP relation endpoint is unavailable while the authenticated Kaneo CLI remains healthy. It is intentionally opt-in: production keeps the origin-bound HTTP graph path and still fails closed when credentials are not available.

View Source
const MaxCLIStderrBytes = 4096

MaxCLIStderrBytes caps captured CLI stderr so credential-bearing or voluminous error dumps cannot flood logs or status projections.

View Source
const OperationKindClaim = "claim"

OperationKindClaim is the kind for ClaimTask transitions.

Variables

View Source
var (
	ErrLabelOwnershipUnknown   = errors.New("label ownership unknown")
	ErrLabelTransactionBlocked = errors.New("label transaction durably blocked")
	ErrLabelGenerationMismatch = errors.New("label generation mismatch")
)
View Source
var ErrBoardFrozen = errors.New("provider: board is frozen")

ErrBoardFrozen is wrapped into every mutation BoundClient refuses while the FAC-103 board-freeze gate is active. Callers use errors.Is against this to distinguish a deliberate freeze refusal from a transport error.

View Source
var ErrDuplicatePage = fmt.Errorf("provider pagination: non-empty duplicate page without empty-page termination")

ErrDuplicatePage is returned when a non-empty page contributes zero new IDs without empty-page termination — incomplete listing, fail closed.

View Source
var ErrGraphCredentialsRequired = errors.New("kaneo: project graph snapshot requires HTTP credentials (KANEO_API_KEY or kaneo profile api_key); refusing silent CLI relation fan-out")

ErrGraphCredentialsRequired is returned when ListProjectRelations would otherwise fall through to N CLI subprocesses (use_cli without API key).

View Source
var ErrPaginationCap = fmt.Errorf("provider pagination: page cap reached without empty-page termination")

ErrPaginationCap is returned when DefaultMaxListPages (or the caller's cap) is exhausted without observing an empty page.

View Source
var ErrRedundantLabelAttach = fmt.Errorf("redundant label attach returned null success")

Functions

func ActiveStatuses

func ActiveStatuses() []string

ActiveStatuses lists the non-terminal columns, in board order. Callers that drop done/archived should read StatusActive rather than the whole board.

func AfterMutation

func AfterMutation(
	parent context.Context,
	r StatusReader,
	deadlines Deadlines,
	provider, op, taskID, want string,
	writeErr error,
) error

AfterMutation handles the post-write path:

  • writeErr == nil → VerifyStatusReadback via GetTask (normal readback)
  • writeErr is timeout/cancel → ReconcileStatus (no second write)
  • other writeErr → returned as-is

parent is the caller context (not the expired write child). deadlines bound the readback/reconcile GetTask.

func ApplyDeadlines

func ApplyDeadlines(tp TaskProvider, d Deadlines)

ApplyDeadlines sets per-op deadlines on known production provider types. Unknown TaskProvider implementations are left unchanged (FAC-155 may wrap).

func AsTimeout

func AsTimeout(provider, op string, kind OpKind, deadline time.Duration, err error) error

AsTimeout wraps err as *TimeoutError when it represents a deadline/cancel, preserving an existing *TimeoutError. Non-timeout errors are returned as-is.

func AttachAuthoritativeReceiver

func AttachAuthoritativeReceiver(tp TaskProvider, store FenceStore)

AttachAuthoritativeReceiver wires AuthBroker + RequireCASMeta on Kaneo.

func AttachCoordinatorMinter

func AttachCoordinatorMinter(k *KaneoProvider, m *FenceBrokerMinter) error

AttachCoordinatorMinter grants a KaneoProvider the ability to mint capabilities at mutate time (coordinator processes only).

func AttachFenceHeaders

func AttachFenceHeaders(ctx context.Context, set func(key, value string))

AttachFenceHeaders sets X-Herd-Fence / X-Herd-Op when CAS meta is present. Production Kaneo HTTP and CLI-compatible sandboxes must honor these.

func AwaitLeaseAck

func AwaitLeaseAck(worktreePath string, cap *LeaseCapability, timeout time.Duration) error

AwaitLeaseAck polls for a valid worker ack of the single capability.

func BoundOp

BoundOp derives a child context for an external provider boundary call. Production callers (daemon/dispatch) must use this (or equivalent) instead of context.Background() across TaskProvider methods.

func CanonicalClaimDir

func CanonicalClaimDir(startDir, override string) (string, error)

CanonicalClaimDir resolves the single shared claim/fence/outbox directory. When HERD_CLAIM_DIR is set (fleet shared volume), that path is authoritative so fence-provision and every production command open the same directory. Otherwise: <canonical-root>/.herd/claim (never worktree-relative). override is typically $HERD_ROOT / $HERD_REPO_ROOT when HERD_CLAIM_DIR is unset.

func CanonicalRepoIdentity

func CanonicalRepoIdentity(startDir string) (string, error)

CanonicalRepoIdentity returns a stable absolute repo identity for LeaseKey.Repo so two worktrees of the same repository cannot mint independent generation-1 leases for the same card (FAC-147).

Prefer git --git-common-dir from startDir so linked worktrees collide on the main repository root. HERD_ROOT / HERD_REPO_ROOT short-circuit only when startDir is empty or "." (process cwd) — never when the caller passes an explicit worktree path, or independent checkouts would falsely share identity via the env override.

func CapabilityPresent

func CapabilityPresent(worktreePath string) (bool, error)

CapabilityPresent reports whether a valid capability file exists.

func ClaimRequestFor

func ClaimRequestFor(key claim.LeaseKey, ownerID, role string) claim.ClaimRequest

ClaimRequestFor is the production-shaped ClaimRequest for tests.

func ClassifyContextErr

func ClassifyContextErr(ctx context.Context, provider, op string, kind OpKind, deadline time.Duration) error

ClassifyContextErr maps a finished context error to a TimeoutError when appropriate. Returns nil when ctx is still live (err should come from the call, not the context).

func ClearCoordinatorMinter

func ClearCoordinatorMinter(k *KaneoProvider)

ClearCoordinatorMinter removes mint authority (tests / worker hardening).

func CommentOpTaggedBody

func CommentOpTaggedBody(body, opID string) string

CommentOpTaggedBody appends an op-bound marker so live ListLiveComments can prove this exact operation (not a prior substring-matching comment).

func CompareRefs

func CompareRefs(a, b string) int

CompareRefs orders ticket refs with numeric awareness: FAC-9 < FAC-61 < FAC-100. Plain string comparison would put FAC-100 before FAC-99, breaking the Priority DESC, Ref ASC claim-order invariant. Non-conforming refs fall back to lexical comparison.

func ConfigureKaneoFenceBroker

func ConfigureKaneoFenceBroker(k *KaneoProvider, client *FenceBrokerClient) error

ConfigureKaneoFenceBroker attaches a worker FenceBroker client after live check. Refuses any client that appears to carry mint authority via type confusion.

func DecodeJSONBytes

func DecodeJSONBytes(statusCode int, body []byte, v interface{}) error

DecodeJSONBytes is the byte-slice form of DecodeJSONResponse for CLI/stdout paths.

func DecodeJSONResponse

func DecodeJSONResponse(resp *http.Response, v interface{}) error

DecodeJSONResponse reads the full response body, rejects non-2xx statuses, rejects 2xx bodies that carry structured error payloads, then unmarshals into v. When v is nil, only status/error-body checks run (useful for mutations).

func DefaultOwnerID

func DefaultOwnerID() string

DefaultOwnerID returns ProcessOwnerID or a fail-closed sentinel that cannot acquire leases (empty string is rejected by Claim). Callers that must surface the error should use ProcessOwnerID.

Deprecated for new call sites: prefer ProcessOwnerID and propagate err.

func DetectErrorBody

func DetectErrorBody(statusCode int, body []byte) error

DetectErrorBody inspects a response body for structured error payloads under any HTTP status, including 200 OK. Returns a non-nil *ProviderError when the body carries an error object, error string, or GraphQL errors array. Empty or non-JSON bodies are not treated as structured errors.

func EmbedStatusReceiptInDescription

func EmbedStatusReceiptInDescription(desc, receiptJSON string) string

EmbedStatusReceiptInDescription replaces any prior receipt footer and appends the signed receipt so a single PUT of description+status is one atomic remote effect on real Kaneo.

func EncodeRevision

func EncodeRevision(t *Task) claim.ProviderRevision

func EncodeStatusReceiptJSON

func EncodeStatusReceiptJSON(r *StatusMutationReceipt) (string, error)

EncodeStatusReceiptJSON serializes the receipt for description embedding.

func EnsureTaskRoleLabel

func EnsureTaskRoleLabel(ctx context.Context, p TaskLabelProvider, targetID, role string) (err error)

EnsureTaskRoleLabel repairs a label-less target without inventing a source task. It is the safe path for orphan/zero-label rows; an empty role remains blocked because no intended authority exists.

func EnsureTaskRoleLabelWithOptions

func EnsureTaskRoleLabelWithOptions(ctx context.Context, p TaskLabelProvider, targetID, role string, opts LabelRepairOptions) error

func Handback

func Handback(ctx context.Context, mgr *claim.ClaimManager, key claim.LeaseKey, ownerID string, generation int64) error

Handback releases the worker-held lease.

func HandbackDoneMatches

func HandbackDoneMatches(worktreePath, owner string, gen int64) bool

HandbackDoneMatches reports whether a valid done receipt matches gen/owner.

func Heartbeat

func Heartbeat(ctx context.Context, mgr *claim.ClaimManager, key claim.LeaseKey, ownerID string, generation int64) (*claim.Lease, error)

Heartbeat renews the lease under the worker session owner.

func HerdrSessionOwnerID

func HerdrSessionOwnerID(tabID, paneID, agentName, receiptToken string) string

HerdrSessionOwnerID binds lease handoff to a live Herdr agent session. Inputs are tab/pane/agent identity plus the proven prompt receipt token so a coordinator cannot invent a fake worker owner without the launch proof material.

func HoldIdentitiesFor

func HoldIdentitiesFor(key claim.LeaseKey, role string) []lifecycle.HoldIdentity

HoldIdentitiesFor builds the exact lane/task composite Claim requires.

func IsActiveStatus

func IsActiveStatus(raw string) bool

IsActiveStatus reports whether a card's own status is non-terminal.

func IsAmbiguous

func IsAmbiguous(err error) bool

IsAmbiguous reports whether err is (or wraps) an AmbiguousMutationError.

func IsClaimConflict

func IsClaimConflict(err error) bool

IsClaimConflict reports whether err is an active-lease conflict.

func IsRecoverableTimeout

func IsRecoverableTimeout(err error) bool

IsRecoverableTimeout reports whether err is a pure timeout (not ambiguous mutation). Safe for capped read retries; unsafe for blind mutation retry.

func IsTimeout

func IsTimeout(err error) bool

IsTimeout reports whether err is (or wraps) a provider TimeoutError or a context deadline/cancel. Prefer this over raw errors.Is for lane BLOCKED projection (provider_timeout).

func KeyFromCapability

func KeyFromCapability(cap *LeaseCapability) claim.LeaseKey

KeyFromCapability rebuilds the lease key from a capability file.

func KnownImplementationRoles

func KnownImplementationRoles() []string

KnownImplementationRoles is the effective vocabulary: generic roles plus any registered project roles. Error messages use this so an operator sees what the check actually accepts rather than a partial list.

func LeaseKey

func LeaseKey(repo, providerType, projectID, taskRef string) claim.LeaseKey

LeaseKey builds the claim.LeaseKey with git-common-dir canonical Repo identity for every non-empty path (not just "."). Two registered worktrees of the same repository MUST produce identical keys.

func MatchCommentOp

func MatchCommentOp(liveBody, wantBody, opID string) bool

MatchCommentOp reports whether liveBody is the op-bound comment for wantBody+opID. When opID is set, the [herd-op:id] marker is required — bare free-text equality is never sufficient (prevents cross-op collapse).

func MatchStatusOpEvidence

func MatchStatusOpEvidence(liveBody, opID, status string) bool

MatchStatusOpEvidence is deliberately always false: forgeable substring tags ([herd-status-op:...]) are never authentic evidence (FAC-147 lm0ihu).

func MintChallenge

func MintChallenge() (string, error)

MintChallenge returns a random challenge nonce for the handshake.

func MintCommentCapability

func MintCommentCapability(
	secret, instanceID string,
	key claim.LeaseKey,
	boardTaskID, ownerID, opID, comment string,
	generation, expiresUnix int64,
) (string, error)

MintCommentCapability signs a comment-body-bound capability.

func MintMutationCapability

func MintMutationCapability(
	secret, instanceID string,
	key claim.LeaseKey,
	boardTaskID, ownerID, opID, status string,
	generation, expiresUnix int64,
) (string, error)

MintMutationCapability signs a status-bound capability.

func MustAcquireLease

func MustAcquireLease(t testing.TB, stack *ClaimStack, key claim.LeaseKey, owner, role, taskID string) *claim.Lease

func NormalizeStatus

func NormalizeStatus(raw string) string

NormalizeStatus maps common provider spellings to canonical lifecycle values. Unknown non-empty statuses are returned as "unknown:<raw>" so callers never treat them as to-do, done, or empty by accident. Empty maps to "unknown".

func ObservedLabelGeneration

func ObservedLabelGeneration(ctx context.Context, p TaskLabelProvider, ids ...string) (string, error)

ObservedLabelGeneration is a deterministic digest of complete task and owned-label readback. Callers may pass it back as the generation authority; arbitrary caller strings are rejected by the transaction path.

func OperationKindComment

func OperationKindComment(body string) string

OperationKindComment builds a stable kind for a comment body.

func OperationKindStatus

func OperationKindStatus(status string) string

OperationKindStatus builds a stable Begin/Complete kind for a status write.

func PaginationTerminalError

func PaginationTerminalError(d PageDecision) error

PaginationTerminalError maps a page decision to a hard error when listing must not succeed. PageStopEmpty returns nil (success). PageStopDuplicate returns ErrDuplicatePage. PageContinue returns nil so the caller loops.

func ParseHandbackDoneLegacyGen

func ParseHandbackDoneLegacyGen(s string) (int64, error)

ParseHandbackDoneLegacyGen is for tests.

func ParseStatusReceiptFromDescription

func ParseStatusReceiptFromDescription(desc string) string

ParseStatusReceiptFromDescription extracts an embedded receipt footer. Format (last occurrence wins): <!-- herd-status-receipt-v1: <json> -->

func PrepareRuntimeDefaults

func PrepareRuntimeDefaults(startDir string) error

PrepareRuntimeDefaults resolves the repo-local claim fence for a normal single-repo invocation. Fleet deployments may still provide HERD_CLAIM_DIR and HERD_FENCE_VOLUME_ID explicitly; local users should not have to copy a SQLite seal into every shell before running herd.

func ProcessOwnerID

func ProcessOwnerID() (string, error)

ProcessOwnerID returns a cryptographic process identity for lease ownership. Format: herd1.<host>.<pid>.<32-byte-hex-nonce>. Ambient HERD_OWNER_ID is never trusted. crypto/rand failure propagates — never falls back to low-entropy host+PID hashes.

func ProvisionSharedFenceForTest

func ProvisionSharedFenceForTest(t interface {
	Helper()
	Setenv(key, value string)
	Fatal(...any)
}, dir string)

ProvisionSharedFenceForTest provisions sealed fence store + env for tests.

func ReadFenceVolumeSeal

func ReadFenceVolumeSeal(claimDir string) (string, error)

ReadFenceVolumeSeal returns store_authority.volume_seal from claimDir/fences.db.

func ReadVolumeSeal

func ReadVolumeSeal(claimDir string) (string, error)

ReadVolumeSeal returns the durable volume_seal from fences.db under claimDir. Used by `herd fence-provision` to print the mint for fleet distribution.

func ReconcileStatus

func ReconcileStatus(ctx context.Context, r StatusReader, deadlines Deadlines, provider, op, taskID, want string, writeErr error) error

ReconcileStatus reads the task after an ambiguous write. Outcomes:

  • read succeeds and status matches want → nil (write landed; no re-apply)
  • read succeeds and status differs → *AmbiguousMutationError with Actual
  • read fails → *AmbiguousMutationError with ReadErr

want is normalized before comparison. This never issues a second write. deadlines bound the reconciliation GetTask (defaults applied for zero fields).

func RefreshLeaseCapabilityExpiry

func RefreshLeaseCapabilityExpiry(worktreePath string, owner string, gen int64, expiresAt time.Time) error

RefreshLeaseCapabilityExpiry updates only ExpiresAt on an existing capability after a successful Renew. Never clears handback intent/done/session — heartbeat must not erase terminal recovery evidence (parallel audit).

func RegisterProjectImplementationRoles

func RegisterProjectImplementationRoles(roles []string)

RegisterProjectImplementationRoles records repository-configured ownership roles. Empty and duplicate entries are ignored. Passing nil clears them, which restores the generic-only vocabulary.

func RemoveLeaseCapability

func RemoveLeaseCapability(worktreePath string) error

RemoveLeaseCapability removes capability + ack via openat unlinkat.

func RepairTaskRoleLabel

func RepairTaskRoleLabel(ctx context.Context, p TaskLabelProvider, sourceID, targetID, role string) (err error)

RepairTaskRoleLabel transfers role authority without moving the source row. It is idempotent when target already has exactly one owned role label. The new label is created for target before it is attached, so a source label ID can never be reused for another task.

func RepairTaskRoleLabelWithOptions

func RepairTaskRoleLabelWithOptions(ctx context.Context, p TaskLabelProvider, sourceID, targetID, role string, opts LabelRepairOptions) error

RepairTaskRoleLabelWithOptions is the production entrypoint. The legacy wrapper remains for adapters and focused unit tests that do not persist evidence.

func RequireTaskRole

func RequireTaskRole(task *Task, role string) (string, error)

RequireTaskRole returns the exact label matching role, or role when the task is unlabeled. Fail-closed when labels exist but none match role — never substitutes an arbitrary first label (hold c6ic8im #4).

func ResolveKaneoAPIKey

func ResolveKaneoAPIKey(override string) string

ResolveKaneoAPIKey is retained for tests/callers that only need a key string. Prefer ResolveKaneoProfileCred + origin checks for HTTP authorization. Order: explicit override → KANEO_API_KEY env → profile key (only when profile has a resolvable api_url; key alone is never returned from profile without origin).

func ResolveKaneoProfileCred

func ResolveKaneoProfileCred() kaneoOriginCred

ResolveKaneoProfileCred loads the selected default_profile's api_key and api_url together and returns an origin-bound credential. Empty TrustedOrigin or Key means unusable. Never scans an arbitrary first profile; never logs the key.

func ResolveKaneoProjectID

func ResolveKaneoProjectID(rootDir string) string

ResolveKaneoProjectID attempts to read project ID from .herd/kaneo.json, falling back to root .kaneo.json

func ResolveTaskRole

func ResolveTaskRole(task *Task, role string) string

ResolveTaskRole picks the exact role string ClaimManager.Claim will accept: a matching label when present, otherwise role (for unlabeled tasks). Prefer TaskOwnershipRole for production board mutations (pt5t7 #1).

func RetryRead

func RetryRead(ctx context.Context, policy RetryPolicy, fn func(context.Context) error) error

RetryRead invokes fn until it succeeds, RetryIf rejects the error, attempts are exhausted, or ctx is done. fn receives the same ctx (caller should derive per-attempt deadlines inside fn if needed).

NEVER use for ClaimTask / UpdateStatus / AddComment — those are not idempotent under ambiguous timeout; use ReconcileStatus instead.

func RunCLIOutput

func RunCLIOutput(ctx context.Context, name string, args ...string) ([]byte, error)

func ScrubWorkerMintEnv

func ScrubWorkerMintEnv()

ScrubWorkerMintEnv removes mint material from the process environment so workers cannot inherit launcher secrets. Safe to call on every worker start.

func StatusOpEvidenceBody

func StatusOpEvidenceBody(opID, status string) string

StatusOpEvidenceBody is empty: never dual-write post-status comment tags.

func StatusReceiptKey

func StatusReceiptKey() ([]byte, error)

StatusReceiptKey returns the HMAC key for status receipts. Production: HERD_FENCE_HMAC_KEY only (min 16 chars). Tests: fixed key when env unset (compiled binary must set the env).

func StripStatusReceiptFooter

func StripStatusReceiptFooter(desc string) string

StripStatusReceiptFooter returns description without receipt footers (for human-facing display / re-embed).

func TaskHasVerifiedStatusReceipt

func TaskHasVerifiedStatusReceipt(t *Task, wantTask, wantOp, wantStatus string, wantFence int64) bool

TaskHasVerifiedStatusReceipt reports whether the task carries a signed receipt (from description footer readback) for the exact binding.

func TaskOwnershipRole

func TaskOwnershipRole(task *Task, preferred string) (string, error)

TaskOwnershipRole resolves the durable implementation role for board mutations. Coordinator/reviewer sessions claim under this task role while ownerID carries the session identity.

Order: preferred if it matches a label AND is a known implementation role (or preferred matches any label when preferred itself is known) → first known implementation label on the task → unlabeled uses preferred only if known → fail closed on unknown/sole-unknown labels.

func TryAutoHandback

func TryAutoHandback(ctx context.Context, mgr *claim.ClaimManager, worktreePath string) error

TryAutoHandback is receipt-driven and fail-closed on lost authority (pt5t7 #2): launched sessions require matching generation-bound done + remote release.

func ValidRelationType

func ValidRelationType(t RelationType) bool

ValidRelationType reports whether t is a known provider relation type.

func ValidateSharedMarker

func ValidateSharedMarker(dir string) error

ValidateSharedMarker reads volume_seal from fences.db (authoritative). A copied volume_id in SHARED alone cannot pass without the sealed DB row.

func VerifyFieldReadback

func VerifyFieldReadback(taskID, field, expected, actual string, fold bool) error

VerifyFieldReadback is a generic string equality check for non-status fields (title, assignee, etc.). Whitespace is trimmed; comparison is case-sensitive unless fold is true.

func VerifyLeaseAck

func VerifyLeaseAck(cap *LeaseCapability, ack *LeaseAck) error

VerifyLeaseAck checks ack matches the single capability challenge.

func VerifyProviderContract

func VerifyProviderContract(ctx context.Context, p TaskProvider, projectID string) error

VerifyProviderContract sanity-checks that a TaskProvider implements basic API guarantees

func VerifyStatusReadback

func VerifyStatusReadback(taskID, expected, actual string) error

VerifyStatusReadback compares expected vs actual status after a mutation. Both sides are normalized before comparison so provider aliases do not produce false drift. Empty actual (missing task / unknown state) always fails.

func VerifyStatusReceipt

func VerifyStatusReceipt(r *StatusMutationReceipt, wantTask, wantOp, wantStatus string, wantFence int64) error

VerifyStatusReceipt checks MAC and field binding (constant-time compare).

func WithCASExpectation

func WithCASExpectation(ctx context.Context, status, commentBody string) context.Context

WithCASExpectation records what the mutation must achieve for ambiguous reconciliation (status and/or comment body).

func WithCASMeta

func WithCASMeta(ctx context.Context, fenceToken int64, opID string) context.Context

WithCASMeta attaches fence token + opID for TaskProvider HTTP transport (Kaneo X-Herd-Fence / X-Herd-Op). FencedCAS.CompareAndSwap injects these into the mutate context so the authoritative service can enforce them.

func WithMintIdentity

func WithMintIdentity(ctx context.Context, id MintIdentity) context.Context

WithMintIdentity attaches immutable mint lease identity for one mutation call.

func WithOpDeadline

func WithOpDeadline(ctx context.Context, d Deadlines, op OpKind) (context.Context, context.CancelFunc)

WithOpDeadline derives a child context bounded by the op deadline. If the parent already has a nearer deadline, that nearer bound wins (context.WithTimeout still respects parent).

Always pair with defer cancel(). Never pass context.Background() across a provider boundary without this wrapper — that is the FAC-150 hang class.

func WriteHandbackDone

func WriteHandbackDone(worktreePath string, rec HandbackDoneReceipt) error

WriteHandbackDone writes a generation-bound durable receipt + parent fsync.

func WriteHandbackIntent

func WriteHandbackIntent(worktreePath string, intent HandbackIntent) error

WriteHandbackIntent persists intent before remote Release (crash recovery).

func WriteHandbackNote

func WriteHandbackNote(worktreePath string) error

WriteHandbackNote writes LEASE-HANDBACK.md under openat-held .herd.

func WriteLaunchSession

func WriteLaunchSession(worktreePath string, sess LaunchSession) error

WriteLaunchSession persists session state (completion mark).

func WriteLeaseAck

func WriteLeaseAck(worktreePath string, ack LeaseAck) error

WriteLeaseAck is the worker-side acknowledgement of the single capability.

func WriteLeaseCapability

func WriteLeaseCapability(worktreePath string, cap LeaseCapability) error

WriteLeaseCapability writes capability + launch session for a NEW handoff. Clears stale done/intent/ack only when generation changes. Same generation refresh must use RefreshLeaseCapabilityExpiry (heartbeat) so terminal recovery evidence is never erased.

func WriteSharedMarker

func WriteSharedMarker(dir string) error

WriteSharedMarker provisions the shared fence authority once:

  1. Creates fences.db with an internal store_authority.volume_seal row (trustworthy: seal lives only inside the atomic SQLite store).
  2. Writes SHARED human-readable pointer (not the source of truth).

ValidateSharedMarker reads the seal FROM THE DB, not from forgeable fields.

Types

type AmbiguousMutationError

type AmbiguousMutationError struct {
	Provider string
	Op       string
	TaskID   string
	Want     string // desired status or comment marker
	// WriteErr is the original timeout/cancel (or transport) failure.
	WriteErr error
	// ReadErr is set when post-timeout reconciliation read also failed.
	ReadErr error
	// Actual is the observed status when the read succeeded but did not match.
	Actual string
}

AmbiguousMutationError reports that a write may or may not have landed after a timeout/cancel. Callers must NOT blind-retry the write and must NOT treat the mutation as success. Reconcile via readback / outbox.

func (*AmbiguousMutationError) Error

func (e *AmbiguousMutationError) Error() string

func (*AmbiguousMutationError) Unwrap

func (e *AmbiguousMutationError) Unwrap() error

type AuthBroker

type AuthBroker struct {

	// CrashAt is an optional injected test seam (never ambient env).
	// Production is always nil. Test builds install via claim.TestSeams.
	// Values: "before-remote" | "after-remote". May os.Exit / panic.
	// after-remote fires AFTER remote backend, BEFORE local revision persist.
	CrashAt func(phase string)
	// RevisionOf optional live revision after a successful backend write
	// (e.g. task.UpdatedAt). Bound into the applied receipt for evidence.
	RevisionOf func(ctx context.Context, taskID string) (string, error)
	// StatusOpEvidence is LIVE provider readback of op-bound status evidence
	// for THIS op. Client HMAC/description receipts are NOT valid production
	// evidence (audit con62fkm). Prefer ServerOpDedupe re-submit when the
	// remote atomically dedupes op IDs with status.
	StatusOpEvidence func(ctx context.Context, taskID, opID, expStatus string) (bool, error)
	// ServerOpDedupe when true: empty-rev Present without StatusOpEvidence
	// re-submits the same op (server must no-op if already applied) instead
	// of fail-closed settle or forgeable client receipts.
	ServerOpDedupe bool
	// contains filtered or unexported fields
}

AuthBroker is the production authoritative receiver over FenceStore.

func NewAuthBroker

func NewAuthBroker(store FenceStore) *AuthBroker

NewAuthBroker builds a broker over the shared ClaimStack FenceStore.

func NewLocalAuthReceiver

func NewLocalAuthReceiver(store FenceStore) *AuthBroker

NewLocalAuthReceiver keeps the prior name as an alias for tests.

func (*AuthBroker) BindRevisionReader

func (b *AuthBroker) BindRevisionReader(get func(ctx context.Context, taskID string) (*Task, error)) *AuthBroker

BindRevisionReader wires live task revision into applied receipts. Required for status mutations so ambiguous recovery can bind exact evidence. Uses EncodeRevision so AuthBroker and FencedCAS share one revision format.

func (*AuthBroker) Execute

func (b *AuthBroker) Execute(
	ctx context.Context,
	taskID string,
	fenceToken int64,
	opID string,
	expStatus, expComment string,
	backend func(ctx context.Context) error,
	effectMet func(ctx context.Context) (EffectState, error),
) error

type AuthoritativeReceiver

type AuthoritativeReceiver interface {
	// Execute is the sole mutate entrypoint. backend is the remote
	// Kaneo CLI/HTTP call. effectMet is LIVE provider readback only
	// (never local receipt substitution): Present / Absent / Unknown.
	Execute(
		ctx context.Context,
		taskID string,
		fenceToken int64,
		opID string,
		expStatus, expComment string,
		backend func(ctx context.Context) error,
		effectMet func(ctx context.Context) (EffectState, error),
	) error
}

AuthoritativeReceiver is the Kaneo-compatible fence/op acceptance boundary (FAC-147). Upstream Kaneo CLI/API do not natively fence; every production mutate must pass through Execute, which:

  1. holds per-task exclusive lock
  2. short-circuits pure applied retries
  3. reconciles in_progress/ambiguous ops via effectMet (no blind re-mutate)
  4. durably records in_progress BEFORE the remote backend runs
  5. runs backend, then MarkApplied with revision evidence when available

A crash after remote success and before MarkApplied leaves in_progress; restart effectMet for the SAME op sees the board effect and commits applied without a second backend call — closing provider-success/local-failure.

If no receiver is attached, production Kaneo must fail closed.

type AzureDevOpsProvider

type AzureDevOpsProvider struct {
	OrgURL     string
	Project    string
	PAT        string
	HTTPClient *http.Client
	Deadlines  Deadlines
	Retry      RetryPolicy
}

func NewAzureDevOpsProvider

func NewAzureDevOpsProvider(orgURL, project, pat string) *AzureDevOpsProvider

func (*AzureDevOpsProvider) AddComment

func (a *AzureDevOpsProvider) AddComment(ctx context.Context, taskID string, body string) error

func (*AzureDevOpsProvider) ClaimTask

func (a *AzureDevOpsProvider) ClaimTask(ctx context.Context, taskID string, role string) error

func (*AzureDevOpsProvider) GetTask

func (a *AzureDevOpsProvider) GetTask(ctx context.Context, id string) (*Task, error)

func (*AzureDevOpsProvider) ListComments

func (a *AzureDevOpsProvider) ListComments(ctx context.Context, taskID string) ([]string, error)

ListComments implements CommentReader (FAC-145 exact effect readback). Azure writes annotations to System.History, so a symmetric readback must include history revisions as well as the comments API — otherwise a delivered effect written to History is invisible and the coordinator would re-deliver it. Both sources are paginated/bounded.

func (*AzureDevOpsProvider) ListTasks

func (a *AzureDevOpsProvider) ListTasks(ctx context.Context, projectID string, status string) ([]*Task, error)

func (*AzureDevOpsProvider) UpdateStatus

func (a *AzureDevOpsProvider) UpdateStatus(ctx context.Context, taskID string, status string) error

type BoundClient

type BoundClient struct {
	Inner     TaskProvider
	Deadlines Deadlines
}

BoundClient is the production TaskProvider wrapper: every external board call gets a configured per-op deadline and timeout/ambiguous failures are labeled BLOCKED(provider_timeout) for control-plane consumers (FAC-150). Non-Kaneo live activation remains FAC-155; this type is provider-agnostic.

func NewBoundClient

func NewBoundClient(inner TaskProvider, d Deadlines) *BoundClient

NewBoundClient wraps inner with d (normalized). Inner must be non-nil.

func (*BoundClient) AddComment

func (b *BoundClient) AddComment(ctx context.Context, taskID, body string) error

func (*BoundClient) AttachTaskLabel

func (b *BoundClient) AttachTaskLabel(ctx context.Context, taskID, labelID string) error

func (*BoundClient) ClaimTask

func (b *BoundClient) ClaimTask(ctx context.Context, taskID, role string) error

func (*BoundClient) CreateRelation

func (b *BoundClient) CreateRelation(ctx context.Context, sourceID, targetID string, typ RelationType) (*Relation, error)

func (*BoundClient) CreateTask

func (b *BoundClient) CreateTask(ctx context.Context, task *Task) (*Task, error)

func (*BoundClient) CreateTaskLabel

func (b *BoundClient) CreateTaskLabel(ctx context.Context, taskID, name string) (TaskLabel, error)

func (*BoundClient) DeleteRelation

func (b *BoundClient) DeleteRelation(ctx context.Context, relationID, sourceID, targetID string) error

func (*BoundClient) DeleteTaskLabel

func (b *BoundClient) DeleteTaskLabel(ctx context.Context, labelID string) error

func (*BoundClient) DetachTaskLabel

func (b *BoundClient) DetachTaskLabel(ctx context.Context, labelID string) error

func (*BoundClient) GetTask

func (b *BoundClient) GetTask(ctx context.Context, id string) (*Task, error)

func (*BoundClient) LabelMutationAuthority

func (b *BoundClient) LabelMutationAuthority() (string, error)

func (*BoundClient) ListComments

func (b *BoundClient) ListComments(ctx context.Context, taskID string) ([]string, error)

ListComments forwards the CommentReader capability when the wrapped provider supports it (FAC-145 exact effect readback).

func (*BoundClient) ListProjectRelations

func (b *BoundClient) ListProjectRelations(ctx context.Context, projectID string) ([]Relation, error)

func (*BoundClient) ListRelations

func (b *BoundClient) ListRelations(ctx context.Context, taskID string) ([]Relation, error)

func (*BoundClient) ListTaskLabels

func (b *BoundClient) ListTaskLabels(ctx context.Context, taskID string) ([]TaskLabel, error)

func (*BoundClient) ListTaskLabelsBulk

func (b *BoundClient) ListTaskLabelsBulk(ctx context.Context, taskIDs []string) (BulkTaskLabels, error)

func (*BoundClient) ListTasks

func (b *BoundClient) ListTasks(ctx context.Context, projectID, status string) ([]*Task, error)

func (*BoundClient) ProveLabelCreation

func (b *BoundClient) ProveLabelCreation(ctx context.Context, created TaskLabel, targetID, name string, opts LabelRepairOptions) error

func (*BoundClient) UpdateStatus

func (b *BoundClient) UpdateStatus(ctx context.Context, taskID, status string) error

type BulkRelationProvider

type BulkRelationProvider interface {
	RelationProvider
	// ListProjectRelations returns the full project relation multiset (deduped
	// by id, dual-end agreement). May be O(board) concurrent requests when the
	// provider has no single project-relation endpoint; must honor ctx deadline
	// and fail closed without credentials.
	ListProjectRelations(ctx context.Context, projectID string) ([]Relation, error)
}

BulkRelationProvider is the project graph surface for SnapshotGraph (FAC-159). Kaneo 0.11.x exposes only GET /api/task-relation/:taskId (no project-level relation RPC). Production ListProjectRelations is therefore an honest O(board) credentialed concurrent HTTP fan-out under the list deadline — never silent CLI fan-out, never mislabeled as O(1) bulk.

type BulkTaskLabelProvider

type BulkTaskLabelProvider interface {
	ListTaskLabelsBulk(context.Context, []string) (BulkTaskLabels, error)
}

BulkTaskLabelProvider is optional so providers without a native task-list label projection remain compatible and fail closed at the capability boundary. The input identities may be provider IDs or human-readable refs; the result preserves each requested identity as a map key.

type BulkTaskLabels

type BulkTaskLabels struct {
	Labels    map[string][]TaskLabel `json:"labels"`
	Requested int                    `json:"requested"`
	Retrieved int                    `json:"retrieved"`
	Complete  bool                   `json:"complete"`
	Truncated bool                   `json:"truncated"`
}

BulkTaskLabels is the result of a board-wide label read. Complete is based on the requested task identities, not on whether the provider returned an error. Consumers must check it before using Labels for board analytics. Truncated is kept as an explicit positive marker for JSON/reporting paths where a missing task would otherwise look like an empty label set.

type CLIResult

type CLIResult struct {
	Stdout []byte
	Stderr []byte
}

CLIResult is the bounded output of one CLI invocation.

func RunCLI

func RunCLI(ctx context.Context, name string, args ...string) (*CLIResult, error)

RunCLI executes name+args under ctx, placing the child in its own process group so cancel/timeout kills the entire tree (not just the direct child). On deadline or cancel it returns a *TimeoutError; stdout/stderr are still populated when available.

stderr is truncated to MaxCLIStderrBytes. Never log the raw args slice when it may contain tokens — callers own redaction of mutation payloads.

func RunCLIEnv

func RunCLIEnv(ctx context.Context, extraEnv []string, name string, args ...string) (*CLIResult, error)

RunCLIEnv is RunCLI with optional extra environment entries (KEY=value). Used to transport HERD_FENCE / HERD_OP into production Kaneo CLI mutates so fence meta is not dropped when use_cli: true (FAC-147).

type CapabilityIssueRequest

type CapabilityIssueRequest struct {
	BoardTaskID string
	TaskID      string
	TaskRef     string
	Repo        string
	Provider    string
	Project     string
	OwnerID     string
	Generation  int64
	OpID        string
	Action      string // status | comment
	Status      string
	Comment     string
}

CapabilityIssueRequest is the lease-bound mint request (minter only).

type ClaimStack

type ClaimStack struct {
	Dir     string
	Leases  *claim.SQLiteLeaseStore
	Outbox  *claim.SQLiteOutbox
	Fences  FenceStore
	CAS     *FencedCAS
	Board   *FencedBoard
	Manager *claim.ClaimManager
	TP      TaskProvider
	Minter  *FenceBrokerMinter // coordinator only; nil on workers
	// OwnedBroker is set when THIS process hosts the fence broker (FAC-564).
	// Its lifetime is the stack's, so Close releases the claim-dir lock.
	OwnedBroker *CoordinatorBroker
}

ClaimStack is the production FAC-147 wiring: durable lease store + outbox + FencedCAS (ProviderCAS) + FencedBoard. cmd/herd, daemon, and dispatch open one of these so board mutations go through BeginProviderTransition/CompleteProviderTransition (and reclaim AdvanceFence) instead of bare TaskProvider writes.

Minter is coordinator-only: loaded only when HERD_FENCE_BROKER_MINT_TOKEN is set. Workers never receive the mint secret; they present pre-minted capabilities only.

func NewTestStack

func NewTestStack(t testing.TB, tp TaskProvider) *ClaimStack

NewTestStack opens an isolated ClaimStack under t.TempDir() and registers Close. Use this in fixtures so every production fail-closed path is exercised with a real stack, never a nil Claims fallback.

func NewTestStackWithBusy

func NewTestStackWithBusy(t testing.TB, tp TaskProvider, busy time.Duration) *ClaimStack

NewTestStackWithBusy builds a ClaimStack whose fence exclusive lock uses a short busy_timeout (contention / timeout tests).

func OpenCanonicalClaimStack

func OpenCanonicalClaimStack(tp TaskProvider) (*ClaimStack, error)

OpenCanonicalClaimStack opens the production claim stack. Prefers HERD_CLAIM_DIR when set so provisioned shared volumes are not ignored.

func OpenClaimStack

func OpenClaimStack(dir string, tp TaskProvider) (*ClaimStack, error)

OpenClaimStack opens (creating if needed) the durable claim/fence/outbox files under dir and wires a ClaimManager with WithProviderCAS + WithDurableOutbox over tp. Production callers must pass the path from CanonicalClaimDir / OpenCanonicalClaimStack — not a worktree-relative ".herd/claim".

func (*ClaimStack) AcquireLease

func (s *ClaimStack) AcquireLease(ctx context.Context, key claim.LeaseKey, ownerID, role, taskRole string) (*claim.Lease, error)

AcquireLease acquires a durable claim lease for taskRef. role and taskRole must satisfy ClaimManager.Claim's exact-match rules. Fail-closed: does not invent generations on conflict.

func (*ClaimStack) Close

func (s *ClaimStack) Close() error

Close releases all durable stores. Safe on nil.

func (*ClaimStack) MutateClaimGuarded

func (s *ClaimStack) MutateClaimGuarded(
	ctx context.Context,
	key claim.LeaseKey,
	ownerID, role, taskRole, taskID string,
) (*claim.Lease, error)

MutateClaimGuarded is the production ClaimTask path (status → in-progress) under a live lease generation via Begin/Complete. Fail-closed on claim conflict.

func (*ClaimStack) MutateCommentGuarded

func (s *ClaimStack) MutateCommentGuarded(
	ctx context.Context,
	key claim.LeaseKey,
	ownerID, role, taskRole, taskID, body string,
) (generation int64, err error)

MutateCommentGuarded posts a board comment under a live lease generation via Begin/Complete. Fail-closed on claim conflict.

func (*ClaimStack) MutateStatusGuarded

func (s *ClaimStack) MutateStatusGuarded(
	ctx context.Context,
	key claim.LeaseKey,
	ownerID, role, taskRole, taskID, status string,
) (generation int64, err error)

MutateStatusGuarded is the production board status write: Claim must succeed (live lease), then AdvanceFence(taskID, generation) + Begin/ Complete. On claim conflict the write is refused — contenders must not mint high+1 and preempt a live owner (FAC-147 audit fix).

type Clock

type Clock interface {
	Now() time.Time
	// After returns a channel that delivers once d has elapsed. Tests may
	// use a fake that closes immediately or on demand.
	After(d time.Duration) <-chan time.Time
}

Clock abstracts time for deterministic retry tests.

type CommentReader

type CommentReader interface {
	// ListComments returns the comment bodies currently visible on taskID.
	ListComments(ctx context.Context, taskID string) ([]string, error)
}

type ConfigDeadlineSource

type ConfigDeadlineSource interface {
	// Resolved returns get,list,mutate,comment,readback durations (0 = default).
	ResolvedDeadlines() (get, list, mutate, comment, readback time.Duration, err error)
}

ConfigDeadlineSource is the subset of config needed without importing pkg/config (avoids cycles). cmd and packages pass Resolved() parts.

type CoordinatorBroker

type CoordinatorBroker struct {
	Broker *FenceBroker
	Minter *FenceBrokerMinter
	Client *FenceBrokerClient
	// contains filtered or unexported fields
}

CoordinatorBroker is a broker owned by this process plus the authority to mint against it.

func StartCoordinatorBroker

func StartCoordinatorBroker(opts CoordinatorBrokerOptions) (*CoordinatorBroker, error)

StartCoordinatorBroker starts a fence broker INSIDE the coordinator process and returns mint authority over it.

This is the production path FAC-564 was missing. Both credentials are generated here and never leave the process: the worker token is used by this process's own client, and the mint token backs the in-process minter. Nothing is written to the claim dir and nothing is placed in the environment, so no same-UID worker can read either one.

The claim-dir flock still guarantees one live broker, so this fails closed if a standalone broker is already serving that claim volume. When a coordinator hosts its own broker, do not also run a standalone one.

func (*CoordinatorBroker) Close

func (c *CoordinatorBroker) Close() error

Close releases the broker and its claim-dir lock.

type CoordinatorBrokerOptions

type CoordinatorBrokerOptions struct {
	ClaimDir string
	// ListenAddr defaults to "unix": a socket under the claim dir, gated by
	// filesystem permissions as well as the token. Set "127.0.0.1:0" when the
	// claim dir path is too long for a unix socket on this platform.
	ListenAddr      string
	UpstreamURL     string
	UpstreamProject string
	UpstreamCLI     bool
}

CoordinatorBrokerOptions configures a coordinator that hosts its own broker.

type Deadlines

type Deadlines struct {
	Get      time.Duration
	List     time.Duration
	Mutate   time.Duration
	Comment  time.Duration
	Readback time.Duration
}

Deadlines holds per-operation bounds. Zero fields resolve to defaults via For / Normalize so callers can override a single op without restating all.

func DeadlinesFromParts

func DeadlinesFromParts(get, list, mutate, comment, readback time.Duration) Deadlines

DeadlinesFromParts builds a Deadlines value from optional config-resolved durations. Zero means "package default" after Normalize.

func DeadlinesFromResolved

func DeadlinesFromResolved(get, list, mutate, comment, readback time.Duration, err error) Deadlines

DeadlinesFromResolved maps Resolved() output to Deadlines.

func DefaultDeadlines

func DefaultDeadlines() Deadlines

DefaultDeadlines returns the repository-safe defaults for every op kind.

func (Deadlines) For

func (d Deadlines) For(op OpKind) time.Duration

For returns the deadline for op, defaulting zero fields.

func (Deadlines) Max

func (d Deadlines) Max() time.Duration

Max returns the longest configured deadline (after normalize). Useful as an HTTP client safety-net timeout independent of a specific op context.

func (Deadlines) Normalize

func (d Deadlines) Normalize() Deadlines

Normalize fills zero fields with defaults. Negative values are treated as zero (defaulted) so a misconfigured config cannot disable the bound.

type EffectState

type EffectState int

EffectState is live-provider readback for recovery (FAC-147 hold). UNKNOWN must never be treated as ABSENT (would re-mutate blindly).

const (
	// EffectAbsent: live read succeeded and the expected effect is not present.
	EffectAbsent EffectState = iota
	// EffectPresent: live read succeeded and the expected effect is present
	// for THIS op (status+revision or op-tagged comment).
	EffectPresent
	// EffectUnknown: live read failed/unavailable — stay ambiguous, never re-mutate.
	EffectUnknown
)

type FenceBroker

type FenceBroker struct {
	// contains filtered or unexported fields
}

FenceBroker is the production-enforcing sidecar for FAC-147. Process exclusivity is the claim-dir flock (one live broker). Per-task serialization uses in-process mutexes — not fences.db.locks — so a client CAS holding the shared store lock while calling the broker cannot deadlock.

func StartFenceBroker

func StartFenceBroker(cfg FenceBrokerConfig) (*FenceBroker, error)

StartFenceBroker acquires exclusive claim-dir lock and serves. Non-copyable authority is the exclusive flock on the claim volume — not a copyable absolute claim_path row. Same logical path on a shared volume cannot host two live brokers. Cross-host without shared flock is fail-closed/undefined.

func WireHermeticFenceBroker

func WireHermeticFenceBroker(t testing.TB, k *KaneoProvider, upstreamURL, claimDir string) *FenceBroker

WireHermeticFenceBroker starts an in-process FenceBroker against upstreamURL for production-shaped tests (daemon pulse, compiled claim paths). It wires worker client + claim-dir minter (after scrubbing env mint material). claimDir must be the same directory used for OpenClaimStack so leases.db is shared.

func (*FenceBroker) Addr

func (b *FenceBroker) Addr() string

func (*FenceBroker) ClientBaseURL

func (b *FenceBroker) ClientBaseURL() string

func (*FenceBroker) Close

func (b *FenceBroker) Close() error

Close shuts down and releases flock; aggregates errors (fail-closed report). Secrets (token, mintToken) are intentionally not exported — no Token()/MintToken().

func (*FenceBroker) GrantMintToChild

func (b *FenceBroker) GrantMintToChild(cmd *exec.Cmd) (func(), error)

GrantMintToChild hands this broker's mint authority to one child process over an inherited pipe, and returns a closer the parent calls after Start.

The parent writes the secret into a pipe whose read end the child inherits. The secret is never placed in the child's environment or on disk. Only this exact child can read it, because only this child has the descriptor.

func (*FenceBroker) InstanceID

func (b *FenceBroker) InstanceID() string

func (*FenceBroker) SeedTestLease

func (b *FenceBroker) SeedTestLease(ctx context.Context, key claim.LeaseKey, ownerID string, generation int64, ttl time.Duration) (*claim.Lease, error)

SeedTestLease inserts a live lease for tests (same claim-dir leases.db).

func (*FenceBroker) UnixSocket

func (b *FenceBroker) UnixSocket() string

type FenceBrokerClient

type FenceBrokerClient struct {
	BaseURL    string // http://127.0.0.1:port or http://unix
	Token      string // worker token only (never mint)
	UnixSocket string
	Client     *http.Client
}

FenceBrokerClient is the worker-facing broker client.

It holds ONLY the worker mutate credential. Per-op capabilities are passed immutably to MutateStatus (never stored on the shared client — concurrent tasks must not cross-wire grants).

func NewFenceBrokerClientForTest

func NewFenceBrokerClientForTest(b *FenceBroker) *FenceBrokerClient

NewFenceBrokerClientForTest builds a worker client for a running broker. Uses unexported broker fields only (no exported secret getters).

func NewFenceBrokerClientFromEnv

func NewFenceBrokerClientFromEnv() (*FenceBrokerClient, error)

NewFenceBrokerClientFromEnv builds a worker client (no mint authority). HERD_FENCE_BROKER_MINT_TOKEN is intentionally ignored if present.

func (*FenceBrokerClient) Live

func (c *FenceBrokerClient) Live(ctx context.Context) error

Live reports whether the broker is reachable (liveness).

func (*FenceBrokerClient) MutateComment

func (c *FenceBrokerClient) MutateComment(ctx context.Context, taskID, commentBody string, fence int64, opID, capability string) error

MutateComment performs broker-enforced comment mutation with per-op capability.

func (*FenceBrokerClient) MutateStatus

func (c *FenceBrokerClient) MutateStatus(ctx context.Context, taskID, status string, fence int64, opID, capability string) error

MutateStatus performs broker-enforced status mutation with an immutable per-op pre-minted capability. Never mints. Never stores capability on client.

func (*FenceBrokerClient) OpApplied

func (c *FenceBrokerClient) OpApplied(ctx context.Context, opID, taskID, wantStatus string) (bool, error)

OpApplied is server-native readback (worker-safe).

type FenceBrokerConfig

type FenceBrokerConfig struct {
	ClaimDir        string
	ListenAddr      string // "unix" (default), 127.0.0.1:port — never non-loopback
	Token           string // worker token
	MintToken       string // mint token (required; must differ from Token)
	UpstreamURL     string
	UpstreamProject string
	UpstreamCLI     bool
}

FenceBrokerConfig configures the sidecar.

type FenceBrokerMinter

type FenceBrokerMinter struct {
	// contains filtered or unexported fields
}

FenceBrokerMinter is the coordinator-only mint channel.

The mint secret is unexported and never returned by methods, String(), or JSON. Construction requires the claim-dir credential file written by StartFenceBroker (mode 0600) — not HERD_FENCE_BROKER_MINT_TOKEN in env. Workers must not receive that file path; env mint alone always fails.

func CoordinatorMinterInProcess

func CoordinatorMinterInProcess(b *FenceBroker) (*FenceBrokerMinter, error)

CoordinatorMinterInProcess grants mint authority to the process that OWNS the broker.

The boundary is the address space: b.mintToken was generated in this process and is never written to a file, an environment variable, or the wire, so no same-UID worker can read it. Prefer this whenever the coordinator can host the broker itself.

func NewFenceBrokerMinterFromClaimDir

func NewFenceBrokerMinterFromClaimDir(claimDir, brokerURL string) (*FenceBrokerMinter, error)

NewFenceBrokerMinterFromClaimDir is BLOCKED outside hermetic tests: a mode-0600 file in a shared claim dir is readable by any same-UID worker process and is not a non-forgeable OS boundary. Production mint authority is deferred to FAC-169 (process/UID/FD boundary). Tests may use this under testing.Testing() or prefer newMinterForTest (in-process unexported secret).

func NewFenceBrokerMinterFromEnv

func NewFenceBrokerMinterFromEnv() (*FenceBrokerMinter, error)

NewFenceBrokerMinterFromEnv is intentionally disabled for induction resistance. Mint secret must not be loadable from process environment alone.

func NewFenceBrokerMinterFromInheritedFD

func NewFenceBrokerMinterFromInheritedFD(brokerURL string) (*FenceBrokerMinter, error)

NewFenceBrokerMinterFromInheritedFD constructs a minter from a descriptor inherited from the broker process.

The descriptor must be a PIPE. A worker cannot satisfy this by writing its own secret to a file and pointing the variable at it, and cannot satisfy it at all without an inherited descriptor: it simply has nothing at that number.

The descriptor is consumed exactly once and closed, so the secret does not linger in the descriptor table for a later exec to inherit.

func (*FenceBrokerMinter) IssueCapability

func (m *FenceBrokerMinter) IssueCapability(ctx context.Context, req CapabilityIssueRequest) (string, error)

IssueCapability mints one single-use op-bound capability JSON for a worker. Returns only the capability document — never the mint secret.

func (*FenceBrokerMinter) MarshalJSON

func (m *FenceBrokerMinter) MarshalJSON() ([]byte, error)

MarshalJSON never serializes the mint secret.

func (*FenceBrokerMinter) String

func (m *FenceBrokerMinter) String() string

String redacts secrets (safe for logs).

type FenceStore

type FenceStore interface {
	Highest(ctx context.Context, taskID string) (int64, error)
	Advance(ctx context.Context, taskID string, fenceToken int64) (int64, error)
	WithExclusive(ctx context.Context, taskID string, fn func(ctx context.Context) error) error
	// LookupApplied returns the receipt for opID if present.
	LookupApplied(ctx context.Context, opID string) (*OpReceipt, error)
	// ListOpsForTask returns all receipts for taskID (applied + ambiguous).
	// Used for empty-rev recovery competing-same-status attribution.
	ListOpsForTask(ctx context.Context, taskID string) ([]OpReceipt, error)
	// MarkApplied persists a successful application. Errors must propagate.
	MarkApplied(ctx context.Context, rec OpReceipt) error
	// MarkAmbiguous records provider-success/local-failure ambiguity.
	MarkAmbiguous(ctx context.Context, rec OpReceipt) error
	BusyTimeout() time.Duration
	Close() error
}

FenceStore: fence high-water + applied op receipts + per-task exclusive lock.

type FencedBoard

type FencedBoard struct {
	CAS *FencedCAS
	TP  TaskProvider
}

FencedBoard is the production consumer of FencedCAS: lease-guarded board mutations go through Begin/Complete + CompareAndSwap with a stable per-operation idempotency key (FAC-147).

func NewFencedBoard

func NewFencedBoard(cas *FencedCAS, tp TaskProvider) (*FencedBoard, error)

func OpenFencedBoard

func OpenFencedBoard(fenceDBPath string, tp TaskProvider) (*FencedBoard, error)

func (*FencedBoard) AddComment

func (b *FencedBoard) AddComment(ctx context.Context, taskID string, fenceToken int64, body string) (claim.ProviderRevision, error)

AddComment is CAS-only helper; production uses MutateComment.

func (*FencedBoard) AdvanceFence

func (b *FencedBoard) AdvanceFence(ctx context.Context, taskID string, fenceToken int64) error

func (*FencedBoard) ClaimOptions

func (b *FencedBoard) ClaimOptions() []claim.Option

func (*FencedBoard) ClaimTask

func (b *FencedBoard) ClaimTask(ctx context.Context, taskID string, fenceToken int64, role string) (claim.ProviderRevision, error)

ClaimTask is CAS-only helper; production uses MutateClaim.

func (*FencedBoard) MutateClaim

func (b *FencedBoard) MutateClaim(
	ctx context.Context,
	mgr *claim.ClaimManager,
	key claim.LeaseKey,
	ownerID string,
	generation int64,
	taskID, role string,
) error

MutateClaim is Begin + Complete with kind claim.

func (*FencedBoard) MutateComment

func (b *FencedBoard) MutateComment(
	ctx context.Context,
	mgr *claim.ClaimManager,
	key claim.LeaseKey,
	ownerID string,
	generation int64,
	taskID, body string,
) error

MutateComment is Begin + Complete with kind comment:<body-hash>.

func (*FencedBoard) MutateStatus

func (b *FencedBoard) MutateStatus(
	ctx context.Context,
	mgr *claim.ClaimManager,
	key claim.LeaseKey,
	ownerID string,
	generation int64,
	taskID, status string,
) error

MutateStatus is Begin + Complete with kind status:<canonical>.

func (*FencedBoard) UpdateStatus

func (b *FencedBoard) UpdateStatus(ctx context.Context, taskID string, fenceToken int64, status string) (claim.ProviderRevision, error)

UpdateStatus is a CAS-only helper for tests. Production status writes use MutateStatus (Begin/Complete). Each call mints a fresh op UUID so two same-state writes are distinct logical operations at the broker.

type FencedCAS

type FencedCAS struct {
	// contains filtered or unexported fields
}

FencedCAS is the local half of FAC-147 fencing. Authoritative acceptance of fence+op MUST occur inside mutate (Kaneo HTTP with X-Herd-* headers or an equivalent broker). Local receipts are a cache for reconcile; the provider-side applied set is the crash-safe source of truth.

func NewFencedCAS

func NewFencedCAS(fences FenceStore, reader TaskProvider) (*FencedCAS, error)

func OpenFencedCAS

func OpenFencedCAS(path string, reader TaskProvider) (*FencedCAS, error)

func (*FencedCAS) AdvanceFence

func (c *FencedCAS) AdvanceFence(ctx context.Context, taskID string, fenceToken int64) error

func (*FencedCAS) Close

func (c *FencedCAS) Close() error

func (*FencedCAS) CompareAndSwap

func (c *FencedCAS) CompareAndSwap(
	ctx context.Context,
	taskID string,
	expected claim.ProviderRevision,
	fenceToken int64,
	opID string,
	mutate func(ctx context.Context) error,
) (claim.ProviderRevision, error)

func (*FencedCAS) ReadRevision

func (c *FencedCAS) ReadRevision(ctx context.Context, taskID string) (claim.ProviderRevision, error)

type GitHubProvider

type GitHubProvider struct {
	Token     string
	Owner     string
	Repo      string
	Client    *http.Client
	Deadlines Deadlines
	Retry     RetryPolicy
}

func NewGitHubProvider

func NewGitHubProvider(token string, owner string, repo string) *GitHubProvider

func (*GitHubProvider) AddComment

func (g *GitHubProvider) AddComment(ctx context.Context, taskID string, body string) error

func (*GitHubProvider) ClaimTask

func (g *GitHubProvider) ClaimTask(ctx context.Context, taskID string, role string) error

func (*GitHubProvider) GetTask

func (g *GitHubProvider) GetTask(ctx context.Context, id string) (*Task, error)

func (*GitHubProvider) ListComments

func (g *GitHubProvider) ListComments(ctx context.Context, taskID string) ([]string, error)

ListComments implements CommentReader (FAC-145 exact effect readback).

func (*GitHubProvider) ListTasks

func (g *GitHubProvider) ListTasks(ctx context.Context, projectID string, status string) ([]*Task, error)

func (*GitHubProvider) UpdateStatus

func (g *GitHubProvider) UpdateStatus(ctx context.Context, taskID string, status string) error

type HandbackDoneReceipt

type HandbackDoneReceipt struct {
	OwnerID    string    `json:"owner_id"`
	Generation int64     `json:"generation"`
	TaskRef    string    `json:"task_ref"`
	Receipt    string    `json:"receipt"`
	Challenge  string    `json:"challenge"`
	ReleasedAt time.Time `json:"released_at"`
}

HandbackDoneReceipt is a generation-bound durable local handback receipt.

func ReadHandbackDone

func ReadHandbackDone(worktreePath string) (*HandbackDoneReceipt, error)

ReadHandbackDone loads and validates a generation-bound done receipt.

type HandbackIntent

type HandbackIntent struct {
	OwnerID        string    `json:"owner_id"`
	Generation     int64     `json:"generation"`
	TaskRef        string    `json:"task_ref"`
	Repo           string    `json:"repo"`
	Provider       string    `json:"provider"`
	Project        string    `json:"project"`
	Receipt        string    `json:"receipt"`
	Challenge      string    `json:"challenge"`
	IntentAt       time.Time `json:"intent_at"`
	RemoteReleased bool      `json:"remote_released"`
}

HandbackIntent is written BEFORE remote Release so a crash after remote success can complete local durability without re-releasing (idempotent).

func ReadHandbackIntent

func ReadHandbackIntent(worktreePath string) (*HandbackIntent, error)

ReadHandbackIntent loads pending handback intent if any.

type JiraProvider

type JiraProvider struct {
	BaseURL    string
	APIToken   string
	UserEmail  string
	HTTPClient *http.Client
	Deadlines  Deadlines
	Retry      RetryPolicy
}

func NewJiraProvider

func NewJiraProvider(baseURL, userEmail, apiToken string) *JiraProvider

func (*JiraProvider) AddComment

func (j *JiraProvider) AddComment(ctx context.Context, taskID string, body string) error

func (*JiraProvider) ClaimTask

func (j *JiraProvider) ClaimTask(ctx context.Context, taskID string, role string) error

func (*JiraProvider) GetTask

func (j *JiraProvider) GetTask(ctx context.Context, id string) (*Task, error)

func (*JiraProvider) ListComments

func (j *JiraProvider) ListComments(ctx context.Context, taskID string) ([]string, error)

ListComments implements CommentReader (FAC-145 exact effect readback). Atlassian document bodies are flattened to their text runs.

func (*JiraProvider) ListTasks

func (j *JiraProvider) ListTasks(ctx context.Context, projectID string, status string) ([]*Task, error)

func (*JiraProvider) UpdateStatus

func (j *JiraProvider) UpdateStatus(ctx context.Context, taskID string, status string) error

type KaneoLinkConfig

type KaneoLinkConfig struct {
	Workspace string `json:"workspace"`
	Project   string `json:"project"`
}

type KaneoProvider

type KaneoProvider struct {
	APIURL    string
	ProjectID string
	UseCLI    bool
	// APIKey authenticates HTTP calls (Bearer). Loaded from api_key_env / KANEO_API_KEY.
	// Bulk project graph snapshots prefer HTTP fan-out even when UseCLI is true
	// to avoid N CLI subprocesses (FAC-159 live-path stampede).
	APIKey string
	// KeyTrustedOrigin is the operator-controlled origin to which APIKey may be
	// sent. It comes from KANEO_API_URL or the selected Kaneo profile, never
	// from repository-controlled APIURL.
	KeyTrustedOrigin string
	Client           *http.Client
	// Deadlines bound every op; zero fields resolve to DefaultDeadlines.
	Deadlines Deadlines
	// Retry applies to idempotent reads only (GetTask/ListTasks).
	Retry RetryPolicy
	// BulkConcurrency bounds concurrent relation fetches in ListProjectRelations.
	// Zero => DefaultBulkRelationConcurrency.
	BulkConcurrency int

	// Receiver is the local AuthBroker over fences.db (in_progress / applied).
	// It is NOT a substitute for server-side fence+op enforcement.
	Receiver AuthoritativeReceiver
	// RequireCASMeta refuses UpdateStatus/AddComment/ClaimTask without
	// CAS meta (fence+op). Set true when attached to a ClaimStack so
	// unfenced bypass cannot skip the receiver.
	RequireCASMeta bool
	// AtomicFenceServer is true when a live FenceBroker (or hermetic enforcing
	// board under test) enforces fence+op+op-dedupe with status. Production
	// sets this only via ConfigureKaneoFenceBroker after health check — not a
	// bare env toggle. Stock Kaneo alone is never sufficient.
	AtomicFenceServer bool
	// FenceBroker is the worker-facing sidecar client (mutate + op readback).
	// Never holds mint credentials.
	FenceBroker *FenceBrokerClient
	// contains filtered or unexported fields
}

func NewKaneoProvider

func NewKaneoProvider(apiURL string, projectID string, useCLI bool) *KaneoProvider

func (*KaneoProvider) AddComment

func (k *KaneoProvider) AddComment(ctx context.Context, taskID string, body string) error

func (*KaneoProvider) AttachTaskLabel

func (k *KaneoProvider) AttachTaskLabel(ctx context.Context, taskID, labelID string) error

func (*KaneoProvider) ClaimTask

func (k *KaneoProvider) ClaimTask(ctx context.Context, taskID string, role string) error

func (*KaneoProvider) CreateRelation

func (k *KaneoProvider) CreateRelation(ctx context.Context, sourceID, targetID string, typ RelationType) (*Relation, error)

CreateRelation creates a directed relation with dual-end readback. Rejects self-edges and unknown types. Ambiguous create is reconciled against both source and target listings so retries never duplicate edges.

func (*KaneoProvider) CreateTask

func (k *KaneoProvider) CreateTask(ctx context.Context, task *Task) (*Task, error)

CreateTask creates a backlog card through Kaneo's authenticated API.

func (*KaneoProvider) CreateTaskLabel

func (k *KaneoProvider) CreateTaskLabel(ctx context.Context, taskID, name string) (TaskLabel, error)

func (*KaneoProvider) DeleteRelation

func (k *KaneoProvider) DeleteRelation(ctx context.Context, relationID, sourceID, targetID string) error

DeleteRelation deletes a relation and verifies absence on BOTH true endpoints. Caller endpoints are a hint; authoritative source/target/type are captured from readback before delete. Ambiguous delete/timeouts never succeed.

func (*KaneoProvider) DeleteTaskLabel

func (k *KaneoProvider) DeleteTaskLabel(ctx context.Context, labelID string) error

func (*KaneoProvider) DetachTaskLabel

func (k *KaneoProvider) DetachTaskLabel(ctx context.Context, labelID string) error

func (*KaneoProvider) GetTask

func (k *KaneoProvider) GetTask(ctx context.Context, id string) (*Task, error)

func (*KaneoProvider) LabelMutationAuthority

func (k *KaneoProvider) LabelMutationAuthority() (string, error)

func (*KaneoProvider) ListComments

func (k *KaneoProvider) ListComments(ctx context.Context, taskID string) ([]string, error)

ListComments implements CommentReader (FAC-145): exact effect readback for verdict delivery. Comment bodies are returned in board order.

func (*KaneoProvider) ListLiveComments

func (k *KaneoProvider) ListLiveComments(ctx context.Context, taskID string) ([]string, error)

ListLiveComments is authoritative live-provider comment readback (CLI or HTTP). NEVER substitutes local AuthBroker receipts. Failures return error (EffectUnknown); empty success means ABSENT.

func (*KaneoProvider) ListProjectRelations

func (k *KaneoProvider) ListProjectRelations(ctx context.Context, projectID string) ([]Relation, error)

ListProjectRelations builds the project relation multiset for SnapshotGraph.

Honest budget (Kaneo 0.11.x / upstream): only GET task-relation/:taskId exists. This is O(board) concurrent HTTP fan-out (not O(1) bulk), measured ~4s for ~164 IDs at concurrency 16 — fits DefaultListDeadline (30s) when credentialed. Without origin-bound HTTP credentials it FAILS CLOSED before any fan-out (never silent CLI N-way stampede).

func (*KaneoProvider) ListRelations

func (k *KaneoProvider) ListRelations(ctx context.Context, taskID string) ([]Relation, error)

ListRelations lists Kaneo relations for a task (as source or target).

func (*KaneoProvider) ListTaskLabels

func (k *KaneoProvider) ListTaskLabels(ctx context.Context, taskID string) ([]TaskLabel, error)

func (*KaneoProvider) ListTaskLabelsBulk

func (k *KaneoProvider) ListTaskLabelsBulk(ctx context.Context, taskIDs []string) (BulkTaskLabels, error)

ListTaskLabelsBulk uses the labels already projected by task list. It is a single provider operation instead of one task label command per identity. Missing identities are returned as an explicit truncated result so callers cannot mistake a partial board scan for a complete one.

func (*KaneoProvider) ListTasks

func (k *KaneoProvider) ListTasks(ctx context.Context, projectID string, status string) ([]*Task, error)

func (*KaneoProvider) ProveLabelCreation

func (k *KaneoProvider) ProveLabelCreation(ctx context.Context, created TaskLabel, targetID, name string, opts LabelRepairOptions) error

ProveLabelCreation binds compensation to the workspace pre/post identity: the row must have been absent before this provider's create and present with the exact name and source-free state afterward. An existing Kaneo orphan is therefore never detached or deleted by the transaction.

func (*KaneoProvider) UpdateStatus

func (k *KaneoProvider) UpdateStatus(ctx context.Context, taskID string, status string) error

type LabelCreationProof

type LabelCreationProof interface {
	ProveLabelCreation(context.Context, TaskLabel, string, string, LabelRepairOptions) error
}

LabelCreationProof is required before a returned create identity may be used for attach or compensation. Backends that cannot prove generation and transaction ownership fail closed rather than risking a foreign row.

type LabelEvidenceReader

type LabelEvidenceReader interface {
	ReadLabelRepairEvidence(transactionID, generation, phase string) (LabelRepairEvidence, error)
}

type LabelEvidenceSink

type LabelEvidenceSink interface {
	RecordLabelRepairEvidence(LabelRepairEvidence) error
}

type LabelRepairEvidence

type LabelRepairEvidence struct {
	Repository         string
	Provider           string
	Project            string
	SourceTaskID       string
	TargetTaskID       string
	PreSourceLabels    string
	PostSourceLabels   string
	PreTargetLabels    string
	PostTargetLabels   string
	PreSourceSnapshot  string
	PostSourceSnapshot string
	PreTargetSnapshot  string
	PostTargetSnapshot string
	CanonicalRole      string
	TransactionID      string
	Generation         string
	Outcome            string
	BlockedReason      string
	Phase              string
	Revision           string
	Operation          string
	CreatedLabelID     string
}

LabelRepairEvidence is the durable transaction record for a role-label mutation. It is deliberately provider-neutral so the daemon can persist it without depending on a particular board adapter.

type LabelRepairOptions

type LabelRepairOptions struct {
	Repository string
	Provider   string
	Project    string
	Evidence   LabelEvidenceSink
	// TransactionID and Generation are caller-owned identities. Empty values
	// are filled with a unique local transaction identity by the repair path.
	TransactionID string
	Revision      string
	Operation     string
	Generation    string
}

type LabelTransactionError

type LabelTransactionError struct {
	Cause        error
	Compensation error
}

LabelTransactionError means compensation could not establish the original source and target state. Callers must persist this as BLOCKED; it is never a successful repair.

func (*LabelTransactionError) Error

func (e *LabelTransactionError) Error() string

func (*LabelTransactionError) Unwrap

func (e *LabelTransactionError) Unwrap() error

type LaunchSession

type LaunchSession struct {
	OwnerID     string    `json:"owner_id"`
	Generation  int64     `json:"generation"`
	TaskRef     string    `json:"task_ref"`
	Repo        string    `json:"repo"`
	Provider    string    `json:"provider"`
	Project     string    `json:"project"`
	Receipt     string    `json:"receipt"`
	Challenge   string    `json:"challenge"`
	LaunchedAt  time.Time `json:"launched_at"`
	Completed   bool      `json:"completed"`
	CompletedAt time.Time `json:"completed_at,omitempty"`
}

LaunchSession distinguishes never-launched from lost authority (pt5t7 #2).

func ReadLaunchSession

func ReadLaunchSession(worktreePath string) (*LaunchSession, error)

ReadLaunchSession loads the durable launch session if present.

type LeaseAck

type LeaseAck struct {
	OwnerID    string    `json:"owner_id"`
	Generation int64     `json:"generation"`
	Receipt    string    `json:"receipt"`
	Challenge  string    `json:"challenge"`
	AckedAt    time.Time `json:"acked_at"`
}

LeaseAck is the worker-written acknowledgement of the single capability.

func ReadLeaseAck

func ReadLeaseAck(worktreePath string) (*LeaseAck, error)

ReadLeaseAck loads worker acknowledgement via openat O_NOFOLLOW.

type LeaseCapability

type LeaseCapability struct {
	OwnerID     string    `json:"owner_id"`
	Generation  int64     `json:"generation"`
	TaskRef     string    `json:"task_ref"`
	Repo        string    `json:"repo"`
	Provider    string    `json:"provider"`
	Project     string    `json:"project"`
	TabID       string    `json:"tab_id,omitempty"`
	PaneID      string    `json:"pane_id,omitempty"`
	AgentName   string    `json:"agent_name,omitempty"`
	Receipt     string    `json:"receipt,omitempty"`
	Challenge   string    `json:"challenge,omitempty"`
	ExpiresAt   time.Time `json:"expires_at"`
	WrittenAt   time.Time `json:"written_at"`
	ClaimDBHint string    `json:"claim_db_hint,omitempty"`
}

LeaseCapability is the durable worker-session authority written into the task worktree after atomic handoff. Written once with final owner+receipt.

func ReadLeaseCapability

func ReadLeaseCapability(worktreePath string) (*LeaseCapability, error)

ReadLeaseCapability loads capability via openat (fail-closed on any error).

type LinearProvider

type LinearProvider struct {
	APIKey    string
	ProjectID string
	Client    *http.Client
	BaseURL   string
	Deadlines Deadlines
	Retry     RetryPolicy
	// BulkConcurrency bounds concurrent relation fetches in ListProjectRelations.
	// Zero => DefaultBulkRelationConcurrency. Honest O(board) fan-out — not O(1).
	BulkConcurrency int
	// contains filtered or unexported fields
}

func NewLinearProvider

func NewLinearProvider(apiKey string) *LinearProvider

func (*LinearProvider) AddComment

func (l *LinearProvider) AddComment(ctx context.Context, taskID string, body string) error

func (*LinearProvider) ClaimTask

func (l *LinearProvider) ClaimTask(ctx context.Context, taskID string, role string) error

func (*LinearProvider) CreateRelation

func (l *LinearProvider) CreateRelation(ctx context.Context, sourceID, targetID string, relType RelationType) (*Relation, error)

CreateRelation creates a dependency edge source → target.

Protocol:

  1. reject blank/self/unsupported (including subtask)
  2. idempotent exact dual-end precheck
  3. one mutation attempt under WithOpDeadline (never blindly retried)
  4. success=false / nil / malformed payload fails closed
  5. on timeout/ambiguous: reconcile exact edge both ends; return existing only if proven
  6. successful mutation requires exact dual-end readback/agreement

func (*LinearProvider) DeleteRelation

func (l *LinearProvider) DeleteRelation(ctx context.Context, relationID, sourceID, targetID string) error

DeleteRelation removes a dependency edge by ID.

Protocol:

  1. require relation/source/target IDs
  2. authoritative pre-capture + exact dual-end agreement (already absent both = idempotent success)
  3. one mutation attempt under WithOpDeadline
  4. ambiguous timeout → success only if fresh absence both ends proven
  5. successful mutation verifies absence both ends; lingering/unknown fails closed

func (*LinearProvider) GetTask

func (l *LinearProvider) GetTask(ctx context.Context, id string) (*Task, error)

func (*LinearProvider) ListComments

func (l *LinearProvider) ListComments(ctx context.Context, taskID string) ([]string, error)

ListComments implements CommentReader (FAC-145 exact effect readback), PAGINATED via GraphQL cursors so a verdict effect stays findable behind any number of earlier comments.

func (*LinearProvider) ListProjectRelations

func (l *LinearProvider) ListProjectRelations(ctx context.Context, projectID string) ([]Relation, error)

ListProjectRelations enumerates every relation involving any task in the project. Honest O(board) fan-out via ListTasks plus a fixed, bounded worker pool.

Dual-end visibility rules:

  • both endpoints in project → identical dual-end observation required
  • exactly one endpoint outside project → single in-project observation allowed
  • half-visible in-project relation → hard error (never silently skipped)

func (*LinearProvider) ListRelations

func (l *LinearProvider) ListRelations(ctx context.Context, taskID string) ([]Relation, error)

ListRelations returns every dependency edge involving taskID (outgoing + incoming).

func (*LinearProvider) ListTasks

func (l *LinearProvider) ListTasks(ctx context.Context, projectID string, status string) ([]*Task, error)

func (*LinearProvider) UpdateStatus

func (l *LinearProvider) UpdateStatus(ctx context.Context, taskID string, status string) error

type MemoryFenceStore

type MemoryFenceStore struct {
	// contains filtered or unexported fields
}

MemoryFenceStore for tests.

func NewMemoryFenceStore

func NewMemoryFenceStore() *MemoryFenceStore

func (*MemoryFenceStore) Advance

func (m *MemoryFenceStore) Advance(_ context.Context, taskID string, fenceToken int64) (int64, error)

func (*MemoryFenceStore) BusyTimeout

func (m *MemoryFenceStore) BusyTimeout() time.Duration

func (*MemoryFenceStore) Close

func (m *MemoryFenceStore) Close() error

func (*MemoryFenceStore) Highest

func (m *MemoryFenceStore) Highest(_ context.Context, taskID string) (int64, error)

func (*MemoryFenceStore) ListExpectedComments

func (m *MemoryFenceStore) ListExpectedComments(_ context.Context, taskID string) ([]string, error)

ListExpectedComments returns durable applied comment bodies for taskID (ambiguous/in_progress rows are excluded so pre-backend journals cannot false-prove effectMet).

func (*MemoryFenceStore) ListOpsForTask

func (m *MemoryFenceStore) ListOpsForTask(_ context.Context, taskID string) ([]OpReceipt, error)

func (*MemoryFenceStore) LookupApplied

func (m *MemoryFenceStore) LookupApplied(_ context.Context, opID string) (*OpReceipt, error)

func (*MemoryFenceStore) MarkAmbiguous

func (m *MemoryFenceStore) MarkAmbiguous(_ context.Context, rec OpReceipt) error

func (*MemoryFenceStore) MarkApplied

func (m *MemoryFenceStore) MarkApplied(_ context.Context, rec OpReceipt) error

func (*MemoryFenceStore) WithExclusive

func (m *MemoryFenceStore) WithExclusive(ctx context.Context, taskID string, fn func(ctx context.Context) error) error

type MemoryProvider

type MemoryProvider struct {
	// contains filtered or unexported fields
}

func NewMemoryProvider

func NewMemoryProvider() *MemoryProvider

func (*MemoryProvider) AddComment

func (m *MemoryProvider) AddComment(ctx context.Context, taskID string, body string) error

func (*MemoryProvider) AddTask

func (m *MemoryProvider) AddTask(t *Task)

func (*MemoryProvider) AttachTaskLabel

func (m *MemoryProvider) AttachTaskLabel(_ context.Context, taskID, labelID string) error

func (*MemoryProvider) ClaimTask

func (m *MemoryProvider) ClaimTask(ctx context.Context, taskID string, role string) error

func (*MemoryProvider) Comments

func (m *MemoryProvider) Comments(taskID string) []string

Comments returns recorded comments for tests (FAC-147 fence coverage).

func (*MemoryProvider) CreateRelation

func (m *MemoryProvider) CreateRelation(ctx context.Context, sourceID, targetID string, typ RelationType) (*Relation, error)

CreateRelation implements RelationProvider with dual-end readback.

func (*MemoryProvider) CreateTask

func (m *MemoryProvider) CreateTask(_ context.Context, task *Task) (*Task, error)

func (*MemoryProvider) CreateTaskLabel

func (m *MemoryProvider) CreateTaskLabel(_ context.Context, taskID, name string) (TaskLabel, error)

func (*MemoryProvider) DeleteRelation

func (m *MemoryProvider) DeleteRelation(ctx context.Context, relationID, sourceID, targetID string) error

DeleteRelation implements RelationProvider with dual-end absence verification.

func (*MemoryProvider) DeleteTaskLabel

func (m *MemoryProvider) DeleteTaskLabel(_ context.Context, labelID string) error

func (*MemoryProvider) DetachTaskLabel

func (m *MemoryProvider) DetachTaskLabel(_ context.Context, labelID string) error

func (*MemoryProvider) GetTask

func (m *MemoryProvider) GetTask(ctx context.Context, id string) (*Task, error)

func (*MemoryProvider) LabelMutationAuthority

func (m *MemoryProvider) LabelMutationAuthority() (string, error)

func (*MemoryProvider) ListComments

func (m *MemoryProvider) ListComments(_ context.Context, taskID string) ([]string, error)

ListComments implements CommentReader for exact effect readback.

func (*MemoryProvider) ListProjectRelations

func (m *MemoryProvider) ListProjectRelations(ctx context.Context, projectID string) ([]Relation, error)

ListProjectRelations implements BulkRelationProvider — O(edges) in-memory.

func (*MemoryProvider) ListRelations

func (m *MemoryProvider) ListRelations(ctx context.Context, taskID string) ([]Relation, error)

ListRelations implements RelationProvider (FAC-159).

func (*MemoryProvider) ListTaskLabels

func (m *MemoryProvider) ListTaskLabels(_ context.Context, taskID string) ([]TaskLabel, error)

func (*MemoryProvider) ListTasks

func (m *MemoryProvider) ListTasks(ctx context.Context, projectID string, status string) ([]*Task, error)

func (*MemoryProvider) ProveLabelCreation

func (m *MemoryProvider) ProveLabelCreation(_ context.Context, created TaskLabel, targetID, name string, opts LabelRepairOptions) error

func (*MemoryProvider) UpdateStatus

func (m *MemoryProvider) UpdateStatus(ctx context.Context, taskID string, status string) error

func (*MemoryProvider) UpdateStatusAtomic

func (m *MemoryProvider) UpdateStatusAtomic(ctx context.Context, taskID, status, receiptJSON string) error

UpdateStatusAtomic applies status and optional signed receipt in one step (hermetic board model of Kaneo single-PATCH atomicity). Resolves taskID by Ref when the map lookup misses (FAC-159 ref-keyed callers).

type MintIdentity

type MintIdentity struct {
	Repo, Provider, Project, TaskRef, OwnerID string
}

MintIdentity is immutable per-call lease binding for coordinator capability mint. Never stored on shared KaneoProvider fields (avoids concurrent cross-wire).

func MintIdentityFrom

func MintIdentityFrom(ctx context.Context) (MintIdentity, bool)

MintIdentityFrom returns per-call mint identity if present.

type MutationCapability

type MutationCapability struct {
	Repo        string `json:"repo"`
	Provider    string `json:"provider"`
	Project     string `json:"project"`
	TaskRef     string `json:"task_ref"`
	BoardTaskID string `json:"board_task_id"`
	Generation  int64  `json:"generation"`
	OwnerID     string `json:"owner_id"`
	OpID        string `json:"op_id"`
	Action      string `json:"action"` // status | comment
	Status      string `json:"status"`
	Comment     string `json:"comment,omitempty"`
	ExpiresUnix int64  `json:"expires_unix"`
	InstanceID  string `json:"instance_id"`
	Nonce       string `json:"nonce"`
	MAC         string `json:"mac"`
}

MutationCapability is a single-use, op-bound grant issued only after authoritative lease validation (mint credential). Workers never mint.

Action is "status" (default) or "comment". Status binds status mutations; Comment binds exact comment body for comment mutations.

func VerifyCommentCapability

func VerifyCommentCapability(secret, instanceID, raw string, boardTaskID, opID, comment string, fence int64) (*MutationCapability, error)

VerifyCommentCapability checks comment-bound capability.

func VerifyMutationCapability

func VerifyMutationCapability(secret, instanceID, raw string, boardTaskID, opID, status string, fence int64) (*MutationCapability, error)

VerifyMutationCapability checks status-bound capability.

func (*MutationCapability) LeaseKey

func (c *MutationCapability) LeaseKey() claim.LeaseKey

LeaseKey returns the full canonical lease key bound in the capability.

type OpFailureClass

type OpFailureClass string

OpFailureClass is the recovery/projection class for a provider operation error. Fleet lanes may map these to BLOCKED(provider_timeout) etc. without importing adapter internals. Empty string means no failure (err == nil).

FAC-150 owns these classes; FAC-155 owns which configured provider is live.

const (
	// OpOK means err was nil.
	OpOK OpFailureClass = ""
	// OpTimeout is a bounded deadline or cancel (IsTimeout).
	OpTimeout OpFailureClass = "provider_timeout"
	// OpAmbiguous is a timed-out mutation whose write may or may not have landed.
	OpAmbiguous OpFailureClass = "provider_ambiguous"
	// OpProvider is a typed *ProviderError (HTTP/board rejection).
	OpProvider OpFailureClass = "provider_error"
	// OpOther is any other hard failure.
	OpOther OpFailureClass = "error"
)

func ClassifyOpError

func ClassifyOpError(err error) OpFailureClass

ClassifyOpError maps a provider operation error to a recovery class. Order: nil → timeout → ambiguous → ProviderError → other.

type OpKind

type OpKind string

OpKind classifies a TaskProvider operation for per-kind deadlines.

const (
	OpGet      OpKind = "get"
	OpList     OpKind = "list"
	OpMutate   OpKind = "mutate" // ClaimTask, UpdateStatus
	OpComment  OpKind = "comment"
	OpReadback OpKind = "readback"
)

type OpReceipt

type OpReceipt struct {
	OpID       string
	TaskID     string
	FenceToken int64
	Revision   string
	// BaseRevision is the live provider revision captured BEFORE remote mutate.
	// Empty-rev Present recovery requires live != BaseRevision to attribute
	// the effect to THIS op; otherwise refuse (pt5t7 #3).
	BaseRevision    string
	ExpectedStatus  string
	ExpectedComment string
	Ambiguous       bool
}

OpReceipt is durable evidence that a logical mutation was accepted.

type PageAccumulator

type PageAccumulator struct {
	// contains filtered or unexported fields
}

PageAccumulator deduplicates items across pages by ID and reports how many fresh IDs each page contributed. Callers terminate via DecidePagination.

func NewPageAccumulator

func NewPageAccumulator() *PageAccumulator

NewPageAccumulator returns an empty page accumulator.

func (*PageAccumulator) Add

func (a *PageAccumulator) Add(id string) bool

Add records id if unseen. Returns true when the id is new.

func (*PageAccumulator) IDs

func (a *PageAccumulator) IDs() []string

IDs returns a copy of collected unique IDs in first-seen order.

func (*PageAccumulator) IngestPage

func (a *PageAccumulator) IngestPage(ids []string) (fresh int, decision PageDecision)

IngestPage adds ids from one page and returns (freshCount, decision).

func (*PageAccumulator) Len

func (a *PageAccumulator) Len() int

Len returns the number of unique IDs collected so far.

type PageDecision

type PageDecision int

PageDecision is the pagination control signal for multi-page listing. Termination is EMPTY-page based: a short-but-nonempty page must continue. Successful listing requires PageStopEmpty; PageStopDuplicate and the page cap are hard errors (incomplete enumeration is not success).

const (
	// PageContinue means more pages may exist; caller should request next page.
	PageContinue PageDecision = iota
	// PageStopEmpty means the page had zero items — definitive end of listing.
	PageStopEmpty
	// PageStopDuplicate means the page added no new IDs (server repeating a page).
	// Callers must treat this as ErrDuplicatePage, not a successful soft stop.
	PageStopDuplicate
)

func DecidePagination

func DecidePagination(pageLen, freshCount int) PageDecision

DecidePagination chooses the next pagination action for a received page.

Rules (fail-closed listing completeness):

  • empty page (pageLen == 0) → PageStopEmpty (only successful termination)
  • page with only already-seen IDs (freshCount == 0) → PageStopDuplicate (hard error)
  • otherwise continue, even when pageLen < pageSize

type Priority

type Priority string
const (
	PriorityUrgent Priority = "urgent"
	PriorityHigh   Priority = "high"
	PriorityMedium Priority = "medium"
	PriorityLow    Priority = "low"
)

func ParsePriorityString

func ParsePriorityString(label string) Priority

ParsePriorityString maps common priority label strings to domain Priority type

type ProviderError

type ProviderError struct {
	Provider   string
	Op         string
	StatusCode int
	Message    string
	RequestID  string
	Retryable  bool
	Body       string
}

ProviderError is a typed adapter failure preserving HTTP status, retryability, and a safe body snippet for diagnostics. Callers must treat any non-nil ProviderError as a hard failure (fail-closed).

func (*ProviderError) Error

func (e *ProviderError) Error() string

type ReadbackDriftError

type ReadbackDriftError struct {
	TaskID   string
	Field    string
	Expected string
	Actual   string
}

ReadbackDriftError reports a write-then-read mismatch after a mutation. Callers must not treat the mutation as successful when this is returned.

func (*ReadbackDriftError) Error

func (e *ReadbackDriftError) Error() string

type Relation

type Relation struct {
	ID           string
	SourceTaskID string
	TargetTaskID string
	Type         RelationType
	CreatedAt    time.Time
}

Relation is one provider relation row (immutable IDs).

type RelationProvider

type RelationProvider interface {
	ListRelations(ctx context.Context, taskID string) ([]Relation, error)
	// CreateRelation rejects self-edges and unknown types; readbacks both ends;
	// reconciles ambiguous create so retries do not duplicate.
	CreateRelation(ctx context.Context, sourceID, targetID string, typ RelationType) (*Relation, error)
	// DeleteRelation requires captured endpoints and verifies absence on BOTH
	// source and target listings. Ambiguous delete/timeouts never return success.
	DeleteRelation(ctx context.Context, relationID, sourceID, targetID string) error
}

RelationProvider is the optional dependency surface. Providers that do not implement it fail the FAC-159 gate with explicit capability unsupported. Kaneo implements full list/create/delete with dual-end readback.

type RelationType

type RelationType string

RelationType is a board relation kind (Kaneo: blocks|related|subtask).

const (
	RelationBlocks  RelationType = "blocks"
	RelationRelated RelationType = "related"
	RelationSubtask RelationType = "subtask"
)

type RetryPolicy

type RetryPolicy struct {
	MaxAttempts    int
	BaseBackoff    time.Duration
	MaxBackoff     time.Duration
	JitterFraction float64 // 0..1; 0 = no jitter (deterministic)
	Clock          Clock
	// RetryIf decides whether an error is retryable. nil → defaultRetryable.
	RetryIf func(error) bool
}

RetryPolicy configures capped exponential backoff for idempotent reads. Jitter is optional; when JitterFraction is 0, backoff is deterministic.

func DefaultReadRetry

func DefaultReadRetry() RetryPolicy

DefaultReadRetry is the safe default for GetTask / ListTasks only.

type SQLiteFenceStore

type SQLiteFenceStore struct {
	// contains filtered or unexported fields
}

SQLiteFenceStore: durable fences + receipts; per-task flock for exclusion.

func NewSQLiteFenceStore

func NewSQLiteFenceStore(path string) (*SQLiteFenceStore, error)

func NewSQLiteFenceStoreWithBusy

func NewSQLiteFenceStoreWithBusy(path string, busy time.Duration) (*SQLiteFenceStore, error)

func (*SQLiteFenceStore) Advance

func (s *SQLiteFenceStore) Advance(ctx context.Context, taskID string, fenceToken int64) (int64, error)

func (*SQLiteFenceStore) BusyTimeout

func (s *SQLiteFenceStore) BusyTimeout() time.Duration

func (*SQLiteFenceStore) Close

func (s *SQLiteFenceStore) Close() error

func (*SQLiteFenceStore) Highest

func (s *SQLiteFenceStore) Highest(ctx context.Context, taskID string) (int64, error)

func (*SQLiteFenceStore) ListExpectedComments

func (s *SQLiteFenceStore) ListExpectedComments(ctx context.Context, taskID string) ([]string, error)

ListExpectedComments returns durable applied comment bodies for taskID.

func (*SQLiteFenceStore) ListOpsForTask

func (s *SQLiteFenceStore) ListOpsForTask(ctx context.Context, taskID string) ([]OpReceipt, error)

func (*SQLiteFenceStore) LookupApplied

func (s *SQLiteFenceStore) LookupApplied(ctx context.Context, opID string) (*OpReceipt, error)

func (*SQLiteFenceStore) MarkAmbiguous

func (s *SQLiteFenceStore) MarkAmbiguous(ctx context.Context, rec OpReceipt) error

func (*SQLiteFenceStore) MarkApplied

func (s *SQLiteFenceStore) MarkApplied(ctx context.Context, rec OpReceipt) error

func (*SQLiteFenceStore) WithExclusive

func (s *SQLiteFenceStore) WithExclusive(ctx context.Context, taskID string, fn func(ctx context.Context) error) error

type StatusMutationReceipt

type StatusMutationReceipt struct {
	OpID         string `json:"op_id"`
	TaskID       string `json:"task_id"`
	Status       string `json:"status"`
	FenceToken   int64  `json:"fence_token"`
	BaseRevision string `json:"base_revision"`
	Actor        string `json:"actor"`
	Nonce        string `json:"nonce"`
	IssuedAtUnix int64  `json:"issued_at_unix"`
	MAC          string `json:"mac"`
}

StatusMutationReceipt is an authenticated binding of one status mutation to exact op, task, status, fence, actor, and base revision.

On real Kaneo (live-probed 2026-08-03): custom JSON keys such as herdStatusReceipt are NOT persisted. The only proven atomic multi-field write is PUT /api/task/{id} with required schema fields including both status and description. The signed receipt is therefore embedded in the description footer of that same PUT — not a second comment, not an ignored custom field.

Verification uses constant-time HMAC + field equality, never substring tags.

func DecodeStatusReceiptJSON

func DecodeStatusReceiptJSON(s string) (*StatusMutationReceipt, error)

DecodeStatusReceiptJSON parses a receipt from description readback.

func MintStatusReceipt

func MintStatusReceipt(taskID, opID, status, baseRev, actor string, fence int64) (*StatusMutationReceipt, error)

MintStatusReceipt builds and signs a receipt for an imminent status mutate.

type StatusReader

type StatusReader interface {
	GetTask(ctx context.Context, id string) (*Task, error)
}

StatusReader is the read surface required for post-mutation reconciliation. TaskProvider implementations satisfy this via GetTask.

type Task

type Task struct {
	ID          string    `json:"id"`
	Ref         string    `json:"ref"`
	Title       string    `json:"title"`
	Description string    `json:"description"`
	Status      string    `json:"status"`
	Priority    Priority  `json:"priority"`
	ProjectID   string    `json:"project_id"`
	Labels      []string  `json:"labels"`
	CreatedAt   time.Time `json:"created_at"`
	// UpdatedAt is the provider's last-mutation timestamp when available
	// (Kaneo updatedAt, GitHub updated_at). Used as part of the opaque
	// ProviderCAS revision token (FAC-147). Zero means the provider did
	// not supply one; revision encoding falls back to status+id+createdAt.
	UpdatedAt time.Time `json:"updated_at,omitempty"`
	// StatusReceipt is the signed receipt extracted from the task description
	// footer after an atomic status PUT (FAC-147). Not a native Kaneo field.
	StatusReceipt string `json:"-"`
	// Position is Kaneo's board rank; required for full-schema PUT rebuilds.
	// HasPosition is true only when the provider returned a position field —
	// zero is a valid board rank and must not be confused with "unknown".
	Position    float64 `json:"-"`
	HasPosition bool    `json:"-"`
	// Residuals are revision-bound incompleteness records propagated into every
	// dependent task packet. They never grant completion authority.
	Residuals []residual.Record `json:"residuals,omitempty"`
}

func DTOToTask

func DTOToTask(id, ref, title, description, status string, p Priority, projectID string, labels []string, createdAt time.Time) *Task

DTOToTask converts raw API DTO parameters to unified Herdforge Task model

func ListActiveTasks

func ListActiveTasks(ctx context.Context, tp TaskProvider, projectID string) ([]*Task, error)

ListActiveTasks returns every non-terminal card without ever reading the terminal columns.

An unfiltered ListTasks walks all six columns, and on a real board the terminal columns dominate: the FAC board carried 525 done cards across 7 pages costing ~45s to page through, against 6 active cards. That single column exceeded the OpList deadline on its own, so every caller that discards done/archived -- deps migrate, dependency fences -- failed with a provider timeout for work it was going to throw away.

This deliberately fans out over the single-status API that every adapter already implements rather than introducing an "active" sentinel status. A sentinel would be silently misread as an unknown status by adapters that have not been taught about it, and would return zero tasks instead of failing -- an empty active board is indistinguishable from a broken query, which is exactly the failure mode a dependency fence must never hit.

Results are sorted by ref so callers get a deterministic order regardless of which column returned first.

type TaskConfig

type TaskConfig struct {
	Type      string
	APIURL    string
	ProjectID string
	UseCLI    bool
	// APIKey for HTTP bulk graph fan-out (even when UseCLI is true).
	APIKey string
	// APIKeyTrustedOrigin is operator-controlled (KANEO_API_URL or selected
	// profile origin). It must never be inferred from repository APIURL.
	APIKeyTrustedOrigin string
	// Optional resolved deadline parts (0 = package default).
	Get, List, Mutate, Comment, Readback time.Duration
	// Enabled is the repository's explicit activation policy (FAC-155): the
	// only provider types this repository may activate. Empty means "exactly
	// the declared Type" — never a discovered or inherited default. A Type
	// outside a non-empty Enabled list is a hard error, so editing
	// task_provider.type without also moving the operator policy fails closed
	// instead of silently pointing the fleet at a different board.
	Enabled []string
}

TaskConfig is the production config surface for building a board provider. Mirrors config.TaskProvider fields used at activation (FAC-150).

type TaskCreator

type TaskCreator interface {
	CreateTask(context.Context, *Task) (*Task, error)
}

TaskCreator creates one board card from an already-normalized task shape. ID, Ref, and timestamps are provider-owned on input and may be ignored by providers that mint those values.

type TaskLabel

type TaskLabel struct {
	ID     string `json:"id"`
	Name   string `json:"name"`
	TaskID string `json:"taskId,omitempty"`
}

TaskLabel is an ownership-bearing label row. A task-bound label must never be attached to a different task; an empty TaskID is unknown ownership and is therefore unsafe for mutation.

type TaskLabelProvider

type TaskLabelProvider interface {
	GetTask(context.Context, string) (*Task, error)
	ListTaskLabels(context.Context, string) ([]TaskLabel, error)
	CreateTaskLabel(context.Context, string, string) (TaskLabel, error)
	AttachTaskLabel(context.Context, string, string) error
	DetachTaskLabel(context.Context, string) error
	DeleteTaskLabel(context.Context, string) error
}

TaskLabelProvider is deliberately separate from TaskProvider: old adapters cannot accidentally claim to support destructive label operations.

type TaskProvider

type TaskProvider interface {
	GetTask(ctx context.Context, id string) (*Task, error)
	ListTasks(ctx context.Context, projectID string, status string) ([]*Task, error)
	ClaimTask(ctx context.Context, taskID string, role string) error
	UpdateStatus(ctx context.Context, taskID string, status string) error
	AddComment(ctx context.Context, taskID string, body string) error
}

TaskProvider defines the interface for task tracking backends (Kaneo, GitHub, Linear)

func MustProductionProvider

func MustProductionProvider(tc TaskConfig) TaskProvider

MustProductionProvider is for tests; panics on error.

func NewFromHerdConfig

func NewFromHerdConfig(cfg *config.Config) (TaskProvider, error)

NewFromHerdConfig activates the configured task provider with FAC-150 deadlines. Linear credentials are read only from its configured api_key_env; it never falls back to Kaneo's ambient credential.

func NewProductionProvider

func NewProductionProvider(tc TaskConfig) (TaskProvider, error)

NewProductionProvider builds the live TaskProvider for herd/daemon/dispatch. Each live provider requires explicit credentials; callers that need in-process tests use NewMemoryProvider / NewBoundClient directly.

func UnwrapTaskProvider

func UnwrapTaskProvider(tp TaskProvider) TaskProvider

UnwrapTaskProvider returns the innermost non-BoundClient provider.

type TimeoutError

type TimeoutError struct {
	Provider string
	Op       string
	Kind     OpKind
	Deadline time.Duration
	// Cause is typically context.DeadlineExceeded or context.Canceled.
	Cause error
}

TimeoutError is a typed provider-operation timeout / cancellation failure. Callers must treat it as a hard error: never map it to empty success, zero tasks, free capacity, or board advancement.

func (*TimeoutError) Error

func (e *TimeoutError) Error() string

func (*TimeoutError) Timeout

func (e *TimeoutError) Timeout() bool

Timeout reports true so net.Error and timeout-aware callers can classify it.

func (*TimeoutError) Unwrap

func (e *TimeoutError) Unwrap() error

Jump to

Keyboard shortcuts

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