Spectre remains a focused OTP-native runtime rather than a framework that owns application business logic. Version 0.3.0 completes the core governed reflective-runtime concept on one canonical Definition and one operational runtime; future work extends adapters and distributed integration without adding a second owner for Agent or operational state.

0.3.0 reflective runtime — complete in core

Opt-in Experience, mechanical Declared/Effective/Observed Reflection, policy-gated introspection, compiled Prism-compatible critics, independent oracle approval, inert Forge Proposals, explicit rebase and stale external evidence checks now compose over the complete 0.2 foundation. Forge cannot publish, approve or activate and has no vocabulary for code, authority, kernel policy, evaluators or projection generators.

The permanent reflective fixture and executable conformance helpers pin the new identities. State remains writer v5, Run writer v2 and canonical Instance checkpoints schema 4; all historical guaranteed fixtures remain readable. Concrete sibling-package adoption remains in those repositories and is not a second core runtime gate.

0.2.9 governed Definition changes — complete

Closed ChangeSets now derive immutable Candidate Definitions through a trusted host Composer and registered operation vocabulary. Exact gate receipts, protected-corpus evaluation deltas, deterministic human reports, risk-based approval, activation re-verification, ancestor rollback, and conservative GC all compose over existing Store, Manifest, closure, owner-fence, and CAS contracts. Runtime data still cannot introduce code, authority, or activation.

State remains writer v5, Run remains writer v2, and canonical Instance checkpoints remain schema 4. The 0.3 Reflection and Forge planes compose over these governed identities without bypassing approval or activation.

0.2.8 data-driven execution — complete

Compiled and runtime-authored precise Work now share one portable Program IR and the existing operational executor. The gate includes exact Execution materialization/projections, prompt receipts, bounded amendments, typed Flow/Work handoffs, registered pure state migrations, and deterministic no-Effect rehearsal. Runtime data cannot introduce code or authority.

State remains writer v5, Run remains writer v2, and canonical Instance checkpoints remain schema 4. Governance composes over these identities and host boundaries rather than adding another executor.

0.2.0 vNext core — complete

The 0.2.0 implementation was completed in dependency order:

  1. canonical Agent state, typed sections, revisioned snapshots, conflict-aware merge, transition journal, and portable CAS checkpoints;
  2. shared operational runtime, isolated one-attempt Runners, fencing, retry, recovery, durable control, budgets, and precise Work;
  3. Flow integration, read-only views, visibility, event routing, and disambiguation;
  4. core Vigil with durable waits, timer generations, significance, and the same Runner topology;
  5. provider-neutral function, Action, Effect, planner, cognitive, memory, and external-controller boundaries;
  6. governed consent and delivery receipts, followed by restart, race, corruption, and coverage hardening.

The 0.1.6 conversational recovery baseline remains protected. Work and Vigil are additive domains; Run was not renamed. See Migrating to 0.2.0.

After 0.2.0

  • Keep 0.2.x changes compatible with the published Work, Vigil, controller, checkpoint, control, event, and delivery contracts.
  • Move concrete Lens, Kinetic, Prism, Mnemonic, Directive, Beam, and Pulse integrations forward in their own repositories against explicit minimum Spectre versions.
  • Add distributed claims or remote handoff only after preserving the local single-owner and fencing invariants.
  • Keep Mission, Wayfinder, transport, model clients, credentials, and durable application storage in their owning libraries or host applications.

What Already Works

  • Router plugs collect evidence without owning the final decision.
  • The arbitrator chooses one route across deterministic and probabilistic providers.
  • Protected actions stop at a policy boundary.
  • Policy approval and side-effect execution are separate operations.
  • State, Effect, Awaitable, Result, and Turn expose lifecycle as data.
  • Sessions serialize conversation turns and can restore durable state.
  • Ordered turn handlers let an optional external runtime own a normal turn without gaining authority over Spectre state, policy, routing, or effects.
  • spectre_kinetic can own Action Language and tool planning without moving side effects into the conversational runtime.

These are the foundations to preserve.

Ecosystem integration direction

