Spectre 0.2.6 is an OTP-native kernel for building conversational and operational agents in Elixir. It is deliberately not a package that owns every model, tool, memory store, browser, transport, and workflow. Those capabilities live in focused libraries that integrate through explicit, versioned contracts.

This document explains the concepts shared by the ecosystem, what each library is for, how the libraries compose, and why Spectre is designed as a small core with optional packages instead of a monolithic framework.

The Shape Of The System

human channels                                      other agents
      │                                                   │
      ▼                                                   ▼
 Spectre Beam                                        Spectre Pulse
      │                                                   │
      └──────────────────────┬────────────────────────────┘
                             ▼
                  ┌─────────────────────┐
                  │    Spectre core     │
                  │                     │
                  │ Flow / Turn / Run   │
                  │ Instance / State    │
                  │ Work / Vigil        │
                  │ policy / lifecycle  │
                  └─────────────────────┘
                    │       │        │
       model choice │       │        │ memory context
                    ▼       │        ▼
             Spectre Prism  │  Spectre Mnemonic
                            │
               action plan │ mission continuity
                            ▼        ▼
                    Spectre Kinetic  Spectre Directive
                            │
                            ▼
                    providers such as
                      Spectre Lens

The host application surrounds every layer with credentials, provider clients,
authorization, durable stores, supervision, and real side-effect boundaries.

The dependency direction is one way: every ecosystem package depends on Spectre, while Spectre core does not depend on any ecosystem package. Satellite packages do not need one another at runtime. The complete set is loaded together only by ecosystem conformance tests.

The Core Concepts

Spectre keeps different kinds of state and work separate. That separation is what lets optional libraries compose without competing for ownership.

ConceptMeaningOwner
Spectre.StackImmutable, validated package configuration for an Agentapplication definition
Spectre.AgentVersioned behavior: routing, Flows, policies, operations, and package bindingsapplication code
Spectre.Definition.RefContent identity of one portable canonical behavior envelopeSpectre core
Spectre.Definition.ManifestSealed effective authority and execution closure for one Definition RefSpectre core + host ceiling
Spectre.Definition.StoreImmutable publication and durable adapter boundaryhost adapter
Spectre.Definition.ResolverVerified Definition, Manifest, receipt, contract, and drift resolutionSpectre core
Spectre.Definition.CandidateImmutable bootstrap proposal for one published Definitiontrusted host + Spectre core
Spectre.Instance.ActivationGeneration-fenced current Definition snapshotSpectre Instance sequencer
Spectre.Event.EnvelopeOwnership-fenced input, continuation, operation, and global eventSpectre Instance sequencer
Spectre.Instance.LifecycleIndependent admission, authority, retention, and activation axesSpectre Instance sequencer
Spectre.Skill.StateBindingGeneration- and branch-fenced private Skill stateSpectre Instance sequencer
Spectre.Foundation.ConformanceExecutable durable-format and Definition compatibility gateSpectre core
Spectre.Stack.ConformanceWhole-ecosystem package compatibility and collision gateSpectre core + package tests
Spectre.Projection.AuditExact deterministic view of a canonical DefinitionSpectre core
Spectre.SubjectCanonical application identity, independent of a channel identityhost application
Spectre.InstanceLocal canonical owner for one AgentRef + SubjectSpectre core
Spectre.TurnPublic projection of the next observable conversational boundarySpectre core
Spectre.RunRecoverable continuation for one conversational unitSpectre core
Spectre.ActionClosed proposal to call a declared capabilityplanner or deterministic Flow
Spectre.EffectStaged side effect with policy and lifecycle stateSpectre core
Spectre.InvocationRevision-fenced request to cross an external boundarySpectre core
Spectre.WorkDurable, bounded, multi-step operational procedureSpectre core
Spectre.VigilDurable recurring observation triggered by timers or eventsSpectre core
Journal/checkpointObservable decisions and portable canonical recovery dataSpectre core plus host adapters

There are two execution planes:

  • The conversational plane uses Flow, Turn, and Run. It handles input, routing, replies, policy boundaries, and staged Effects.
  • The operational plane uses Work and Vigil. It handles long procedures, retries, progress, pause/update/resume, timers, events, and restart recovery.

A Work is not a renamed Run, and a Directive mission is not a second Instance. Each abstraction has one job and one owner.

Which Library To Use

