Documentation
¶
Overview ¶
Package verb — I2.5 allow-rule composition.
Allow gates an agent's verb invocation against the union of currently-active scopes attached to the agent. Per docs/design/provenance-model.md §"Composition with entity FSMs":
allow(verb v on entity e by actor a) =
legalEntityTransition(e, v.target_state) // existing entity FSM
AND scopeAllows(a, v, e) // new scope check
For human/... actors with no --principal flag, scopeAllows is skipped entirely — humans need no delegation. For ai/... or other non-human actors, at least one active scope must answer "yes" to scopeAllows; if none does, the verb refuses with provenance-no- active-scope and no commit lands.
The function is intentionally pure: tree (forward-reachability) and pre-loaded scopes are passed in. The cmd dispatcher does the git I/O (loadActiveScopesForActor) and tree.Load.
Package verb — I2.5 audit-only recovery mode (G24).
When a mutating verb fails partway through (e.g., `.git/index.lock` contention) and the operator finishes the work with a plain `git commit`, the framework currently goes silent — `aiwf history` filters the manual commit out and the audit trail has an unsignalled hole. The audit-only mode is the recovery path:
aiwf cancel <id> --audit-only --reason "<text>" aiwf promote <id> <state> --audit-only --reason "<text>" aiwf promote <composite-id> --phase <p> --audit-only --reason "<text>"
Each mode produces an empty-diff commit carrying the standard trailer block plus `aiwf-audit-only: <reason>` so the commit is distinguishable from a normal verb commit at read time. The verb refuses unless the entity is *already* at the named target state — audit-only records what's already true; it never makes a transition (that's --force's job; the two are mutually exclusive per coherence.go).
Reference: docs/archive/pocv3/provenance-model-plan.md §"Step 5b" and docs/archive/pocv3/gaps-pre-migration.md G24.
Package verb — I2.5 trailer-coherence rules.
Package verb implements aiwf's mutating verbs: add, promote, cancel, rename, reallocate, and friends.
Most verbs are *validate-then-write* per docs/design/design-decisions.md: the verb computes the projected new tree in memory, runs projectionFindings (which wraps check.Run) against the projection, and returns either findings (no disk writes occurred) or a Plan (file ops + commit metadata). The orchestrator in cmd/aiwf applies the plan only when findings are clean. There is no rollback path because nothing is written until the projection is known good.
A documented minority of verbs never call projectionFindings, for one of four concrete reasons: the field they mutate is validated only by a CLI-composed, git-history-dependent rule (area membership) that needs data — a touchedByEntity map built by scanning commit history — no in-memory projection has, so the rule can never fire there regardless of which verb calls it; the commit they produce has an empty diff (a sovereign or audit-only act) with no entity-content mutation to project; the operation is a purely structural multi-entity sweep (archive, rewidth) where mid-sweep check noise would be spurious and validation is deferred entirely to the pre-push hook's full aiwf check; or the verb belongs to the contract subsystem, which writes aiwf.yaml rather than an entity file and runs its own narrower validation gate by design. The reviewed, exhaustive allowlist — one entry per exempt verb with its specific reason — lives in internal/policies/projection_findings_presence.go, mechanically enforced: any verb newly falling outside both the presence check and the allowlist fails CI.
Index ¶
- Constants
- Variables
- func Apply(ctx context.Context, root string, p *Plan) (sha string, err error)
- func CheckTrailerCoherence(trailers []gitops.Trailer) error
- func RewriteLinkDestinations(body []byte, linkingFile string, moves []EntityMove) []byte
- type AddOptions
- type AllowInput
- type AllowResult
- type AuthorizeKindError
- type AuthorizeMode
- type AuthorizeOptions
- type CoherenceError
- type ContractBindOptions
- type EntityMove
- type EpicCancelNonTerminalChildrenError
- type EpicPromoteNonTerminalChildrenError
- type FileOp
- type ImportOptions
- type ImportResult
- type MilestoneCancelNonTerminalACsError
- type MilestonePromoteNonTerminalACsError
- type NoActiveScopeError
- type OpType
- type Plan
- type PreflightBranchContextRequiredError
- type PreflightBranchNotFoundError
- type PreflightRungPairError
- type PromoteOptions
- type RecipeInstallOptions
- type Result
- func AcknowledgeIllegal(ctx context.Context, root, sha, forEntity, actor, reason string) (*Result, error)
- func AcknowledgeMistag(ctx context.Context, t *tree.Tree, id, actor, reason string) (*Result, error)
- func Add(ctx context.Context, t *tree.Tree, kind entity.Kind, title, actor string, ...) (*Result, error)
- func AddAC(ctx context.Context, t *tree.Tree, parentID, title, actor string) (*Result, error)
- func AddACBatch(ctx context.Context, t *tree.Tree, parentID string, titles []string, ...) (*Result, error)
- func Archive(ctx context.Context, root, actor, kindFilter string) (*Result, error)
- func Authorize(ctx context.Context, t *tree.Tree, id, actor string, opts AuthorizeOptions) (*Result, error)
- func Cancel(ctx context.Context, t *tree.Tree, id, actor, reason string, force bool) (*Result, error)
- func CancelAuditOnly(ctx context.Context, t *tree.Tree, id, actor, reason string) (*Result, error)
- func ContractBind(ctx context.Context, t *tree.Tree, doc *aiwfyaml.Doc, ...) (*Result, error)
- func ContractUnbind(ctx context.Context, t *tree.Tree, doc *aiwfyaml.Doc, ...) (*Result, error)
- func EditBody(ctx context.Context, t *tree.Tree, id string, body []byte, ...) (*Result, error)
- func MilestoneDependsOn(ctx context.Context, t *tree.Tree, id string, deps []string, clearList bool, ...) (*Result, error)
- func Move(ctx context.Context, t *tree.Tree, id, newEpicID, actor string) (*Result, error)
- func Promote(ctx context.Context, t *tree.Tree, id, newStatus, actor, reason string, ...) (*Result, error)
- func PromoteACPhase(ctx context.Context, t *tree.Tree, compositeID, newPhase, actor, reason string, ...) (*Result, error)
- func PromoteACPhaseAuditOnly(ctx context.Context, t *tree.Tree, compositeID, newPhase, actor, reason string) (*Result, error)
- func PromoteAuditOnly(ctx context.Context, t *tree.Tree, id, newStatus, actor, reason string) (*Result, error)
- func Reallocate(ctx context.Context, t *tree.Tree, idOrPath, actor string) (*Result, error)
- func RecipeInstall(ctx context.Context, t *tree.Tree, doc *aiwfyaml.Doc, ...) (*Result, error)
- func RecipeRemove(ctx context.Context, t *tree.Tree, doc *aiwfyaml.Doc, ...) (*Result, error)
- func Rename(ctx context.Context, t *tree.Tree, id, newSlug, actor string, ...) (*Result, error)
- func RenameArea(ctx context.Context, t *tree.Tree, doc *aiwfyaml.Doc, members []config.Member, ...) (*Result, error)
- func Retitle(ctx context.Context, t *tree.Tree, id, newTitle, actor, reason string, ...) (*Result, error)
- func Rewidth(ctx context.Context, root, actor string) (*Result, error)
- func SetArea(ctx context.Context, t *tree.Tree, members []string, id, member string, ...) (*Result, error)
- func SetPriority(ctx context.Context, t *tree.Tree, id, level string, clearTag bool, ...) (*Result, error)
- type ScopeOutOfReachError
- type VerbKind
Constants ¶
const ( CoherenceRuleOnBehalfOfMissingAuthorizedBy = "on-behalf-of-missing-authorized-by" CoherenceRuleAuthorizedByMissingOnBehalfOf = "authorized-by-missing-on-behalf-of" CoherenceRulePrincipalMissingForNonHumanActor = "principal-missing-for-non-human-actor" CoherenceRulePrincipalForbiddenForHumanActor = "principal-forbidden-for-human-actor" CoherenceRuleOnBehalfOfForbiddenForHumanActor = "on-behalf-of-forbidden-for-human-actor" CoherenceRuleForceWithOnBehalfOf = "force-with-on-behalf-of" CoherenceRuleForceNonHuman = "force-non-human" CoherenceRuleAuditOnlyWithForce = "audit-only-with-force" CoherenceRuleAuditOnlyNonHuman = "audit-only-non-human" )
Coherence rule names. These are referenced by `aiwf check`'s provenance findings (step 7) — keep them stable.
const ( OnCollisionFail = "fail" OnCollisionSkip = "skip" OnCollisionUpdate = "update" )
On-collision modes for the Import verb.
Variables ¶
var CodeAuthorizeKindNotAllowed = codes.Code{ID: "authorize-kind-not-allowed", Class: codes.ClassLegality}
CodeAuthorizeKindNotAllowed is the typed kernel-code descriptor carried by AuthorizeKindError when aiwf authorize refuses a scope-entity that is not an epic or milestone (D-0007). It declares codes.ClassLegality, the marker the closed legality set is enumerated from (D-0011). Consumers see its codes.Code.ID string via AuthorizeKindError.Code and in the message text.
var CodeEpicCancelNonTerminalChildren = codes.Code{ID: "epic-cancel-non-terminal-children", Class: codes.ClassLegality}
CodeEpicCancelNonTerminalChildren is the typed kernel-code descriptor carried by EpicCancelNonTerminalChildrenError when `aiwf cancel` refuses an epic that still owns one or more non-terminal child milestones (D-0003). It declares codes.ClassLegality, the marker the closed legality set is enumerated from (D-0011). Consumers see its codes.Code.ID string via EpicCancelNonTerminalChildrenError.Code and in the message text.
var CodeEpicPromoteNonTerminalChildren = codes.Code{ID: "epic-promote-non-terminal-children", Class: codes.ClassLegality}
CodeEpicPromoteNonTerminalChildren is the typed kernel-code descriptor carried by EpicPromoteNonTerminalChildrenError when `aiwf promote` refuses to move an epic straight to a terminal status while it still owns one or more non-terminal child milestones (G-0393 / G-0394: two independently-filed gaps converging on the same guard). Mirrors CodeEpicCancelNonTerminalChildren's D-0003 guard onto both of Promote's terminal targets for KindEpic — `done` and `cancelled` — so a done epic with an in-progress milestone is exactly as incoherent as a cancelled one, and `aiwf promote <epic> cancelled` can't bypass Cancel's own dedicated guard by going through Promote instead. Declares codes.ClassLegality (D-0011).
var CodeMilestoneCancelNonTerminalACs = codes.Code{ID: "milestone-cancel-non-terminal-acs", Class: codes.ClassLegality}
CodeMilestoneCancelNonTerminalACs is the typed kernel-code descriptor carried by MilestoneCancelNonTerminalACsError when `aiwf cancel` refuses a milestone that still carries one or more `open` acceptance criteria (D-0004). It declares codes.ClassLegality (D-0011). Consumers see its codes.Code.ID string via MilestoneCancelNonTerminalACsError.Code and in the message text.
var CodeMilestonePromoteNonTerminalACs = codes.Code{ID: "milestone-promote-non-terminal-acs", Class: codes.ClassLegality}
CodeMilestonePromoteNonTerminalACs is the typed kernel-code descriptor carried by MilestonePromoteNonTerminalACsError when `aiwf promote` refuses to move a milestone straight to `cancelled` while it still carries one or more `open` acceptance criteria (G-0335, mirroring G-0393 / G-0394's epic-level fix). Mirrors CodeMilestoneCancelNonTerminalACs's D-0004 guard onto Promote's own `cancelled` target for KindMilestone, so `aiwf promote <milestone> cancelled` can't bypass Cancel's own dedicated guard by going through Promote instead. Declares codes.ClassLegality (D-0011).
var CodePreflightBranchContextRequired = codes.Code{ID: "branch-context-required", Class: codes.ClassLegality}
CodePreflightBranchContextRequired is the typed kernel-code descriptor carried by PreflightBranchContextRequiredError when aiwf authorize refuses opening a scope on an ai/* agent because neither --branch was passed nor the current checkout matches a ritual shape (M-0103 / ADR-0010). Class is codes.ClassLegality.
var CodePreflightBranchNotFound = codes.Code{ID: "branch-not-found", Class: codes.ClassLegality}
CodePreflightBranchNotFound is the typed kernel-code descriptor carried by PreflightBranchNotFoundError when aiwf authorize refuses opening a scope on an ai/* agent because --branch <name> was passed but no local branch by that name exists (M-0103 / ADR-0010). Class is codes.ClassLegality.
var CodePreflightRungPair = codes.Code{ID: "rung-pair-illegal", Class: codes.ClassLegality}
CodePreflightRungPair is the typed kernel-code descriptor carried by PreflightRungPairError when aiwf authorize refuses opening a scope on an ai/* agent because the (CurrentBranch rung, --branch rung) pair is not in the legal set {(trunk, epic), (epic, milestone), (milestone, patch), (epic, patch)} per ADR-0010 (M-0161/AC-2 / G-0201). Class is codes.ClassLegality.
Functions ¶
func Apply ¶
Apply executes a verb's Plan against the consumer repo at root: it runs every OpMove via a pure filesystem rename, every OpWrite atomically to disk via pathutil.AtomicWriteFile (creating parent directories as needed), then builds the single commit and reconciles exactly the touched paths into the live index via gitops.CommitVerbChange — the one exported commit-construction seam (M-0186/AC-5).
Moves run before writes so that when a verb (notably reallocate) renames a file/dir and also rewrites files inside that dir, the writes land at the new locations.
Isolation (M-0186): CommitTree builds the commit from HEAD's tree plus the verb's own removes/writes, entirely against a throwaway index — it never reads or writes the live index or worktree. Phase 1/2 are pure filesystem operations too (os.Rename, pathutil.AtomicWriteFile), so nothing is ever staged into the live index before a successful commit. This replaces the earlier git-stash isolation dance (G-0275/G-0276): there is nothing left to stash, because the live index is never touched until the one, narrowly-scoped ReconcilePaths call after the commit lands.
Conflict guard: if the user has already staged a path the verb is about to write, Apply refuses before any disk mutation. The two intents — the user's staged content, the verb's computed content — disagree on what that path should hold; letting the verb proceed would have ReconcilePaths silently overwrite the user's staged version with the verb's once the commit lands.
Atomicity: Apply is all-or-nothing up to the commit. If any step before a successful commit fails (write error, commit failure, panic), the worktree is restored to its pre-Apply state via a deferred rollback — a pure filesystem operation with no git call, so it cannot itself be blocked by lock contention or any other git failure. Once the commit lands, it is never rolled back (it's git history); a subsequent reconciliation failure is reported but does not undo the commit — see reconcileFailureError.
sha is non-empty if and only if err is nil: even in the reconcile- failure case (the commit itself landed but syncing the live index afterward failed), Apply reports "", err rather than surfacing a sha alongside a non-nil error — the sha is not lost, it is already embedded in that error's own text (reconcileFailureError), so a caller gets a simple "sha present means clean success" contract instead of having to special-case a partial-success sha.
func CheckTrailerCoherence ¶
CheckTrailerCoherence validates the I2.5 required-together / mutually-exclusive trailer rules on an assembled trailer set. Returns nil when the set is coherent; returns a *CoherenceError naming a single rule violation otherwise.
The check intentionally returns the FIRST violation encountered — surfacing all of them at once would force callers to display a list when typically one fix unblocks the rest. Standing-rule callers (aiwf check) re-run per commit so each commit's first violation surfaces.
Per provenance-model.md §"Required-together and mutually-exclusive rules":
- on-behalf-of ↔ authorized-by: both present or both absent.
- principal ↔ non-human actor: required-together; principal is forbidden for a human actor.
- on-behalf-of: forbidden for a human actor (direct human acts have no on-behalf-of).
- force + on-behalf-of: mutually exclusive (force is human-only; on-behalf-of implies an agent operator).
- force + non-human actor: forbidden (force is sovereign, human- only).
- audit-only + force: mutually exclusive (force makes a transition; audit-only records one that already happened — distinct intents).
- audit-only + non-human actor: forbidden (audit-only is sovereign, same rationale as force).
The (authorize, on-behalf-of) sub-agent-delegation pair is deliberately NOT enforced — that policy decision is reserved for G22 per the design doc.
func RewriteLinkDestinations ¶ added in v0.27.0
func RewriteLinkDestinations(body []byte, linkingFile string, moves []EntityMove) []byte
RewriteLinkDestinations rewrites, within body, every markdown link destination that resolves to one of moves' From paths, pointing it at the matching To path. linkingFile is the repo-relative, forward-slash path of the file body belongs to (M-0246/M-0247 callers pass the file's own path; the shared primitive doesn't resolve it itself). A relative destination (`../work/…`, any `../` depth) is resolved against linkingFile's own directory; a destination already rooted at a known entity directory (`work/…`, `docs/adr/…`) is treated as root-relative and compared as-is — matching rewidth's existing convention for `work/`-prefixed links.
Everything else is left byte-identical: prose, inline-code spans, fenced code blocks, URL-shaped destinations, and links whose destination does not resolve to a moved entity. Pure — no I/O — and idempotent: a destination already rewritten to a To path is not a From path in the same move set, so a second pass is a no-op.
Masking (fence detection, inline-code-span exclusion, link-path region splitting) is shared with rewidth's width-rewrite via walkBodyLines / maskCodeSpans / splitLinkPathRegions in linkregion.go; only the destination-rewrite predicate below is specific to this primitive.
Types ¶
type AddOptions ¶
type AddOptions struct {
// Milestone: id of the parent epic. Required.
EpicID string
// Milestone: TDD policy declaration. Required for kind=milestone;
// must be one of "required" / "advisory" / "none". Empty on
// non-milestone kinds (validated by validateAddOptsForKind).
// Closes G-055 layer #1 — pre-fix, milestones could be created
// with the field absent and the kernel silently treated absence
// as `tdd: none`. Post-fix, the policy decision is a single
// explicit act recorded in the create commit.
TDD string
// Milestone: optional list of milestone ids the new milestone
// depends on. Each id must resolve to an existing milestone
// (allocation-time referent validation, M-076/AC-4); the list is
// written verbatim into the entity's depends_on frontmatter array.
// Cycle detection stays in `aiwf check`. Empty list (or absence)
// produces no depends_on block.
DependsOn []string
// Gap: optional reference to the milestone or epic where the gap
// was discovered.
DiscoveredIn string
// Decision: optional list of entity ids the decision relates to.
RelatesTo []string
// Contract: optional list of ADR ids motivating the contract.
LinkedADRs []string
// Contract: when all three of BindValidator/BindSchema/BindFixtures
// are non-empty, Add atomically appends the binding to
// aiwf.yaml.contracts.entries[] in the same commit. Partial
// triplets are an error; the verb is all-or-nothing on the bind.
BindValidator string
BindSchema string
BindFixtures string
// Contract: when atomic-bind is requested, AiwfDoc must be the
// editable aiwf.yaml document and AiwfContracts the parsed
// contracts: block (nil ok if absent). The CLI dispatcher loads
// these only when the bind flags are present.
AiwfDoc *aiwfyaml.Doc
AiwfContracts *aiwfyaml.Contracts
// Contract: repo root, needed to verify that the bound schema and
// fixtures paths exist on disk (G18). Required when the bind
// triplet is provided; ignored otherwise.
RepoRoot string
// BodyOverride, when non-nil, replaces the kind's default body
// template. Used by `aiwf add --body-file` so the body content
// rides along with the create commit instead of forcing a
// follow-up untrailered hand-edit (M-056). Nil leaves current
// behavior — the per-kind template lands as the body. The bytes
// must not begin with a YAML frontmatter delimiter (`---\n`);
// callers pass body content only, not a full markdown document.
BodyOverride []byte
// TitleMaxLength caps the length of --title. The CLI dispatcher
// sets it from `aiwf.yaml`'s `entities.title_max_length` (default
// 80 per G-0102). Zero or negative means "uncapped" — used by
// tests that don't thread a config and don't care about cap
// policy. Verbs reject titles over this length so the on-disk
// slug, frontmatter title, and rendered surfaces stay in sync.
TitleMaxLength int
// Area is the optional workstream grouping tag (E-0043, M-0173)
// for the five root kinds (epic, ADR, gap, decision, contract).
// A milestone derives its area from its parent epic and never
// stores one, so a non-empty Area on kind=milestone is rejected by
// validateAddOptsForKind. The CLI dispatcher validates the value
// against `aiwf.yaml: areas.members` (the M-0171 accessor) before
// setting it — the verb-time twin of the M-0172 area-unknown check
// — and, for a gap with --discovered-in and no explicit --area,
// derives it from the discovered-in entity's effective area.
Area string
// Priority is the optional closed-set urgency tag (G-0078, E-0066)
// for the two kinds that carry one (gap, decision;
// entity.CarriesOwnPriority). A non-empty Priority on any other
// kind is rejected by validateAddOptsForKind. The value is checked
// against entity.IsAllowedPriorityLevel — the same SSOT predicate
// the `set-priority` verb and the `priority-valid` check rule
// read — so there is no parallel value check here.
Priority string
// Force bypasses the born-complete-kind empty-body gate (G-0326):
// without it, `aiwf add` refuses to create a gap/decision/adr/
// contract whose resolved body has an empty load-bearing section
// (entity.IsBornComplete; see requireNonEmptyBornCompleteBody).
// Mirrors the sovereign-override shape of `aiwf promote --force
// --reason` — the CLI dispatcher requires a non-empty Reason
// whenever Force is set. Has no effect on kinds the gate doesn't
// apply to (epic, milestone) — passing it there is inert.
Force bool
// Reason is the free-form justification recorded in the create
// commit's `aiwf-force:` trailer and body when Force is set.
// Ignored when Force is false.
Reason string
}
AddOptions carries the per-kind extra arguments to Add. Only the fields relevant to the kind are read; others are ignored.
type AllowInput ¶
type AllowInput struct {
Kind VerbKind
TargetID string
CreationRefs []string
MoveSource string
Actor string
Principal string
Scopes []*scope.Scope
Tree *tree.Tree
}
AllowInput bundles every input the allow-rule consumes. Lets the cmd dispatcher build a single struct rather than passing seven arguments through a chain of helpers.
Kind picks the reachability variant. TargetID is the entity the verb mutates (or the destination, for VerbMove). For VerbCreate, CreationRefs lists the outbound reference targets the new entity would carry; for VerbMove, MoveSource is the original location.
Actor is the operator (whoever ran the verb). Principal is the human on whose behalf the operator is acting (always human/...); empty when the actor is acting directly.
Scopes is the union of active scopes attached to Actor — the cmd dispatcher loads it via loadActiveScopesForActor (filters authorize commits by aiwf-to: <Actor>).
Tree is the in-memory entity tree used for forward reachability.
type AllowResult ¶
type AllowResult struct {
Allowed bool
Scope *scope.Scope
Reason string
// Err is the denial error when Allowed is false (nil when allowed).
// For the scope-reachability and no-active-scope denials it is a
// Coded error (entity.Code-extractable: *ScopeOutOfReachError /
// *NoActiveScopeError); the pre-scope usage denials carry a plain
// error. The cmd dispatcher returns it (wrapped with %w) so a Coded
// refusal surfaces as error.code in the --format=json envelope.
// Invariant: every Allowed==false return sets Err.
Err error
}
AllowResult carries the verdict. When Allowed is true and Scope is non-nil, the cmd dispatcher decorates the verb's plan with aiwf-on-behalf-of: <Scope.Principal> and aiwf-authorized-by: <Scope.AuthSHA>. When Allowed is true and Scope is nil, the actor is human/... and no scope decoration is needed (direct human act).
Reason carries a one-line explanation when Allowed is false; the cmd dispatcher surfaces it as the user-facing error.
func Allow ¶
func Allow(in AllowInput) AllowResult
Allow runs the I2.5 allow-rule over the given inputs. Pure: no I/O, no git access. Returns a verdict the cmd dispatcher acts on.
Decision tree:
Empty actor → denied (kernel never lets an unidentified operator commit; the cmd dispatcher should have refused earlier, but defensive). Reason: "actor is required".
human/... actor: -> Principal must be empty (humans act directly; principal forbidden per the trailer-coherence rules). When non-empty, denied with "principal forbidden for human actor". -> Otherwise: Allowed = true, Scope = nil.
non-human actor (ai/... / bot/...): -> Principal must be set (every agent needs a human accountor; enforced again by trailer coherence). When empty, denied with "principal required for non-human actor". -> Iterate Scopes (the union of active scopes attached to this actor). Pick the most-recently-opened that satisfies scopeAllows for the given (Kind, TargetID). On match: Allowed = true, Scope = the matching scope. On no match the denial is split (D-0014): if at least one active scope existed but none reached the target, Err is a *ScopeOutOfReachError (provenance-authorization-out-of-scope); if there was no active scope at all, Err is a *NoActiveScopeError (provenance-no-active-scope).
Per the design's "if multiple match, pick the most-recently-opened deterministically" rule, scopes are walked in reverse insertion order so the latest open wins.
type AuthorizeKindError ¶ added in v0.9.0
AuthorizeKindError reports an aiwf authorize refused because the scope-entity is not an epic or milestone (D-0007). It implements entity.Coded, carrying CodeAuthorizeKindNotAllowed. Error preserves the established message text (including the code) so message-matching consumers keep working while machine consumers use Code.
func (*AuthorizeKindError) Code ¶ added in v0.9.0
func (e *AuthorizeKindError) Code() string
Code returns CodeAuthorizeKindNotAllowed's ID, satisfying entity.Coded.
func (*AuthorizeKindError) Error ¶ added in v0.9.0
func (e *AuthorizeKindError) Error() string
Error implements error.
type AuthorizeMode ¶
type AuthorizeMode int
AuthorizeMode picks one of the three sub-verbs of `aiwf authorize`. Each mode produces exactly one commit; mixing modes is a usage error caught by the cmd dispatcher before this package sees the call.
const ( // AuthorizeOpen opens a fresh scope on the named entity, granting // the agent identified by AuthorizeOptions.Agent. Refused when the // entity is at a terminal status, unless overridden with Force + // non-empty Reason. AuthorizeOpen AuthorizeMode = iota // AuthorizePause pauses the most-recently-opened active scope on // the named entity. Reason is required (non-empty after trim). AuthorizePause // AuthorizeResume resumes the most-recently-paused scope on the // named entity. Reason is required (non-empty after trim). AuthorizeResume )
Authorize sub-verbs.
type AuthorizeOptions ¶
type AuthorizeOptions struct {
Mode AuthorizeMode
// Agent is the role/<id> the scope authorizes (e.g. "ai/claude").
Agent string
// Reason is the rationale; required for pause/resume, optional for
// open (required when Force is set).
Reason string
// Branch (M-0102 / ADR-0010) is the ritual branch a scope is bound
// to. Optional: when empty *and* the target agent is non-ai/*, no
// aiwf-branch: trailer is emitted (backward-compatible). For ai/*
// targets, the M-0103 preflight resolves an empty Branch from
// CurrentBranch when the current checkout is a ritual shape, and
// refuses with PreflightBranchContextRequiredError when it is not.
// Validated against the git-ref shape rule in gitops.ValidateTrailer
// at trailer-assembly time.
Branch string
// CurrentBranch (M-0103) is the short name of the consumer's
// currently-checked-out branch at verb-call time, as the CLI layer
// resolves it via `git symbolic-ref --short HEAD`. Empty when HEAD
// is detached, when the CLI's git invocation fails, or when this
// package is exercised by a verb-level test that does not set it.
// Read by the M-0103 preflight only when the target agent is ai/*
// and --force is not set; in every other code path the field is
// ignored, so leaving it empty in unrelated tests is harmless.
CurrentBranch string
// BranchExists (M-0103) reports whether Branch refers to a local
// branch that resolves under refs/heads/<Branch>, as the CLI layer
// determines via `git show-ref --verify`. The CLI sets this iff
// Branch is non-empty (when Branch is empty there is nothing to
// check). Read by the M-0103 preflight only when the target agent
// is ai/* and --force is not set; in every other code path the
// field is ignored.
BranchExists bool
// BranchSHA (M-0161/AC-6, G-0206) is the bound branch's tip
// SHA at scope-open time, populated by the CLI layer when
// Branch exists locally (via `git rev-parse <branch>`). Empty
// when the branch doesn't yet exist (future-branch carve-out
// per M-0103 / M-0105) or when the CLI's git invocation
// fails. The verb emits `aiwf-branch-sha:` iff non-empty,
// keeping the trailer absent for the future-branch case so
// the rule falls back to name-only resolution there. The
// value is validated against the canonical 40-char lowercase
// hex shape by validateAuthorizeTrailers below (gitops
// shape rule for TrailerBranchSHA).
BranchSHA string
// TrunkShort (M-0161/AC-1, G-0200) is the consumer's configured
// trunk short-name as derived from `aiwf.yaml.allocate.trunk` via
// `Config.TrunkBranchShortName()`. Used by the AI-target
// preflight's "trunk + ritual --branch" carve-out so the predicate
// honors the operator's configured trunk rather than the literal
// `"main"`. Populated by the CLI layer via
// `cliutil.ConfiguredTrunkBranchShortName`; empty (e.g., from a
// verb-level test that doesn't set it) is treated as "no
// resolvable trunk; do not match" — the carve-out's left arm
// fails and preflight falls through to the implicit-ritual-
// current path.
TrunkShort string
Force bool
Scopes []*scope.Scope
}
AuthorizeOptions configures one invocation of the authorize verb.
Scopes carries every scope ever opened on the target entity, in open-order (oldest first), with each scope's current State derived from the entity's commit history. The cmd dispatcher loads it via loadEntityScopes; this package never reads git directly. For AuthorizeOpen the slice is unused (a fresh scope doesn't depend on existing ones); for AuthorizePause / AuthorizeResume it is the source of truth for the most-recently-opened-active / most-recently-paused selection.
type CoherenceError ¶
CoherenceError is the typed error CheckTrailerCoherence returns when a trailer set violates one of the I2.5 required-together or mutually- exclusive rules. Rule names a single canonical violation per error so the caller (verb refusal path or aiwf check standing rule) can map it to a finding code without parsing prose.
The Rule strings are the load-bearing identifiers: do not change one without updating the corresponding `aiwf check` standing-rule subcode in internal/check/provenance.go (added in step 7).
func AsCoherenceError ¶
func AsCoherenceError(err error) (ce *CoherenceError, rule string)
AsCoherenceError returns the *CoherenceError if err is one (or wraps one), and the rule name; otherwise returns nil and "". Helper for callers (notably the standing rule in step 7) that switch on rule names rather than message text.
func (*CoherenceError) Error ¶
func (e *CoherenceError) Error() string
type ContractBindOptions ¶
ContractBindOptions carries the bind-time arguments. All three path/name fields are required; Force is the escape hatch for the "binding already exists with different values" guard.
type EntityMove ¶ added in v0.27.0
EntityMove is one entity's old→new repo-relative path, forward-slash, as planned by a file-moving verb (archive, rename, retitle, reallocate). RewriteLinkDestinations rewrites markdown link destinations that resolve to Move.From, pointing them at Move.To.
type EpicCancelNonTerminalChildrenError ¶ added in v0.9.0
type EpicCancelNonTerminalChildrenError struct {
// Epic is the id of the epic whose cancel was refused.
Epic string
// Children holds the sorted ids of the offending non-terminal child
// milestones.
Children []string
}
EpicCancelNonTerminalChildrenError reports that `aiwf cancel` refused an epic because one or more of its child milestones are still non-terminal (D-0003: refuse-with-listing, no auto-cascade). The operator must dispose each listed milestone (cancel or done) before the epic can be cancelled. It implements entity.Coded, carrying CodeEpicCancelNonTerminalChildren.
func (*EpicCancelNonTerminalChildrenError) Code ¶ added in v0.9.0
func (e *EpicCancelNonTerminalChildrenError) Code() string
Code returns CodeEpicCancelNonTerminalChildren's ID, satisfying entity.Coded.
func (*EpicCancelNonTerminalChildrenError) Error ¶ added in v0.9.0
func (e *EpicCancelNonTerminalChildrenError) Error() string
Error implements error. It names the epic, the count of offending milestones, the sorted ids, instructs the operator to dispose each first, and includes CodeEpicCancelNonTerminalChildren.ID so message-matching consumers can recognize the refusal.
type EpicPromoteNonTerminalChildrenError ¶ added in v0.27.0
type EpicPromoteNonTerminalChildrenError struct {
// Epic is the id of the epic whose promote was refused.
Epic string
// NewStatus is the terminal status the promote attempted to reach.
NewStatus string
// Children holds the sorted ids of the offending non-terminal child
// milestones.
Children []string
}
EpicPromoteNonTerminalChildrenError reports that `aiwf promote` refused to move an epic to a terminal status (NewStatus) because one or more of its child milestones are still non-terminal (refuse-with-listing, no auto-cascade, mirroring D-0003's cancel guard). The operator must dispose each listed milestone (cancel or done) before the epic can reach a terminal status by any path. Runs unconditionally, even under --force — matching Cancel's own D-0003 guard, which has no force-bypass either: force relaxes FSM- transition legality, not this structural children precondition. Archive's independent subtree-terminality guard (internal/verb/archive.go) is the defense-in-depth backstop for the state a raw frontmatter hand-edit (bypassing the verb layer entirely) can still produce. It implements entity.Coded, carrying CodeEpicPromoteNonTerminalChildren.
func (*EpicPromoteNonTerminalChildrenError) Code ¶ added in v0.27.0
func (e *EpicPromoteNonTerminalChildrenError) Code() string
Code returns CodeEpicPromoteNonTerminalChildren's ID, satisfying entity.Coded.
func (*EpicPromoteNonTerminalChildrenError) Error ¶ added in v0.27.0
func (e *EpicPromoteNonTerminalChildrenError) Error() string
Error implements error. It names the epic, the attempted terminal status, the count of offending milestones, the sorted ids, instructs the operator to dispose each first, and includes CodeEpicPromoteNonTerminalChildren.ID so message-matching consumers can recognize the refusal.
type FileOp ¶
type FileOp struct {
Type OpType
Path string // source path (relative to repo root)
NewPath string // destination path (only for OpMove)
Content []byte // file contents (only for OpWrite)
}
FileOp is a single planned filesystem mutation.
type ImportOptions ¶
type ImportOptions struct {
// OnCollision selects behavior when a manifest entry has an
// explicit id that already exists in the tree. Empty defaults
// to "fail".
OnCollision string
// TitleMaxLength caps each manifest entry's title length. The
// CLI dispatcher sets it from `aiwf.yaml`'s
// `entities.title_max_length` (default 80 per G-0102). Zero or
// negative means "uncapped" — used by tests that don't thread a
// config. An import is rejected atomically if any entry's title
// exceeds the cap; the kernel's "one verb = one commit" rule
// makes per-entry partial-acceptance untenable.
TitleMaxLength int
}
ImportOptions controls how Import handles edge cases.
type ImportResult ¶
ImportResult is what Import returns. Either Findings is non-empty (validation rejected the projection; nothing should be applied) or Plans is non-empty (one plan in single-commit mode, N plans in per-entity mode; the orchestrator applies each in order).
func Import ¶
func Import(ctx context.Context, t *tree.Tree, m *manifest.Manifest, actor string, opts ImportOptions) (*ImportResult, error)
Import processes a manifest against the existing tree and returns either findings (validation failed) or plans (validation passed, orchestrator should apply). Pure with respect to the filesystem; no writes happen here.
The processing pipeline is:
- resolve --on-collision against entries with explicit ids that already exist in the tree (fail/skip/update).
- detect intra-manifest duplicate explicit ids (always an error).
- allocate auto-id entries from max(existing ∪ reserved) + 1.
- resolve paths for every entry (new or updating an existing one).
- project the tree (add or replace per entry) with PlannedFiles populated for every OpWrite path.
- run projectionFindings; abort with findings if any error level issues are introduced.
- assemble plans according to the manifest's commit mode.
type MilestoneCancelNonTerminalACsError ¶ added in v0.9.0
type MilestoneCancelNonTerminalACsError struct {
// Milestone is the id of the milestone whose cancel was refused.
Milestone string
// ACs holds the composite ids (`M-NNNN/AC-N`) of the offending open
// acceptance criteria.
ACs []string
}
MilestoneCancelNonTerminalACsError reports that `aiwf cancel` refused a milestone because one or more of its acceptance criteria are still `open` (D-0004: refuse-with-listing, no auto-cascade). The operator must dispose each listed AC (met, deferred, or cancelled) before the milestone can be cancelled. It implements entity.Coded, carrying CodeMilestoneCancelNonTerminalACs.
func (*MilestoneCancelNonTerminalACsError) Code ¶ added in v0.9.0
func (e *MilestoneCancelNonTerminalACsError) Code() string
Code returns CodeMilestoneCancelNonTerminalACs's ID, satisfying entity.Coded.
func (*MilestoneCancelNonTerminalACsError) Error ¶ added in v0.9.0
func (e *MilestoneCancelNonTerminalACsError) Error() string
Error implements error. It names the milestone, the count of offending ACs, the composite ids, instructs the operator to dispose each first, and includes CodeMilestoneCancelNonTerminalACs.ID so message-matching consumers can recognize the refusal.
type MilestonePromoteNonTerminalACsError ¶ added in v0.27.0
type MilestonePromoteNonTerminalACsError struct {
// Milestone is the id of the milestone whose promote was refused.
Milestone string
// NewStatus is the terminal status the promote attempted to reach
// (always "cancelled" today; see the type doc).
NewStatus string
// ACs holds the composite ids (`M-NNNN/AC-N`) of the offending open
// acceptance criteria.
ACs []string
}
MilestonePromoteNonTerminalACsError reports that `aiwf promote` refused to move a milestone to `cancelled` (NewStatus) because one or more of its acceptance criteria are still `open` (refuse-with-listing, no auto-cascade, mirroring D-0004's cancel guard). The operator must dispose each listed AC (met, deferred, or cancelled) before the milestone can reach `cancelled` by any path. The `done` target carries its own, independent precondition — the milestone-done-incomplete-acs check-rule that projectionFindings runs further down Promote — so it never reaches this error type; NewStatus is carried for message symmetry with the sibling EpicPromoteNonTerminalChildrenError, not because this type fires for more than one target today. It implements entity.Coded, carrying CodeMilestonePromoteNonTerminalACs.
func (*MilestonePromoteNonTerminalACsError) Code ¶ added in v0.27.0
func (e *MilestonePromoteNonTerminalACsError) Code() string
Code returns CodeMilestonePromoteNonTerminalACs's ID, satisfying entity.Coded.
func (*MilestonePromoteNonTerminalACsError) Error ¶ added in v0.27.0
func (e *MilestonePromoteNonTerminalACsError) Error() string
Error implements error. It names the milestone, the attempted target status, the count of offending ACs, the composite ids, instructs the operator to dispose each first, and includes CodeMilestonePromoteNonTerminalACs.ID so message-matching consumers can recognize the refusal.
type NoActiveScopeError ¶ added in v0.9.0
type NoActiveScopeError struct {
// Actor is the non-human operator whose act was refused.
Actor string
}
NoActiveScopeError reports that a non-human actor attempted a verb with no active scope at all — distinct from out-of-reach, where a scope exists but does not contain the target. It implements entity.Coded, carrying check.CodeProvenanceNoActiveScope.
func (*NoActiveScopeError) Code ¶ added in v0.9.0
func (e *NoActiveScopeError) Code() string
Code returns check.CodeProvenanceNoActiveScope, satisfying entity.Coded.
func (*NoActiveScopeError) Error ¶ added in v0.9.0
func (e *NoActiveScopeError) Error() string
Error implements error.
type OpType ¶
type OpType int
OpType discriminates between file operations.
The set is deliberately closed to OpWrite and OpMove — there is no OpDelete, by design, not omission. aiwf never deletes an entity file: "removal" is a status flip to a terminal value (cancelled / wontfix / rejected / retired) followed by an archive sweep that OpMoves the file into its per-kind archive/ subdirectory (ADR-0004). A verb that needs to "remove" something promotes it to a terminal status and lets `aiwf archive` relocate it; nothing in the kernel unlinks a tracked entity.
type Plan ¶
type Plan struct {
Subject string
Body string
Trailers []gitops.Trailer
Ops []FileOp
AllowEmpty bool
}
Plan describes the work the orchestrator must do after validation passes: a set of file operations to apply on disk, plus the commit subject, optional body, and trailers to record once they're staged.
Body is free-form prose: typically the human-supplied --reason for a status transition. Empty when the verb has no narrative to record. Stored in the commit body (between subject and trailers), surfaced by `aiwf history` for events that carry one.
AllowEmpty signals that the plan's commit has no file-level diff and must be created via `git commit --allow-empty`. Used by `aiwf authorize` (which records a scope event in trailers without touching any entity file) and the `--audit-only` recovery mode added in plan step 5b. The default (false) is the normal verb behaviour: a commit without staged changes errors.
type PreflightBranchContextRequiredError ¶ added in v0.12.0
type PreflightBranchContextRequiredError struct {
// Agent is the --to ai/<id> value that triggered the preflight.
Agent string
// CurrentBranch is whatever the caller reported as the current
// checkout (empty when HEAD is detached or git failed); included
// in the message so the operator can see what was checked.
CurrentBranch string
}
PreflightBranchContextRequiredError reports an aiwf authorize refused because the AI-target preflight (M-0103) found no ritual branch context in play — neither was --branch supplied, nor does the current checkout match a ritual shape recognized by internal/branchparse/. Implements entity.Coded via Code; carries CodePreflightBranchContextRequired.
func (*PreflightBranchContextRequiredError) Code ¶ added in v0.12.0
func (e *PreflightBranchContextRequiredError) Code() string
Code returns CodePreflightBranchContextRequired's ID, satisfying entity.Coded.
func (*PreflightBranchContextRequiredError) Error ¶ added in v0.12.0
func (e *PreflightBranchContextRequiredError) Error() string
Error implements error.
M-0161/AC-7: when CurrentBranch is empty (the CLI reports empty on detached HEAD or when git fails), the message names "detached HEAD has no ritual context" explicitly so the operator sees the exact state rather than just "current checkout ” does not match". The override path (--force --reason) and the ritual-branch landing options are listed the same way for both cases.
type PreflightBranchNotFoundError ¶ added in v0.12.0
type PreflightBranchNotFoundError struct {
// Branch is the --branch value that did not resolve under refs/heads/.
Branch string
}
PreflightBranchNotFoundError reports an aiwf authorize refused because the AI-target preflight (M-0103) was given --branch <name> but the named branch does not exist locally. Implements entity.Coded via Code; carries CodePreflightBranchNotFound.
func (*PreflightBranchNotFoundError) Code ¶ added in v0.12.0
func (e *PreflightBranchNotFoundError) Code() string
Code returns CodePreflightBranchNotFound's ID, satisfying entity.Coded.
func (*PreflightBranchNotFoundError) Error ¶ added in v0.12.0
func (e *PreflightBranchNotFoundError) Error() string
Error implements error.
type PreflightRungPairError ¶ added in v0.12.0
type PreflightRungPairError struct {
// CurrentBranch is the operator's current checkout.
CurrentBranch string
// CurrentRung is the rung CurrentBranch classified as
// (one of "trunk"/"epic"/"milestone"/"patch"/"").
CurrentRung string
// TargetBranch is the --branch value that was rejected.
TargetBranch string
// TargetRung is the rung TargetBranch classified as.
TargetRung string
}
PreflightRungPairError reports an aiwf authorize refused because the (CurrentBranch rung, --branch rung) pair is not in the legal set per ADR-0010 (M-0161/AC-2). Carries the offending branches AND their classified rungs so the operator can see exactly which pair was rejected. Implements entity.Coded via Code; carries CodePreflightRungPair.
func (*PreflightRungPairError) Code ¶ added in v0.12.0
func (e *PreflightRungPairError) Code() string
Code returns CodePreflightRungPair's ID, satisfying entity.Coded.
func (*PreflightRungPairError) Error ¶ added in v0.12.0
func (e *PreflightRungPairError) Error() string
Error implements error.
type PromoteOptions ¶
PromoteOptions carries optional fields for Promote — resolver pointers (gap.addressed_by / gap.addressed_by_commit, adr.superseded_by) that need to be written atomically with the status change so the matching check rule (gap-addressed-has-resolver, adr-supersession-mutual) is satisfied without a follow-up hand-edit.
AddressedBy / AddressedByCommit are valid only when the target is a gap and newStatus is "addressed". SupersededBy is valid only when the target is an ADR and newStatus is "superseded". Mismatches return a Go error before any disk work — usage misalignment, not a finding.
When a slice or string is set, it replaces the existing field on the entity (this is a one-shot setting at status-change time, not a merge). Unset fields leave the entity's existing values untouched.
type RecipeInstallOptions ¶
type RecipeInstallOptions struct {
Force bool
}
RecipeInstallOptions carries the recipe-install arguments shared between embedded-name and --from-path entry points. Force allows replacing a validator that already exists with a different shape.
type Result ¶
type Result struct {
Findings []check.Finding
Plan *Plan
NoOp bool
NoOpMessage string
// Metadata carries per-verb-appropriate facts about the mutation
// (M-0239/AC-2) — e.g. entity_id/from/to for a status transition,
// swept_count for an archive sweep. Surfaced under the JSON
// envelope's metadata key, alongside AC-1's correlation_id and
// (on a successful apply) commit_sha. nil for verbs that report
// nothing beyond those two.
Metadata map[string]any
}
Result is what every verb returns. Exactly one of Findings, Plan, or the NoOp signal is populated:
- Findings non-empty → validation failed; no disk changes pending. Caller renders findings and exits 1.
- Plan non-nil → projection is clean; caller should apply Operations, stage them, and commit with the plan's subject + trailers.
- NoOp == true → validation passed, but the requested change is already in place. Caller prints NoOpMessage on stdout and exits 0. Used by idempotent verbs (bind on exact match, etc.).
func AcknowledgeIllegal ¶ added in v0.9.0
func AcknowledgeIllegal(ctx context.Context, root, sha, forEntity, actor, reason string) (*Result, error)
AcknowledgeIllegal records a retroactive sovereign override for a historical commit that one of the kernel's audit rules flags. The acknowledgment lives as a current-day empty commit carrying:
aiwf-verb: acknowledge-illegal aiwf-force-for: <sha> aiwf-actor: human/<name> aiwf-reason: <free-form text> aiwf-entity: <id> (only when forEntity is non-empty)
The fsm-history-consistent rule (M-0136/AC-2) walks HEAD's reachable history for `aiwf-force-for` trailers and exempts illegal-transition findings whose offending commit appears as a target. Six other rules consume the same SHA set via the M-0159/AC-3 lift.
G-0231 item 3: a SEVENTH consumer rule — `provenance-untrailered-entity-commit` — was added with TIGHTER scope. Its findings are per-(commit, entity) pairs, so it requires per-(SHA, entity) acks: the ack commit must carry BOTH `aiwf-force- for: <sha>` AND `aiwf-entity: <id>`, and the verb verifies at write time that <sha>'s diff actually touches <id>'s file. The kernel-integrity property this adds: even if the operator (human or LLM) writes the wrong entity id with the right SHA, the verb refuses before the ack lands. SHA existence was already verified pre-G-0231 by shaAckable; entity binding is the new check.
Constraints (M-0136/AC-1, extended by G-0231 item 3):
- reason must be non-empty after trim (sovereign acts require a written rationale).
- actor must be `human/...` (sovereign acts trace to a named human; no LLM / bot ack).
- sha must match the 7-40-hex SHA pattern (the trailer's value constraint, enforced via gitops.ValidateTrailer).
- forEntity is OPTIONAL. When empty, the ack is per-SHA blanket (the legacy shape covering the first six rules). When set, the verb verifies <sha>'s diff touches <id>'s file and emits `aiwf-entity: <id>` in the ack commit (the per-(SHA, entity) shape required by provenance-untrailered-entity-commit).
M-0136/AC-4 + G-0236: sha must resolve to a real commit in the local object database (see shaAckable) — covering both a SHA still on trunk (the AC-4 case) and an orphan SHA reachable only via reflog (the G-0236 case: `isolation-escape-orphaned-ai-commit` findings' offending SHAs are by construction unreachable from HEAD, force-pushed-away tips surfaced via the reflog walker at internal/check/reflog_walk.go).
Typo guard preserved: a SHA that resolves to no commit is rejected. The per-SHA closed-set scoping (each ack covers only the named SHA, and rules only fire on SHAs they independently enumerated) bounds the silencing surface — accepting any object-DB-present SHA doesn't widen any rule's reach.
Returns a Result with a Plan carrying the empty commit's trailers. The Apply pipeline materializes the `git commit --allow-empty` once the human gate clears.
func AcknowledgeMistag ¶ added in v0.18.0
func AcknowledgeMistag(ctx context.Context, t *tree.Tree, id, actor, reason string) (*Result, error)
AcknowledgeMistag records a sovereign acceptance that an entity's area tag and its commits' landing zone legitimately disagree — the escape valve for the area-mistag check (M-0181/AC-6). Like AcknowledgeIllegal it is a current-day empty commit, but keyed per-ENTITY rather than per-SHA, carrying:
aiwf-verb: acknowledge-mistag aiwf-entity: <canonical-id> aiwf-actor: human/<name> aiwf-reason: <free-form text>
The check's WalkAcknowledgedMistags walks HEAD for these commits and exempts the named entities from area-mistag. The acknowledgement lives in git (queryable via aiwf history), aligns with the existing sovereign-act semantics, and does not pollute aiwf.yaml — the same rationale as acknowledge-illegal.
Constraints (mirroring acknowledge-illegal):
- reason must be non-empty after trim (sovereign acts require a written rationale);
- actor must be `human/...` (no LLM / bot ack — the cross-cutting judgment is the human's);
- the id must resolve to a real entity (composite AC ids roll up to their milestone); a typo is refused rather than recording a no-op ack that silently suppresses nothing.
"What verb undoes this?" — none, by deliberate design (the acknowledge-illegal answer): the ack is one-way; if regretted, the operator re-tags the entity (`aiwf set-area`) so the mistag no longer fires, or lives with the suppressed finding. The verb is human-sovereign by construction.
func Add ¶
func Add(ctx context.Context, t *tree.Tree, kind entity.Kind, title, actor string, opts AddOptions) (*Result, error)
Add creates a new entity of the given kind. Allocates the next free id, builds the entity, projects it onto the tree, runs `aiwf check` against the projection, and either returns findings (no changes staged) or a Plan that the orchestrator applies.
For contracts with all three Bind* options set, Add additionally splices the binding into aiwf.yaml.contracts.entries[] and the returned Plan carries a second OpWrite so the entity creation and the binding land as a single commit.
Returns a Go error only when arguments are malformed (missing required option, parent epic not found, contract-only flag on non-contract kind, partial bind triplet). Tree-integrity issues arising from the addition are returned as findings, not errors.
func AddAC ¶
AddAC creates a new acceptance criterion under the named milestone. Single-title convenience wrapper around AddACBatch — the existing signature is preserved so callers that want exactly one AC don't need to wrap their input in a slice. See AddACBatch for the batched-creation contract; this entry point applies the same rules with len(titles)=1. No body content is supplied; callers that want to populate the AC body in the same atomic commit use AddACBatch directly with a non-nil bodies slice.
func AddACBatch ¶
func AddACBatch(ctx context.Context, t *tree.Tree, parentID string, titles []string, bodies [][]byte, actor string) (*Result, error)
AddACBatch creates one or more acceptance criteria under the same milestone in a single atomic commit (M-057). Each title gets a consecutive AC id starting at len(parent.ACs)+1, position-stable per existing rules (cancelled entries count toward position). A matching `### AC-<N> — <title>` heading is appended to the milestone body for each created AC.
Every new AC is seeded at the pre-cycle empty phase regardless of the parent's tdd: policy — `red` means "a failing test exists," which a just-created AC has not written. The live "" -> red promote records the failing test.
Validation is whole-batch (M-057/AC-2): titles are checked first (empty-after-trim, not-prosey), then the milestone projection runs once with all N new entries; if any rule fires, the entire batch aborts with no commit. The plan returns exactly one OpWrite for the milestone file regardless of N (M-057/AC-4), so per-mutation atomicity is preserved.
The commit carries N `aiwf-entity:` trailers — one per created composite id, in allocation order (M-057/AC-3). `aiwf history M-NNN/AC-X` finds the commit because git's --grep matches any trailer line. The single-title invocation produces exactly one aiwf-entity trailer, matching pre-batch behavior (M-057/AC-5).
When bodies is non-nil and bodies[i] is non-empty, its bytes are appended under the matching AC's `### AC-N — <title>` heading in the same atomic commit (M-067/AC-1). Other AC-067 ACs (count validation, frontmatter rejection, stdin pairing) bring their own rules in subsequent cycles; the AC-1 cycle does only the wiring.
Returns a Go error for setup failures (empty titles slice, empty or prosey title, milestone not found, kind mismatch). Tree-level findings caused by the addition are returned in Result.Findings.
func Archive ¶ added in v0.8.0
Archive sweeps terminal-status entities from the active tree into their per-kind `archive/` subdirectories per ADR-0004's storage table. The verb is multi-entity: one invocation rewrites every qualifying entity and produces a single commit (CLAUDE.md §7).
Behavior:
- Default is dry-run: the verb computes a Plan and the caller prints planned ops without applying. `--apply` (caller flag) causes the dispatcher to run verb.Apply on the Plan.
- Single commit per --apply per kernel principle #7. Trailer is `aiwf-verb: archive`; no `aiwf-entity:` trailer (multi-entity sweep, same shape as `aiwf rewidth`).
- Idempotent. An already-swept tree returns a NoOp Result; the caller prints "no changes needed" and exits 0.
- Sweep is by status, not by id. There is no positional id arg — ADR-0004 §"`aiwf archive` verb" rejects per-id housekeeping ("that would be a hand-edit detour, not a verb").
Per-kind storage table (verbatim from ADR-0004 §"Storage — per-kind layout"):
| Kind | Active | Archive | |----------|-------------------------------------|----------------------------------------------| | Epic | work/epics/<epic>/ | work/epics/archive/<epic>/ (whole subtree) | | Milestone| work/epics/<epic>/M-NNNN-<slug>.md | does not archive independently — rides w/ epic| | Contract | work/contracts/<contract>/ | work/contracts/archive/<contract>/ | | Gap | work/gaps/<id>-<slug>.md | work/gaps/archive/<id>-<slug>.md | | Decision | work/decisions/<id>-<slug>.md | work/decisions/archive/<id>-<slug>.md | | ADR | docs/adr/<id>-<slug>.md | docs/adr/archive/<id>-<slug>.md |
`internal/entity/transition.go::IsTerminal` is the single source of truth for terminal statuses.
kindFilter scopes the sweep. "" sweeps every kind; a non-empty value must be one of entity.AllKinds().
func Authorize ¶
func Authorize(ctx context.Context, t *tree.Tree, id, actor string, opts AuthorizeOptions) (*Result, error)
Authorize runs the `aiwf authorize` verb. Refusal rules per docs/design/provenance-model.md §"The aiwf authorize verb":
- Actor must be human/...; only humans authorize.
- For AuthorizeOpen, the scope-entity must not be in a terminal status (overridable with Force + non-empty Reason).
- For AuthorizePause, an active scope on the entity must exist.
- For AuthorizeResume, a paused scope on the entity must exist.
- Reason is required for pause/resume (non-empty after trim); optional for AuthorizeOpen unless Force is set.
Each invocation produces exactly one commit. The commit's diff is empty; Plan.AllowEmpty makes Apply use `git commit --allow-empty`. The agent is recorded in `aiwf-to:` (consistent with the existing trailer schema: the scope is the "entity" being acted on, with its target state encoded by who can act under it).
func Cancel ¶
func Cancel(ctx context.Context, t *tree.Tree, id, actor, reason string, force bool) (*Result, error)
Cancel promotes an entity to its kind's terminal-cancel status — `cancelled` for epic/milestone, `rejected` for adr/decision, `wontfix` for gap, `retired` for contract. Errors when the entity is already in a terminal state or when the kind is unknown.
reason is optional free-form prose; when non-empty, it lands in the commit body so the cancellation's "why" is preserved for future readers. Empty reason matches today's body-less behaviour.
force=true emits an `aiwf-force: <reason>` trailer alongside the standard ones so the cancellation is auditable as a forced action. Cancel has no FSM transition rule to relax (it always sets status to the kind's terminal-cancel target), so force is purely an audit signal here. The "already at target" guard remains in place even under force — there is no diff to write. Force requires a non-empty reason; the caller is responsible for enforcing that.
func CancelAuditOnly ¶
CancelAuditOnly records that <id> was cancelled via a path that bypassed the kernel. Refuses when the entity is not already at the kind's terminal-cancel target. Composite ids dispatch to cancelACAuditOnly (which checks against the AC `cancelled` state).
func ContractBind ¶
func ContractBind(ctx context.Context, t *tree.Tree, doc *aiwfyaml.Doc, current *aiwfyaml.Contracts, id, actor, repoRoot string, opts ContractBindOptions) (*Result, error)
ContractBind creates or replaces the binding for a contract entity in aiwf.yaml.contracts.entries[].
The verb is idempotent against an exact match (returns a NoOp result), errors out when the existing binding differs unless opts.Force is set, and validates that:
- the contract entity exists in the tree;
- the validator name is declared in aiwf.yaml.contracts.validators (unless current is nil — then the verb refuses, since there is no validator universe to choose from yet);
- the bound schema and fixtures paths exist on disk (G18) — verified by running contractcheck.Run on the projected config and surfacing any introduced contract-config findings as a Result with Findings populated. Without this projection check, the only enforcement was the pre-push hook (a watch-point violation per design-lessons §2).
On success, the returned Plan carries one OpWrite for aiwf.yaml with the spliced contracts: block; the orchestrator commits it with the bind trailers.
repoRoot is the consumer repo root, needed to resolve schema and fixtures paths for the existence check. The CLI dispatcher passes the same value it uses to load the tree.
func ContractUnbind ¶
func ContractUnbind(ctx context.Context, t *tree.Tree, doc *aiwfyaml.Doc, current *aiwfyaml.Contracts, id, actor, repoRoot string) (*Result, error)
ContractUnbind removes the binding for a contract from aiwf.yaml.contracts.entries[]. The contract entity is left untouched; its status governs whether pre-push verification still runs (it doesn't, once unbound). Errors when no binding exists.
t and repoRoot feed the shared diff-based gate (D-0041): unbinding can never resolve a missing-schema/-fixtures finding since it only removes an entry, but it can introduce a fresh no-binding warning on the now-unbound contract entity — the gate is a safety net that makes that visible rather than one more unchecked mutation path.
func EditBody ¶
func EditBody(ctx context.Context, t *tree.Tree, id string, body []byte, actor, reason string) (*Result, error)
EditBody replaces the markdown body of an existing entity file. The frontmatter is left untouched — that stays the domain of the structured-state verbs (promote, rename, cancel, reallocate).
M-058 introduced the explicit-content path; M-060 added bless mode. The verb has two modes, dispatched on body:
body == nil: bless mode (M-060). Read the working-copy bytes and HEAD bytes, refuse if there is no diff (no changes to commit), refuse if the diff includes frontmatter changes (point at promote/rename/cancel/reallocate), commit the working-copy bytes verbatim with edit-body trailers. This is the natural human workflow: edit the file in $EDITOR, then bless the change with a verb route.
body != nil: explicit-content mode (M-058). The supplied bytes replace the body; the verb re-serializes the existing entity frontmatter with the new body and writes the result. This is the AI/script workflow — the body content was drafted elsewhere and is supplied via `--body-file <path>` or stdin.
Both modes refuse leading-`---` content via validateUserBodyBytes, refuse composite ids, return one OpWrite, and emit the same trailer set (`aiwf-verb edit-body`, `aiwf-entity`, `aiwf-actor`).
reason is optional free-form prose; when non-empty it lands in the commit body so future readers can see *why* the body was rewritten, not just *what* changed.
Returns a Go error for "couldn't even start": id not found, composite id, body validation failure, no-diff in bless mode, frontmatter-changed in bless mode. Tree-level findings caused by the projection are returned in Result.Findings.
func MilestoneDependsOn ¶
func MilestoneDependsOn(ctx context.Context, t *tree.Tree, id string, deps []string, clearList bool, actor, reason string) (*Result, error)
MilestoneDependsOn writes the depends_on frontmatter array on a milestone. Closes the post-allocation half of G-072 (the create-time half is the --depends-on flag on `aiwf add milestone`).
Two modes, dispatched on `clear`:
- clear == false: replace-not-append. The supplied `deps` list becomes the milestone's depends_on. To add a single dependency to an existing list, the caller passes the full updated list.
- clear == true: empty the list. `deps` must be empty (the mutex is enforced by the dispatcher; this verb pins the contract).
Both modes emit one OpWrite with `aiwf-verb: milestone-depends-on` trailers, producing the kernel's per-mutation atomicity guarantee.
Each id in `deps` must resolve to an existing milestone; the verb refuses before the commit otherwise. Cycle detection stays at `aiwf check`'s layer — different concern, different chokepoint.
Forward-compatibility note: the verb shape `aiwf milestone depends-on M-NNN --on <ids>` is a clean subset of the future `aiwf <kind> depends-on <id> --on <ids>` cross-kind generalisation (G-073). The verb-name segment "milestone" is the *kind*; the generalisation extends to other kinds without renaming this verb.
reason is optional free-form prose; when non-empty it lands in the commit body so the rationale surfaces in `aiwf history`.
func Move ¶
Move relocates a milestone from its current epic to a different epic. The id is preserved (so references in other entities still resolve); only the file's location on disk and the milestone's `parent:` frontmatter field change. One commit per move with trailers `aiwf-verb: move`, `aiwf-entity: <M-id>`, `aiwf-prior-parent: <old-epic>`, `aiwf-actor: …` so `aiwf history` can answer "where did this milestone come from?" from either the milestone's or the old epic's perspective.
Returns a Go error for "couldn't even start": id not found, kind not milestone, target epic missing or wrong kind, milestone already under the target epic. Tree-level findings caused by the move (e.g. a depends_on cycle introduced by the new neighborhood) are returned in Result.Findings.
func Promote ¶
func Promote(ctx context.Context, t *tree.Tree, id, newStatus, actor, reason string, force bool, opts PromoteOptions) (*Result, error)
Promote advances an entity's status. The transition is validated against the kind's FSM (entity.ValidateTransition) before any projection runs, so unknown statuses and illegal jumps are rejected with a clear error rather than as a `status-valid` finding.
reason is optional free-form prose explaining *why* the transition happens. When non-empty, it lands in the commit body (between subject and trailers) so future readers can see the why, not just the what. Empty reason produces a body-less commit.
force=true relaxes the FSM transition rule so any-to-any moves are permitted; coherence (closed-set membership of the target status, id format, ref resolution) still runs via projection findings, so promoting to an unknown status is still rejected. Force requires a non-empty reason; the caller (cmd dispatcher) is responsible for enforcing that. When force is set, the standard trailers gain `aiwf-force: <reason>` so the audit trail is queryable.
opts carries optional resolver pointers that need to be written in the same commit as the status change (see PromoteOptions). The resolver is validated against the entity's kind and the target status before any disk work — a mismatch is a Go error.
Returns a Go error for "couldn't even start": id not found, illegal transition (when not forced), resolver-flag/kind/status mismatch. Tree-level findings caused by the change are returned as a Result with non-empty Findings.
func PromoteACPhase ¶
func PromoteACPhase(ctx context.Context, t *tree.Tree, compositeID, newPhase, actor, reason string, force bool, tests *gitops.TestMetrics) (*Result, error)
PromoteACPhase handles `aiwf promote M-NNN/AC-N --phase <p>`. Advances the AC's tdd_phase along the linear FSM (red → green → (refactor →) done). Mutex with status changes — the dispatcher rejects passing both a positional state and --phase. force=true skips the FSM transition rule but coherence (closed-set membership of newPhase) still runs via projection findings.
Trailers: aiwf-to: carries the new phase value (same trailer as for status changes; the verb name + composite id make it unambiguous which dimension moved). aiwf-force: when forced.
func PromoteACPhaseAuditOnly ¶
func PromoteACPhaseAuditOnly(ctx context.Context, t *tree.Tree, compositeID, newPhase, actor, reason string) (*Result, error)
PromoteACPhaseAuditOnly is the audit-only variant of PromoteACPhase: refuses unless the AC's tdd_phase already equals newPhase. Same trailer + empty-commit shape as the status variant.
func PromoteAuditOnly ¶
func PromoteAuditOnly(ctx context.Context, t *tree.Tree, id, newStatus, actor, reason string) (*Result, error)
PromoteAuditOnly records that <id> reached <newStatus> via a path that bypassed the kernel (manual commit, import, etc.). Refuses when the entity is not already at newStatus — audit-only never transitions, only documents.
Composite ids dispatch to promoteACAuditOnly. Top-level ids run against the per-kind FSM only insofar as the closed-set membership of newStatus must hold (an unknown status is rejected).
func Reallocate ¶
Reallocate gives an entity a new id of the same kind, renames its file/dir to reflect the new id, and rewrites every reference to the old id — both in frontmatter and in body prose — across the whole tree, including the entity's own body.
The reference grammar (E-NN, M-NNN, ADR-NNNN, G-NNN, D-NNN, C-NNN) is regular and unambiguous, so prose rewriting is mechanical and safe. Word boundaries prevent false matches against longer ids (e.g., reallocating M-001 leaves M-0010 untouched).
The argument may be an id (e.g., "M-007") when unambiguous, or a repo-relative path (e.g., "work/epics/E-01-platform/M-007-cache.md") when the id is duplicated — required after a merge collision where two files share the same id.
The commit gets an aiwf-prior-entity: <old-id> trailer in addition to the standard three, so `aiwf history <old-id>` continues to find the entity's lifecycle even after the renumber.
func RecipeInstall ¶
func RecipeInstall(ctx context.Context, t *tree.Tree, doc *aiwfyaml.Doc, current *aiwfyaml.Contracts, name string, validator aiwfyaml.Validator, actor, repoRoot string, opts RecipeInstallOptions) (*Result, error)
RecipeInstall registers `validator` under `name` in aiwf.yaml.contracts.validators. The verb is idempotent on exact match (NoOp result), errors when an existing validator carries the same name with different fields unless opts.Force is set.
The returned Plan trailers carry one `aiwf-entity:` per binding currently referencing `name` in aiwf.yaml.contracts.entries[] so `aiwf history` for those contracts surfaces the recipe change.
t and repoRoot feed the shared diff-based gate (D-0041): installing a validator only touches contracts.validators, never entries[], so in practice the gate can never find an introduced finding here — it is wired in as a safety net, not because this mutation is expected to trip it.
func RecipeRemove ¶
func RecipeRemove(ctx context.Context, t *tree.Tree, doc *aiwfyaml.Doc, current *aiwfyaml.Contracts, name, actor, repoRoot string) (*Result, error)
RecipeRemove removes the named validator from aiwf.yaml.contracts.validators. Errors when one or more bindings in entries[] still reference the validator — the user must `unbind` or rebind those contracts first. That referential- integrity check runs before the shared gate and keeps its own precise error message (the milestone's constraint): the gate is an additional safety net on top, not a replacement for it.
func Rename ¶
func Rename(ctx context.Context, t *tree.Tree, id, newSlug, actor string, slugMaxLength int) (*Result, error)
Rename changes the slug portion of an entity's file or directory path. The id is preserved (per the design's "ids are immortal" invariant); the title in frontmatter is unchanged. Hand-edit the title in markdown if you want it to track the new slug.
For epic and contract (directory-based kinds), the directory itself is moved; nested files (milestones under an epic, the schema/ subdir under a contract) move with it. For file-based kinds, the single file moves.
For composite ids (M-NNN/AC-N), Rename dispatches to renameAC: the second argument is interpreted as a new title (not a slug), the AC's frontmatter title is updated, and the matching `### AC-<N>` body heading is rewritten in place. No path change.
Returns a Go error for "couldn't even start": id not found, slug produces an invalid path, source path missing on disk. Tree-level findings caused by the move are returned in Result.Findings. slugMaxLength caps the rewritten slug per `entities.title_max_length` (G-0102, kernel default 80). Title and slug share the same length budget so on-disk filenames and frontmatter titles stay in sync. Pass 0 from tests that don't care about cap policy.
func RenameArea ¶ added in v0.18.0
func RenameArea( ctx context.Context, t *tree.Tree, doc *aiwfyaml.Doc, members []config.Member, oldName, newName, actor string, ) (*Result, error)
RenameArea renames a declared workstream area (E-0044, M-0177). It rewrites the `areas.members` entry in aiwf.yaml from oldName to newName AND rewrites the `area:` frontmatter of every entity tagged oldName to newName, in one atomic commit — the same referential- integrity discipline `aiwf reallocate` applies to ids. Renaming an area by hand-editing aiwf.yaml would orphan every entity still carrying the old value (the `area-unknown` finding flags them and the grouping view buckets them into the untagged complement); this verb closes that hole.
members is the consumer's declared areas (the validated single source of truth from config.Load, passed by the CLI layer); the verb never invents members and reads it only to validate the rename. doc is the parsed aiwf.yaml the CLI loads for the comment-preserving splice, mirroring how ContractBind receives its aiwfyaml.Doc.
Validation (no Plan on failure, so nothing is written):
- oldName and newName are non-empty and distinct;
- oldName is a declared member;
- newName is NOT already a declared member.
The commit carries `aiwf-verb: rename-area`, one `aiwf-entity:` trailer per rewritten entity (sorted by id for determinism), and `aiwf-actor:`. The `aiwf-verb` trailer suppresses the untrailered- entity audit; the per-entity trailers make the rename appear in each affected entity's `aiwf history`. When no entity references oldName, only the verb+actor trailers ride along (an aiwf.yaml-only change).
What undoes this? The same verb with swapped args: after `rename-area platform infra`, `rename-area infra platform` restores the prior member name and every entity tag.
func Retitle ¶
func Retitle(ctx context.Context, t *tree.Tree, id, newTitle, actor, reason string, titleMaxLength int) (*Result, error)
Retitle updates the frontmatter `title:` of an existing entity (top-level kind) or AC (composite id). For top-level entities, the on-disk slug is also re-derived from the new title and the file is renamed atomically in the same commit (G-0108) — so frontmatter title and filesystem slug never drift apart. A canonical `# <ID> — <title>` body H1, if present, is rewritten to track the new title in the same commit (G-0083); bodies without a canonical H1 are left untouched, so an operator-shaped non-canonical heading is never silently clobbered. Use `aiwf rename` when you want a slug change without touching the title.
For composite ids (M-NNN/AC-N), Retitle dispatches to retitleAC, which updates the AC's title in the parent milestone's acs[] array AND regenerates the matching `### AC-<N> — <title>` body heading. Both changes land in one atomic commit per kernel rule. ACs have no slug, so no rename happens on the composite path.
reason is optional free-form prose; when non-empty it lands in the commit body so the rationale surfaces in `aiwf history`.
Returns a Go error for "couldn't even start": id not found, empty new title (after trimming), no-op (current title equals new title), or a title that slugifies to the empty string (e.g., punctuation- only). Tree-level findings caused by the projection are returned in Result.Findings.
titleMaxLength caps the new title per `entities.title_max_length` (G-0102, kernel default 80). Title and slug share the same budget; retitle is also the natural verb to migrate existing entities whose pre-cap titles are over the cap (the operator picks the shorter form). Pass 0 from tests that don't care about cap policy.
func Rewidth ¶
Rewidth sweeps a consumer's active planning tree from narrow legacy id widths (E-NN, M-NNN, G-NNN, D-NNN, C-NNN) to canonical 4-digit width (E-NNNN, M-NNNN, G-NNNN, D-NNNN, C-NNNN). Per ADR-0008:
- Default is dry-run: the verb computes a Plan and the caller prints planned ops without applying. `--apply` (caller flag) causes the dispatcher to run verb.Apply on the Plan.
- Single commit per --apply per kernel principle #7. Trailer is `aiwf-verb: rewidth`; no `aiwf-entity:` trailer (multi-entity sweep, same shape as `aiwf archive`).
- Active-tree only. Files under `<kind>/archive/` are skipped entirely per ADR-0004's forget-by-default principle.
- Idempotent. An already-canonical or empty tree returns a NoOp Result; the caller prints "no changes needed" and exits 0.
The verb walks each kind's active directory in a fixed sequence (epic, milestone, gap, decision, contract, adr) and within a kind iterates in alphabetical order by current filename. Determinism is load-bearing: a second invocation on the same tree visits files in the same order and produces zero ops.
Three reference patterns are rewritten in active-tree markdown bodies:
- Bare id mentions in prose (`E-22` → `E-0022`). Word-boundary guarded so `E-220` doesn't match.
- Composite ids (`M-22/AC-1` → `M-0022/AC-1`).
- Markdown links to active-tree paths (`(work/epics/E-22-foo)`). Links targeting `<kind>/archive/...` are excluded by design.
Code fences (triple-backtick) and inline-code spans (single-backtick) are excluded from rewriting — content inside them stays as-is.
The F-prefix is included in the regex by spec (planned 7th kind from the §07 TDD architecture proposal). No F entities exist today, so it's a forward-compatible no-op for current consumers.
Rewidth does not call check.Run on a projected tree the way other verbs do: the operation is purely structural (rename + body rewrite) and `aiwf check` is the chokepoint for post-migration validation. The pre-push hook will run check after the user pushes the rewidth commit; spurious mid-verb check noise from a tree mid-rename is not what we want.
func SetArea ¶ added in v0.18.0
func SetArea( ctx context.Context, t *tree.Tree, members []string, id, member string, clearTag bool, actor string, ) (*Result, error)
SetArea points a single entity at an existing declared area member — or clears its area tag — in one trailered commit (E-0044, M-0183). It is the membership-change sibling of RenameArea: where RenameArea rewrites the *vocabulary* (a member's name, carrying every referrer), SetArea changes one entity's *membership* against a fixed vocabulary.
It is the guaranteed remediation for `areas.required` (M-0178): when the knob flags an untagged entity, `aiwf set-area <id> <member>` is the one-command unblock. It also owns the inverse — `aiwf set-area <id> --clear` untags an entity back to the untagged state (legitimate and never-flagged unless `areas.required` is set, under which an untagged entity is itself flagged by area-required), the clean correction for a mis-tag.
Two modes, dispatched on `clear`:
- clear == false: set the entity's area to <member>. <member> must be a declared member of `members` (the validated single source of truth the CLI passes from config.Load); the verb never invents a member (that is RenameArea's and config's job). With no areas block declared, `members` is empty and every member is undeclared, so a set refuses.
- clear == true: empty the area field. `omitempty` on entity.Area drops the cleared key on serialize, so the on-disk frontmatter returns to the untagged shape byte-for-byte.
Validation precedes any Plan, so a refusal writes nothing:
- a composite/AC id or a milestone target refuses — area derives from the parent epic; the message names the epic and the remediation command;
- an unknown id refuses;
- <member> and clear given together refuse (mutex);
- a non-empty <member> not in `members` refuses, naming the declared set;
- a no-op (already tagged <member>, or --clear on an already-untagged entity) refuses.
The commit carries `aiwf-verb: set-area`, `aiwf-entity: <canonical id>`, and `aiwf-actor:`. The verb trailer suppresses the `provenance-untrailered-entity-commit` audit a hand-edit would trip — the whole point of the verb, for tag, retag, AND untag.
What undoes this? The same verb, total: a tag (untagged→tagged) reverses with `--clear`; a retag reverses with the prior member; a `--clear` reverses by setting the prior member. One verb owns one field with a complete reversal story.
func SetPriority ¶ added in v0.27.0
func SetPriority( ctx context.Context, t *tree.Tree, id, level string, clearTag bool, actor string, ) (*Result, error)
SetPriority points a single gap or decision at a closed-set priority level — or clears its priority tag — in one trailered commit (G-0078, E-0066, M-0262). It is the write-surface sibling of SetArea: where SetArea validates <member> against a config-declared set, SetPriority validates <level> against the fixed Go-hardcoded set (entity.IsAllowedPriorityLevel) — the same SSOT predicate the priority-valid check rule reads, so there is no parallel value check here.
Two modes, dispatched on `clear`:
- clear == false: set the entity's priority to <level>. <level> must be one of entity.AllowedPriorityLevels().
- clear == true: empty the priority field. `omitempty` on entity.Priority drops the cleared key on serialize, so the on-disk frontmatter returns to the unset shape byte-for-byte.
Validation precedes any Plan, so a refusal writes nothing:
- an unknown id refuses;
- a target whose kind does not carry a priority (!CarriesOwnPriority) refuses — priority is legal only on gap and decision;
- <level> and clear given together refuse (mutex);
- an out-of-range <level> refuses, naming the allowed set;
- a no-op (already set to <level>, or --clear on an already-unset entity) refuses.
The commit carries `aiwf-verb: set-priority`, `aiwf-entity: <canonical id>`, and `aiwf-actor:`. The verb trailer suppresses the `provenance-untrailered-entity-commit` audit a hand-edit would trip — the whole point of the verb, for set, reset, AND clear.
What undoes this? The same verb, total: a set (unset->set) reverses with --clear; a reset reverses with the prior level; a --clear reverses by setting the prior level. One verb owns one field with a complete reversal story.
type ScopeOutOfReachError ¶ added in v0.9.0
type ScopeOutOfReachError struct {
// Actor is the non-human operator whose act was refused.
Actor string
// Target is the entity the verb would have mutated (the destination,
// for a move). Empty for a creation act, where Refs carries the
// proposed outbound references instead.
Target string
// Refs are the creation act's proposed outbound references, used for
// the message subject when Target is empty.
Refs []string
}
ScopeOutOfReachError reports that an authorized agent's verb was refused because its target falls outside the three-edge reach (D-0006) of every active scope attached to the actor. It implements entity.Coded, carrying check.CodeProvenanceAuthorizationOutOfScope — the same code the check-time provenance audit emits for the identical violation (one predicate, two enforcement times; D-0014).
func (*ScopeOutOfReachError) Code ¶ added in v0.9.0
func (e *ScopeOutOfReachError) Code() string
Code returns check.CodeProvenanceAuthorizationOutOfScope.ID, satisfying entity.Coded.
func (*ScopeOutOfReachError) Error ¶ added in v0.9.0
func (e *ScopeOutOfReachError) Error() string
Error implements error, naming the actor and the unreachable subject and including the code id so message-matching consumers recognize it.
type VerbKind ¶
type VerbKind int
VerbKind discriminates the act being gated. Different kinds use different reachability rules per scopeAllows: a creation act checks the new entity's outbound references against the scope- entity; a move act requires both endpoints to reach scope; every other act checks only the target. Step 6's first cut covers the simple act (target-only); creation and move become relevant when the cmd-level wiring lands them.
const ( // VerbAct is the default: the act has a single target entity // (promote, cancel, rename frontmatter changes, etc.). The // target must reach the scope-entity. VerbAct VerbKind = iota // VerbCreate is a creation act: a new entity is added with // outbound references. At least one outbound reference (or // the parent, for milestones) must reach the scope-entity. VerbCreate // VerbMove is a relocation act: both source and destination // endpoints must reach the scope-entity. VerbMove )
VerbKind values.
Source Files
¶
- ac.go
- acknowledgeillegal.go
- acknowledgemistag.go
- add.go
- allow.go
- apply.go
- archive.go
- auditonly.go
- authorize.go
- cancel.go
- cancel_guards.go
- coherence.go
- common.go
- contractbind.go
- contractgate.go
- contractrecipe.go
- editbody.go
- import.go
- linkregion.go
- linkrewrite.go
- linkrewrite_ops.go
- milestone_depends_on.go
- move.go
- pathrewrite.go
- promote.go
- promote_branch_guard.go
- promote_phase_gate.go
- promote_sovereign_act.go
- reallocate.go
- rename.go
- renamearea.go
- retitle.go
- rewidth.go
- scope_errors.go
- setarea.go
- setpriority.go
- verb.go