Future packages should compose through one local turn vocabulary and explicit ownership boundaries rather than a universal plugin system:

  • Spectre.turn/3 remains the canonical local host port for Agent modules and Sessions;
  • SpectreDirective may claim an active durable mission through a turn handler;
  • SpectreMnemonic should adapt to the existing memory port;
  • SpectreLens should expose browser work as trusted actions/Skills and safe context data;
  • SpectrePulse may define agent-to-agent envelopes, addressing, task and correlation identifiers, delivery, retries, streaming, and cancellation, then adapt a validated payload to and from Spectre.turn/3;
  • a Pulse exchange should use a turn handler only when an already-active local task genuinely owns subsequent input before normal routing.

New libraries can register ordered handlers without creating a core dependency, but the handler contract will remain deliberately unable to mutate input, state, routes, effects, or awaitables. Broader needs should first justify a dedicated narrow port.

Implementation Journal

2026-07-20: Lifecycle, Execution, Journal, And Arbitration Hardening

Phases 1 through 6 are functionally complete for the library scope:

  • Spectre.Lifecycle.apply/2, next/1, and projection/1 are the canonical command and host-decision surface; State, Result, Turn.Decision, Runner, and Policy delegate to it;
  • Spectre.Policy.Matcher is pure, and user text plus trusted host decisions share Spectre.Policy.Resolution, including optional structured metadata;
  • no-match attempts, exhaustion, rejection, cancellation, and expiration are explicit lifecycle commands; global interrupts remain closed by default and can be enabled with policy_global_interrupts?: true;
  • Spectre.ActionDispatcher owns capability calls only, while Spectre.Execution validates effects and applies terminal lifecycle commands; Runtime and Session retain the durable two-commit workflow;
  • journal records now cover arbitration, policy, lifecycle, execution, and persistence, with redaction callbacks, per-phase sampling, retention metadata, schema restoration, and telemetry derived from records;
  • arbitration explanations now expose eligibility, configured thresholds, rejected alternatives, the winner, and deterministic precedence in receipts and journal evidence;
  • operational telemetry covers provider calls, policy retries, execution, restore, persistence conflicts, stale session commands, journal records, and idle shutdown without requiring :telemetry as a dependency;
  • the strategy matrix contains 800 generated cases across ten differently configured agents, backed by local FastEmbed-compatible fixtures; the full suite exceeds 1,000 tests.

Current verification is 1,177 passing tests and 90.08% line coverage, together with warnings-as-errors compilation, strict Credo, Dialyzer, formatting, and diff checks. The default 90% coverage threshold remains enforced.

Project decisions for this cycle:

  • the freelance.fast integration fixture from Phase 0 is waived because it belongs in the consuming application, not this library repository;
  • Vettore remains a required dependency by design, including regex-only deployments;
  • Phase 8 release/editorial stabilization is complete for the 0.1.0 public preview: package metadata, changelog, public guides, API documentation, production checklist, testing guide, security policy, and contribution policy are versioned with the source.

2026-07-17: Canonical Provider Facts And Reply Validation

This hardening pass closed the remaining gap between route traces and the shared provider boundary:

  • evaluation receipts now record privacy-safe provider outcome and duration facts emitted directly by Spectre.Provider.Call;
  • LLM-use policies reflect an actual adapter-worker invocation, so a failure while constructing the classifier prompt is not misreported as model usage;
  • local-classifier and semantic-cache route maps now validate labels, acceptance, numeric scores, strategy, metadata, and score maps before they reach arbitration;
  • embedding and LLM shape validation now happens inside the same observed call boundary, keeping normalized outcomes consistent;
  • receipt route, candidate, attempt, and provider-call fields are independently sanitized, including values supplied by custom pipelines;
  • a valid non-accepted semantic-cache reply degrades as a miss instead of crashing a router plug;
  • rejected candidates cannot become eligible merely because a rule assigned hard evidence strength;
  • the dynamically detected ex_fastembed integration is no longer declared as a transitive Git dependency, so the local release package can be built while host applications can still opt into the adapter explicitly.

These facts remain local receipt data. No exporter, pricing layer, automatic retry, or circuit breaker was added.

2026-07-16: Provider Execution Resilience

