diff --git a/.github/tests/test_detached_supervision.py b/.github/tests/test_detached_supervision.py
index 6ef2943..ca87217 100644
--- a/.github/tests/test_detached_supervision.py
+++ b/.github/tests/test_detached_supervision.py
@@ -2,6 +2,7 @@
from __future__ import annotations
+import hashlib
import json
import os
import subprocess
@@ -247,7 +248,7 @@ def test_authority_free_frontier_does_not_block_authorized_plan_creation(self) -
"next", "--repo", self.repo, *goal, *flow,
"--human", "contract", "--repository-authority",
)
- self.assertEqual(progressing["decision"]["kind"], "PRESCRIBED")
+ self.assertEqual(progressing["decision"]["kind"], "CANDIDATE")
self.assertEqual(progressing["decision"]["transition"]["id"], "plan.create")
plan = Path(self.work.name) / "source-plan.md"
@@ -301,7 +302,7 @@ def test_one_delivery_context_rematerializes_repository_authority_after_initiali
prescribed = self.helper_json(
"next", "--repo", self.repo, *goal, *flow, *actor,
)
- self.assertEqual(prescribed["decision"]["kind"], "PRESCRIBED")
+ self.assertEqual(prescribed["decision"]["kind"], "CANDIDATE")
self.assertEqual(
prescribed["decision"]["transition"]["id"], "installation.initialize"
)
@@ -321,6 +322,28 @@ def test_one_delivery_context_rematerializes_repository_authority_after_initiali
}
)
)
+ canonical_config = json.loads(config.read_text())
+ canonical_config["hosts"] = sorted(canonical_config["hosts"])
+ canonical_config["policy"]["external_effect_authority"] = (
+ "human-or-autonomy-plus-provider"
+ )
+ config_fingerprint = hashlib.sha256(
+ json.dumps(canonical_config, separators=(",", ":")).encode()
+ ).hexdigest()
+ bound_initialization = self.helper_json(
+ "next", "--repo", self.repo,
+ "--transition", "installation.initialize", *goal, *flow, *actor,
+ "--param", f"source_revision={self._git(self.repo, 'rev-parse', 'HEAD').stdout.strip()}",
+ "--param", f"runtime_path={self.binary.resolve()}",
+ "--param", f"runtime_sha256={hashlib.sha256(self.binary.read_bytes()).hexdigest()}",
+ "--param", f"config_path={config}",
+ "--param", f"config_sha256={config_fingerprint}",
+ )
+ self.assertEqual(bound_initialization["decision"]["kind"], "PRESCRIBED")
+ self.assertEqual(
+ bound_initialization["decision"]["transition"]["id"],
+ "installation.initialize",
+ )
initialized_process = self.run_helper(
"init", "--repo", self.repo, *goal, *flow, *actor,
"--param", f"config_path={config}",
@@ -357,9 +380,20 @@ def test_one_delivery_context_rematerializes_repository_authority_after_initiali
"next", "--repo", self.repo, *goal, *flow, *actor,
"--repository-authority",
)
- self.assertEqual(plan["decision"]["kind"], "PRESCRIBED")
+ self.assertEqual(plan["decision"]["kind"], "CANDIDATE")
self.assertEqual(plan["decision"]["transition"]["id"], "plan.create")
+ plan_source = Path(self.work.name) / "retained-authority-plan.md"
+ plan_source.write_text("# Retained authority\n\nContinue in one operation context.\n")
+ bound = self.helper_json(
+ "next", "--repo", self.repo, "--transition", "plan.create",
+ *goal, *flow, *actor, "--repository-authority",
+ "--param", f"source_path={plan_source}",
+ "--param", "delivery_id=preserve-repository-authority-context",
+ )
+ self.assertEqual(bound["decision"]["kind"], "PRESCRIBED")
+ self.assertEqual(bound["decision"]["transition"]["id"], "plan.create")
+
def test_repository_authority_rematerialization_fails_closed_without_verified_config(self) -> None:
# control-law: repository-authority-requires-exact-verified-fingerprint
root = Path(self.work.name) / "unverified"
diff --git a/boatstack/core/transitions.json b/boatstack/core/transitions.json
index 7eeb7f2..f12414d 100644
--- a/boatstack/core/transitions.json
+++ b/boatstack/core/transitions.json
@@ -2672,6 +2672,7 @@
"class": "authority",
"source_phases": [
"OBSERVED",
+ "DORMANT",
"ACTIVE",
"FRONTIER",
"TERMINAL",
diff --git a/boatstack/flow/standard/supervisor_parity_test.go b/boatstack/flow/standard/supervisor_parity_test.go
index 4b0b3f0..bd35233 100644
--- a/boatstack/flow/standard/supervisor_parity_test.go
+++ b/boatstack/flow/standard/supervisor_parity_test.go
@@ -140,6 +140,60 @@ func TestUntargetedResolutionReconfiguresDifferentGoalAndSkipsSatisfiedGoal(t *t
}
}
+func TestDormantBootstrapGoalReconfiguresBeforeEngagement(t *testing.T) {
+ // control-law: a retained bootstrap goal cannot be bypassed by engagement
+ snapshot := snapshotFor(t, model.PhaseDormant, model.TerminalNonterminal)
+ requested := model.Goal{ID: "basic-project", Kind: model.GoalApprovedPlan, DeliveryID: "basic-project"}
+ authority := catalog.AuthoritySet{catalog.AuthorityHuman: true, catalog.AuthorityRepository: true}
+
+ untargeted := New(testprogram.StandardRegistry(), testGoalContracts()).Resolve(snapshot, requested, authority, "")
+ if untargeted.Kind != DecisionPrescribed || untargeted.Transition == nil || untargeted.Transition.ID != "goal.configure" {
+ t.Fatalf("untargeted decision = %#v, want goal.configure", untargeted)
+ }
+ targeted := New(testprogram.StandardRegistry(), testGoalContracts()).Resolve(snapshot, requested, authority, untargeted.Transition.ID)
+ if targeted.Kind != DecisionPrescribed || targeted.Transition == nil || targeted.Transition.ID != untargeted.Transition.ID {
+ t.Fatalf("targeted decision = %#v, want parity with %#v", targeted, untargeted)
+ }
+ engagement := New(testprogram.StandardRegistry(), testGoalContracts()).Resolve(snapshot, requested, authority, "engagement.begin")
+ if engagement.Kind != DecisionRefused {
+ t.Fatalf("engagement decision = %#v, want refusal until goal.configure", engagement)
+ }
+}
+
+func TestDisabledHostIsRefusedBeforeUntargetedSelection(t *testing.T) {
+ // control-law: host policy applies before both targeted and untargeted selection
+ snapshot := snapshotFor(t, model.PhaseActive, model.TerminalNonterminal)
+ snapshot.Invocation.Host = "codex"
+ snapshot = recanonicalize(t, snapshot)
+ decision := New(testprogram.StandardRegistry(), testGoalContracts()).Resolve(snapshot, goalFor(), catalog.AuthoritySet{catalog.AuthorityRepository: true}, "")
+ if decision.Kind != DecisionRefused || decision.Transition != nil {
+ t.Fatalf("disabled-host decision = %#v, want REFUSED", decision)
+ }
+}
+
+func TestPublicationObservationRemainsSelectableForVolatileExternalState(t *testing.T) {
+ // control-law: a nonterminal provider observation is evidence, not permanent progress
+ snapshot, goal := openPRSnapshot(t, "build", "test", "review", "change", "journey")
+ goal.Kind = model.GoalMerged
+ snapshot.Goal = model.Known(goal, snapshot.Goal.Evidence[0])
+ snapshot.Publication = model.Known(model.PublicationOpen, snapshot.Publication.Evidence[0])
+ snapshot = recanonicalize(t, snapshot)
+ var transitions []catalog.Transition
+ for _, transition := range testprogram.StandardRegistry().All() {
+ if transition.ID == "publication.observe" || transition.Class == catalog.EventRecovery {
+ transitions = append(transitions, transition)
+ }
+ }
+ registry, err := catalog.New(transitions)
+ if err != nil {
+ t.Fatal(err)
+ }
+ decision := New(registry, testGoalContracts()).Resolve(snapshot, goal, catalog.AuthoritySet{catalog.AuthorityRepository: true}, "")
+ if decision.Kind != DecisionPrescribed || decision.Transition == nil || decision.Transition.ID != "publication.observe" {
+ t.Fatalf("volatile publication decision = %#v, want publication.observe", decision)
+ }
+}
+
func TestUntargetedResolutionExcludesExplicitControlTransitions(t *testing.T) {
// control-law: untargeted-resolution-cannot-invent-repair-or-slice-intent
snapshot := snapshotFor(t, model.PhaseActive, model.TerminalNonterminal)
diff --git a/boatstack/flow/standard/transitions.json b/boatstack/flow/standard/transitions.json
index 49be596..a9553a8 100644
--- a/boatstack/flow/standard/transitions.json
+++ b/boatstack/flow/standard/transitions.json
@@ -5583,7 +5583,9 @@
"privacy_classification": "metadata-only",
"telemetry_classification": "transition-receipt",
"cost_class": "declared-neutral",
- "policy": {},
+ "policy": {
+ "rechecks_external_state": true
+ },
"priority": 77
},
{
diff --git a/boatstack/internal/effects/host_skills.go b/boatstack/internal/effects/host_skills.go
index af56d79..438dab5 100644
--- a/boatstack/internal/effects/host_skills.go
+++ b/boatstack/internal/effects/host_skills.go
@@ -92,8 +92,10 @@ materialized authority receipts.
%s
-Begin each cycle with an untargeted authority-bearing `+"`next`"+`. Apply only the
-stable transition ID from the immediately preceding prescription and only its
+Begin each cycle with an untargeted authority-bearing `+"`next`"+`. A `+"`CANDIDATE`"+`
+identifies the next transition but is not permission to apply it: bind only its
+declared parameters and re-resolve that exact transition. Apply only the stable
+transition ID from the immediately preceding `+"`PRESCRIBED`"+` result and only its
declared parameters. Preserve the complete apply response and stderr, including
admission, receipt, postcondition, error, recovery, and transaction fields.
Re-resolve with the same context after every complete receipt.
diff --git a/boatstack/internal/effects/host_skills_test.go b/boatstack/internal/effects/host_skills_test.go
index 2aa487b..76cc293 100644
--- a/boatstack/internal/effects/host_skills_test.go
+++ b/boatstack/internal/effects/host_skills_test.go
@@ -50,7 +50,7 @@ func TestHostSkillProjectionPreservesAuthorityBoundaries(t *testing.T) {
for _, contract := range []string{
"authority-free\n`FRONTIER`", "command-scoped context", "every `next`, `apply`, `recover`, and re-resolution",
"requested authority sources separately from currently\nmaterialized authority receipts",
- "complete apply response and stderr", "authority-bearing `FRONTIER`", "Never synthesize missing\nauthority",
+ "complete apply response and stderr", "authority-bearing `FRONTIER`", "Never synthesize missing\nauthority", "`CANDIDATE`", "immediately preceding `PRESCRIBED`",
"every requested authority source is materialized\nor conclusively rejected against the post-receipt state",
} {
if !strings.Contains(value, contract) {
diff --git a/boatstack/internal/kernel/catalog/transition.go b/boatstack/internal/kernel/catalog/transition.go
index 7485211..ae3a954 100644
--- a/boatstack/internal/kernel/catalog/transition.go
+++ b/boatstack/internal/kernel/catalog/transition.go
@@ -184,6 +184,7 @@ type PolicyContract struct {
ManagedOperations []string `json:"managed_operations,omitempty"`
BindsRequestedGoal bool `json:"binds_requested_goal,omitempty"`
ReconcilesProgram bool `json:"reconciles_program,omitempty"`
+ RechecksExternalState bool `json:"rechecks_external_state,omitempty"`
}
// FacetCondition is an executable, serializable predicate over one canonical
diff --git a/boatstack/internal/kernel/engine/engine.go b/boatstack/internal/kernel/engine/engine.go
index e0d24f0..01d8415 100644
--- a/boatstack/internal/kernel/engine/engine.go
+++ b/boatstack/internal/kernel/engine/engine.go
@@ -40,6 +40,7 @@ type ResolveRequest struct {
Invocation model.InvocationContext
Goal model.Goal
Authority protocol.AuthorityBundle
+ Parameters protocol.Parameters
Requested catalog.TransitionID
}
@@ -76,6 +77,30 @@ func (e Engine) Resolve(ctx context.Context, request ResolveRequest) (Resolution
return Resolution{}, err
}
decision := e.control.Resolve(snapshot, goal, request.Authority.Set(now), request.Requested)
+ if decision.Kind == supervisor.DecisionPrescribed && decision.Transition != nil {
+ if applicabilityErr := protocol.ValidateApplicability(snapshot, goal, *decision.Transition, request.Authority, request.Parameters, now); applicabilityErr != nil {
+ if protocol.IsMissingParameter(applicabilityErr) {
+ decision.Kind = supervisor.DecisionCandidate
+ decision.Reason = applicabilityErr.Error() + "; bind the declared parameters and re-resolve this transition"
+ decision.Candidates = []catalog.TransitionID{decision.Transition.ID}
+ } else {
+ decision.Kind = supervisor.DecisionRefused
+ decision.Reason = applicabilityErr.Error()
+ decision.Transition = nil
+ }
+ } else {
+ admission, admissionErr := protocol.NewAdmission(snapshot, goal, *decision.Transition, request.Authority, request.Parameters, now, 2*time.Minute)
+ if admissionErr != nil {
+ decision.Kind = supervisor.DecisionUnresolved
+ decision.Reason = admissionErr.Error()
+ decision.Transition = nil
+ } else if _, preflightErr := e.effects.Prepare(ctx, admission, *decision.Transition); preflightErr != nil {
+ decision.Kind = supervisor.DecisionUnresolved
+ decision.Reason = fmt.Sprintf("transition %q failed deterministic effect preflight: %v", admission.TransitionID, preflightErr)
+ decision.Transition = nil
+ }
+ }
+ }
return Resolution{Snapshot: snapshot, Goal: goal, Decision: decision}, nil
}
@@ -170,6 +195,7 @@ func (e Engine) Apply(ctx context.Context, request ApplyRequest) (result ApplyRe
if request.AdmissionLifetime <= 0 {
request.AdmissionLifetime = 2 * time.Minute
}
+ request.ResolveRequest.Parameters = request.Parameters
resolution, err := e.Resolve(ctx, request.ResolveRequest)
result.Source, result.Goal, result.Decision = resolution.Snapshot, resolution.Goal, resolution.Decision
if err != nil {
diff --git a/boatstack/internal/kernel/engine/engine_test.go b/boatstack/internal/kernel/engine/engine_test.go
index 94f22f2..9c4e56b 100644
--- a/boatstack/internal/kernel/engine/engine_test.go
+++ b/boatstack/internal/kernel/engine/engine_test.go
@@ -105,9 +105,13 @@ type fakeEffects struct {
executions, rollbacks int
result ports.EffectResult
err error
+ prepareErr error
}
func (e *fakeEffects) Prepare(context.Context, protocol.Admission, catalog.Transition) (ports.PreparedEffect, error) {
+ if e.prepareErr != nil {
+ return nil, e.prepareErr
+ }
return e, nil
}
func (e *fakeEffects) Manifest() []ports.ResourceMutation { return nil }
@@ -274,6 +278,66 @@ func TestRequiredObserverFailureReturnsTypedUnresolvedDecision(t *testing.T) {
}
}
+func TestResolutionDoesNotPrescribeBeforeRequiredParametersAreBound(t *testing.T) {
+ // control-law: a selected transition is only a candidate until deterministic admission inputs are complete
+ now := time.Unix(30, 0).UTC()
+ transitions := testRegistry(t).All()
+ for index := range transitions {
+ if transitions[index].ID == "test.advance" {
+ transitions[index].Parameters = []catalog.ParameterSpec{{Name: "value", Required: true}}
+ }
+ }
+ registry, err := catalog.New(transitions)
+ if err != nil {
+ t.Fatal(err)
+ }
+ observer := &sequenceObserver{items: []model.Observation{observation(model.PhaseObserved, "source"), observation(model.PhaseObserved, "source")}}
+ kernel, err := New(registry, syntheticGoalContracts(t), syntheticProgramFingerprint, observer, fixedClock{now}, fakeLocker{&fakeLock{}}, &fakeJournal{}, &fakeEffects{}, &memoryReceipts{})
+ if err != nil {
+ t.Fatal(err)
+ }
+ req := request(now).ResolveRequest
+ req.Requested = ""
+ candidate, err := kernel.Resolve(context.Background(), req)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if candidate.Decision.Kind != supervisor.DecisionCandidate || candidate.Decision.Transition == nil || candidate.Decision.Transition.ID != "test.advance" {
+ t.Fatalf("incomplete resolution = %+v, want CANDIDATE", candidate.Decision)
+ }
+ req.Requested = "test.advance"
+ req.Parameters = protocol.Parameters{{Name: "value", Value: "bound"}}
+ prescribed, err := kernel.Resolve(context.Background(), req)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if prescribed.Decision.Kind != supervisor.DecisionPrescribed || prescribed.Decision.Transition == nil || prescribed.Decision.Transition.ID != "test.advance" {
+ t.Fatalf("complete resolution = %+v, want PRESCRIBED", prescribed.Decision)
+ }
+}
+
+func TestResolutionDoesNotPrescribeAnEffectThatDeterministicPreflightRejects(t *testing.T) {
+ // control-law: effect preparation cannot introduce a deterministic apply-only refusal
+ now := time.Unix(30, 0).UTC()
+ effects := &fakeEffects{prepareErr: errors.New("malformed artifact")}
+ observer := &sequenceObserver{items: []model.Observation{observation(model.PhaseObserved, "source")}}
+ journal := &fakeJournal{}
+ kernel, err := New(testRegistry(t), syntheticGoalContracts(t), syntheticProgramFingerprint, observer, fixedClock{now}, fakeLocker{&fakeLock{}}, journal, effects, &memoryReceipts{})
+ if err != nil {
+ t.Fatal(err)
+ }
+ resolved, err := kernel.Resolve(context.Background(), request(now).ResolveRequest)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if resolved.Decision.Kind != supervisor.DecisionUnresolved || resolved.Decision.Transition != nil || !strings.Contains(resolved.Decision.Reason, "malformed artifact") {
+ t.Fatalf("preflight decision = %+v, want typed UNRESOLVED without prescription", resolved.Decision)
+ }
+ if effects.executions != 0 || journal.begun != 0 {
+ t.Fatalf("preflight crossed mutation boundary: effects=%d journals=%d", effects.executions, journal.begun)
+ }
+}
+
func TestApplyCrossesAdmissionEffectVerificationAndReceiptBoundary(t *testing.T) {
// control-law: synthetic-flow-crosses-exact-admission-and-postcondition-without-standard-flow
now := time.Unix(30, 0).UTC()
diff --git a/boatstack/internal/kernel/ports/ports.go b/boatstack/internal/kernel/ports/ports.go
index 1af9c58..0c80bb5 100644
--- a/boatstack/internal/kernel/ports/ports.go
+++ b/boatstack/internal/kernel/ports/ports.go
@@ -92,6 +92,8 @@ type PreparedEffect interface {
}
type EffectDriver interface {
+ // Prepare is a side-effect-free preflight. It may read exact plant state and
+ // construct a mutation manifest, but it must not execute or install it.
Prepare(context.Context, protocol.Admission, catalog.Transition) (PreparedEffect, error)
}
diff --git a/boatstack/internal/kernel/protocol/admission.go b/boatstack/internal/kernel/protocol/admission.go
index 028cc50..0f3ebef 100644
--- a/boatstack/internal/kernel/protocol/admission.go
+++ b/boatstack/internal/kernel/protocol/admission.go
@@ -32,49 +32,13 @@ type Admission struct {
}
func NewAdmission(snapshot model.Snapshot, goal model.Goal, transition catalog.Transition, authority AuthorityBundle, parameters Parameters, now time.Time, lifetime time.Duration) (Admission, error) {
- if !transition.Controllable() {
- return Admission{}, fmt.Errorf("transition %q is uncontrollable and cannot be admitted", transition.ID)
- }
- if err := snapshot.Invocation.Validate(true); err != nil {
- return Admission{}, err
- }
- if err := goal.Validate(); err != nil {
+ if err := ValidateApplicability(snapshot, goal, transition, authority, parameters, now); err != nil {
return Admission{}, err
}
- if snapshot.Fingerprint == "" || len(snapshot.ProgramFingerprint) != 64 || !transition.SourceMatches(snapshot) || !transition.SupportsGoal(goal) {
- return Admission{}, fmt.Errorf("transition %q is not admissible from snapshot %q", transition.ID, snapshot.Fingerprint)
- }
if lifetime <= 0 {
return Admission{}, fmt.Errorf("admission lifetime must be positive")
}
- if err := authority.Validate(now); err != nil {
- return Admission{}, err
- }
- if err := validateAuthorityEvidence(snapshot, authority); err != nil {
- return Admission{}, err
- }
- if err := parameters.Validate(transition); err != nil {
- return Admission{}, err
- }
- if err := validateProviderAuthorityBinding(authority, transition, parameters); err != nil {
- return Admission{}, err
- }
sourceRevision, worktreeFingerprint := gitBinding(snapshot)
- if transition.BindsSourceRevision {
- declared, _ := parameters.Get("source_revision")
- if sourceRevision == "" || worktreeFingerprint == "" || declared != sourceRevision {
- return Admission{}, fmt.Errorf("transition %q must bind the current Git revision and worktree fingerprint", transition.ID)
- }
- }
- if !authority.Set(now).Satisfies(transition.Authority, transition.AuthorityAll) {
- return Admission{}, fmt.Errorf("transition %q lacks required authority", transition.ID)
- }
- if err := validatePolicyAuthority(snapshot, transition, authority.Set(now)); err != nil {
- return Admission{}, err
- }
- if err := validateRecoveryPermission(snapshot, transition); err != nil {
- return Admission{}, err
- }
a := Admission{
SchemaVersion: AdmissionSchemaVersion, TransitionID: transition.ID, TransitionVersion: transition.Version,
ProgramFingerprint: snapshot.ProgramFingerprint, SnapshotFingerprint: snapshot.Fingerprint, SourceRevision: sourceRevision, WorktreeFingerprint: worktreeFingerprint,
@@ -101,6 +65,56 @@ func NewAdmission(snapshot model.Snapshot, goal model.Goal, transition catalog.T
return a, nil
}
+// ValidateApplicability is the deterministic transition law shared by
+// resolution and admission. A transition that fails here must never be
+// reported as prescribed for the same snapshot and context.
+func ValidateApplicability(snapshot model.Snapshot, goal model.Goal, transition catalog.Transition, authority AuthorityBundle, parameters Parameters, now time.Time) error {
+ if !transition.Controllable() {
+ return fmt.Errorf("transition %q is uncontrollable and cannot be admitted", transition.ID)
+ }
+ if err := snapshot.Invocation.Validate(true); err != nil {
+ return err
+ }
+ if err := goal.Validate(); err != nil {
+ return err
+ }
+ if snapshot.Fingerprint == "" || len(snapshot.ProgramFingerprint) != 64 || !transition.SourceMatches(snapshot) || !transition.SupportsGoal(goal) {
+ return fmt.Errorf("transition %q is not admissible from snapshot %q", transition.ID, snapshot.Fingerprint)
+ }
+ if snapshot.Goal.Status == model.FactKnown && snapshot.Goal.Value != goal && !transition.Policy.BindsRequestedGoal {
+ return fmt.Errorf("transition %q cannot replace configured goal; goal.configure is required", transition.ID)
+ }
+ if err := authority.Validate(now); err != nil {
+ return err
+ }
+ if err := validateAuthorityEvidence(snapshot, authority); err != nil {
+ return err
+ }
+ if err := parameters.Validate(transition); err != nil {
+ return err
+ }
+ if err := validateProviderAuthorityBinding(authority, transition, parameters); err != nil {
+ return err
+ }
+ sourceRevision, worktreeFingerprint := gitBinding(snapshot)
+ if transition.BindsSourceRevision {
+ declared, _ := parameters.Get("source_revision")
+ if sourceRevision == "" || worktreeFingerprint == "" || declared != sourceRevision {
+ return fmt.Errorf("transition %q must bind the current Git revision and worktree fingerprint", transition.ID)
+ }
+ }
+ if !authority.Set(now).Satisfies(transition.Authority, transition.AuthorityAll) {
+ return fmt.Errorf("transition %q lacks required authority", transition.ID)
+ }
+ if err := validatePolicyAuthority(snapshot, transition, authority.Set(now)); err != nil {
+ return err
+ }
+ if err := validateRecoveryPermission(snapshot, transition); err != nil {
+ return err
+ }
+ return nil
+}
+
func (a Admission) ValidateCurrent(snapshot model.Snapshot, goal model.Goal, transition catalog.Transition, now time.Time) error {
if err := a.ValidateIdentity(); err != nil {
return err
diff --git a/boatstack/internal/kernel/protocol/parameters.go b/boatstack/internal/kernel/protocol/parameters.go
index 5c2214d..f619614 100644
--- a/boatstack/internal/kernel/protocol/parameters.go
+++ b/boatstack/internal/kernel/protocol/parameters.go
@@ -1,6 +1,7 @@
package protocol
import (
+ "errors"
"fmt"
"path/filepath"
"sort"
@@ -15,6 +16,20 @@ type Parameter struct {
type Parameters []Parameter
+type MissingParameterError struct {
+ Transition catalog.TransitionID
+ Parameter string
+}
+
+func (e MissingParameterError) Error() string {
+ return fmt.Sprintf("transition %q requires parameter %q", e.Transition, e.Parameter)
+}
+
+func IsMissingParameter(err error) bool {
+ var missing MissingParameterError
+ return errors.As(err, &missing)
+}
+
func (p Parameters) Canonical() Parameters {
result := append(Parameters(nil), p...)
sort.Slice(result, func(i, j int) bool { return result[i].Name < result[j].Name })
@@ -63,7 +78,7 @@ func (p Parameters) Validate(transition catalog.Transition) error {
}
for _, spec := range transition.Parameters {
if spec.Required && !seen[spec.Name] {
- return fmt.Errorf("transition %q requires parameter %q", transition.ID, spec.Name)
+ return MissingParameterError{Transition: transition.ID, Parameter: spec.Name}
}
}
return nil
diff --git a/boatstack/internal/kernel/supervisor/supervisor.go b/boatstack/internal/kernel/supervisor/supervisor.go
index f73f87e..c12b79b 100644
--- a/boatstack/internal/kernel/supervisor/supervisor.go
+++ b/boatstack/internal/kernel/supervisor/supervisor.go
@@ -12,6 +12,7 @@ type DecisionKind string
const (
DecisionPrescribed DecisionKind = "PRESCRIBED"
+ DecisionCandidate DecisionKind = "CANDIDATE"
DecisionTerminal DecisionKind = "TERMINAL"
DecisionFrontier DecisionKind = "FRONTIER"
DecisionBlocked DecisionKind = "BLOCKED"
@@ -57,11 +58,7 @@ func (s Supervisor) Resolve(snapshot model.Snapshot, goal model.Goal, authority
return base
}
}
- if requested != "" && snapshot.Goal.Status == model.FactKnown && snapshot.Goal.Value != goal && requested != "goal.configure" {
- base.Kind, base.Reason = DecisionRefused, "requested goal differs from configured goal; goal.configure is required"
- return base
- }
- if requested != "" && snapshot.ConfigurationPolicy.Status == model.FactKnown && !hostEnabled(snapshot.ConfigurationPolicy.Value.Hosts, snapshot.Invocation.Host) {
+ if snapshot.ConfigurationPolicy.Status == model.FactKnown && !hostEnabled(snapshot.ConfigurationPolicy.Value.Hosts, snapshot.Invocation.Host) {
base.Kind, base.Reason = DecisionRefused, fmt.Sprintf("host %q is not enabled by repository policy", snapshot.Invocation.Host)
return base
}
@@ -70,6 +67,15 @@ func (s Supervisor) Resolve(snapshot model.Snapshot, goal model.Goal, authority
return base
}
admissible := s.registry.Admissible(snapshot, goal)
+ if snapshot.Goal.Status == model.FactKnown && snapshot.Goal.Value != goal {
+ filtered := admissible[:0]
+ for _, candidate := range admissible {
+ if candidate.Policy.BindsRequestedGoal {
+ filtered = append(filtered, candidate)
+ }
+ }
+ admissible = filtered
+ }
if snapshot.Phase.Value == model.PhaseRecovery {
filtered := admissible[:0]
for _, candidate := range admissible {
@@ -150,6 +156,9 @@ func (s Supervisor) Resolve(snapshot model.Snapshot, goal model.Goal, authority
}
func targetAlreadySatisfied(snapshot model.Snapshot, goal model.Goal, transition catalog.Transition) bool {
+ if transition.Policy.RechecksExternalState {
+ return false
+ }
if transition.Policy.BindsRequestedGoal {
return snapshot.Goal.Status == model.FactKnown && snapshot.Goal.Value == goal
}
diff --git a/boatstack/internal/plant/observer.go b/boatstack/internal/plant/observer.go
index e930842..a7f901c 100644
--- a/boatstack/internal/plant/observer.go
+++ b/boatstack/internal/plant/observer.go
@@ -646,7 +646,7 @@ func pendingJournalEvidence(root, ignoreAdmissionID string, now time.Time) (pend
if budget < 0 {
budget = 0
}
- permitted := recoveryContract(header.TransitionID, external, budget)
+ permitted := recoveryContract(header.TransitionID, external, len(header.Mutations) > 0, budget)
cause := header.Reason
if cause == "" {
cause = "process ended before transition receipt"
@@ -739,7 +739,7 @@ func conflictingPending(records []pendingJournalRecord) pendingJournalSet {
return result
}
-func recoveryContract(transitionID string, external bool, budget int) []string {
+func recoveryContract(transitionID string, external, staged bool, budget int) []string {
if budget == 0 {
return []string{"recovery.escalate"}
}
@@ -756,6 +756,9 @@ func recoveryContract(transitionID string, external bool, budget int) []string {
case "workspace.cleanup", "workspace.reap":
return []string{"recovery.escalate"}
default:
+ if !staged {
+ return []string{"recovery.rollback", "recovery.escalate"}
+ }
return []string{"recovery.resume", "recovery.rollback", "recovery.escalate"}
}
}
diff --git a/boatstack/internal/plant/observer_test.go b/boatstack/internal/plant/observer_test.go
index 54dbd8f..bb5e380 100644
--- a/boatstack/internal/plant/observer_test.go
+++ b/boatstack/internal/plant/observer_test.go
@@ -249,6 +249,19 @@ func TestRecoveryAttemptsExhaustToEscalationOnly(t *testing.T) {
}
}
+func TestRecoveryWithoutStagedManifestCannotPrescribeResume(t *testing.T) {
+ // control-law: recovery selection cannot promise a replay that prepare will reject
+ permitted := recoveryContract("plan.create", false, false, 3)
+ for _, transition := range permitted {
+ if transition == "recovery.resume" {
+ t.Fatalf("unstaged recovery permits resume: %v", permitted)
+ }
+ }
+ if len(permitted) != 2 || permitted[0] != "recovery.rollback" || permitted[1] != "recovery.escalate" {
+ t.Fatalf("unstaged recovery contract = %v", permitted)
+ }
+}
+
func TestInterruptedRecoveryAttemptCollapsesToEscalatableTransactionGroup(t *testing.T) {
// control-law: recovery-of-recovery-does-not-create-an-unselectable-conflict
root := t.TempDir()
diff --git a/boatstack/kernel.go b/boatstack/kernel.go
index ee15805..fe18b47 100644
--- a/boatstack/kernel.go
+++ b/boatstack/kernel.go
@@ -109,7 +109,7 @@ func (k Kernel) Handle(ctx context.Context, request surfaces.Request) (surfaces.
}
switch request.Operation {
case surfaces.OperationResolve:
- resolution, resolveErr := k.engine.Resolve(ctx, engine.ResolveRequest{Invocation: invocation, Goal: request.Goal, Authority: request.Authority, Requested: request.TransitionID})
+ resolution, resolveErr := k.engine.Resolve(ctx, engine.ResolveRequest{Invocation: invocation, Goal: request.Goal, Authority: request.Authority, Parameters: request.Parameters, Requested: request.TransitionID})
response.Goal, response.Decision = resolution.Goal, &resolution.Decision
if resolution.Snapshot.Fingerprint != "" {
response.Snapshot = &resolution.Snapshot
diff --git a/boatstack/references/workflow.md b/boatstack/references/workflow.md
index ccf843b..c6c3735 100644
--- a/boatstack/references/workflow.md
+++ b/boatstack/references/workflow.md
@@ -17,8 +17,9 @@ Event families:
- recovery;
- observed external plant changes.
-The supervisor returns one of `PRESCRIBED`, `TERMINAL`, `FRONTIER`,
-`BLOCKED`, `REFUSED`, or `UNRESOLVED`. Only `PRESCRIBED` can produce an
+The supervisor returns one of `CANDIDATE`, `PRESCRIBED`, `TERMINAL`, `FRONTIER`,
+`BLOCKED`, `REFUSED`, or `UNRESOLVED`. `CANDIDATE` identifies the deterministic
+next transition while required parameters remain unbound. Only `PRESCRIBED` can produce an
admission. Only an independently verified postcondition can produce a receipt.
Untargeted resolution excludes transitions whose target is already established
and transitions that encode separate maintenance, repair, abandonment, or
diff --git a/docs/architecture/boatstack-v2-kernel.md b/docs/architecture/boatstack-v2-kernel.md
index ba775f1..f782f3a 100644
--- a/docs/architecture/boatstack-v2-kernel.md
+++ b/docs/architecture/boatstack-v2-kernel.md
@@ -531,6 +531,7 @@ it is not an independently maintained graph.
`supervisor.Resolve(snapshot, goal, authority, optionalObservedEvent)` is pure and
deterministic. It evaluates the executable registry and returns exactly one:
+- `CANDIDATE`: one deterministic next transition still needs declared parameters;
- `PRESCRIBED`: one exact next transition and prescription;
- `TERMINAL`: goal predicate established by current terminal evidence;
- `FRONTIER`: a genuine human/reasoning authority decision is required;
@@ -547,6 +548,9 @@ repository presence is not engagement; a saved plan is not active authority.
Resolution never fabricates progress. If several controllable events remain
equally admissible after declared deterministic priority, the answer is
`FRONTIER` or `UNRESOLVED`, never map-order selection or first-match behavior.
+Before `PRESCRIBED`, resolution also runs the effect driver's side-effect-free
+preflight over the exact admission context; deterministic artifact, durable-state,
+or recovery refusals therefore cannot first appear at apply.
## 9. Admission and authority model
diff --git a/docs/architecture/boatstack-v2-locus-liveness.json b/docs/architecture/boatstack-v2-locus-liveness.json
index 37e8ced..31796d9 100644
--- a/docs/architecture/boatstack-v2-locus-liveness.json
+++ b/docs/architecture/boatstack-v2-locus-liveness.json
@@ -3403,6 +3403,39 @@
],
"basis": "inferred"
},
+ {
+ "from": "DORMANT",
+ "event": "goal.configure",
+ "to": "OBSERVED",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "DORMANT",
+ "event": "goal.configure",
+ "to": "ACTIVE",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "DORMANT",
+ "event": "goal.configure",
+ "to": "FRONTIER",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
{
"from": "ACTIVE",
"event": "goal.configure",
diff --git a/docs/architecture/boatstack-v2-locus-safety.json b/docs/architecture/boatstack-v2-locus-safety.json
index 17989d4..5d0a7e3 100644
--- a/docs/architecture/boatstack-v2-locus-safety.json
+++ b/docs/architecture/boatstack-v2-locus-safety.json
@@ -3406,6 +3406,39 @@
],
"basis": "inferred"
},
+ {
+ "from": "DORMANT",
+ "event": "goal.configure",
+ "to": "OBSERVED",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "DORMANT",
+ "event": "goal.configure",
+ "to": "ACTIVE",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
+ {
+ "from": "DORMANT",
+ "event": "goal.configure",
+ "to": "FRONTIER",
+ "evidence": [
+ 0,
+ 1,
+ 4
+ ],
+ "basis": "inferred"
+ },
{
"from": "ACTIVE",
"event": "goal.configure",
diff --git a/docs/architecture/boatstack-v2-transition-catalog.md b/docs/architecture/boatstack-v2-transition-catalog.md
index 6811882..4bac11a 100644
--- a/docs/architecture/boatstack-v2-transition-catalog.md
+++ b/docs/architecture/boatstack-v2-transition-catalog.md
@@ -7,67 +7,67 @@ Controlling facets: `phase`, `program`, `topology`, `engagement`, `delivery`, `w
| Transition | Origin | Owner | Selection | Class | Source phases | Target phases | Authority | Parameters | Owned resources | Verifier | Recovery | Cost |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
-| `catalog.reconcile` | core-system:`boatstack.core@1.0.0`
`ab8e145315d72ae5ab17f916775a40db0a8d12a0529ac3de8ed194d116bab9bc` | `boatstack.core` | EXPLICIT_ONLY | owned-local | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED / TERMINAL / ABANDONED | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED / TERMINAL / ABANDONED | human | `prior_program_fingerprint*`, `accept_obligation_change*` | `catalog-identity` | `verifier:fresh-observation:catalog.reconcile` | `recovery.resume` | `declared-neutral` |
-| `configuration.initialize` | core-system:`boatstack.core@1.0.0`
`ab8e145315d72ae5ab17f916775a40db0a8d12a0529ac3de8ed194d116bab9bc` | `boatstack.core` | GOAL_REQUIRED | owned-local | OBSERVED | OBSERVED / TERMINAL | human/repository-policy | `config_path*`, `config_sha256*` | `configuration` | `verifier:fresh-observation:configuration.initialize` | `configuration.reconcile` | `declared-neutral` |
-| `configuration.mutate` | core-system:`boatstack.core@1.0.0`
`ab8e145315d72ae5ab17f916775a40db0a8d12a0529ac3de8ed194d116bab9bc` | `boatstack.core` | EXPLICIT_ONLY | owned-local | OBSERVED / ACTIVE / FRONTIER / UNRESOLVED | OBSERVED / ACTIVE / TERMINAL | human/autonomy | `config_path*`, `config_sha256*` | `configuration` | `verifier:fresh-observation:configuration.mutate` | `configuration.reconcile` | `declared-neutral` |
-| `configuration.reconcile` | core-system:`boatstack.core@1.0.0`
`ab8e145315d72ae5ab17f916775a40db0a8d12a0529ac3de8ed194d116bab9bc` | `boatstack.core` | SYSTEM_RECOVERY | recovery | RECOVERY / UNRESOLVED | OBSERVED / FRONTIER / TERMINAL | human/repository-policy | `transaction_id*` | `configuration` | `verifier:fresh-observation:configuration.reconcile` | `recovery.escalate` | `declared-neutral` |
-| `delivery.slice.advance` | primary-flow:`boatstack.standard@1.0.0`
`4ced330c8ca69159c2661e674c223f25077adb6eeec569e506d599d843d959de` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | ACTIVE | ACTIVE / TERMINAL | human/autonomy | `slice_id*`, `source_revision*` | `delivery-state` | `verifier:fresh-observation:delivery.slice.advance` | `recovery.resume` | `declared-neutral` |
-| `engagement.begin` | core-system:`boatstack.core@1.0.0`
`ab8e145315d72ae5ab17f916775a40db0a8d12a0529ac3de8ed194d116bab9bc` | `boatstack.core` | GOAL_REQUIRED | authority | DORMANT / OBSERVED | OBSERVED / ACTIVE | repository-policy | - | `engagement` | `verifier:fresh-observation:engagement.begin` | `recovery.resume` | `declared-neutral` |
-| `engagement.release` | core-system:`boatstack.core@1.0.0`
`ab8e145315d72ae5ab17f916775a40db0a8d12a0529ac3de8ed194d116bab9bc` | `boatstack.core` | EXPLICIT_ONLY | authority | ACTIVE / FRONTIER | DORMANT | repository-policy | - | `engagement` | `verifier:fresh-observation:engagement.release` | `recovery.resume` | `declared-neutral` |
-| `engagement.renew` | core-system:`boatstack.core@1.0.0`
`ab8e145315d72ae5ab17f916775a40db0a8d12a0529ac3de8ed194d116bab9bc` | `boatstack.core` | EXPLICIT_ONLY | authority | ACTIVE | ACTIVE | repository-policy/autonomy | - | `engagement` | `verifier:fresh-observation:engagement.renew` | `recovery.resume` | `declared-neutral` |
-| `evidence.approval.revoke` | primary-flow:`boatstack.standard@1.0.0`
`4ced330c8ca69159c2661e674c223f25077adb6eeec569e506d599d843d959de` | `boatstack.standard` | EXPLICIT_ONLY | authority | ACTIVE / FRONTIER | FRONTIER | human | - | `approval` | `verifier:fresh-observation:evidence.approval.revoke` | `recovery.resume` | `declared-neutral` |
-| `evidence.visual.attach` | primary-flow:`boatstack.standard@1.0.0`
`4ced330c8ca69159c2661e674c223f25077adb6eeec569e506d599d843d959de` | `boatstack.standard` | FLOW_PROGRESS | owned-local | ACTIVE | ACTIVE / TERMINAL | human/repository-policy | `manifest_path*`, `privacy_receipt*`, `source_revision*` | `evidence` | `verifier:fresh-observation:evidence.visual.attach` | `recovery.resume` | `declared-neutral` |
-| `external.branch-changed` | core-system:`boatstack.core@1.0.0`
`ab8e145315d72ae5ab17f916775a40db0a8d12a0529ac3de8ed194d116bab9bc` | `boatstack.core` | OBSERVED_EXTERNAL | observed-external | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED | OBSERVED | none | - | - | `verifier:fresh-observation:external.branch-changed` | `-` | `declared-neutral` |
-| `external.ci-completed` | core-system:`boatstack.core@1.0.0`
`ab8e145315d72ae5ab17f916775a40db0a8d12a0529ac3de8ed194d116bab9bc` | `boatstack.core` | OBSERVED_EXTERNAL | observed-external | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED | OBSERVED / ACTIVE / TERMINAL | none | - | - | `verifier:fresh-observation:external.ci-completed` | `-` | `declared-neutral` |
-| `external.configuration-drifted` | core-system:`boatstack.core@1.0.0`
`ab8e145315d72ae5ab17f916775a40db0a8d12a0529ac3de8ed194d116bab9bc` | `boatstack.core` | OBSERVED_EXTERNAL | observed-external | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED | OBSERVED / UNRESOLVED | none | - | - | `verifier:fresh-observation:external.configuration-drifted` | `-` | `declared-neutral` |
-| `external.files-changed` | core-system:`boatstack.core@1.0.0`
`ab8e145315d72ae5ab17f916775a40db0a8d12a0529ac3de8ed194d116bab9bc` | `boatstack.core` | OBSERVED_EXTERNAL | observed-external | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED | OBSERVED | none | - | - | `verifier:fresh-observation:external.files-changed` | `-` | `declared-neutral` |
-| `external.head-changed` | core-system:`boatstack.core@1.0.0`
`ab8e145315d72ae5ab17f916775a40db0a8d12a0529ac3de8ed194d116bab9bc` | `boatstack.core` | OBSERVED_EXTERNAL | observed-external | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED | OBSERVED | none | - | - | `verifier:fresh-observation:external.head-changed` | `-` | `declared-neutral` |
-| `external.host-interrupted` | core-system:`boatstack.core@1.0.0`
`ab8e145315d72ae5ab17f916775a40db0a8d12a0529ac3de8ed194d116bab9bc` | `boatstack.core` | OBSERVED_EXTERNAL | observed-external | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED | RECOVERY | none | - | - | `verifier:fresh-observation:external.host-interrupted` | `-` | `declared-neutral` |
-| `external.lease-expired` | core-system:`boatstack.core@1.0.0`
`ab8e145315d72ae5ab17f916775a40db0a8d12a0529ac3de8ed194d116bab9bc` | `boatstack.core` | OBSERVED_EXTERNAL | observed-external | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED | DORMANT / FRONTIER | none | - | - | `verifier:fresh-observation:external.lease-expired` | `-` | `declared-neutral` |
-| `external.pr-closed` | core-system:`boatstack.core@1.0.0`
`ab8e145315d72ae5ab17f916775a40db0a8d12a0529ac3de8ed194d116bab9bc` | `boatstack.core` | OBSERVED_EXTERNAL | observed-external | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED | OBSERVED / ACTIVE / FRONTIER | none | - | - | `verifier:fresh-observation:external.pr-closed` | `-` | `declared-neutral` |
-| `external.pr-merged` | core-system:`boatstack.core@1.0.0`
`ab8e145315d72ae5ab17f916775a40db0a8d12a0529ac3de8ed194d116bab9bc` | `boatstack.core` | OBSERVED_EXTERNAL | observed-external | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED | OBSERVED / ACTIVE / TERMINAL | none | - | - | `verifier:fresh-observation:external.pr-merged` | `-` | `declared-neutral` |
-| `external.pr-opened` | core-system:`boatstack.core@1.0.0`
`ab8e145315d72ae5ab17f916775a40db0a8d12a0529ac3de8ed194d116bab9bc` | `boatstack.core` | OBSERVED_EXTERNAL | observed-external | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED | OBSERVED / ACTIVE / TERMINAL | none | - | - | `verifier:fresh-observation:external.pr-opened` | `-` | `declared-neutral` |
-| `external.pr-updated` | core-system:`boatstack.core@1.0.0`
`ab8e145315d72ae5ab17f916775a40db0a8d12a0529ac3de8ed194d116bab9bc` | `boatstack.core` | OBSERVED_EXTERNAL | observed-external | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED | OBSERVED / ACTIVE / TERMINAL | none | - | - | `verifier:fresh-observation:external.pr-updated` | `-` | `declared-neutral` |
-| `external.provider-unavailable` | core-system:`boatstack.core@1.0.0`
`ab8e145315d72ae5ab17f916775a40db0a8d12a0529ac3de8ed194d116bab9bc` | `boatstack.core` | OBSERVED_EXTERNAL | observed-external | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED | UNRESOLVED / RECOVERY | none | - | - | `verifier:fresh-observation:external.provider-unavailable` | `-` | `declared-neutral` |
-| `external.runtime-disappeared` | core-system:`boatstack.core@1.0.0`
`ab8e145315d72ae5ab17f916775a40db0a8d12a0529ac3de8ed194d116bab9bc` | `boatstack.core` | OBSERVED_EXTERNAL | observed-external | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED | OBSERVED / RECOVERY | none | - | - | `verifier:fresh-observation:external.runtime-disappeared` | `-` | `declared-neutral` |
-| `gate.build.record` | primary-flow:`boatstack.standard@1.0.0`
`4ced330c8ca69159c2661e674c223f25077adb6eeec569e506d599d843d959de` | `boatstack.standard` | FLOW_PROGRESS | owned-local | ACTIVE | ACTIVE | repository-policy | `source_revision*`, `evidence_path*`, `evidence_fingerprint*` | `gate-evidence` | `verifier:fresh-observation:gate.build.record` | `recovery.resume` | `declared-neutral` |
-| `gate.change.record` | primary-flow:`boatstack.standard@1.0.0`
`4ced330c8ca69159c2661e674c223f25077adb6eeec569e506d599d843d959de` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | ACTIVE | ACTIVE | repository-policy | `source_revision*`, `evidence_path*`, `evidence_fingerprint*` | `gate-evidence` | `verifier:fresh-observation:gate.change.record` | `recovery.resume` | `declared-neutral` |
-| `gate.journey.record` | primary-flow:`boatstack.standard@1.0.0`
`4ced330c8ca69159c2661e674c223f25077adb6eeec569e506d599d843d959de` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | ACTIVE | ACTIVE | repository-policy | `source_revision*`, `evidence_path*`, `evidence_fingerprint*` | `gate-evidence` | `verifier:fresh-observation:gate.journey.record` | `recovery.resume` | `declared-neutral` |
-| `gate.review.record` | primary-flow:`boatstack.standard@1.0.0`
`4ced330c8ca69159c2661e674c223f25077adb6eeec569e506d599d843d959de` | `boatstack.standard` | FLOW_PROGRESS | owned-local | ACTIVE | ACTIVE / TERMINAL | human/repository-policy | `source_revision*`, `evidence_path*`, `evidence_fingerprint*` | `gate-evidence` | `verifier:fresh-observation:gate.review.record` | `recovery.resume` | `declared-neutral` |
-| `gate.test.record` | primary-flow:`boatstack.standard@1.0.0`
`4ced330c8ca69159c2661e674c223f25077adb6eeec569e506d599d843d959de` | `boatstack.standard` | FLOW_PROGRESS | owned-local | ACTIVE | ACTIVE / TERMINAL | repository-policy | `source_revision*`, `evidence_path*`, `evidence_fingerprint*` | `gate-evidence` | `verifier:fresh-observation:gate.test.record` | `recovery.resume` | `declared-neutral` |
-| `goal.configure` | core-system:`boatstack.core@1.0.0`
`ab8e145315d72ae5ab17f916775a40db0a8d12a0529ac3de8ed194d116bab9bc` | `boatstack.core` | GOAL_REQUIRED | authority | OBSERVED / ACTIVE / FRONTIER / TERMINAL / ABANDONED | OBSERVED / ACTIVE / FRONTIER | human/autonomy | `goal_kind*`, `delivery_id*` | `goal` | `verifier:fresh-observation:goal.configure` | `recovery.resume` | `declared-neutral` |
-| `installation.initialize` | core-system:`boatstack.core@1.0.0`
`ab8e145315d72ae5ab17f916775a40db0a8d12a0529ac3de8ed194d116bab9bc` | `boatstack.core` | GOAL_REQUIRED | owned-local | DORMANT / OBSERVED | OBSERVED | human | `source_revision*`, `runtime_path*`, `runtime_sha256*`, `config_path*`, `config_sha256*` | `installation` | `verifier:fresh-observation:installation.initialize` | `runtime.reconcile` | `declared-neutral` |
-| `installation.update` | core-system:`boatstack.core@1.0.0`
`ab8e145315d72ae5ab17f916775a40db0a8d12a0529ac3de8ed194d116bab9bc` | `boatstack.core` | EXPLICIT_ONLY | owned-local | OBSERVED / ACTIVE | OBSERVED / ACTIVE / TERMINAL | human/autonomy | `source_revision*`, `runtime_path*`, `runtime_sha256*` | `installation` | `verifier:fresh-observation:installation.update` | `runtime.reconcile` | `declared-neutral` |
-| `invocation.rebind` | core-system:`boatstack.core@1.0.0`
`ab8e145315d72ae5ab17f916775a40db0a8d12a0529ac3de8ed194d116bab9bc` | `boatstack.core` | EXPLICIT_ONLY | owned-local | OBSERVED / UNRESOLVED | OBSERVED | repository-policy | - | `identity-binding` | `verifier:fresh-observation:invocation.rebind` | `recovery.resume` | `declared-neutral` |
-| `plan.abandon` | primary-flow:`boatstack.standard@1.0.0`
`4ced330c8ca69159c2661e674c223f25077adb6eeec569e506d599d843d959de` | `boatstack.standard` | EXPLICIT_ONLY | authority | OBSERVED / ACTIVE / FRONTIER | ABANDONED | human | - | `plan` | `verifier:fresh-observation:plan.abandon` | `recovery.resume` | `declared-neutral` |
-| `plan.activate` | primary-flow:`boatstack.standard@1.0.0`
`4ced330c8ca69159c2661e674c223f25077adb6eeec569e506d599d843d959de` | `boatstack.standard` | FLOW_PROGRESS | owned-local | OBSERVED / ACTIVE | ACTIVE | human/autonomy | - | `delivery-state` | `verifier:fresh-observation:plan.activate` | `recovery.resume` | `declared-neutral` |
-| `plan.amend` | primary-flow:`boatstack.standard@1.0.0`
`4ced330c8ca69159c2661e674c223f25077adb6eeec569e506d599d843d959de` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | ACTIVE / FRONTIER | ACTIVE | human/autonomy | `source_path*`, `delivery_id*` | `plan` | `verifier:fresh-observation:plan.amend` | `recovery.resume` | `declared-neutral` |
-| `plan.approve` | primary-flow:`boatstack.standard@1.0.0`
`4ced330c8ca69159c2661e674c223f25077adb6eeec569e506d599d843d959de` | `boatstack.standard` | FLOW_PROGRESS | authority | ACTIVE / FRONTIER | ACTIVE / TERMINAL | human/autonomy | `plan_fingerprint*`, `actor*` | `approval` | `verifier:fresh-observation:plan.approve` | `recovery.resume` | `declared-neutral` |
-| `plan.approve-amendment` | primary-flow:`boatstack.standard@1.0.0`
`4ced330c8ca69159c2661e674c223f25077adb6eeec569e506d599d843d959de` | `boatstack.standard` | FLOW_PROGRESS | authority | ACTIVE / FRONTIER | ACTIVE | human/autonomy | `plan_fingerprint*`, `actor*` | `approval` | `verifier:fresh-observation:plan.approve-amendment` | `recovery.resume` | `declared-neutral` |
-| `plan.create` | primary-flow:`boatstack.standard@1.0.0`
`4ced330c8ca69159c2661e674c223f25077adb6eeec569e506d599d843d959de` | `boatstack.standard` | FLOW_PROGRESS | owned-local | OBSERVED / ACTIVE | ACTIVE | human/autonomy | `source_path*`, `delivery_id*` | `plan` | `verifier:fresh-observation:plan.create` | `recovery.resume` | `declared-neutral` |
-| `plan.invalidate` | primary-flow:`boatstack.standard@1.0.0`
`4ced330c8ca69159c2661e674c223f25077adb6eeec569e506d599d843d959de` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | ACTIVE / OBSERVED | FRONTIER | repository-policy | - | `plan-evidence` | `verifier:fresh-observation:plan.invalidate` | `recovery.resume` | `declared-neutral` |
-| `plan.validate` | primary-flow:`boatstack.standard@1.0.0`
`4ced330c8ca69159c2661e674c223f25077adb6eeec569e506d599d843d959de` | `boatstack.standard` | FLOW_PROGRESS | owned-local | OBSERVED / ACTIVE | ACTIVE / FRONTIER | repository-policy | - | `plan-evidence` | `verifier:fresh-observation:plan.validate` | `recovery.resume` | `declared-neutral` |
-| `publication.abandon` | primary-flow:`boatstack.standard@1.0.0`
`4ced330c8ca69159c2661e674c223f25077adb6eeec569e506d599d843d959de` | `boatstack.standard` | EXPLICIT_ONLY | authority | ACTIVE / FRONTIER | ABANDONED | human | - | `publication` | `verifier:fresh-observation:publication.abandon` | `recovery.resume` | `declared-neutral` |
-| `publication.correct` | primary-flow:`boatstack.standard@1.0.0`
`4ced330c8ca69159c2661e674c223f25077adb6eeec569e506d599d843d959de` | `boatstack.standard` | EXPLICIT_ONLY | owned-external | OBSERVED / ACTIVE / TERMINAL | ACTIVE / RECOVERY | human/autonomy AND external-provider | `publication_id*`, `body_path*`, `body_sha256*` | `publication` | `verifier:fresh-observation:publication.correct` | `publication.reconcile` | `declared-neutral` |
-| `publication.execute` | primary-flow:`boatstack.standard@1.0.0`
`4ced330c8ca69159c2661e674c223f25077adb6eeec569e506d599d843d959de` | `boatstack.standard` | FLOW_PROGRESS | owned-external | ACTIVE | ACTIVE / RECOVERY | human/autonomy AND external-provider | `preview_fingerprint*` | `publication` | `verifier:fresh-observation:publication.execute` | `publication.reconcile` | `declared-neutral` |
-| `publication.observe` | primary-flow:`boatstack.standard@1.0.0`
`4ced330c8ca69159c2661e674c223f25077adb6eeec569e506d599d843d959de` | `boatstack.standard` | FLOW_PROGRESS | owned-local | OBSERVED / ACTIVE / RECOVERY / UNRESOLVED | ACTIVE / TERMINAL / FRONTIER / UNRESOLVED | repository-policy | `publication_id*` | `publication-evidence` | `verifier:fresh-observation:publication.observe` | `recovery.resume` | `declared-neutral` |
-| `publication.preview` | primary-flow:`boatstack.standard@1.0.0`
`4ced330c8ca69159c2661e674c223f25077adb6eeec569e506d599d843d959de` | `boatstack.standard` | FLOW_PROGRESS | owned-local | ACTIVE | ACTIVE | repository-policy | `base_ref*`, `head_ref*`, `body_path*` | `publication-preview` | `verifier:fresh-observation:publication.preview` | `recovery.resume` | `declared-neutral` |
-| `publication.reconcile` | primary-flow:`boatstack.standard@1.0.0`
`4ced330c8ca69159c2661e674c223f25077adb6eeec569e506d599d843d959de` | `boatstack.standard` | FLOW_RECOVERY | recovery | RECOVERY / UNRESOLVED | ACTIVE / TERMINAL / FRONTIER / UNRESOLVED | human/external-provider | `publication_id*`, `transaction_id*` | `publication` | `verifier:fresh-observation:publication.reconcile` | `recovery.escalate` | `declared-neutral` |
-| `recovery.escalate` | core-system:`boatstack.core@1.0.0`
`ab8e145315d72ae5ab17f916775a40db0a8d12a0529ac3de8ed194d116bab9bc` | `boatstack.core` | SYSTEM_RECOVERY | recovery | RECOVERY / UNRESOLVED | FRONTIER | repository-policy | `transaction_id*` | `recovery-journal` | `verifier:fresh-observation:recovery.escalate` | `recovery.escalate` | `declared-neutral` |
-| `recovery.resume` | core-system:`boatstack.core@1.0.0`
`ab8e145315d72ae5ab17f916775a40db0a8d12a0529ac3de8ed194d116bab9bc` | `boatstack.core` | SYSTEM_RECOVERY | recovery | RECOVERY | DORMANT / OBSERVED / ACTIVE / FRONTIER / TERMINAL / ABANDONED | human/autonomy/repository-policy | `transaction_id*` | `recovery-journal` | `verifier:fresh-observation:recovery.resume` | `recovery.escalate` | `declared-neutral` |
-| `recovery.rollback` | core-system:`boatstack.core@1.0.0`
`ab8e145315d72ae5ab17f916775a40db0a8d12a0529ac3de8ed194d116bab9bc` | `boatstack.core` | SYSTEM_RECOVERY | recovery | RECOVERY | DORMANT / OBSERVED / ACTIVE / FRONTIER / TERMINAL / ABANDONED | human/repository-policy | `transaction_id*` | `recovery-journal` | `verifier:fresh-observation:recovery.rollback` | `recovery.escalate` | `declared-neutral` |
-| `repository.attach` | core-system:`boatstack.core@1.0.0`
`ab8e145315d72ae5ab17f916775a40db0a8d12a0529ac3de8ed194d116bab9bc` | `boatstack.core` | EXPLICIT_ONLY | owned-local | DORMANT / OBSERVED | OBSERVED | human | `topology*`, `config_authority*` | `repository-binding` | `verifier:fresh-observation:repository.attach` | `recovery.resume` | `declared-neutral` |
-| `repository.detach` | core-system:`boatstack.core@1.0.0`
`ab8e145315d72ae5ab17f916775a40db0a8d12a0529ac3de8ed194d116bab9bc` | `boatstack.core` | EXPLICIT_ONLY | owned-local | DORMANT / OBSERVED / FRONTIER | DORMANT | human | - | `repository-binding` | `verifier:fresh-observation:repository.detach` | `recovery.resume` | `declared-neutral` |
-| `runtime.hydrate` | core-system:`boatstack.core@1.0.0`
`ab8e145315d72ae5ab17f916775a40db0a8d12a0529ac3de8ed194d116bab9bc` | `boatstack.core` | GOAL_REQUIRED | owned-local | OBSERVED / RECOVERY / UNRESOLVED | OBSERVED / ACTIVE / TERMINAL | repository-policy | `source_revision*`, `runtime_path*`, `runtime_sha256*` | `runtime` | `verifier:fresh-observation:runtime.hydrate` | `runtime.reconcile` | `declared-neutral` |
-| `runtime.reconcile` | core-system:`boatstack.core@1.0.0`
`ab8e145315d72ae5ab17f916775a40db0a8d12a0529ac3de8ed194d116bab9bc` | `boatstack.core` | SYSTEM_RECOVERY | recovery | RECOVERY / UNRESOLVED | OBSERVED / FRONTIER / TERMINAL | repository-policy | `source_revision*`, `runtime_path*`, `runtime_sha256*`, `transaction_id*` | `runtime` | `verifier:fresh-observation:runtime.reconcile` | `recovery.escalate` | `declared-neutral` |
-| `runtime.replace` | core-system:`boatstack.core@1.0.0`
`ab8e145315d72ae5ab17f916775a40db0a8d12a0529ac3de8ed194d116bab9bc` | `boatstack.core` | EXPLICIT_ONLY | owned-local | OBSERVED / RECOVERY | OBSERVED / TERMINAL | human/repository-policy | `source_revision*`, `runtime_path*`, `runtime_sha256*` | `runtime` | `verifier:fresh-observation:runtime.replace` | `runtime.reconcile` | `declared-neutral` |
-| `workspace.abandon` | primary-flow:`boatstack.standard@1.0.0`
`4ced330c8ca69159c2661e674c223f25077adb6eeec569e506d599d843d959de` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | ACTIVE / FRONTIER | ABANDONED | human | `branch*` | `workspace` | `verifier:fresh-observation:workspace.abandon` | `recovery.resume` | `declared-neutral` |
-| `workspace.activate` | primary-flow:`boatstack.standard@1.0.0`
`4ced330c8ca69159c2661e674c223f25077adb6eeec569e506d599d843d959de` | `boatstack.standard` | FLOW_PROGRESS | owned-local | OBSERVED / ACTIVE | ACTIVE | repository-policy | `branch*` | `workspace` | `verifier:fresh-observation:workspace.activate` | `recovery.resume` | `declared-neutral` |
-| `workspace.cleanup` | primary-flow:`boatstack.standard@1.0.0`
`4ced330c8ca69159c2661e674c223f25077adb6eeec569e506d599d843d959de` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | OBSERVED / ACTIVE / TERMINAL / ABANDONED | OBSERVED / TERMINAL / ABANDONED | human/autonomy | `branch*` | `workspace` | `verifier:fresh-observation:workspace.cleanup` | `recovery.escalate` | `declared-neutral` |
-| `workspace.cut` | primary-flow:`boatstack.standard@1.0.0`
`4ced330c8ca69159c2661e674c223f25077adb6eeec569e506d599d843d959de` | `boatstack.standard` | FLOW_PROGRESS | owned-local | OBSERVED / ACTIVE | ACTIVE | human/autonomy | `branch*`, `base_ref*`, `destination*` | `workspace` | `verifier:fresh-observation:workspace.cut` | `workspace.reconcile` | `declared-neutral` |
-| `workspace.publish` | primary-flow:`boatstack.standard@1.0.0`
`4ced330c8ca69159c2661e674c223f25077adb6eeec569e506d599d843d959de` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | ACTIVE | ACTIVE | repository-policy | `branch*` | `workspace-state` | `verifier:fresh-observation:workspace.publish` | `recovery.resume` | `declared-neutral` |
-| `workspace.reap` | primary-flow:`boatstack.standard@1.0.0`
`4ced330c8ca69159c2661e674c223f25077adb6eeec569e506d599d843d959de` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | OBSERVED / TERMINAL / ABANDONED | OBSERVED / TERMINAL / ABANDONED | human | `branch*` | `workspace` | `verifier:fresh-observation:workspace.reap` | `recovery.escalate` | `declared-neutral` |
-| `workspace.reconcile` | primary-flow:`boatstack.standard@1.0.0`
`4ced330c8ca69159c2661e674c223f25077adb6eeec569e506d599d843d959de` | `boatstack.standard` | FLOW_RECOVERY | recovery | RECOVERY / UNRESOLVED | DORMANT / OBSERVED / ACTIVE / FRONTIER / TERMINAL / ABANDONED | human/repository-policy | `transaction_id*` | `workspace` | `verifier:fresh-observation:workspace.reconcile` | `recovery.escalate` | `declared-neutral` |
-| `workspace.sync` | primary-flow:`boatstack.standard@1.0.0`
`4ced330c8ca69159c2661e674c223f25077adb6eeec569e506d599d843d959de` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | ACTIVE | ACTIVE / FRONTIER | human/autonomy | `branch*` | `workspace` | `verifier:fresh-observation:workspace.sync` | `recovery.resume` | `declared-neutral` |
+| `catalog.reconcile` | core-system:`boatstack.core@1.0.0`
`8f5b0163c0b62c1020b1d11208d08bb247de3727ed6dda6f2a41a12b2efd35e2` | `boatstack.core` | EXPLICIT_ONLY | owned-local | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED / TERMINAL / ABANDONED | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED / TERMINAL / ABANDONED | human | `prior_program_fingerprint*`, `accept_obligation_change*` | `catalog-identity` | `verifier:fresh-observation:catalog.reconcile` | `recovery.resume` | `declared-neutral` |
+| `configuration.initialize` | core-system:`boatstack.core@1.0.0`
`8f5b0163c0b62c1020b1d11208d08bb247de3727ed6dda6f2a41a12b2efd35e2` | `boatstack.core` | GOAL_REQUIRED | owned-local | OBSERVED | OBSERVED / TERMINAL | human/repository-policy | `config_path*`, `config_sha256*` | `configuration` | `verifier:fresh-observation:configuration.initialize` | `configuration.reconcile` | `declared-neutral` |
+| `configuration.mutate` | core-system:`boatstack.core@1.0.0`
`8f5b0163c0b62c1020b1d11208d08bb247de3727ed6dda6f2a41a12b2efd35e2` | `boatstack.core` | EXPLICIT_ONLY | owned-local | OBSERVED / ACTIVE / FRONTIER / UNRESOLVED | OBSERVED / ACTIVE / TERMINAL | human/autonomy | `config_path*`, `config_sha256*` | `configuration` | `verifier:fresh-observation:configuration.mutate` | `configuration.reconcile` | `declared-neutral` |
+| `configuration.reconcile` | core-system:`boatstack.core@1.0.0`
`8f5b0163c0b62c1020b1d11208d08bb247de3727ed6dda6f2a41a12b2efd35e2` | `boatstack.core` | SYSTEM_RECOVERY | recovery | RECOVERY / UNRESOLVED | OBSERVED / FRONTIER / TERMINAL | human/repository-policy | `transaction_id*` | `configuration` | `verifier:fresh-observation:configuration.reconcile` | `recovery.escalate` | `declared-neutral` |
+| `delivery.slice.advance` | primary-flow:`boatstack.standard@1.0.0`
`b2d86642677b67c6b5a4ba77fae3ecbe2a301f3a595926ba339be44f077698a9` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | ACTIVE | ACTIVE / TERMINAL | human/autonomy | `slice_id*`, `source_revision*` | `delivery-state` | `verifier:fresh-observation:delivery.slice.advance` | `recovery.resume` | `declared-neutral` |
+| `engagement.begin` | core-system:`boatstack.core@1.0.0`
`8f5b0163c0b62c1020b1d11208d08bb247de3727ed6dda6f2a41a12b2efd35e2` | `boatstack.core` | GOAL_REQUIRED | authority | DORMANT / OBSERVED | OBSERVED / ACTIVE | repository-policy | - | `engagement` | `verifier:fresh-observation:engagement.begin` | `recovery.resume` | `declared-neutral` |
+| `engagement.release` | core-system:`boatstack.core@1.0.0`
`8f5b0163c0b62c1020b1d11208d08bb247de3727ed6dda6f2a41a12b2efd35e2` | `boatstack.core` | EXPLICIT_ONLY | authority | ACTIVE / FRONTIER | DORMANT | repository-policy | - | `engagement` | `verifier:fresh-observation:engagement.release` | `recovery.resume` | `declared-neutral` |
+| `engagement.renew` | core-system:`boatstack.core@1.0.0`
`8f5b0163c0b62c1020b1d11208d08bb247de3727ed6dda6f2a41a12b2efd35e2` | `boatstack.core` | EXPLICIT_ONLY | authority | ACTIVE | ACTIVE | repository-policy/autonomy | - | `engagement` | `verifier:fresh-observation:engagement.renew` | `recovery.resume` | `declared-neutral` |
+| `evidence.approval.revoke` | primary-flow:`boatstack.standard@1.0.0`
`b2d86642677b67c6b5a4ba77fae3ecbe2a301f3a595926ba339be44f077698a9` | `boatstack.standard` | EXPLICIT_ONLY | authority | ACTIVE / FRONTIER | FRONTIER | human | - | `approval` | `verifier:fresh-observation:evidence.approval.revoke` | `recovery.resume` | `declared-neutral` |
+| `evidence.visual.attach` | primary-flow:`boatstack.standard@1.0.0`
`b2d86642677b67c6b5a4ba77fae3ecbe2a301f3a595926ba339be44f077698a9` | `boatstack.standard` | FLOW_PROGRESS | owned-local | ACTIVE | ACTIVE / TERMINAL | human/repository-policy | `manifest_path*`, `privacy_receipt*`, `source_revision*` | `evidence` | `verifier:fresh-observation:evidence.visual.attach` | `recovery.resume` | `declared-neutral` |
+| `external.branch-changed` | core-system:`boatstack.core@1.0.0`
`8f5b0163c0b62c1020b1d11208d08bb247de3727ed6dda6f2a41a12b2efd35e2` | `boatstack.core` | OBSERVED_EXTERNAL | observed-external | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED | OBSERVED | none | - | - | `verifier:fresh-observation:external.branch-changed` | `-` | `declared-neutral` |
+| `external.ci-completed` | core-system:`boatstack.core@1.0.0`
`8f5b0163c0b62c1020b1d11208d08bb247de3727ed6dda6f2a41a12b2efd35e2` | `boatstack.core` | OBSERVED_EXTERNAL | observed-external | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED | OBSERVED / ACTIVE / TERMINAL | none | - | - | `verifier:fresh-observation:external.ci-completed` | `-` | `declared-neutral` |
+| `external.configuration-drifted` | core-system:`boatstack.core@1.0.0`
`8f5b0163c0b62c1020b1d11208d08bb247de3727ed6dda6f2a41a12b2efd35e2` | `boatstack.core` | OBSERVED_EXTERNAL | observed-external | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED | OBSERVED / UNRESOLVED | none | - | - | `verifier:fresh-observation:external.configuration-drifted` | `-` | `declared-neutral` |
+| `external.files-changed` | core-system:`boatstack.core@1.0.0`
`8f5b0163c0b62c1020b1d11208d08bb247de3727ed6dda6f2a41a12b2efd35e2` | `boatstack.core` | OBSERVED_EXTERNAL | observed-external | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED | OBSERVED | none | - | - | `verifier:fresh-observation:external.files-changed` | `-` | `declared-neutral` |
+| `external.head-changed` | core-system:`boatstack.core@1.0.0`
`8f5b0163c0b62c1020b1d11208d08bb247de3727ed6dda6f2a41a12b2efd35e2` | `boatstack.core` | OBSERVED_EXTERNAL | observed-external | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED | OBSERVED | none | - | - | `verifier:fresh-observation:external.head-changed` | `-` | `declared-neutral` |
+| `external.host-interrupted` | core-system:`boatstack.core@1.0.0`
`8f5b0163c0b62c1020b1d11208d08bb247de3727ed6dda6f2a41a12b2efd35e2` | `boatstack.core` | OBSERVED_EXTERNAL | observed-external | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED | RECOVERY | none | - | - | `verifier:fresh-observation:external.host-interrupted` | `-` | `declared-neutral` |
+| `external.lease-expired` | core-system:`boatstack.core@1.0.0`
`8f5b0163c0b62c1020b1d11208d08bb247de3727ed6dda6f2a41a12b2efd35e2` | `boatstack.core` | OBSERVED_EXTERNAL | observed-external | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED | DORMANT / FRONTIER | none | - | - | `verifier:fresh-observation:external.lease-expired` | `-` | `declared-neutral` |
+| `external.pr-closed` | core-system:`boatstack.core@1.0.0`
`8f5b0163c0b62c1020b1d11208d08bb247de3727ed6dda6f2a41a12b2efd35e2` | `boatstack.core` | OBSERVED_EXTERNAL | observed-external | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED | OBSERVED / ACTIVE / FRONTIER | none | - | - | `verifier:fresh-observation:external.pr-closed` | `-` | `declared-neutral` |
+| `external.pr-merged` | core-system:`boatstack.core@1.0.0`
`8f5b0163c0b62c1020b1d11208d08bb247de3727ed6dda6f2a41a12b2efd35e2` | `boatstack.core` | OBSERVED_EXTERNAL | observed-external | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED | OBSERVED / ACTIVE / TERMINAL | none | - | - | `verifier:fresh-observation:external.pr-merged` | `-` | `declared-neutral` |
+| `external.pr-opened` | core-system:`boatstack.core@1.0.0`
`8f5b0163c0b62c1020b1d11208d08bb247de3727ed6dda6f2a41a12b2efd35e2` | `boatstack.core` | OBSERVED_EXTERNAL | observed-external | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED | OBSERVED / ACTIVE / TERMINAL | none | - | - | `verifier:fresh-observation:external.pr-opened` | `-` | `declared-neutral` |
+| `external.pr-updated` | core-system:`boatstack.core@1.0.0`
`8f5b0163c0b62c1020b1d11208d08bb247de3727ed6dda6f2a41a12b2efd35e2` | `boatstack.core` | OBSERVED_EXTERNAL | observed-external | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED | OBSERVED / ACTIVE / TERMINAL | none | - | - | `verifier:fresh-observation:external.pr-updated` | `-` | `declared-neutral` |
+| `external.provider-unavailable` | core-system:`boatstack.core@1.0.0`
`8f5b0163c0b62c1020b1d11208d08bb247de3727ed6dda6f2a41a12b2efd35e2` | `boatstack.core` | OBSERVED_EXTERNAL | observed-external | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED | UNRESOLVED / RECOVERY | none | - | - | `verifier:fresh-observation:external.provider-unavailable` | `-` | `declared-neutral` |
+| `external.runtime-disappeared` | core-system:`boatstack.core@1.0.0`
`8f5b0163c0b62c1020b1d11208d08bb247de3727ed6dda6f2a41a12b2efd35e2` | `boatstack.core` | OBSERVED_EXTERNAL | observed-external | DORMANT / OBSERVED / ACTIVE / RECOVERY / FRONTIER / UNRESOLVED | OBSERVED / RECOVERY | none | - | - | `verifier:fresh-observation:external.runtime-disappeared` | `-` | `declared-neutral` |
+| `gate.build.record` | primary-flow:`boatstack.standard@1.0.0`
`b2d86642677b67c6b5a4ba77fae3ecbe2a301f3a595926ba339be44f077698a9` | `boatstack.standard` | FLOW_PROGRESS | owned-local | ACTIVE | ACTIVE | repository-policy | `source_revision*`, `evidence_path*`, `evidence_fingerprint*` | `gate-evidence` | `verifier:fresh-observation:gate.build.record` | `recovery.resume` | `declared-neutral` |
+| `gate.change.record` | primary-flow:`boatstack.standard@1.0.0`
`b2d86642677b67c6b5a4ba77fae3ecbe2a301f3a595926ba339be44f077698a9` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | ACTIVE | ACTIVE | repository-policy | `source_revision*`, `evidence_path*`, `evidence_fingerprint*` | `gate-evidence` | `verifier:fresh-observation:gate.change.record` | `recovery.resume` | `declared-neutral` |
+| `gate.journey.record` | primary-flow:`boatstack.standard@1.0.0`
`b2d86642677b67c6b5a4ba77fae3ecbe2a301f3a595926ba339be44f077698a9` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | ACTIVE | ACTIVE | repository-policy | `source_revision*`, `evidence_path*`, `evidence_fingerprint*` | `gate-evidence` | `verifier:fresh-observation:gate.journey.record` | `recovery.resume` | `declared-neutral` |
+| `gate.review.record` | primary-flow:`boatstack.standard@1.0.0`
`b2d86642677b67c6b5a4ba77fae3ecbe2a301f3a595926ba339be44f077698a9` | `boatstack.standard` | FLOW_PROGRESS | owned-local | ACTIVE | ACTIVE / TERMINAL | human/repository-policy | `source_revision*`, `evidence_path*`, `evidence_fingerprint*` | `gate-evidence` | `verifier:fresh-observation:gate.review.record` | `recovery.resume` | `declared-neutral` |
+| `gate.test.record` | primary-flow:`boatstack.standard@1.0.0`
`b2d86642677b67c6b5a4ba77fae3ecbe2a301f3a595926ba339be44f077698a9` | `boatstack.standard` | FLOW_PROGRESS | owned-local | ACTIVE | ACTIVE / TERMINAL | repository-policy | `source_revision*`, `evidence_path*`, `evidence_fingerprint*` | `gate-evidence` | `verifier:fresh-observation:gate.test.record` | `recovery.resume` | `declared-neutral` |
+| `goal.configure` | core-system:`boatstack.core@1.0.0`
`8f5b0163c0b62c1020b1d11208d08bb247de3727ed6dda6f2a41a12b2efd35e2` | `boatstack.core` | GOAL_REQUIRED | authority | OBSERVED / DORMANT / ACTIVE / FRONTIER / TERMINAL / ABANDONED | OBSERVED / ACTIVE / FRONTIER | human/autonomy | `goal_kind*`, `delivery_id*` | `goal` | `verifier:fresh-observation:goal.configure` | `recovery.resume` | `declared-neutral` |
+| `installation.initialize` | core-system:`boatstack.core@1.0.0`
`8f5b0163c0b62c1020b1d11208d08bb247de3727ed6dda6f2a41a12b2efd35e2` | `boatstack.core` | GOAL_REQUIRED | owned-local | DORMANT / OBSERVED | OBSERVED | human | `source_revision*`, `runtime_path*`, `runtime_sha256*`, `config_path*`, `config_sha256*` | `installation` | `verifier:fresh-observation:installation.initialize` | `runtime.reconcile` | `declared-neutral` |
+| `installation.update` | core-system:`boatstack.core@1.0.0`
`8f5b0163c0b62c1020b1d11208d08bb247de3727ed6dda6f2a41a12b2efd35e2` | `boatstack.core` | EXPLICIT_ONLY | owned-local | OBSERVED / ACTIVE | OBSERVED / ACTIVE / TERMINAL | human/autonomy | `source_revision*`, `runtime_path*`, `runtime_sha256*` | `installation` | `verifier:fresh-observation:installation.update` | `runtime.reconcile` | `declared-neutral` |
+| `invocation.rebind` | core-system:`boatstack.core@1.0.0`
`8f5b0163c0b62c1020b1d11208d08bb247de3727ed6dda6f2a41a12b2efd35e2` | `boatstack.core` | EXPLICIT_ONLY | owned-local | OBSERVED / UNRESOLVED | OBSERVED | repository-policy | - | `identity-binding` | `verifier:fresh-observation:invocation.rebind` | `recovery.resume` | `declared-neutral` |
+| `plan.abandon` | primary-flow:`boatstack.standard@1.0.0`
`b2d86642677b67c6b5a4ba77fae3ecbe2a301f3a595926ba339be44f077698a9` | `boatstack.standard` | EXPLICIT_ONLY | authority | OBSERVED / ACTIVE / FRONTIER | ABANDONED | human | - | `plan` | `verifier:fresh-observation:plan.abandon` | `recovery.resume` | `declared-neutral` |
+| `plan.activate` | primary-flow:`boatstack.standard@1.0.0`
`b2d86642677b67c6b5a4ba77fae3ecbe2a301f3a595926ba339be44f077698a9` | `boatstack.standard` | FLOW_PROGRESS | owned-local | OBSERVED / ACTIVE | ACTIVE | human/autonomy | - | `delivery-state` | `verifier:fresh-observation:plan.activate` | `recovery.resume` | `declared-neutral` |
+| `plan.amend` | primary-flow:`boatstack.standard@1.0.0`
`b2d86642677b67c6b5a4ba77fae3ecbe2a301f3a595926ba339be44f077698a9` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | ACTIVE / FRONTIER | ACTIVE | human/autonomy | `source_path*`, `delivery_id*` | `plan` | `verifier:fresh-observation:plan.amend` | `recovery.resume` | `declared-neutral` |
+| `plan.approve` | primary-flow:`boatstack.standard@1.0.0`
`b2d86642677b67c6b5a4ba77fae3ecbe2a301f3a595926ba339be44f077698a9` | `boatstack.standard` | FLOW_PROGRESS | authority | ACTIVE / FRONTIER | ACTIVE / TERMINAL | human/autonomy | `plan_fingerprint*`, `actor*` | `approval` | `verifier:fresh-observation:plan.approve` | `recovery.resume` | `declared-neutral` |
+| `plan.approve-amendment` | primary-flow:`boatstack.standard@1.0.0`
`b2d86642677b67c6b5a4ba77fae3ecbe2a301f3a595926ba339be44f077698a9` | `boatstack.standard` | FLOW_PROGRESS | authority | ACTIVE / FRONTIER | ACTIVE | human/autonomy | `plan_fingerprint*`, `actor*` | `approval` | `verifier:fresh-observation:plan.approve-amendment` | `recovery.resume` | `declared-neutral` |
+| `plan.create` | primary-flow:`boatstack.standard@1.0.0`
`b2d86642677b67c6b5a4ba77fae3ecbe2a301f3a595926ba339be44f077698a9` | `boatstack.standard` | FLOW_PROGRESS | owned-local | OBSERVED / ACTIVE | ACTIVE | human/autonomy | `source_path*`, `delivery_id*` | `plan` | `verifier:fresh-observation:plan.create` | `recovery.resume` | `declared-neutral` |
+| `plan.invalidate` | primary-flow:`boatstack.standard@1.0.0`
`b2d86642677b67c6b5a4ba77fae3ecbe2a301f3a595926ba339be44f077698a9` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | ACTIVE / OBSERVED | FRONTIER | repository-policy | - | `plan-evidence` | `verifier:fresh-observation:plan.invalidate` | `recovery.resume` | `declared-neutral` |
+| `plan.validate` | primary-flow:`boatstack.standard@1.0.0`
`b2d86642677b67c6b5a4ba77fae3ecbe2a301f3a595926ba339be44f077698a9` | `boatstack.standard` | FLOW_PROGRESS | owned-local | OBSERVED / ACTIVE | ACTIVE / FRONTIER | repository-policy | - | `plan-evidence` | `verifier:fresh-observation:plan.validate` | `recovery.resume` | `declared-neutral` |
+| `publication.abandon` | primary-flow:`boatstack.standard@1.0.0`
`b2d86642677b67c6b5a4ba77fae3ecbe2a301f3a595926ba339be44f077698a9` | `boatstack.standard` | EXPLICIT_ONLY | authority | ACTIVE / FRONTIER | ABANDONED | human | - | `publication` | `verifier:fresh-observation:publication.abandon` | `recovery.resume` | `declared-neutral` |
+| `publication.correct` | primary-flow:`boatstack.standard@1.0.0`
`b2d86642677b67c6b5a4ba77fae3ecbe2a301f3a595926ba339be44f077698a9` | `boatstack.standard` | EXPLICIT_ONLY | owned-external | OBSERVED / ACTIVE / TERMINAL | ACTIVE / RECOVERY | human/autonomy AND external-provider | `publication_id*`, `body_path*`, `body_sha256*` | `publication` | `verifier:fresh-observation:publication.correct` | `publication.reconcile` | `declared-neutral` |
+| `publication.execute` | primary-flow:`boatstack.standard@1.0.0`
`b2d86642677b67c6b5a4ba77fae3ecbe2a301f3a595926ba339be44f077698a9` | `boatstack.standard` | FLOW_PROGRESS | owned-external | ACTIVE | ACTIVE / RECOVERY | human/autonomy AND external-provider | `preview_fingerprint*` | `publication` | `verifier:fresh-observation:publication.execute` | `publication.reconcile` | `declared-neutral` |
+| `publication.observe` | primary-flow:`boatstack.standard@1.0.0`
`b2d86642677b67c6b5a4ba77fae3ecbe2a301f3a595926ba339be44f077698a9` | `boatstack.standard` | FLOW_PROGRESS | owned-local | OBSERVED / ACTIVE / RECOVERY / UNRESOLVED | ACTIVE / TERMINAL / FRONTIER / UNRESOLVED | repository-policy | `publication_id*` | `publication-evidence` | `verifier:fresh-observation:publication.observe` | `recovery.resume` | `declared-neutral` |
+| `publication.preview` | primary-flow:`boatstack.standard@1.0.0`
`b2d86642677b67c6b5a4ba77fae3ecbe2a301f3a595926ba339be44f077698a9` | `boatstack.standard` | FLOW_PROGRESS | owned-local | ACTIVE | ACTIVE | repository-policy | `base_ref*`, `head_ref*`, `body_path*` | `publication-preview` | `verifier:fresh-observation:publication.preview` | `recovery.resume` | `declared-neutral` |
+| `publication.reconcile` | primary-flow:`boatstack.standard@1.0.0`
`b2d86642677b67c6b5a4ba77fae3ecbe2a301f3a595926ba339be44f077698a9` | `boatstack.standard` | FLOW_RECOVERY | recovery | RECOVERY / UNRESOLVED | ACTIVE / TERMINAL / FRONTIER / UNRESOLVED | human/external-provider | `publication_id*`, `transaction_id*` | `publication` | `verifier:fresh-observation:publication.reconcile` | `recovery.escalate` | `declared-neutral` |
+| `recovery.escalate` | core-system:`boatstack.core@1.0.0`
`8f5b0163c0b62c1020b1d11208d08bb247de3727ed6dda6f2a41a12b2efd35e2` | `boatstack.core` | SYSTEM_RECOVERY | recovery | RECOVERY / UNRESOLVED | FRONTIER | repository-policy | `transaction_id*` | `recovery-journal` | `verifier:fresh-observation:recovery.escalate` | `recovery.escalate` | `declared-neutral` |
+| `recovery.resume` | core-system:`boatstack.core@1.0.0`
`8f5b0163c0b62c1020b1d11208d08bb247de3727ed6dda6f2a41a12b2efd35e2` | `boatstack.core` | SYSTEM_RECOVERY | recovery | RECOVERY | DORMANT / OBSERVED / ACTIVE / FRONTIER / TERMINAL / ABANDONED | human/autonomy/repository-policy | `transaction_id*` | `recovery-journal` | `verifier:fresh-observation:recovery.resume` | `recovery.escalate` | `declared-neutral` |
+| `recovery.rollback` | core-system:`boatstack.core@1.0.0`
`8f5b0163c0b62c1020b1d11208d08bb247de3727ed6dda6f2a41a12b2efd35e2` | `boatstack.core` | SYSTEM_RECOVERY | recovery | RECOVERY | DORMANT / OBSERVED / ACTIVE / FRONTIER / TERMINAL / ABANDONED | human/repository-policy | `transaction_id*` | `recovery-journal` | `verifier:fresh-observation:recovery.rollback` | `recovery.escalate` | `declared-neutral` |
+| `repository.attach` | core-system:`boatstack.core@1.0.0`
`8f5b0163c0b62c1020b1d11208d08bb247de3727ed6dda6f2a41a12b2efd35e2` | `boatstack.core` | EXPLICIT_ONLY | owned-local | DORMANT / OBSERVED | OBSERVED | human | `topology*`, `config_authority*` | `repository-binding` | `verifier:fresh-observation:repository.attach` | `recovery.resume` | `declared-neutral` |
+| `repository.detach` | core-system:`boatstack.core@1.0.0`
`8f5b0163c0b62c1020b1d11208d08bb247de3727ed6dda6f2a41a12b2efd35e2` | `boatstack.core` | EXPLICIT_ONLY | owned-local | DORMANT / OBSERVED / FRONTIER | DORMANT | human | - | `repository-binding` | `verifier:fresh-observation:repository.detach` | `recovery.resume` | `declared-neutral` |
+| `runtime.hydrate` | core-system:`boatstack.core@1.0.0`
`8f5b0163c0b62c1020b1d11208d08bb247de3727ed6dda6f2a41a12b2efd35e2` | `boatstack.core` | GOAL_REQUIRED | owned-local | OBSERVED / RECOVERY / UNRESOLVED | OBSERVED / ACTIVE / TERMINAL | repository-policy | `source_revision*`, `runtime_path*`, `runtime_sha256*` | `runtime` | `verifier:fresh-observation:runtime.hydrate` | `runtime.reconcile` | `declared-neutral` |
+| `runtime.reconcile` | core-system:`boatstack.core@1.0.0`
`8f5b0163c0b62c1020b1d11208d08bb247de3727ed6dda6f2a41a12b2efd35e2` | `boatstack.core` | SYSTEM_RECOVERY | recovery | RECOVERY / UNRESOLVED | OBSERVED / FRONTIER / TERMINAL | repository-policy | `source_revision*`, `runtime_path*`, `runtime_sha256*`, `transaction_id*` | `runtime` | `verifier:fresh-observation:runtime.reconcile` | `recovery.escalate` | `declared-neutral` |
+| `runtime.replace` | core-system:`boatstack.core@1.0.0`
`8f5b0163c0b62c1020b1d11208d08bb247de3727ed6dda6f2a41a12b2efd35e2` | `boatstack.core` | EXPLICIT_ONLY | owned-local | OBSERVED / RECOVERY | OBSERVED / TERMINAL | human/repository-policy | `source_revision*`, `runtime_path*`, `runtime_sha256*` | `runtime` | `verifier:fresh-observation:runtime.replace` | `runtime.reconcile` | `declared-neutral` |
+| `workspace.abandon` | primary-flow:`boatstack.standard@1.0.0`
`b2d86642677b67c6b5a4ba77fae3ecbe2a301f3a595926ba339be44f077698a9` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | ACTIVE / FRONTIER | ABANDONED | human | `branch*` | `workspace` | `verifier:fresh-observation:workspace.abandon` | `recovery.resume` | `declared-neutral` |
+| `workspace.activate` | primary-flow:`boatstack.standard@1.0.0`
`b2d86642677b67c6b5a4ba77fae3ecbe2a301f3a595926ba339be44f077698a9` | `boatstack.standard` | FLOW_PROGRESS | owned-local | OBSERVED / ACTIVE | ACTIVE | repository-policy | `branch*` | `workspace` | `verifier:fresh-observation:workspace.activate` | `recovery.resume` | `declared-neutral` |
+| `workspace.cleanup` | primary-flow:`boatstack.standard@1.0.0`
`b2d86642677b67c6b5a4ba77fae3ecbe2a301f3a595926ba339be44f077698a9` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | OBSERVED / ACTIVE / TERMINAL / ABANDONED | OBSERVED / TERMINAL / ABANDONED | human/autonomy | `branch*` | `workspace` | `verifier:fresh-observation:workspace.cleanup` | `recovery.escalate` | `declared-neutral` |
+| `workspace.cut` | primary-flow:`boatstack.standard@1.0.0`
`b2d86642677b67c6b5a4ba77fae3ecbe2a301f3a595926ba339be44f077698a9` | `boatstack.standard` | FLOW_PROGRESS | owned-local | OBSERVED / ACTIVE | ACTIVE | human/autonomy | `branch*`, `base_ref*`, `destination*` | `workspace` | `verifier:fresh-observation:workspace.cut` | `workspace.reconcile` | `declared-neutral` |
+| `workspace.publish` | primary-flow:`boatstack.standard@1.0.0`
`b2d86642677b67c6b5a4ba77fae3ecbe2a301f3a595926ba339be44f077698a9` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | ACTIVE | ACTIVE | repository-policy | `branch*` | `workspace-state` | `verifier:fresh-observation:workspace.publish` | `recovery.resume` | `declared-neutral` |
+| `workspace.reap` | primary-flow:`boatstack.standard@1.0.0`
`b2d86642677b67c6b5a4ba77fae3ecbe2a301f3a595926ba339be44f077698a9` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | OBSERVED / TERMINAL / ABANDONED | OBSERVED / TERMINAL / ABANDONED | human | `branch*` | `workspace` | `verifier:fresh-observation:workspace.reap` | `recovery.escalate` | `declared-neutral` |
+| `workspace.reconcile` | primary-flow:`boatstack.standard@1.0.0`
`b2d86642677b67c6b5a4ba77fae3ecbe2a301f3a595926ba339be44f077698a9` | `boatstack.standard` | FLOW_RECOVERY | recovery | RECOVERY / UNRESOLVED | DORMANT / OBSERVED / ACTIVE / FRONTIER / TERMINAL / ABANDONED | human/repository-policy | `transaction_id*` | `workspace` | `verifier:fresh-observation:workspace.reconcile` | `recovery.escalate` | `declared-neutral` |
+| `workspace.sync` | primary-flow:`boatstack.standard@1.0.0`
`b2d86642677b67c6b5a4ba77fae3ecbe2a301f3a595926ba339be44f077698a9` | `boatstack.standard` | EXPLICIT_ONLY | owned-local | ACTIVE | ACTIVE / FRONTIER | human/autonomy | `branch*` | `workspace` | `verifier:fresh-observation:workspace.sync` | `recovery.resume` | `declared-neutral` |
`*` marks a required parameter. OR authority is shown with `/`; mandatory authority clauses are shown with `AND`. Source and target facet predicates remain in the canonical JSON returned by `boatstack catalog --format json`.
diff --git a/docs/architecture/boatstack-v2-transition-catalog.mmd b/docs/architecture/boatstack-v2-transition-catalog.mmd
index 2dd67b2..164403a 100644
--- a/docs/architecture/boatstack-v2-transition-catalog.mmd
+++ b/docs/architecture/boatstack-v2-transition-catalog.mmd
@@ -95,6 +95,7 @@ flowchart TB
p_FRONTIER --> t03
t03 --> p_FRONTIER
p_OBSERVED --> t04
+ p_DORMANT --> t04
p_ACTIVE --> t04
p_FRONTIER --> t04
p_TERMINAL --> t04
diff --git a/release-notes/2026-08-11-transition-law-consistency.md b/release-notes/2026-08-11-transition-law-consistency.md
new file mode 100644
index 0000000..1ef5ee7
--- /dev/null
+++ b/release-notes/2026-08-11-transition-law-consistency.md
@@ -0,0 +1,3 @@
+### Keep prescriptions consistent with deterministic apply checks
+
+Boatstack now applies one shared applicability law and side-effect-free effect preflight before reporting a transition as prescribed. Required parameters are surfaced as a candidate before admission, retained goals are reconfigured before engagement, recovery omits impossible resume actions, and volatile provider observations remain selectable until the configured goal is established.