PackageUse it when you needIt deliberately does not own
spectreAgent definitions, routing, policy, state, Runs, Instances, Work, Vigil, execution lifecycle, checkpointsprovider SDKs, credentials, business authorization, application side effects
spectre_prismConstraint-aware model/profile selection by purpose, modality, privacy, cost, latency, and capability, with optional bundled provider adapterscredentials, live provider sessions, provider scheduling, Run or Instance lifecycle
spectre_kineticTool retrieval and validated Action planning from Action Languageauthorization or execution of the selected Action
spectre_mnemonicActive and durable memory, recall, search, consolidation, provenance, and subject-scoped contextcanonical Agent state or the application database
spectre_directiveA resumable mission and evolving-plan loop with correlated questions, invocations, and outcomesgeneral memory, tool discovery, core Work scheduling, or Instance ownership
spectre_lensBrowser perception and browser Actions through backend-neutral protocolsbrowser authorization, core Effect lifecycle, or canonical process persistence
spectre_beamHuman/external channel normalization and delivery, including Telegram and WhatsApp adaptersidentity inference, application consent, policy, or automatic Effect execution
spectre_pulseTransport-independent Agent-to-Agent envelopes, discovery, routing, and delivery receiptsshared workflow state, global presence truth, or agent coordination policy

The short distinctions are useful:

  • Prism chooses which cognitive capability may answer.
  • Kinetic chooses which declared action may be proposed.
  • Lens supplies browser capabilities that a Flow or Kinetic may select.
  • Mnemonic supplies recalled context, not canonical state.
  • Directive advances one mission and its living plan.
  • Work advances one durable operational controller.
  • Beam crosses human or provider channel boundaries.
  • Pulse crosses Agent-to-Agent protocol boundaries.

The Names

Ecosystem packages carry evocative names rather than descriptive ones, on purpose: a descriptive name such as "model-selection-middleware" goes stale the moment a package grows, while a metaphor stays stable. Learn them once — each name encodes the package's one job:

  • Prism splits light into its components. Spectre Prism splits "call the model" into declared inference profiles and selects the one whose constraints — privacy, modality, cost, latency, depth — all hold.
  • Kinetic is motion held ready. Spectre Kinetic turns declared application functions into a registry of candidate Actions and plans which one to propose. The energy becomes movement only when the core approves execution.
  • Mnemonic is a memory aid, not the memory itself. Spectre Mnemonic recalls context for a turn; it never becomes canonical state.
  • Directive is a mission order. Spectre Directive advances one mission and its living plan across many inputs.
  • Lens is how the agent sees. Spectre Lens provides browser perception and browser actions, with everything it sees marked untrusted until explicitly converted.
  • Beam carries signal between the agent and humans — and yes, it also runs on the BEAM. Spectre Beam normalizes provider channels such as Telegram and WhatsApp into Spectre input and delivery.
  • Pulse is a signal between peers. Spectre Pulse moves versioned envelopes between agents over any transport.

The core vocabulary follows the same rule: a Vigil keeps watch (a durable observation loop between triggers); Work is exactly what it says (a bounded, terminating operational procedure); a Subject is who the work is about; an Instance is the process that owns both for that Subject.

Core 0.3.0 and satellite compatibility

The stable Spectre core is distributed through Hex:

{:spectre, "~> 0.3.0"}

The satellite releases listed below belong to the historical 0.2.x release train. They remain owned and versioned by their repositories and must not be assumed compatible with core 0.3.0 until their manifests and adapter suites say so. This separation is intentional: installing the new core never silently upgrades another spectre_* package.

PackageReleaseSpectre requirement
spectre0.3.0
spectre_beam0.2.0~> 0.2.0
spectre_directive0.2.0~> 0.2.0
spectre_kinetic0.2.0~> 0.2.0
spectre_lens0.2.0~> 0.2.0
spectre_mnemonic0.2.0~> 0.2.0
spectre_prism0.2.0~> 0.2.0
spectre_pulse0.2.0~> 0.2.0

The core upgrade does not discard the recoverable historical contracts. Permanent State and Run fixtures continue to prove that existing persisted values remain readable where the public core contract promises it. Each satellite owns the equivalent proof for its durable and wire formats.

Applications that intentionally remain on the complete historical 0.2.x stack must pin that stack consistently:

defp deps do
  [
    {:spectre, github: "elchemista/spectre", tag: "0.2.6"},
    {:spectre_prism, github: "elchemista/spectre_prism", tag: "0.2.0"},
    {:spectre_kinetic, github: "elchemista/spectre_kinetic", tag: "0.2.0"},
    {:spectre_mnemonic, github: "elchemista/spectre_mnemonic", tag: "0.2.0"},
    {:spectre_directive, github: "elchemista/spectre_directive", tag: "0.2.0"},
    {:spectre_lens, github: "elchemista/spectre_lens", tag: "0.2.0"},
    {:spectre_beam, github: "elchemista/spectre_beam", tag: "0.2.0"},
    {:spectre_pulse, github: "elchemista/spectre_pulse", tag: "0.2.0"}
  ]
end

An application rarely needs all seven satellite packages. Adding one library does not silently enable the others.

Compose Packages With A Stack

Spectre.Stack is the composition boundary. Each install block is parsed by that package, so package-specific words such as model, isolate_by, backend, and channel do not become global framework macros.

defmodule MyApp.AI do
  use Spectre.Stack, id: :my_app

  install Spectre.Prism, max_attempts: 2 do
    provider(:openrouter, MyApp.OpenRouter)
    model(:fast, id: "small-model")
    model(:deep, id: "reasoning-model")
    purpose(:route_classification, prefer: :fast)
    default(:deep)
  end

  install Spectre.Kinetic,
    mode: :closed_moves,
    actions: MyApp.Actions,
    modes: [create_report: :write]

  install Spectre.Mnemonic, namespace: :my_app do
    isolate_by([:agent, :subject, :conversation])
  end

  install(Spectre.Directive, turn_handler: true)

  install Spectre.Lens, planner_exposure: [:look, :discover] do
    backend(SpectreLens.Browsers.Lightpanda,
      instances: 2,
      protocol: SpectreLens.Protocol.Lightpanda
    )
  end

  install Spectre.Beam do
    channel(:telegram,
      type: :telegram,
      adapter: Spectre.Beam.Adapters.ExGram,
      before_decode: [MyApp.VerifyTelegramSignature]
    )
  end

  install Spectre.Pulse do
    transport(:local, Spectre.Pulse.Transports.Local)
    directory(MyApp.AgentDirectory)
  end
end

Bind the Stack once:

defmodule MyApp.Agent do
  use Spectre.Agent, stack: MyApp.AI

  router(via: [:regex, :classifier])

  flow :analysis, prism: [minimum: :deep] do
    on :EXPLAIN, regex: ~r/^explain\b/i do
      reason(:explain)
    end
  end

  flow :browser do
    on :INSPECT, regex: ~r/^inspect\b/i do
      action({:lens, :look},
        args: %{
          url: "https://example.com",
          opts: [include: [:markdown, :links]]
        },
        mode: :read
      )
    end
  end

  flow :notify_human do
    on :NOTIFY_HUMAN, regex: ~r/^notify human$/i do
      beam(:owner, via: :telegram, text: "The report is ready")
    end
  end

  flow :notify_agent do
    on :NOTIFY_AGENT, regex: ~r/^notify agent$/i do
      pulse(:researcher,
        act: :inform,
        type: "report.ready",
        data: %{report_id: "report-42"}
      )
    end
  end
end

Selecting a Stack activates package adapters, but activation is not authorization. It does not make every action planner-visible, approve a protected Effect, authenticate a channel identity, or start every runtime resource.

Run The Boundaries Explicitly

Live processes and secrets enter at runtime, not in the compiled Stack:

{:ok, stack_runtime} =
  Spectre.Stack.start_link(MyApp.AI,
    packages: [lens: [binary: "/opt/lightpanda"]]
  )

{:ok, instance} =
  Spectre.instance(MyApp.SpectreSupervisor, MyApp.Agent, account_id)

{:ok, turn} =
  Spectre.turn(instance, "inspect the release page",
    stack_runtime: stack_runtime
  )

case turn.decision do
  {:awaiting, awaitable, result} ->
    MyApp.UI.present_approval(awaitable, result)

  {:needs, _effect, result} ->
    Spectre.execute(instance, result, stack_runtime: stack_runtime)

  {:reply, result} ->
    MyApp.Delivery.reply(result.reply_text)

  {:completed, outcome, _result} ->
    MyApp.Operations.record(outcome)

  {:no_response, _result} ->
    :ok