This iteration added a small operational boundary instead of embedding provider-specific policy throughout the router:

  • Spectre.Provider.Call isolates main LLM, LLM-classifier, local-classifier, embedding, and semantic-cache lookup calls from the requesting process;
  • provider-specific deadlines have conservative defaults, application-level configuration, per-agent/per-call overrides, and an explicit :infinity escape hatch;
  • a timed-out call or dead caller terminates the local adapter worker, while documenting that remote cancellation still depends on the adapter client;
  • Spectre.Provider.Failure normalizes infrastructure failures without retaining prompts, input, raw output, exception messages, or stack traces;
  • deliberate adapter {:error, reason} results remain compatible and are not assigned a speculative retry policy;
  • main-model fallback remains available after timeout and receives the normalized primary failure;
  • local classifier, embedding, and semantic-cache failures remain optional evidence so routing can continue to LLM arbitration or clarification;
  • contract tests cover success, declared error, timeout, exception, exit, throw, hard crash, malformed reply, caller-death cancellation, fallback, and routing degradation.

No automatic retry, circuit breaker, token pricing, or metrics exporter was added to core. Provider-specific retry/rate-limit policy remains an adapter or optional middleware responsibility. Provider duration and normalized outcome were added to the routing receipt in the 2026-07-17 pass; minimal telemetry can later be derived from that canonical fact.

2026-07-16: Routing Evaluation Harness

This iteration made routing quality and LLM use measurable without expanding the normal turn runtime:

  • Spectre.Router.evaluate/3 now runs the real input and routing pipelines but never loads state/memory adapters or executes the selected handler;
  • evaluation forces journal delivery and online semantic learning off;
  • Spectre.Router.Receipt captures the winning outcome, strategy, sanitized provider evidence, duration, and LLM arbitration intent while excluding inputs, prompts, model outputs, matches, and handlers;
  • Spectre.Eval loads version-controlled JSONL cases and checks expected routes/outcomes together with forbidden, allowed, or required LLM use;
  • aggregate reports include pass rate, route accuracy, strategy and outcome distribution, LLM-policy violations, duration percentiles, confusion data, and per-tag results;
  • mix spectre.eval provides strict-by-default CI thresholds and optional JSON artifacts;
  • a representative fixture and end-to-end tests cover deterministic, local, LLM, privacy, state/history, no-handler, no-journal, and no-semantic-write behavior.

The initial receipt normalized evidence emitted by router traces and candidates. Provider execution resilience and direct per-provider duration/outcome facts landed in the following passes. Eligibility thresholds and winning precedence still need to become canonical receipt data instead of being inferred by later consumers.

2026-07-16: Routing Intelligence And Journal Foundation

This iteration hardened the classifier/LLM boundary and implemented the first observability slice without moving lifecycle ownership:

  • hard, runnable evidence now skips later semantic-cache, embedding, and local classifier work that cannot change the default winner;
  • the default arbitrator asks an enabled and available LLM classifier when no cheaper candidate clears its threshold, not only when candidates conflict;
  • confident local evidence still wins without an LLM call;
  • route-level via: restrictions are preserved during LLM arbitration, and an empty visible label set never reaches a model;
  • classifier prompts now receive canonical labels, structured evidence, and bounded recent chat from state, while caller overrides remain explicit;
  • LLM output must resolve to exactly one known label; unknown, explanatory, or multi-label output degrades safely instead of causing recursive arbitration;
  • Spectre.Journal.Record, Spectre.Journal.Store, Spectre.Journal.Recorder, and a bounded supervised async buffer now exist;
  • the journal/2 DSL, application default, per-turn override, stable turn and record identities, deterministic sampling, privacy defaults, and explicit failure policies are implemented;
  • completed routing contexts emit one structured :arbitration record with decision, reason code, evidence, and thresholds but no conversation content by default.

This is deliberately not the completion of Phase 5. Policy, lifecycle, execution, and persistence records still depend on the canonical transition work in Phases 1 through 4. The current recorder observes the existing Arbitration boundary only; it does not invent lifecycle transitions.

The next routing work is to make threshold eligibility and winning precedence canonical data on the arbitration receipt, then have the journal consume that receipt instead of reconstructing any explanation. The next journal work is to add redaction callbacks, retention metadata, buffer telemetry/stress coverage, and telemetry sourced from the same record.

The Main Architectural Problem

Lifecycle responsibility is currently distributed across several modules:

ConcernCurrent ownersRisk
Stage an effectRunner, StateTransition rules can diverge by handler path
Policy resolutionPolicy, StateMatching, mutation, events, and results are coupled
Effect completionActionExecutor, StateCapability I/O and transitions share one module
Determine current workState, ResultTwo views require precedence rules
Choose the host stepResult, Turn.DecisionTerminal and pending precedence is repeated
Persist progressRuntime, Session, hostApproval and execution have different owners
Agent journalruntime traces, host logsNo durable, structured explanation stream

