cliutil

package
v0.8.1 Latest Latest
Warning

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

Go to latest
Published: May 21, 2026 License: Apache-2.0 Imports: 23 Imported by: 0

Documentation

Overview

Package cliutil holds the verb-support kernel for aiwf's Cobra dispatchers: exit-code constants and the typed error shuttle, the post-verb apply path, identity resolution, flag re-ordering, repo locking, tree-loading with trunk-collision stamping, platform gating, and provenance gate-and-decorate plumbing.

Every cmd/aiwf verb file calls into this package; nothing in internal/* depends on it (it lives under internal/cli/ so it stays invisible to external consumers per Go's internal-package rule).

Index

Constants

View Source
const (
	ActorSourceFlag      = "--actor flag"
	ActorSourceGitConfig = "git config user.email"
)

Actor source labels surfaced by `aiwf whoami` and used as the second return value of ResolveActorWithSource. Stable strings; do not change without updating tests and documentation.

The pre-I2.5 `aiwf.yaml` source is gone — identity is now runtime- derived per `provenance-model.md`, with `--actor` overriding the git-config default. The aiwf.yaml `actor:` key (if still present) is ignored for resolution; `aiwf doctor` surfaces a deprecation note.

View Source
const (
	ExitOK       = 0 // no error-severity findings (warnings allowed)
	ExitFindings = 1 // at least one error-severity finding
	ExitUsage    = 2
	ExitInternal = 3
)

Exit codes per CLAUDE.md § Go conventions § CLI conventions.

Variables

This section is empty.

Functions

func AcquireRepoLock

func AcquireRepoLock(rootDir, verbDisplay string) (release func(), rc int)

AcquireRepoLock takes the per-repo mutation lock and returns a release function plus a zero exit code on success. On failure it prints an explanation to stderr and returns a non-zero exit code the caller must propagate (release will be nil).

Usage in every mutating verb:

release, rc := cliutil.AcquireRepoLock(rootDir, "aiwf add")
if release == nil {
    return rc
}
defer release()

Read-only verbs (check, history, status, render, doctor, whoami) must NOT call this — they can run concurrently with mutations.

func AllKindNames

func AllKindNames() []string

AllKindNames returns the entity-kind names as strings, in the canonical iteration order from entity.AllKinds(). Used by the `aiwf add` and `aiwf schema` / `aiwf template` completion functions.

func AssertSupportedOS

func AssertSupportedOS(goos string) error

AssertSupportedOS gates aiwf to platforms whose POSIX assumptions the engine relies on. The PoC is Unix-only by design (per docs/pocv3/design/design-decisions.md): the pre-push hook is a /bin/sh script, contract validators shell out via POSIX semantics, and the repo lock uses flock(2). On Windows the engine would fail at the first such call with an opaque error; a clear up-front refusal is kinder to the user than a confusing failure deeper in the stack.

Pass runtime.GOOS in main; tests pass canned values to exercise every branch without depending on the test runner's host OS.

func CompleteEntityIDArg

func CompleteEntityIDArg(filter entity.Kind, position int) func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective)

CompleteEntityIDArg is the standard Cobra positional-arg completion adapter over CompleteEntityIDs. Callers assign it as a command's ValidArgsFunction. Unlike the flag adapter, this version respects the args slice — if the positional in question isn't the first one, it returns no suggestions (so e.g. `aiwf promote E-01 <TAB>` doesn't re-suggest entity ids when the second positional is the new-status).

func CompleteEntityIDFlag

func CompleteEntityIDFlag(filter entity.Kind) func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective)

CompleteEntityIDFlag is the standard Cobra flag-completion adapter over CompleteEntityIDs. Callers wire it via `cmd.RegisterFlagCompletionFunc(name, cliutil.CompleteEntityIDFlag(kind))` where kind is either "" for all kinds or a specific entity.Kind.

func CompleteEntityIDs

func CompleteEntityIDs(filter entity.Kind) ([]string, cobra.ShellCompDirective)

CompleteEntityIDs returns the live ids in the consumer repo's planning tree, optionally filtered to a single kind. Designed for use as a Cobra ValidArgsFunction or RegisterFlagCompletionFunc body: failures (no aiwf.yaml, malformed tree, unreadable disk) collapse to an empty list rather than spamming the user's shell with errors, satisfying M-054 AC-2's graceful-no-op rule.

func ConfiguredTitleMaxLength

func ConfiguredTitleMaxLength(rootDir string) int

ConfiguredTitleMaxLength returns the consumer's `entities.title_max_length` from aiwf.yaml, or the kernel default when absent (G-0102). Tolerant of a missing aiwf.yaml — the kernel default applies in that case too, so the verb dispatchers in cmd/aiwf can call this unconditionally without a precondition check.

func DecorateAndFinish

func DecorateAndFinish(
	ctx context.Context,
	root, label string,
	t *tree.Tree,
	result *verb.Result,
	vErr error,
	pctx ProvenanceContext,
) int

DecorateAndFinish wraps the verb's post-execution path: when the verb produced a Plan, it runs gateAndDecorate (which enforces the I2.5 allow-rule and stamps provenance trailers), then hands off to FinishVerb to apply the plan and report the outcome.

On allow-rule denial, the plan is abandoned (verb output is validate-then-write, so no disk state has been mutated yet) and the dispatcher exits with the findings code so the user sees the refusal as a clean error.

func FinishVerb

func FinishVerb(ctx context.Context, root, label string, result *verb.Result, err error) int

FinishVerb is the post-verb handler shared by every mutating subcommand: it surfaces a Go error as a usage error, renders any findings, applies the plan when present, and prints a one-line summary on success. NoOp results bypass the apply path entirely and print NoOpMessage on stdout.

func HasCommits

func HasCommits(ctx context.Context, root string) bool

HasCommits reports whether root's HEAD points at a real commit. `git log` on an empty repo errors with "your current branch X does not have any commits yet"; this guard converts that into "no events".

func IsTerminalPromote

func IsTerminalPromote(k entity.Kind, newStatus string) bool

IsTerminalPromote reports whether `(kind, newStatus)` is a state transition into a terminal status — the trigger for the scope-end side effect. Mirrors entity.AllowedTransitions semantics: a status with no outgoing edges is terminal.

Returns false for unknown kinds or unknown statuses (cautious; the trigger should fire on real terminal moves only).

func JoinKinds

func JoinKinds(ks []entity.Kind) string

JoinKinds renders a slice of entity.Kind as a comma-separated string (e.g. for "unknown kind X (known: epic, milestone, …)" error messages). Stable ordering: iterates the input slice as-given; callers that need canonical order pass entity.AllKinds() (which is canonical).

func LoadContractsBlock

func LoadContractsBlock(rootDir string) (*aiwfyaml.Contracts, error)

LoadContractsBlock reads aiwf.yaml from rootDir and returns the contracts: block (nil if absent or if the file itself is absent). A malformed contracts: block is an internal error — the caller (any verb that needs trustworthy bindings) can't proceed.

func LoadContractsDoc

func LoadContractsDoc(rootDir string) (*aiwfyaml.Doc, *aiwfyaml.Contracts, error)

LoadContractsDoc reads aiwf.yaml and returns both the editable Doc and the parsed contracts block. Used by mutating verbs that need to splice the block back into the source.

func LoadEntityScopes

func LoadEntityScopes(ctx context.Context, root, id string) ([]*scope.Scope, error)

LoadEntityScopes returns every scope ever opened on entity id, in open-order (oldest first), with each scope's current state derived from the entity's commit history. Empty / no-commit repos return (nil, nil).

The walker scans every commit whose `aiwf-entity:` trailer matches id (the same filter `aiwf history` uses) once, applying transitions in commit order:

  • aiwf-verb=authorize, aiwf-scope=opened → append a new scope starting in state active.
  • aiwf-verb=authorize, aiwf-scope=paused → flip the most-recently- opened still-active scope to paused.
  • aiwf-verb=authorize, aiwf-scope=resumed → flip the most-recently- paused scope to active.
  • aiwf-scope-ends: <auth-sha> on any commit → mark the matching scope ended (terminal). Auto-end fires when a terminal-promote of the scope-entity carries this trailer (added by step 6); for the initial step 5 cut, hand-crafted fixtures use the same path.

The "most recently X" rule mirrors the verb's pause/resume picker: scopes are appended in chronological open-order, so a backwards walk finds the latest match. Multiple parallel scopes on the same entity are supported; transitions land on the freshest matching state.

func LoadTreeWithTrunk

func LoadTreeWithTrunk(ctx context.Context, rootDir string) (*tree.Tree, []tree.LoadError, error)

LoadTreeWithTrunk loads the consumer repo's entity tree and stamps the configured trunk ref's ids onto Tree.TrunkIDs, so the allocator (entity.AllocateID) and the cross-tree ids-unique check both see trunk in their view.

Behavior matches package trunk: when the repo has no remotes the trunk read is silently skipped; when remotes exist but the configured trunk ref does not resolve the call returns an error and the operator sees a clear message.

A missing aiwf.yaml is not fatal — the trunk read uses the default trunk ref in that case.

func ParseKind

func ParseKind(s string) (entity.Kind, bool)

ParseKind parses a CLI kind argument (lowercase string) into the entity.Kind constant. Returns the canonical Kind and true on match; the empty Kind and false when no kind in entity.AllKinds() matches s.

func ParseTestsFlag

func ParseTestsFlag(raw, verbLabel string) (*gitops.TestMetrics, error)

ParseTestsFlag parses the --tests flag value (e.g. "pass=12 fail=0 skip=0") into a *gitops.TestMetrics. Empty input returns (nil, nil) (the flag was unset — not an error). Malformed input writes a one-line error to stderr (prefixed with verbLabel) and returns the parse error so the dispatcher exits with cliutil.ExitUsage.

The "metrics parsed to zero" defensive branch returns (nil, nil) — gitops.ParseStrictTestMetrics returns the zero TestMetrics for empty input, but here the trimmed input was non-empty so this shouldn't fire. If it does, treat the flag as not set to avoid emitting a meaningless trailer.

func ReadBodyFile

func ReadBodyFile(path string) ([]byte, error)

ReadBodyFile loads body content for `aiwf add --body-file`. A path of "-" reads stdin (so callers can pipe body text without a temp file). Any other value is read as a regular file. Returns the raw bytes; the verb-side resolveAddBody is the rule-checking layer (it refuses content that begins with a frontmatter delimiter so the create commit can't accidentally produce a double-frontmatter file).

func RegisterFormatCompletion

func RegisterFormatCompletion(cmd *cobra.Command)

RegisterFormatCompletion wires `--format=` shell completion to the closed set {text, json}. Called by every read-only verb that accepts --format so the shell-completion experience is uniform across the surface (E-14's auto-completion-friendliness rule).

func ReorderFlagsFirst

func ReorderFlagsFirst(args, knownFlags, knownBoolFlags []string) []string

ReorderFlagsFirst hoists known flags (and their values) to the front of args so Go's stdlib `flag` package — which stops parsing at the first non-flag token — accepts the natural CLI shape:

aiwf cancel M-001 --reason "..."     (flag after positional)

Without this, the user is forced to write:

aiwf cancel --reason "..." M-001     (flags first)

which is technically correct but goes against everyone's habits.

knownFlags lists value-taking flags (`--name value`); knownBoolFlags lists boolean flags that do NOT consume a following token. A bool flag wrapped in `--name=value` form is recognized either way. When the same name appears in both lists, the value-taking interpretation wins (defensive — callers should not duplicate).

The function is conservative: a token is treated as a flag only when it starts with `--` or `-` AND its name is in one of the known sets. Unknown flags fall through to the original position so flag.Parse can produce its usual error.

func ResolveActor

func ResolveActor(explicit, root string) (string, error)

ResolveActor picks the actor string for a verb's commit trailer. Precedence: explicit `--actor` > git config user.email derivation. Returns an error when neither yields a valid value or when the explicit value is malformed.

The root parameter is unused but kept for call-site compatibility; future per-repo identity policy (if it ever lands) would consult it.

func ResolveActorWithSource

func ResolveActorWithSource(explicit, root string) (actor, source string, err error)

ResolveActorWithSource is ResolveActor plus the human-readable label of which source produced the value. Used by `aiwf whoami` to explain the precedence outcome to the user.

func ResolveCurrentEntityID

func ResolveCurrentEntityID(ctx context.Context, root, id string) (string, error)

ResolveCurrentEntityID walks the aiwf-prior-entity chain forward from id to its current id. When id has never been reallocated, returns id unchanged. When the entity was renumbered, follows the rename chain — each `aiwf reallocate` commit carries aiwf-prior-entity: <old> and aiwf-entity: <new> — until no further commit names the current id as `prior-entity`.

Used by the I2.5 allow-rule's scope-entity resolution: an authorize commit's aiwf-entity trailer is the id at the time of authorization. After a reallocate, the historical commit stays byte-identical (its SHA remains valid as aiwf-authorized-by:), but the live entity now lives under a new id. Forward-walking the prior-entity chain produces the current id so tree.Reaches can run against the live tree.

Cycle guard: each id may appear at most once. A cycle (which would indicate corrupted history) returns the chain's current position rather than looping.

func ResolveRoot

func ResolveRoot(explicit string) (string, error)

ResolveRoot picks the consumer repo root. If explicit is non-empty, it is used as-is (resolved to absolute). Otherwise, walks up from cwd looking for aiwf.yaml; if found, uses its parent. If not found, falls back to cwd (lenient pre-init behavior).

This is the canonical exported home for root-dir resolution across the cmd/aiwf surface. Every verb that takes a `--root` flag funnels through ResolveRoot so the flag-empty + autodiscover behavior stays uniform.

func SplitCommaList

func SplitCommaList(s string) []string

SplitCommaList parses comma-separated CLI values into a clean slice (trimmed, empty entries dropped). Shared between --relates-to, --linked-adr, --depends-on, and similar multi-value flags.

func StatusesForID

func StatusesForID(id string) []string

StatusesForID returns the closed set of statuses that an entity's kind allows, derived from the id's prefix without loading the repo's tree. Used as the static-completion source for `aiwf promote <id> <new-status>`. Returns nil for ids whose kind isn't recognized (composite ids, malformed input) — the completion source then falls back to file completion at the shell level.

func WrapExitCode

func WrapExitCode(code int) error

WrapExitCode lifts a verb's int return code into the error channel Cobra's RunE expects. ExitOK collapses to nil (success); anything else becomes an *ExitError that the run() loop unwraps. Centralizing the translation keeps every RunE one-liner-shaped.

Types

type ExitError

type ExitError struct {
	Code int
}

ExitError carries a verb-handler return code through Cobra's Execute boundary. The CLI's run() loop unwraps it so the wrapped code becomes the process exit status. Without this typed shuttle, Cobra would collapse the 0/1/2/3 contract to "0 or non-zero" because it only knows about its own usage-error return path.

func (*ExitError) Error

func (e *ExitError) Error() string

type ProvenanceContext

type ProvenanceContext struct {
	Actor             string
	Principal         string
	VerbKind          verb.VerbKind
	TargetID          string
	CreationRefs      []string
	MoveSource        string
	IsTerminalPromote bool
}

ProvenanceContext carries the inputs the cmd dispatcher feeds into the I2.5 allow-rule and trailer-decoration step. Built once per verb invocation.

Actor is the operator (the value `aiwf-actor:` will carry). Principal is the human who authorized the act, set via the `--principal` flag. For human/... actors Principal must be empty (the human is acting directly); for non-human actors Principal is required and gates the agent through Allow.

VerbKind discriminates the act for reachability (see verb.VerbKind). TargetID, CreationRefs, MoveSource feed the allow-rule's reachability check.

IsTerminalPromote is true when the verb is a `promote` whose target state is terminal for the entity's kind. Triggers the scope-end side effect — writing aiwf-scope-ends for every active scope on the target entity.

Directories

Path Synopsis
Package testutil holds test-only helpers shared across the CLI test surface.
Package testutil holds test-only helpers shared across the CLI test surface.

Jump to

Keyboard shortcuts

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