end

The host decides whether and where to execute. Spectre owns the deterministic lifecycle around that decision; the package owns only its capability boundary.

Spectre Core

Use the core alone when routing, state, policy, actions, or operational loops are sufficient.

defmodule MyApp.SupportAgent do
  use Spectre.Agent

  actions(MyApp.SupportActions)

  policy :confirm_delete do
    request(:confirm_delete)
    accept(:confirmed, regex: ~r/^yes$/i)
    reject(:cancelled, regex: ~r/^no$/i)
  end

  protect(:delete_account, with: :confirm_delete)

  flow :support do
    on :HELP, regex: ~r/^help$/i do
      reply("How can I help?")
    end

    on :DELETE_ACCOUNT, regex: ~r/^delete my account$/i do
      action(:delete_account)
    end
  end
end

Use reason for inference that must not plan actions, act when action planning is intentional, and an explicit action when the Flow already knows the capability. Use Work for a bounded procedure that must survive beyond a Turn:

{:ok, work_ref, _view} =
  Spectre.start_work(instance, MyApp.ExportReport, %{report_id: "report-42"})

{:ok, view} = Spectre.loop(instance, work_ref)
{:ok, paused} = Spectre.pause_loop(instance, work_ref)
{:ok, resumed} = Spectre.resume_loop(instance, work_ref)

Spectre Prism: Select The Cognitive Capability

Prism turns a set of model declarations into inference profiles and selects a profile only when every requested constraint is satisfied.

install Spectre.Prism, max_attempts: 2 do
  provider(:openrouter, MyApp.OpenRouter)
  model(:fast, id: "small-model")
  model(:deep, id: "reasoning-model")
  purpose(:route_classification, prefer: :fast)
  default(:deep)
  selector(Spectre.Prism.Selector.Adaptive)
end

Use Prism when several inference capabilities differ in privacy, modality, context window, latency, cost, or depth. Prism ships optional adapters for OpenAI, OpenRouter, Ollama, and Gemini behind an injectable HTTP transport; credentials are resolved only at runtime, and live provider sessions remain caller-owned and outside the compiled Stack. Prism selects; Spectre performs the inference step and owns the Run boundary.

Spectre Kinetic: Plan Closed Actions

Kinetic extracts a registry of declared application functions, retrieves a small candidate set, maps typed slots, and returns a provider-neutral Action.

defmodule MyApp.Actions do
  use SpectreKinetic

  @al ~s(CREATE REPORT WITH: ACCOUNT="acme")
  @doc "Creates a report for an account."
  @spec create_report(String.t()) :: {:ok, map()} | {:error, term()}
  def create_report(account), do: MyApp.Reports.create(account)
end

defmodule MyApp.ReportingAgent do
  use Spectre.Agent
  use Spectre.Kinetic, actions: MyApp.Actions, modes: [create_report: :write]

  flow :reports do
    on :CREATE_REPORT, regex: ~r/^create report/i do
      act(:create_report)
    end
  end
end

Kinetic does not call create_report/1 while planning. Spectre stages the Action as an Effect, applies policy, persists lifecycle state, and crosses the provider boundary only after explicit execution.

Spectre Mnemonic: Recall Context Without Replacing State

Mnemonic provides hot memory, durable stores, hybrid search, graph associations, consolidation, freshness, contradictions, and provenance.

install Spectre.Mnemonic, namespace: :support do
  isolate_by([:agent, :subject, :conversation])
end

With the Stack bound, Spectre's normal memory hooks recall context before a Run advances and remember a compact projection after commit. Standalone APIs remain useful too:

{:ok, _packet} =
  SpectreMnemonic.remember("Acme prefers weekly PDF reports",
    scope: {:account, "acme"},
    persist?: true
  )

{:ok, recalled} =
  SpectreMnemonic.recall("report preferences", scope: {:account, "acme"})

Memory can be stale, ranked, compacted, or forgotten. Canonical Agent state is revisioned and authoritative. Keeping those concepts separate prevents a retrieval result from silently becoming system truth.

Spectre Directive: Advance A Mission And Living Plan

Directive is useful when a mission can ask for information, request an invocation, revise a guided plan, and continue across several inputs.

alias SpectreDirective.Request

{:ok, directive} =
  Spectre.Directive.new(
    mission: "Research Acme",
    success: "Return a sourced summary",
    mode: :autonomous
  )