The types themselves are useful. The problem is that too many modules are allowed to decide how those types transition.

Lifecycle Invariants

Before moving code, freeze these rules as executable contract tests:

  1. State is the only authoritative machine snapshot.
  2. At most one executable or policy-gated effect is active per conversation.
  3. At most one open policy awaitable gates that active effect.
  4. A protected effect moves through pending -> waiting_policy -> approved -> completed | failed.
  5. A rejected, expired, or exhausted policy moves the effect to cancelled.
  6. waiting_policy is never executable.
  7. Approval never calls the action module.
  8. The approved state is committed before capability execution.
  9. Every completed, failed, or cancelled transition is idempotently recorded.
  10. A terminal effect cannot return to a pending state.
  11. An invalid host resolution cannot mutate state.
  12. A policy reply bypasses normal routing while its awaitable is open.
  13. A live session never replaces committed state with an older result.
  14. Host decisions are derived from authoritative state plus the current transition, never from stale local arrays alone.
  15. Journal recording is observational by default and cannot change routing, policy, lifecycle, or execution outcomes.
  16. Every journal record carries stable correlation identifiers and a versioned schema.
  17. Record identifiers and per-turn sequence numbers make appends idempotent and timelines reconstructable.

The first deliverable should be a transition matrix containing every legal source state, command, target state, emitted event, and expected host decision.

Target Ownership

Introduce one pure lifecycle kernel. The final name can change, but Spectre.Lifecycle is used here for clarity.

ModuleTarget responsibility
Spectre.StateData structure, serialization, migration, and read-only queries
Spectre.EffectEffect value object, constructors, predicates, and outcome formatting
Spectre.AwaitableAwaitable value object, constructors, and predicates
Spectre.LifecycleThe only module allowed to apply lifecycle commands to state
Spectre.TransitionImmutable receipt returned by the lifecycle kernel
Spectre.Policy.MatcherPure text or trusted-label matching; no state mutation
Spectre.RuntimeInput, routing, transition orchestration, and persistence workflow
Spectre.ExecutionPersist-before-execute workflow and terminal-result persistence
Spectre.ActionDispatcherCalls the registered application capability; no state mutation
Spectre.ResultPublic receipt/read model built from a transition
Spectre.TurnPublic host wrapper; delegates next-step calculation to the lifecycle kernel
Spectre.SessionSerializes commands and retains only committed state
Spectre.Journal.RecordVersioned, privacy-aware explanation record
Spectre.Journal.RecorderFilters, redacts, samples, and delivers journal records
Spectre.Journal.StoreMinimal append behaviour implemented by host storage adapters

The desired pure API is approximately:

@spec apply(State.t(), command()) ::
        {:ok, Transition.t()} | {:error, transition_error()}

Lifecycle.apply(state, {:stage_effect, effect, policy})
Lifecycle.apply(state, {:resolve_policy, {:accept, label}})
Lifecycle.apply(state, {:policy_no_match, input})
Lifecycle.apply(state, {:complete_effect, effect_id, value})
Lifecycle.apply(state, {:fail_effect, effect_id, reason})
Lifecycle.apply(state, {:cancel_effect, effect_id, reason})

A transition contains all emitted data once:

%Spectre.Transition{
  previous_revision: 12,
  state: next_state,
  effects: [changed_effect],
  awaitables: [changed_awaitable],
  events: [domain_event],
  reply_text: "",
  next: {:needs, changed_effect}
}

Result and Turn should project this receipt instead of independently reconstructing lifecycle precedence. This does not require full event sourcing; commands and emitted events can remain an internal consistency mechanism.

Journal And Agent Observability

Chat history, the journal, and telemetry solve different problems:

  • chat history is conversational context that may influence a later turn;
  • the journal explains why the runtime selected and changed something;
  • telemetry aggregates counters, timings, and health without being the durable source of an individual decision.

The new feature should be named journal, not history, because history/1 already controls conversational transcript retention.

A proposed agent DSL is:

journal MyApp.SpectreJournalStore,
  events: [:routing, :arbitration, :policy, :lifecycle, :execution],
  include_input: false,
  include_reply: false,
  include_effect_payload: false,
  state: :revision,
  mode: :async,
  on_error: :warn,
  sample_rate: 1.0

Without a journal declaration, the runtime emits nothing. journal(false) can explicitly disable an inherited application default for one agent.

The store implements a deliberately small write contract:

defmodule MyApp.SpectreJournalStore do
  @behaviour Spectre.Journal.Store

  @impl true
  def append(%Spectre.Journal.Record{} = record, opts) do
    MyApp.AgentJournal.insert(record, opts)
  end
end

An optional query callback may support the built-in inspection API later, but the runtime should depend only on append/2. Applications remain free to use PostgreSQL, ClickHouse, OpenSearch, an event stream, or an in-memory store.

A versioned record should contain structured explanation data:

%Spectre.Journal.Record{
  schema_version: 1,
  id: record_id,
  agent: MyApp.SupportAgent,
  agent_version: agent_version,
  conversation_id: conversation_id,
  turn_id: turn_id,
  sequence: 3,
  trace_id: trace_id,
  state_revision: revision,
  phase: :arbitration,
  decision: %{kind: :route_selected, label: :BILLING},
  reason: %{rule: :provider_agreement, providers: [:bag, :semantic_cache]},
  evidence: [
    %{provider: :bag, label: :BILLING, score: 0.82, accepted?: true},
    %{provider: :semantic_cache, label: :BILLING, score: 0.87, accepted?: true}
  ],
  transition: nil,
  policy: nil,
  effect: nil,
  duration_native: duration,
  metadata: %{router_version: router_version},
  occurred_at: DateTime.utc_now()
}

The journal should record decisions at stable boundaries:

  1. routing evidence summary, including provider, label, score, margin, threshold, and whether the candidate was accepted;
  2. arbitration winner, tie-break or fallback reason, and alternatives rejected;
  3. policy accept, reject, no-match, attempt count, cancellation, and whether the source was user input or a trusted host;
  4. lifecycle command, previous status, next status, and emitted events;
  5. execution dispatch, idempotency identity, completion class, and duration;
  6. persistence success, warning, conflict, or stale-revision rejection.

Input text, reply text, complete state, effect arguments, and action results must be excluded by default. They can contain personal data, credentials, or business secrets. An application may opt in per field with a redaction callback and retention policy.

Delivery semantics must be explicit:

  • mode: :async, on_error: :warn is the monitoring default; recording cannot change the agent decision and a supervised buffer protects turn latency;
  • mode: :sync, on_error: :error is available when the record is a required audit artifact;
  • strict atomic audit requirements should use a host outbox in the same transaction as state persistence;
  • policy and execution records should not be sampled by default;
  • all records need schema versioning, retention controls, and deterministic correlation IDs.

This feature makes it possible to answer:

  • why an agent selected one route instead of another;
  • which classifier or cache disagreed with the final decision;
  • how often an agent falls back, clarifies, rejects, or exhausts a policy;
  • which effects fail, are retried, or are rejected before execution;
  • whether a model, classifier, threshold, or cache revision changed behaviour;
  • how one agent version compares with another in freelance.fast;
  • which real decisions should become regression fixtures.

The journal must consume canonical Arbitration and Transition data rather than adding new decision logic. That keeps observability useful without creating another owner of lifecycle semantics.

Work Plan

Phase 0: Freeze Current Behaviour

Status: complete for the Spectre repository. The external freelance.fast fixture is explicitly out of scope; its application suite remains a downstream integration test.

Goal: prevent an architectural refactor from changing externally visible semantics accidentally.

  • Keep focused tests for routing, arbitration, turns, policy rejection, attempts exhaustion, host resolution, sessions, persistence failures, and idempotency.
  • Add a table-driven lifecycle contract suite covering every invariant above.
  • Validate freelance.fast in its own consuming repository when desired.
  • Document ask/3, turn/3, resolve_policy/4, and execute/3 as the current compatibility surface.
  • Record the current serialized State version and migration fixtures.

Exit criteria:

  • every legal transition and every forbidden transition has a test;
  • downstream consumers can use only public Spectre APIs;
  • no test needs to inspect private runtime functions.

Phase 1: Extract The Pure Lifecycle Kernel

Status: complete.