{:request, %Request{kind: :reason} = request, directive} =
  Spectre.Directive.next(directive)

{:request, next_request, directive} =
  Spectre.Directive.respond(
    directive,
    request.id,
    {:propose_plan, [%{id: "research", title: "Research Acme"}]}
  )

The pure API performs no model call or side effect. The host resolves each correlated request. Directive also offers an optional supervised runtime and an opt-in Spectre turn handler.

Choose Directive for a mission whose plan is part of the domain. Choose core Work for a bounded operational controller with attempts, budgets, retries, pause/update/resume, progress, and canonical recovery. A Directive may start or supervise application work through explicit boundaries, but it does not replace Work or the Instance.

Spectre Lens: Perceive And Act In A Browser

Lens separates browser lifecycle from agent-readable page semantics. It can start Lightpanda, connect to a remote CDP endpoint, produce untrusted views, discover goal-relevant regions, perform browser actions, and export artifacts.

{:ok, lens} = SpectreLens.open(instances: 2)
{:ok, tab} = SpectreLens.new_tab(lens, url: "https://example.com")

{:ok, view} =
  SpectreLens.look(tab,
    include: [:markdown, :semantic_tree, :interactive, :forms, :links]
  )

{:ok, safe_context} = SpectreLens.agent_context(view)
:ok = SpectreLens.act(tab, {:click, ref: "button[type=submit]"})
:ok = SpectreLens.close(lens)

Top-level projections are marked trust: :untrusted; convert them with agent_context/2 before inserting browser content into a model prompt. In a Stack, browser PIDs remain inside an explicitly started runtime and only portable TabRef values may cross a Spectre checkpoint.

Spectre Beam: Connect Human And Provider Channels

Beam normalizes provider events into Spectre input and delivers replies through the same endpoint. It preserves conversation affinity and exposes plug stages for authentication, enrichment, filtering, redaction, and telemetry.

install Spectre.Beam do
  channel(:telegram,
    type: :telegram,
    adapter: Spectre.Beam.Adapters.ExGram,
    before_decode: [MyApp.VerifyTelegramSignature],
    inbound_pipeline: [MyApp.EnrichSender]
  )
end

{:ok, exchange} =
  Spectre.Beam.handle(MyApp.Agent, :telegram, raw_update,
    adapter_opts: [client: telegram_client]
  )

For subject-scoped continuity, the host authenticates the provider principal, binds its Spectre.ExternalIdentity to a canonical Subject, then calls handle_instance/5. Beam never guesses identity from a name, phone number, conversation id, message, or model result.

Reactive delivery and proactive delivery are different boundaries. A proactive beam(...) handler stages an Effect; it does not send automatically.

Spectre Pulse: Communicate Between Agents

Pulse gives agents a versioned envelope whose meaning is independent of local mailboxes, PubSub, WebSocket, REST, distributed Erlang, or a custom transport.

defmodule MyApp.Researcher do
  use Spectre.Agent
  use Spectre.Pulse

  pulsing do
    identity("spectre://acme/researcher")
    advertise(capabilities: [:research])
  end

  flow :remote_requests do
    on :RESEARCH, pulse: "research.perform" do
      run(:research)
    end
  end

  def research(input, _context), do: "accepted: #{input.text}"
end

A sender names a logical contact and stages the protocol Effect:

pulse(:researcher,
  act: :request,
  type: "research.perform",
  data: %{topic: "nautical market"},
  expect: "research.completed"
)

Pulse owns envelope validation, address resolution, route selection, and a technical receipt. It does not promise exactly-once delivery, share Agent state, or decide whether the recipient should accept the request.

Common Compositions

Application needSuggested packages
Deterministic conversational servicecore only
Provider-aware assistant with long-term contextcore + Prism + Mnemonic
Safe tool-using agentcore + Kinetic; add Prism for model selection
Browser research agentcore + Lens + Kinetic + Prism; add Mnemonic for recall
Resumable guided missioncore + Directive; add Mnemonic for recalled context
Telegram or WhatsApp assistantcore + Beam; add Prism/Mnemonic as needed
Cooperative agents across transportscore + Pulse
Long-running monitored automationcore Work/Vigil plus only the capability packages its operations need

Start with the core and one concrete need. Add a package when its boundary is clear; do not install the entire ecosystem as a default preset.