Goal: centralize state transitions without changing public APIs.

  • Add Spectre.Transition.
  • Add Spectre.Lifecycle.apply/2 and next/1.
  • Move staging, approval, rejection, cancellation, completion, and failure rules out of State.
  • Keep temporary delegating functions in State to preserve compatibility.
  • Make Policy produce matcher decisions and lifecycle commands.
  • Make Runner request {:stage_effect, effect, policy} instead of mutating state directly.
  • Make Result.lifecycle/1 and Turn.Decision delegate to one canonical projection.

Exit criteria:

  • State contains no business transition branches;
  • all lifecycle mutations pass through Spectre.Lifecycle;
  • existing public return values remain compatible.

Phase 2: Separate Policy Matching From Policy Transitions

Status: complete.

Goal: make policy behavior deterministic and independently testable.

  • Extract Spectre.Policy.Matcher.
  • Give user text and trusted host labels the same validated resolution type.
  • Keep normalization before policy matching.
  • Model no-match, maximum attempts, expiration, cancellation, and rejection as explicit commands.
  • Add optional structured rejection metadata for UI and audit consumers.
  • Define whether global interrupts may run while a policy is open; keep the default closed unless explicitly configured.

Exit criteria:

  • policy matching is a pure function;
  • invalid labels and repeated resolutions cannot mutate state;
  • user and host rejection produce the same lifecycle outcome shape.

Phase 3: Unify Execution And Persistence

Status: complete. Runtime owns durable commits, Execution owns validated dispatch-to-transition orchestration, and ActionDispatcher owns the module call only.

Goal: remove ambiguity around who stores terminal execution state.

  • Split capability invocation from lifecycle mutation.
  • Rename or introduce Spectre.ActionDispatcher for the module call only.
  • Add a Spectre.Execution workflow that:
    1. validates the executable effect;
    2. confirms the approved state is committed;
    3. invokes the action with its idempotency key;
    4. applies a completion or failure command;
    5. persists the terminal state;
    6. returns a Result or Turn.
  • Add a session-aware execution API so hosts do not need a manual Spectre.reset/2 after every action.
  • Require durable idempotency at the application capability boundary.

Potential public shape:

Spectre.execute(agent, approved_result, opts)
Spectre.execute(session, approved_turn, opts)

Keep Spectre.execute(state, ctx, opts) during a deprecation window.

Exit criteria:

  • approval and terminal execution state have one documented commit workflow;
  • sessions cannot retain an approved effect after it completed;
  • stale or repeated execution returns the recorded terminal outcome.

Phase 4: Harden Sessions And Recovery

Status: complete for the implemented state-adapter and Session contracts.

Goal: make one conversation safe across crashes and concurrent hosts.

  • Add a monotonic revision separate from the serialized schema version.
  • Support optimistic compare-and-swap in state adapters.
  • Reject stale results and stale policy resolutions explicitly.
  • Restore open awaitables and pending effects without replaying side effects.
  • Define crash points before execution, during execution, and after execution.
  • Add session execution serialization and recovery tests.
  • Add telemetry for restore, conflict, retry, stale command, and idle shutdown.

Exit criteria:

  • every crash point has a documented recovery path;
  • two hosts cannot both advance the same state revision silently;
  • session and stateless adapter behavior share the same transition contract.

Phase 5: Add The Journal

Goal: make every important agent decision inspectable without storing sensitive conversation content by default.

Status: complete for schema v1. Arbitration, policy, lifecycle, execution, and persistence records, privacy defaults, redaction, retention metadata, per-phase sampling, telemetry, strict mode, and bounded asynchronous delivery are implemented.

  • Add Spectre.Journal.Record with a versioned schema.
  • Add Spectre.Journal.Recorder as the filtering and delivery boundary.
  • Add the Spectre.Journal.Store behaviour with append/2.
  • Add a journal/2 DSL macro and equivalent application configuration.
  • Emit records from canonical arbitration, lifecycle, policy, execution, and persistence boundaries.
  • Record reason codes and structured evidence, not preformatted log strings.
  • Add filtering, redaction, per-event sampling, and retention metadata.
  • Add supervised asynchronous delivery with bounded buffering, explicit overflow policy, and backpressure.
  • Define synchronous failure and transactional-outbox guidance for audit use.
  • Add telemetry derived from the same records without making telemetry the durable source.
  • Provide query examples for per-agent timelines and aggregate dashboards.