Why Spectre Was Built This Way

Agent systems combine several hard problems: conversation, durable operations, inference, memory, planning, tools, browsers, human channels, agent protocols, identity, and authorization. Putting all of them in one framework object makes the first demo short, but it also tends to make ownership and failure behavior implicit.

Spectre chooses a different tradeoff for five reasons.

One canonical owner

The Instance owns canonical state and lifecycle. A memory package cannot quietly become state, a transport cannot become a scheduler, and a planner cannot become an executor. Crash recovery and concurrency are tractable because there is one place where a transition becomes authoritative.

Capabilities are explicit and replaceable

A Stack says exactly which packages exist. Logical refs and package-local DSLs avoid a global registry where unrelated libraries can overwrite one another. Applications can replace Prism, Kinetic, Lens, memory, or transport adapters without rewriting the core Agent model.

Proposal is separate from authority

Models and planners may propose an Action. Policy and deterministic lifecycle code decide whether it becomes executable. The host still controls credentials and the final side-effect boundary. This avoids the common shortcut where generating a tool call also grants permission to run it.

Portable data is separate from live infrastructure

Definitions, Runs, and checkpoints contain portable values. PIDs, sockets, browser sessions, provider clients, and secrets stay in supervised runtime resources supplied by the host. A restart therefore restores intent and lifecycle without pretending that a dead connection was durable.

Applications pay only for what they use

The core does not force browser, model-provider, vector-search, transport, or channel SDKs into every deployment. Each library can evolve and test its own public surface while the core remains focused on the contracts that require a single authority.

This is not separation for its own sake. The Stack still gives applications one place to compose capabilities, and Agent extensions remove repetitive glue. The difference is that composition stays visible, versioned, and reversible.

The compass: OTP and the actor model

Spectre's design questions are answered by one compass: what is the OTP shape of this problem? The mapping is already direct, and future concepts will keep converging on it rather than drifting toward framework-specific abstractions:

Spectre conceptActor-model ancestor
Instancea process that owns its state and serializes writes through its mailbox
Runnera supervised one-shot task: one attempt, isolated crash, no shared state
Vigila timer- and event-driven loop that holds no live process while waiting
Effect / Invocationan explicit message across a boundary, never a hidden call
Checkpointwhat a restart restores: intent and lifecycle, not dead connections
Pulse envelopelocation-transparent message passing without shared state

A new capability enters the ecosystem the same way every time: find the single owner, make every boundary a message, make recovery a restart. If a proposed feature has no clean actor-model shape — shared mutable state, ambient authority, a hidden synchronous call — that is evidence against the feature, not against the model.

Rules For Ecosystem Packages

An ecosystem package should preserve these boundaries:

  1. Depend on Spectre; never make Spectre core depend on the package.
  2. Publish a Spectre.Stack.Installable manifest with a precise Spectre requirement and portable metadata.
  3. Keep package DSL names inside the package's install block.
  4. Put runtime clients, processes, credentials, and secrets behind explicit host-owned runtime options or supervised resources.
  5. Stage Effects and Actions; do not bypass core policy or lifecycle.
  6. Re-resolve live adapters after restore instead of serializing their handles.
  7. Fail closed on unknown identities, capabilities, schemas, or ambiguous side-effect outcomes.
  8. Maintain a normative public API manifest and permanent compatibility fixtures for persisted or wire formats.

These rules are what make a collection of libraries an ecosystem rather than a bundle of unrelated integrations.

Validate The Sibling Repositories

Each package accepts SPECTRE_PATH for local compatibility testing. Pulse also accepts paths for all satellite packages and runs the complete seven-package Stack conformance test:

SPECTRE_PATH=../spectre \
SPECTRE_BEAM_PATH=../spectre_beam \
SPECTRE_DIRECTIVE_PATH=../spectre_directive \
SPECTRE_KINETIC_PATH=../spectre_kinetic \
SPECTRE_LENS_PATH=../spectre_lens \
SPECTRE_MNEMONIC_PATH=../spectre_mnemonic \
SPECTRE_PRISM_PATH=../spectre_prism \
mix test

Run that command from spectre_pulse. Run SPECTRE_PATH=../spectre mix test from every other sibling repository. The path dependency must report version 0.2.0; a stale ~> 0.1.x requirement is an incompatibility, even if the source code would otherwise compile.

Further Reading