Exit criteria:

  • one turn can be reconstructed as a decision timeline using correlation IDs and sequence numbers;
  • route and policy outcomes can be explained without storing input or reply text;
  • disabling or losing the default recorder cannot change runtime behaviour;
  • strict recorder failures follow the explicitly configured policy;
  • sensitive fields are absent unless the application opts in;
  • the record schema has compatibility and migration tests.

Phase 6: Clarify Routing And Arbitration Ownership

Goal: keep probabilistic evidence outside machine-state decisions.

Status: complete. Canonical threshold and precedence explanations are carried by routing contexts and receipts; deterministic permutation tests and the generated multi-strategy matrix cover ordering and precedence.

  • Keep router plugs limited to evidence collection.
  • Keep Candidate as an immutable evidence value.
  • Make provider thresholds and agreement rules observable in arbitration traces.
  • Define deterministic tie-breaking for equal candidates.
  • Version semantic-cache entries and classifier artifacts.
  • Keep unverified online examples quarantined until explicit review.
  • Add property tests for candidate ordering and precedence.

Exit criteria:

  • a router provider cannot mutate conversation state;
  • every selected route explains its provider, score, threshold, and winning precedence;
  • the same evidence set always produces the same decision.

Phase 7: Reduce Optional Dependency Weight

Status: superseded by project decision. Vettore intentionally remains a small, required dependency of Spectre.

Goal: let applications use the runtime without paying for every classifier implementation.

  • Keep core runtime, routing contracts, policies, and OTP sessions lightweight.
  • Keep Vettore mandatory and keep external embedding engines such as ex_fastembed adapter-driven.
  • Fail with clear configuration errors when an optional adapter is unavailable.
  • Add minimal, classifier, and full-feature dependency profiles to CI.

Exit criteria:

  • regex-only applications retain the same Vettore-backed runtime contract;
  • external optional adapters have explicit compatibility versions.

Phase 8: Stabilize The Public API

Status: complete for the 0.1.0 public preview. Compatibility remains governed by Semantic Versioning's 0.x rules and the documented public/internal module boundary.

Goal: prepare a versioned release rather than exposing internal refactors.

  • Publish lifecycle invariants and adapter behaviours.
  • Add @since or changelog entries for public functions.
  • Deprecate old APIs with migration examples before removal.
  • Add upgrade fixtures for serialized state.
  • Document supported Elixir, OTP, NIF, and adapter versions.
  • Validate the package from consuming products such as freelance.fast; those product-specific fixtures remain in their own repositories.

Exit criteria:

  • one canonical host integration path is documented in Getting Started;
  • serialized state upgrades are tested through schema-v5 fixtures;
  • release notes and a changelog policy are present;

The next concrete tickets should be:

  1. Write the legal transition matrix.
  2. Add forbidden-transition tests.
  3. Introduce Spectre.Transition without changing callers.
  4. Introduce Spectre.Lifecycle.next/1 and delegate Turn.Decision to it.
  5. Move policy accept, reject, and attempt exhaustion into lifecycle commands.
  6. Move effect stage, complete, fail, and cancel into lifecycle commands.
  7. Reduce State to data, migrations, and queries.
  8. Split action invocation from completion mutation.
  9. Design and implement session-aware execution persistence.
  10. Add state revisions and stale-write protection.
  11. Freeze Spectre.Journal.Record fields, privacy defaults, and reason codes.
  12. Add the Spectre.Journal.Store behaviour and journal/2 DSL.
  13. Emit journal records from Arbitration, Lifecycle, and Execution.
  14. Add asynchronous buffering, strict mode, and outbox documentation.
  15. Run the freelance.fast compatibility fixture with journal monitoring.
  16. Only then simplify or deprecate public APIs.

This order keeps each change reviewable and preserves the current host contract while responsibility moves inward.

Package Direction

Spectre should continue to sit in a small family of focused packages:

  • spectre_kinetic owns Action Language extraction, tool registration, slot mapping, and planning.
  • spectre_lens can provide browsing and retrieval capabilities through normal action adapters.
  • spectre_mnemonic can provide recall and durable memory through the memory behaviour.
  • spectre_directive can orchestrate multi-step goals above Spectre without turning one conversation turn into a workflow engine.

Spectre itself should own one thing well: a deterministic, inspectable, and recoverable lifecycle for one agent conversation.