Skip to content

HTTP API: reasoning

Provider-independent reasoning (ADR 0098): which optional advisors can be asked and what they offer, what a material uncertainty calls for, the session's typed reasoning records — assessments, plans, consultations, disagreements, resolutions, conclusions, validations, attempts — and the state, timeline and provenance they derive. Nothing here talks to an advisor, and nothing here needs one: with none available, the plan is a structured local review and the session concludes on its own evidence.

Rendered from docs/generated/openapi.json, which the executable generates from its capability registry and CI refuses stale. The raw document is at /openapi.json; a running server serves the same document at its own /openapi.json and Swagger UI at /swagger.

reasoning

Provider-independent reasoning (ADR 0098): which optional advisors can be asked and what they offer, what a material uncertainty calls for, the session's typed reasoning records — assessments, plans, consultations, disagreements, resolutions, conclusions, validations, attempts — and the state, timeline and provenance they derive. Nothing here talks to an advisor, and nothing here needs one: with none available, the plan is a structured local review and the session concludes on its own evidence.

GET /api/v1/reasoning reasoning.status

The session's reasoning state

The reasoning state of the open task (or of a named one, or of all): assessments and where each stands, consultations and how each ended, disagreements and what settled them, conclusions with their computed review and validation, the timeline, token totals the adapters reported, and the Markdown report a handover carries. Derived from the records alone, deterministically.

  • query
  • experimental
  • benchmark required
  • cache disabled
  • MCP tool majordomus_reasoning
  • MCP resource {"name": "reasoning", "uri": "majordomus://reasoning"}
  • $ reasoning status

Query parameters

nametyperequireddescriptionexamples
task string no A task id, or `all`; the open task when absent. all: "all"

Try it

The port is whatever the server logged; 8741 is the one it asks for first. The values are the operation's first example.

curl -s "http://127.0.0.1:8741/api/v1/reasoning?task=all"

Tested by apps/majordomus-cli/tests/http_serve.rs, which replays the route with these values against a server on a real socket.

Responses

statusbodymeaning
200 ReasoningStatus The session's reasoning state: the result.
400 ErrorBody invalid_input: The input does not fit the schema: a parameter of the wrong type, an unknown one, or a value the capability rejects.
404 ErrorBody not_found: The input names something the repository does not hold.
500 ErrorBody internal: The capability failed for a reason of its own; the message names it.
default ErrorBody Any other failure the transport reports: 405 method_not_allowed for another method on this path, 400 invalid_input for a body that is not a JSON object.
GET /api/v1/reasoning/advisors reasoning.advisors

The advisors and what they can do now

Every declared advisor — then every linked mesh peer carrying the review feature — with its status (available, unavailable, not_configured, disabled, temporarily_failed, rate_limited) and why, from presence alone (an executable on PATH, a credential variable set; never a value), the reasoning mode and the recorded outcomes of earlier consultations. Plus the capacity per advisory capability. No advisor is an ordinary answer: reasoning is operational either way.

  • query
  • experimental
  • benchmark required
  • cache disabled
  • MCP tool majordomus_reasoning_advisors
  • MCP resource {"name": "reasoning-advisors", "uri": "majordomus://reasoning/advisors"}
  • $ reasoning advisors

No parameters.

Try it

The port is whatever the server logged; 8741 is the one it asks for first. The values are the operation's first example.

curl -s "http://127.0.0.1:8741/api/v1/reasoning/advisors"

Tested by apps/majordomus-cli/tests/http_serve.rs, which replays the route with these values against a server on a real socket.

Responses

statusbodymeaning
200 AdvisorsReport The advisors and what they can do now: the result.
400 ErrorBody invalid_input: The input does not fit the schema: a parameter of the wrong type, an unknown one, or a value the capability rejects.
404 ErrorBody not_found: The input names something the repository does not hold.
500 ErrorBody internal: The capability failed for a reason of its own; the message names it.
default ErrorBody Any other failure the transport reports: 405 method_not_allowed for another method on this path, 400 invalid_input for a body that is not a JSON object.
GET /api/v1/reasoning/check reasoning.check

Check that reasoning stays provider-independent

The executable half of the rules advisors-are-optional and review-is-recorded-not-claimed: the advisor catalogue resolves against the provider table and the model catalogue; every adapter it names has its transport module; no provider-independent source names an advisor; no CI workflow or gate names a model credential; no document asserts a refused claim; every stored consultation names an advisor its plan selected, and every conclusion's review matches what it cites. The CI and claims checks judge what this repository's own workflows and documents say, so they apply only where the rule advisors-are-optional is declared; an adopter is held to what it adopted.

  • query
  • experimental
  • benchmark required
  • cache disabled
  • MCP tool majordomus_reasoning_check
  • $ reasoning check

No parameters.

Try it

The port is whatever the server logged; 8741 is the one it asks for first. The values are the operation's first example.

curl -s "http://127.0.0.1:8741/api/v1/reasoning/check"

Tested by apps/majordomus-cli/tests/http_serve.rs, which replays the route with these values against a server on a real socket.

Responses

statusbodymeaning
200 ReasoningCheck Check that reasoning stays provider-independent: the result.
400 ErrorBody invalid_input: The input does not fit the schema: a parameter of the wrong type, an unknown one, or a value the capability rejects.
404 ErrorBody not_found: The input names something the repository does not hold.
500 ErrorBody internal: The capability failed for a reason of its own; the message names it.
default ErrorBody Any other failure the transport reports: 405 method_not_allowed for another method on this path, 400 invalid_input for a body that is not a JSON object.
GET /api/v1/reasoning/explain reasoning.explain

Why a decision was made

One reasoning record with the whole chain of its assessment: the uncertainty and its evidence, the plan and who it selected or why nobody, each consultation, each disagreement and the experiment that settled it, the conclusion and its validation — the provenance of a decision without the conversation that produced it.

  • query
  • experimental
  • benchmark waived: transient_state
  • cache disabled
  • MCP tool majordomus_reasoning_explain
  • $ reasoning explain

Query parameters

nametyperequireddescriptionexamples
id string yes A record id: a conclusion, usually. absent: "conclusion-benchmark-absent"

Try it

The port is whatever the server logged; 8741 is the one it asks for first. The values are the operation's first example.

curl -s "http://127.0.0.1:8741/api/v1/reasoning/explain?id=conclusion-benchmark-absent"

Tested by apps/majordomus-cli/tests/http_serve.rs, which replays the route with these values against a server on a real socket.

Responses

statusbodymeaning
200 ReasoningExplanation Why a decision was made: the result.
400 ErrorBody invalid_input: The input does not fit the schema: a parameter of the wrong type, an unknown one, or a value the capability rejects.
404 ErrorBody not_found: The input names something the repository does not hold.
500 ErrorBody internal: The capability failed for a reason of its own; the message names it.
default ErrorBody Any other failure the transport reports: 405 method_not_allowed for another method on this path, 400 invalid_input for a body that is not a JSON object.
GET /api/v1/reasoning/plan reasoning.plan

What an uncertainty calls for

Decide, without recording anything, whether a stated uncertainty warrants independent review, how much the mode allows, and which available advisors a capability-driven selection would ask — with every advisor left out and why. Trivial and low uncertainty is decided locally; material uncertainty with no suitable advisor gets the structured local review instead. Pure over the catalogue, the availability and the input.

  • query
  • experimental
  • benchmark required
  • cache disabled
  • MCP tool majordomus_reasoning_plan
  • $ reasoning plan

Query parameters

nametyperequireddescriptionexamples
materiality Materiality | null no `trivial`, `low`, `material`, `high` or `critical`. material: "material"
confidence ReasoningConfidence | null no `low`, `medium` or `high`. material: "medium"
capabilities string no Advisory capabilities review needs, comma-separated. material: "independent_reasoning,code_review"

Try it

The port is whatever the server logged; 8741 is the one it asks for first. The values are the operation's first example.

curl -s "http://127.0.0.1:8741/api/v1/reasoning/plan?materiality=material&confidence=medium&capabilities=independent_reasoning%2Ccode_review"

Tested by apps/majordomus-cli/tests/http_serve.rs, which replays the route with these values against a server on a real socket.

Responses

statusbodymeaning
200 ReviewPlan What an uncertainty calls for: the result.
400 ErrorBody invalid_input: The input does not fit the schema: a parameter of the wrong type, an unknown one, or a value the capability rejects.
404 ErrorBody not_found: The input names something the repository does not hold.
500 ErrorBody internal: The capability failed for a reason of its own; the message names it.
default ErrorBody Any other failure the transport reports: 405 method_not_allowed for another method on this path, 400 invalid_input for a body that is not a JSON object.
POST /api/v1/reasoning/records reasoning.record

Record a reasoning step

Write one typed reasoning record of the open task under .ai/local/state/reasoning/: an assessment (with its evidence), a plan (computed here from the availability of this moment, never supplied), a consultation (only of an advisor its plan selected), a disagreement, a resolution (evidence, never a count), a conclusion (its review count computed from the consultations it cites; refused while a disagreement on its assessment is unsettled), a validation or a failed attempt. Secret shapes are redacted before anything is written.

  • command
  • experimental
  • benchmark required
  • cache disabled
  • MCP tool majordomus_reasoning_record
  • $ reasoning record

Request body — ReasoningRecordInput, JSON

example refused

{"record":{"kind":"plan","assessment":"assessment-benchmark-absent"}}

Try it

The port is whatever the server logged; 8741 is the one it asks for first. The values are the operation's first example.

curl -s -X POST "http://127.0.0.1:8741/api/v1/reasoning/records" -H "content-type: application/json" -d '{"record":{"kind":"plan","assessment":"assessment-benchmark-absent"}}'

Tested by apps/majordomus-cli/tests/http_serve.rs, which replays the route with these values against a server on a real socket.

Responses

statusbodymeaning
200 ReasoningRecorded Record a reasoning step: the result.
400 ErrorBody invalid_input: The input does not fit the schema: a parameter of the wrong type, an unknown one, or a value the capability rejects.
404 ErrorBody not_found: The input names something the repository does not hold.
422 ErrorBody refused: The command was understood and turned down for the reason the message gives.
500 ErrorBody internal: The capability failed for a reason of its own; the message names it.
default ErrorBody Any other failure the transport reports: 405 method_not_allowed for another method on this path, 400 invalid_input for a body that is not a JSON object.

Schemas

The components only this tag's operations use. Each row is the type's own field, its doc comment the description; a type another page owns links there.

AdvisorCapacity

object
One advisory capability and who can provide it now: a row of the capacity table reasoning.advisors reports. An empty available is answered by the local review.

use majordomus_cli::capability::builtin::reasoning::AdvisorCapacity;

let row: AdvisorCapacity = serde_json::from_value(serde_json::json!({
    "capability": "code_review", "description": "Critique of a change.", "available": [],
})).unwrap();
assert_eq!(row.capability, "code_review");
assert!(row.available.is_empty());
propertytyperequireddescription
capability string yes The capability word.
description string yes What it means.
available array of string yes Available advisors offering it, in preference order.

Used by AdvisorsReport

AdvisorCircuit

object
An open circuit: the consultation that opened it, the failures behind it, and when it closes. Derived from the records and the clock, never stored.

use majordomus_cli::reasoning::availability::*;
use majordomus_cli::reasoning::catalogue::{AdvisorCatalogue, References};
use majordomus_cli::reasoning::record::ConsultationStatus;

struct All;
impl Presence for All {
    fn executable(&self, _: &str) -> bool { true }
    fn variable(&self, _: &str) -> bool { true }
}
let c: AdvisorCatalogue = majordomus_cli::metadata::yaml::parse_into(
    "version: 1\ncapabilities:\n  - id: code_review\n    description: x\nadvisors:\n  - id: a\n    title: A\n    transport: cli\n    adapter: a-cli\n    executable: a\n    capabilities: [code_review]\n",
).unwrap();
let limited = [Outcome {
    id: "consultation-1".into(), advisor: "a".into(),
    status: ConsultationStatus::RateLimited, at: 0, retry_after: Some(60),
}];
let refs = References::default();
let states = derive_availability(&c, &Inputs {
    refs: &refs, presence: &All, mode: ReasoningMode::Standard, disabled: &[],
    history: &limited, peers: &[], now: 10, cooldown: None,
});
let circuit: &AdvisorCircuit = states[0].circuit.as_ref().unwrap();
assert_eq!(circuit.opened_by, "consultation-1");
assert_eq!(circuit.until, "1970-01-01T00:01:00Z");
propertytyperequireddescription
opened_by string yes The consultation outcome that opened it.
failures integer (uint32) yes The consecutive failures behind it.
until string yes When it closes again, RFC 3339.

Used by AdvisorState

AdvisorState

object
One advisor as it stands now: what the catalogue declares of it, its status, the one machine word saying why, and what was probed. The row reasoning.advisors reports and the policy plans over.

use majordomus_cli::reasoning::availability::*;
use majordomus_cli::reasoning::catalogue::{AdvisorCatalogue, References};

struct Nothing;
impl Presence for Nothing {
    fn executable(&self, _: &str) -> bool { false }
    fn variable(&self, _: &str) -> bool { false }
}
let c: AdvisorCatalogue = majordomus_cli::metadata::yaml::parse_into(
    "version: 1\ncapabilities:\n  - id: code_review\n    description: x\nadvisors:\n  - id: a\n    title: A\n    transport: local_runtime\n    adapter: a-rt\n    executable: rt\n    capabilities: [code_review]\n",
).unwrap();
let refs = References::default();
let state: AdvisorState = derive_availability(&c, &Inputs {
    refs: &refs, presence: &Nothing, mode: ReasoningMode::Standard, disabled: &[],
    history: &[], peers: &[], now: 0, cooldown: None,
})
.remove(0);
assert_eq!((state.id.as_str(), state.status), ("a", AdvisorStatus::Unavailable));
assert_eq!(state.probe, "executable rt on PATH");
assert!(state.circuit.is_none() && !state.recovering);
propertytyperequireddescription
id string yes The advisor id (a peer is peer:<runtime>).
title string yes The human name.
transport AdvisorTransport → AdvisorTransport yes How it is reached.
adapter string yes The adapter that speaks to it.
capabilities array of string yes The advisory capabilities it offers.
model string | null no The model it asks, when the catalogue names one: what its adapter sends.
executable string | null no The executable its adapter runs, when it runs one.
credential string | null no The name of the credential variable its adapter reads — a name, never a value.
status AdvisorStatus → AdvisorStatus yes Its standing.
reason string yes Why, as one machine word: present, executable_not_found, credential_absent, mode_ci, mode_offline, disabled_by_configuration, consecutive_failures, rate_limited, authentication_failed, peer_linked.
probe string yes What was observed, for a person: which executable, which variable (never a value).
recovering boolean no Available again after an open circuit's cooldown: the next consultation is its test.
circuit AdvisorCircuit | null → AdvisorCircuit no The open circuit, when there is one.

Used by AdvisorsReport StoredPlan

AdvisorStatus

"available" | "unavailable" | "not_configured" | "disabled" | "temporarily_failed" | "rate_limited"
An advisor's standing: whether it may be asked now and, when not, which kind of reason keeps it out. Only available advisors enter a plan.

use majordomus_cli::reasoning::availability::AdvisorStatus;

assert_eq!(serde_json::to_value(AdvisorStatus::NotConfigured).unwrap(), "not_configured");
let s: AdvisorStatus = serde_json::from_str("\"rate_limited\"").unwrap();
assert_eq!(s, AdvisorStatus::RateLimited);
  • availableIt may be asked. Presence was observed; whether it answers is learned by asking.
  • unavailableNot installed: its executable is not on PATH.
  • not_configuredInstalled or reachable in principle, but its credential is not configured.
  • disabledLeft out by the mode or by an explicit disable.
  • temporarily_failedIts recent consultations failed; its circuit is open until the cooldown passes.
  • rate_limitedIt answered with a rate limit; not asked again until the limit passes.

Used by AdvisorState ExcludedAdvisor

AdvisorTransport

"api" | "cli" | "local_runtime" | "peer"
How an advisor is reached. The transport decides how its presence is probed (a credential variable, an executable, a mesh link) and whether offline mode admits it.

use majordomus_cli::reasoning::catalogue::AdvisorTransport;

let t: AdvisorTransport = serde_json::from_str("\"local_runtime\"").unwrap();
assert_eq!(t, AdvisorTransport::LocalRuntime);
assert!(!t.is_remote());
  • apiA remote API; present when its vendor's credential variable is set.
  • cliA client tool run as a process; present when its executable is on PATH.
  • local_runtimeA model runtime on this machine; present when its executable is on PATH.
  • peerA linked mesh runtime carrying the review feature; present while linked.

Used by AdvisorState

AdvisorUsage

object
Token counts an adapter reported. Absent when the transport reports none; never estimated.

The state sums these into its totals. A missing count reads as zero, so an adapter that reports only one of the two still records what it reported.

use majordomus_cli::reasoning::record::AdvisorUsage;

let u: AdvisorUsage = serde_json::from_str(r#"{"input_tokens": 120}"#).unwrap();
assert_eq!(u, AdvisorUsage { input_tokens: 120, output_tokens: 0 });
propertytyperequireddescription
input_tokens integer (uint64) no Input tokens.
output_tokens integer (uint64) no Output tokens.

Used by ReasoningBody ReasoningInput

AdvisorsReport

object
The answer of reasoning.advisors: the mode in force, how many advisors may and may not be asked, every advisor's state, the capacity per advisory capability, and the catalogue's own findings. operational is true whatever the counts.

use majordomus_cli::capability::builtin::reasoning::AdvisorsReport;

let report: AdvisorsReport = serde_json::from_value(serde_json::json!({
    "operational": true,
    "mode": { "mode": "ci", "source": "CI" },
    "available": 0, "unavailable": 0, "advisors": [], "capacity": [],
})).unwrap();
assert!(report.operational && report.advisors.is_empty());
assert_eq!(report.mode.source, "CI");
assert!(report.diagnostics.is_empty(), "absent findings are the ordinary case");
propertytyperequireddescription
operational boolean yes Reasoning works whatever the advisors: always true, stated so a reader need not infer it from a count.
mode ModeResolution → ModeResolution yes The mode in force and what decided it.
available integer (uint) yes Advisors that may be asked now.
unavailable integer (uint) yes Advisors that may not, each optional.
advisors array of AdvisorState → AdvisorState yes Every advisor, declared ones in preference order, then linked peers.
capacity array of AdvisorCapacity → AdvisorCapacity yes Per advisory capability, who can provide it now. A row with nobody is answered by the local review path.
diagnostics array of string no The catalogue's own findings.

Used by reasoning.advisors

AssessmentStanding

"open" | "planned" | "disputed" | "concluded" | "validated" | "validation_failed"
Where an assessment stands, from open through validated.

The standing is derived, never stored: an unresolved disagreement makes it disputed whatever else is recorded; otherwise its standing conclusion and that conclusion's validations decide it, and without a conclusion a recorded plan makes it planned.

#### use majordomus_cli::reasoning::record::ReasoningRecord;
#### use serde_json::json;
#### fn rec(id: &str, n: u32, body: serde_json::Value) -> ReasoningRecord {
####     serde_json::from_value(json!({
####         "schema": "majordomus/reasoning-record", "version": 1, "id": id, "task": "t",
####         "recorded_at": format!("2026-10-01T00:00:{n:02}Z"), "body": body,
####     }))
####     .unwrap()
#### }
use majordomus_cli::reasoning::state::{derive, AssessmentStanding};
use majordomus_cli::reasoning::store::Loaded;

let mut loaded = Loaded::default();
loaded.records.push(rec("assessment-1", 1, json!({ "kind": "assessment", "subject": "s" })));
assert_eq!(derive(&loaded, None).assessments[0].standing, AssessmentStanding::Open);

loaded.records.push(rec("disagreement-2", 2, json!({
    "kind": "disagreement", "assessment": "assessment-1",
    "positions": [
        { "source": "primary", "position": "A" },
        { "source": "consultation-9", "position": "B" },
    ],
    "discriminating_question": "do callers retry?",
})));
assert_eq!(derive(&loaded, None).assessments[0].standing, AssessmentStanding::Disputed);
  • openStated; no plan yet.
  • plannedPlanned; no conclusion yet.
  • disputedA disagreement on it is unsettled.
  • concludedConcluded; not yet validated.
  • validatedConcluded, and every validation recorded passed.
  • validation_failedConcluded, and a validation failed.

Used by AssessmentView

AssessmentView

object
One assessment and where it stands.

The view carries what a reader needs to pick the work up: the subject, its materiality, its derived standing, the conclusion that stands for it and the questions it left open.

#### use majordomus_cli::reasoning::record::ReasoningRecord;
#### use serde_json::json;
#### fn rec(id: &str, n: u32, body: serde_json::Value) -> ReasoningRecord {
####     serde_json::from_value(json!({
####         "schema": "majordomus/reasoning-record", "version": 1, "id": id, "task": "t",
####         "recorded_at": format!("2026-10-01T00:00:{n:02}Z"), "body": body,
####     }))
####     .unwrap()
#### }
use majordomus_cli::reasoning::policy::Materiality;
use majordomus_cli::reasoning::state::{derive, AssessmentView};
use majordomus_cli::reasoning::store::Loaded;

let mut loaded = Loaded::default();
loaded.records.push(rec("assessment-1", 1, json!({
    "kind": "assessment", "subject": "lock scope", "materiality": "trivial",
    "questions": ["is it held across an await?"],
})));
let state = derive(&loaded, None);
let view: &AssessmentView = &state.assessments[0];
assert_eq!((view.id.as_str(), view.task.as_str()), ("assessment-1", "t"));
assert_eq!(view.materiality, Materiality::Trivial);
assert_eq!(view.conclusion, None);
assert_eq!(view.questions, ["is it held across an await?"]);
propertytyperequireddescription
id string yes Record id.
task string yes The task it was recorded under.
subject string yes What is uncertain.
materiality Materiality → Materiality yes How much it matters.
standing AssessmentStanding → AssessmentStanding yes Where it stands.
conclusion string | null no Its standing conclusion.
questions array of string no Questions it left open.

Used by ReasoningState

ConclusionView

object
One conclusion, as the state reports it.

Besides the decision and its computed review, the view counts the validations recorded against the conclusion and names the later conclusion that superseded it, if any. A superseded conclusion stays in the list; the report leaves it out.

#### use majordomus_cli::reasoning::record::ReasoningRecord;
#### use serde_json::json;
#### fn rec(id: &str, n: u32, body: serde_json::Value) -> ReasoningRecord {
####     serde_json::from_value(json!({
####         "schema": "majordomus/reasoning-record", "version": 1, "id": id, "task": "t",
####         "recorded_at": format!("2026-10-01T00:00:{n:02}Z"), "body": body,
####     }))
####     .unwrap()
#### }
use majordomus_cli::reasoning::state::{derive, ConclusionView};
use majordomus_cli::reasoning::store::Loaded;

let conclusion = |decision: &str, supersedes: Option<&str>| {
    json!({
        "kind": "conclusion", "assessment": "assessment-0", "decision": decision,
        "rationale": "r", "supersedes": supersedes,
        "reviewed_by": [], "independent_review_count": 0,
    })
};
let mut loaded = Loaded::default();
loaded.records.push(rec("conclusion-1", 1, conclusion("A", None)));
loaded.records.push(rec("validation-2", 2, json!({
    "kind": "validation", "conclusion": "conclusion-1", "check": "x", "outcome": "fail",
})));
loaded.records.push(rec("conclusion-3", 3, conclusion("B", Some("conclusion-1"))));

let state = derive(&loaded, None);
let first: &ConclusionView = &state.conclusions[0];
assert_eq!((first.validations_passed, first.validations_failed), (0, 1));
assert_eq!(first.superseded_by.as_deref(), Some("conclusion-3"));
assert_eq!(state.conclusions[1].superseded_by, None);
propertytyperequireddescription
id string yes Record id.
assessment string yes The assessment it concludes.
decision string yes The decision.
reviewed_by array of string yes Advisors whose answers it weighed.
independent_review_count integer (uint) yes How many.
validations_passed integer (uint) yes Validations recorded: passed, failed.
validations_failed integer (uint) yes Validations failed.
superseded_by string | null no Superseded by a later conclusion.
validation_plan array of string yes Its validation plan.
risks array of string no Risks that remain.

Used by ReasoningState

ConsultationStatus

"completed" | "timeout" | "rate_limited" | "auth_failed" | "malformed" | "empty" | "unavailable" | "cancelled" | "error"
How a consultation ended, as the adapter classified it.

Only a completed consultation carries advice and counts as review; every other status is a degradation that is recorded so the state can report it, never hidden. The wire form is the snake_case word that ConsultationStatus::as_str returns.

use majordomus_cli::reasoning::record::ConsultationStatus;

let s: ConsultationStatus = serde_json::from_str("\"rate_limited\"").unwrap();
assert_eq!(s, ConsultationStatus::RateLimited);
assert!(!s.is_transient());
assert!(ConsultationStatus::Timeout.is_transient());
  • completedAn answer was received and normalised.
  • timeoutNo answer within the time allowed.
  • rate_limitedThe advisor refused for rate.
  • auth_failedThe credential was refused.
  • malformedAn answer arrived that could not be normalised.
  • emptyAn answer arrived with nothing in it.
  • unavailableThe advisor could not be reached: not running, not answering, gone.
  • cancelledThe session withdrew the request.
  • errorAnything else the adapter could not classify.

Used by ConsultationView ReasoningBody ReasoningInput

ConsultationView

object
One consultation, as the state reports it.

A failed consultation is reported as plainly as a completed one: its status and the adapter's diagnostic stand where the advice would, so degradation is visible rather than silently absent.

#### use majordomus_cli::reasoning::record::ReasoningRecord;
#### use serde_json::json;
#### fn rec(id: &str, n: u32, body: serde_json::Value) -> ReasoningRecord {
####     serde_json::from_value(json!({
####         "schema": "majordomus/reasoning-record", "version": 1, "id": id, "task": "t",
####         "recorded_at": format!("2026-10-01T00:00:{n:02}Z"), "body": body,
####     }))
####     .unwrap()
#### }
use majordomus_cli::reasoning::record::ConsultationStatus;
use majordomus_cli::reasoning::state::{derive, ConsultationView};
use majordomus_cli::reasoning::store::Loaded;

let mut loaded = Loaded::default();
loaded.records.push(rec("consultation-1", 1, json!({
    "kind": "consultation", "plan": "plan-0", "advisor": "alpha",
    "status": "timeout", "diagnostic": "timeout: no answer in 60s",
})));
let state = derive(&loaded, None);
let view: &ConsultationView = &state.consultations[0];
assert_eq!(view.status, ConsultationStatus::Timeout);
assert_eq!(view.conclusion, None);
assert_eq!(view.diagnostic.as_deref(), Some("timeout: no answer in 60s"));
assert_eq!(state.totals.consultations_failed, 1);
propertytyperequireddescription
id string yes Record id.
plan string yes The plan that selected it.
advisor string yes The advisor.
status ConsultationStatus → ConsultationStatus yes How it ended.
stance Stance | null → Stance no Where the advice stands.
conclusion string | null no The advice, summarised.
diagnostic string | null no The adapter's diagnostic, for a failure.

Used by ReasoningState

DisagreementPosition

object
One side of a disagreement: whose position it is, and the position in one line.

The source is primary for the executor, or the id of a completed consultation; the store refuses a position sourced from a consultation that did not complete.

use majordomus_cli::reasoning::record::DisagreementPosition;

let p = DisagreementPosition { source: "primary".into(), position: "keep the lock".into() };
let json = serde_json::to_value(&p).unwrap();
assert_eq!(json, serde_json::json!({ "source": "primary", "position": "keep the lock" }));
propertytyperequireddescription
source string yes Whose position: primary, or a completed consultation's record id.
position string yes The position, in one line.

Used by ReasoningBody ReasoningInput

DisagreementView

object
One disagreement, as the state reports it.

The view names the question that decides the disagreement and, once a resolution is recorded, the resolution and the position its evidence favoured. A view without a resolution is an unsettled disagreement that blocks its assessment.

#### use majordomus_cli::reasoning::record::ReasoningRecord;
#### use serde_json::json;
#### fn rec(id: &str, n: u32, body: serde_json::Value) -> ReasoningRecord {
####     serde_json::from_value(json!({
####         "schema": "majordomus/reasoning-record", "version": 1, "id": id, "task": "t",
####         "recorded_at": format!("2026-10-01T00:00:{n:02}Z"), "body": body,
####     }))
####     .unwrap()
#### }
use majordomus_cli::reasoning::state::{derive, DisagreementView};
use majordomus_cli::reasoning::store::Loaded;

let mut loaded = Loaded::default();
loaded.records.push(rec("disagreement-1", 1, json!({
    "kind": "disagreement", "assessment": "assessment-0",
    "positions": [
        { "source": "primary", "position": "A" },
        { "source": "consultation-9", "position": "B" },
    ],
    "discriminating_question": "do callers retry?",
})));
loaded.records.push(rec("resolution-2", 2, json!({
    "kind": "resolution", "disagreement": "disagreement-1",
    "experiment": "read the callers", "outcome": "none retries",
    "favours": "consultation-9",
    "evidence": [{ "kind": "file", "reference": "src/caller.rs" }],
})));
let state = derive(&loaded, None);
let view: &DisagreementView = &state.disagreements[0];
assert_eq!(view.discriminating_question, "do callers retry?");
assert_eq!(view.resolution.as_deref(), Some("resolution-2"));
assert_eq!(view.favours.as_deref(), Some("consultation-9"));
propertytyperequireddescription
id string yes Record id.
assessment string yes The assessment.
discriminating_question string yes The question that decides it.
resolution string | null no The resolution, when settled.
favours string | null no The position the evidence favoured.

Used by ReasoningState

ExcludedAdvisor

object
One advisor left out of the plan, its standing when the plan was made, and why: its status, a capability it does not offer, or a budget already spent.

use majordomus_cli::reasoning::availability::{AdvisorState, AdvisorStatus, ModeResolution, ReasoningMode};
use majordomus_cli::reasoning::policy::{plan, ExcludedAdvisor, Materiality, PlanRequest};

fn advisor(id: &str, adapter: &str, status: &str) -> AdvisorState {
    serde_json::from_value(serde_json::json!({
        "id": id, "title": id, "transport": "api", "adapter": adapter,
        "capabilities": ["independent_reasoning"], "status": status,
        "reason": "present", "probe": "",
    }))
    .unwrap()
}
let mode = ModeResolution { mode: ReasoningMode::Standard, source: "default".into() };
let request = PlanRequest { materiality: Materiality::Material, ..Default::default() };
let states = [advisor("a", "x-api", "available"), advisor("b", "y-api", "available")];
let p = plan(&request, &mode, &states);
let left: &ExcludedAdvisor = &p.excluded[0];
assert_eq!((left.advisor.as_str(), left.status), ("b", AdvisorStatus::Available));
assert!(left.reason.contains("budget was spent"));
propertytyperequireddescription
advisor string yes The advisor id.
status AdvisorStatus → AdvisorStatus yes Its standing when the plan was made.
reason string yes Why it is not in the plan.

Used by ReviewPlan

Materiality

"trivial" | "low" | "material" | "high" | "critical"
How much the uncertain decision could matter. Ordered from trivial to critical: with the mode it sets the review budget, and from material up a decision taken alone carries the structured local review.

use majordomus_cli::reasoning::policy::Materiality;

assert_eq!(Materiality::default(), Materiality::Low);
assert!(Materiality::Material < Materiality::Critical);
let m: Materiality = serde_json::from_str("\"high\"").unwrap();
assert_eq!(m, Materiality::High);
  • trivialA name, a format, an obvious convention: never reviewed.
  • lowLocal and reversible.
  • materialCould affect correctness, an interface, persistent data, concurrency, operations.
  • highArchitecture, security, compatibility or a public contract.
  • criticalIrreversible or destructive, or repository governance itself.

Used by reasoning.plan AssessmentView ReasoningBody ReasoningInput ReviewPlan

ModeResolution

object
The mode in force and what decided it, as resolve_mode returns it. A plan and a report carry the source beside the mode, so a reader learns why the mode is what it is.

use majordomus_cli::reasoning::availability::{resolve_mode, ModeResolution, ReasoningMode};

let r: ModeResolution = resolve_mode(None, true, None);
assert_eq!(r, ModeResolution { mode: ReasoningMode::Ci, source: "CI".into() });
propertytyperequireddescription
mode ReasoningMode → ReasoningMode yes The mode.
source string yes What decided it: MAJORDOMUS_REASONING_MODE, CI, profile <name>, or default.

Used by AdvisorsReport ReasoningStatus

PlanOutcome

"consult" | "decide_locally" | "reuse_prior"
What the plan decided: consult the selected advisors, decide on local evidence, or reuse a standing conclusion. Deciding locally is an ordinary outcome, not a failure.

use majordomus_cli::reasoning::availability::{ModeResolution, ReasoningMode};
use majordomus_cli::reasoning::policy::{plan, Materiality, PlanOutcome, PlanRequest};

let mode = ModeResolution { mode: ReasoningMode::Ci, source: "CI".into() };
let request = PlanRequest { materiality: Materiality::Critical, ..Default::default() };
let outcome: PlanOutcome = plan(&request, &mode, &[]).outcome;
assert_eq!(outcome, PlanOutcome::DecideLocally);
assert_eq!(serde_json::to_value(outcome).unwrap(), "decide_locally");
  • consultConsult the selected advisors, then conclude.
  • decide_locallyDecide on local evidence; no independent review is called for or none is available.
  • reuse_priorA standing conclusion already answers this; reuse it rather than ask again.

Used by ReviewPlan

ReasoningBody

one of 8 variants
A stored record's body: the admitted input, with whatever the writer computed.

It mirrors ReasoningInput variant for variant, except that a plan is stored as a StoredPlan and a conclusion as a StoredConclusion. Only the store produces one, by admitting an input; its JSON form is tagged by kind like the input's.

use majordomus_cli::reasoning::record::{AssessmentInput, ReasoningBody};

let body = ReasoningBody::Assessment(AssessmentInput {
    subject: "lock scope".into(),
    ..Default::default()
});
assert_eq!(body.kind(), "assessment");
assert_eq!(serde_json::to_value(&body).unwrap()["kind"], "assessment");
variant A material uncertainty.
subjectstringyesWhat is uncertain, as one line. Later assessments of the same subject find the conclusions of earlier ones through it.
materialityMateriality → MaterialitynoHow much the decision could matter.
confidenceReasoningConfidence → ReasoningConfidencenoHow sure the executor is of its hypothesis.
hypothesisstring | nullnoThe current hypothesis.
affectsarray of stringnoWhat the decision could affect: public_api, persistent_data, security, concurrency, architecture, ...
factsarray of stringnoWhat is known.
assumptionsarray of stringnoWhat is assumed.
questionsarray of stringnoWhat is still open.
evidencearray of ReasoningEvidence → ReasoningEvidencenoThe evidence gathered locally.
capabilitiesarray of stringnoThe advisory capabilities review would need; empty means independent_reasoning.
new_evidencebooleannoThe assessment brings evidence a standing conclusion on the subject did not have, so that conclusion does not answer it.
kind"assessment"yes
variant A plan, with its availability snapshot.
kind"plan"yes
variant One advisor exchange.
planstringyesThe recorded plan that selected this advisor.
advisorstringyesThe advisor asked.
statusConsultationStatus → ConsultationStatusyesHow it ended.
questionstring | nullnoThe question asked, in one line (the full prompt is not kept).
evidencearray of ReasoningEvidence → ReasoningEvidencenoThe evidence the question carried.
conclusionstring | nullnoThe advisor's conclusion, summarised. Required when completed, refused otherwise.
stanceStance | null → StancenoWhere it stands relative to the hypothesis.
assumptionsarray of stringnoAssumptions it identified.
risksarray of stringnoRisks it identified.
actionsarray of stringnoActions it recommended.
falsifiersarray of stringnoWhat would falsify its conclusion.
confidenceReasoningConfidence | null → ReasoningConfidencenoIts stated confidence.
duration_msinteger | nullnoHow long it took, milliseconds.
usageAdvisorUsage | null → AdvisorUsagenoWhat the transport reported it cost.
diagnosticstring | nullnoThe adapter's diagnostic for a failure: a class and a sentence, never a payload.
retry_after_secondsinteger | nullnoA rate limit's retry-after, seconds.
kind"consultation"yes
variant A disagreement.
assessmentstringyesThe assessment it is about.
positionsarray of DisagreementPosition → DisagreementPositionyesThe positions; at least two.
divergent_assumptionsarray of stringnoThe assumptions the positions divide on.
discriminating_questionstringyesThe question whose answer decides between them.
experimentstring | nullnoThe experiment proposed to answer it.
kind"disagreement"yes
variant A disagreement settled by evidence.
disagreementstringyesThe disagreement it settles.
experimentstringyesThe experiment performed.
outcomestringyesWhat it showed.
favoursstringyesThe position it favours: a source of the disagreement, or neither.
evidencearray of ReasoningEvidence → ReasoningEvidenceyesThe evidence it produced; at least one reference.
kind"resolution"yes
variant An engineering conclusion, with its computed review.
kind"conclusion"yes
variant A validation step performed.
conclusionstringyesThe conclusion validated.
checkstringyesWhat was checked, in one line.
commandstring | nullnoThe command that checked it.
outcomeValidationOutcome → ValidationOutcomeyesHow it came out.
evidencearray of ReasoningEvidence → ReasoningEvidencenoWhat it produced.
kind"validation"yes
variant A failed attempt.
assessmentstring | nullnoThe assessment it bears on, when there is one.
attemptstringyesWhat was tried.
observed_failurestringyesWhat failed, observed.
assumption_invalidatedstring | nullnoThe assumption the failure invalidated.
evidencearray of ReasoningEvidence → ReasoningEvidencenoEvidence the failure produced.
updated_hypothesisstring | nullnoThe hypothesis now.
kind"attempt"yes

Used by ReasoningRecord

ReasoningCheck

object
The answer of reasoning.check: whether it is clean, the checks performed in order, and every finding. ok is true exactly when findings is empty.

use majordomus_cli::capability::builtin::reasoning::ReasoningCheck;

let clean: ReasoningCheck = serde_json::from_value(serde_json::json!({
    "ok": true, "checks": ["catalogue"], "findings": [],
})).unwrap();
assert!(clean.ok && clean.findings.is_empty());
assert_eq!(clean.checks, ["catalogue"]);
propertytyperequireddescription
ok boolean yes No finding.
checks array of string yes The checks performed, in order.
findings array of ReasoningFinding → ReasoningFinding yes What they found.

Used by reasoning.check

ReasoningConfidence

"low" | "medium" | "high"
How sure the primary executor is of its current hypothesis. high lowers the review budget by one; the other levels leave it as the mode and materiality set it.

use majordomus_cli::reasoning::availability::{ModeResolution, ReasoningMode};
use majordomus_cli::reasoning::policy::{plan, Materiality, PlanRequest, ReasoningConfidence};

let mode = ModeResolution { mode: ReasoningMode::Strict, source: "default".into() };
let budget = |confidence: ReasoningConfidence| {
    let request = PlanRequest { materiality: Materiality::Critical, confidence, ..Default::default() };
    plan(&request, &mode, &[]).budget
};
assert_eq!(budget(ReasoningConfidence::Medium), 3);
assert_eq!(budget(ReasoningConfidence::High), 2);
  • lowA guess the evidence does not yet support.
  • mediumSupported, with open questions.
  • highSupported by evidence that would be hard to overturn.

Used by reasoning.plan ReasoningBody ReasoningInput ReviewPlan StoredConclusion

ReasoningEvidence

object
A reference to something that supports a claim.

Evidence is a pointer, never a copy: a path, a case, an id or a command line, with an optional one-line note of what it shows. Unknown fields are refused, so a misspelt field is an error rather than a silently dropped claim.

use majordomus_cli::reasoning::record::{ReasoningEvidence, ReasoningEvidenceKind};

let e: ReasoningEvidence = serde_json::from_value(serde_json::json!({
    "kind": "file",
    "reference": "src/worker.rs:42",
}))
.unwrap();
assert_eq!(e.kind, ReasoningEvidenceKind::File);
assert_eq!(e.note, None);

let typo = serde_json::from_value::<ReasoningEvidence>(serde_json::json!({
    "kind": "file", "reference": "x", "notes": "misspelt",
}));
assert!(typo.is_err());
propertytyperequireddescription
kind ReasoningEvidenceKind → ReasoningEvidenceKind yes What it is.
reference string yes Where it is: a path, a case, an id, a command line.
note string | null no What it shows, in one line.

Used by ReasoningBody ReasoningInput StoredConclusion

ReasoningEvidenceKind

"file" | "test" | "adr" | "rule" | "commit" | "command" | "record" | "document" | "runtime" | "prior_external_opinion"
What a piece of evidence is.

The kind tells a reader how to follow the reference: open a file, run a test, read an ADR or a rule, show a commit. A prior external opinion is a kind of its own so that an advisor's earlier conclusion, handed to another advisor, is never read as a fact.

use majordomus_cli::reasoning::record::ReasoningEvidenceKind;

let kind: ReasoningEvidenceKind = serde_json::from_str("\"prior_external_opinion\"").unwrap();
assert_eq!(kind, ReasoningEvidenceKind::PriorExternalOpinion);
assert_eq!(serde_json::to_string(&ReasoningEvidenceKind::Adr).unwrap(), "\"adr\"");
  • fileA file of the repository, optionally with a line: src/x.rs:42.
  • testA test case or test name.
  • adrAn architecture decision record.
  • ruleA rule of the layer.
  • commitA commit.
  • commandA command that was run, and what it showed.
  • recordAnother reasoning record.
  • documentDocumentation.
  • runtimeA runtime observation: a log, a measurement, a diagnostic.
  • prior_external_opinionA prior external opinion: an advisor's conclusion handed to another advisor. Marked so it is never mistaken for a fact.

Used by ReasoningEvidence

ReasoningExplanation

object
A record with everything it rests on and everything that rests on it.

It carries the record itself, the assessment it belongs to, and that assessment's whole provenance chain as timeline steps in order. A record that belongs to no assessment has a chain of itself alone.

#### use majordomus_cli::reasoning::record::ReasoningRecord;
#### use serde_json::json;
#### fn rec(id: &str, n: u32, body: serde_json::Value) -> ReasoningRecord {
####     serde_json::from_value(json!({
####         "schema": "majordomus/reasoning-record", "version": 1, "id": id, "task": "t",
####         "recorded_at": format!("2026-10-01T00:00:{n:02}Z"), "body": body,
####     }))
####     .unwrap()
#### }
use majordomus_cli::reasoning::state::{explain, ReasoningExplanation};
use majordomus_cli::reasoning::store::Loaded;

let mut loaded = Loaded::default();
loaded.records.push(rec("attempt-1", 1, json!({
    "kind": "attempt", "attempt": "inline it", "observed_failure": "still slow",
})));
let e: ReasoningExplanation = explain(&loaded, "attempt-1").unwrap();
assert_eq!(e.record.id, "attempt-1");
assert_eq!(e.assessment, None);
assert_eq!(e.chain.len(), 1);
propertytyperequireddescription
record ReasoningRecord → ReasoningRecord yes The record asked about.
assessment string | null no The assessment it belongs to, when it belongs to one.
chain array of ReasoningStep → ReasoningStep yes Every record of that assessment, in order: its provenance chain.

Used by reasoning.explain

ReasoningFinding

object
One finding of a reasoning check: which check, where, and what is wrong.

check is one of CHECKS; subject is a repository-relative path, a record id or an advisor; message is one sentence saying what the rule refuses. Findings are reported, never repaired.

use majordomus_cli::reasoning::catalogue::{AdvisorCatalogue, References};
use majordomus_cli::reasoning::check::{run, ReasoningFinding, CHECKS};
use majordomus_cli::reasoning::store::Loaded;

let root = tempfile::tempdir().unwrap();
let loaded = Loaded { records: vec![], unreadable: vec!["t/broken.json".into()] };
let found = run(root.path(), &AdvisorCatalogue::default(), &References::default(), &loaded);
let expected = ReasoningFinding {
    check: "records".into(),
    subject: "t/broken.json".into(),
    message: "a reasoning record that does not parse".into(),
};
assert_eq!(found, [expected]);
assert!(CHECKS.contains(&found[0].check.as_str()));
propertytyperequireddescription
check string yes The check that found it.
subject string yes Where: a file, a record, an advisor.
message string yes What is wrong.

Used by ReasoningCheck

ReasoningInput

one of 8 variants
Anything a session records, as authored.

The JSON form is tagged by kind, with the variant's fields beside the tag. This is what the CLI, HTTP and MCP surfaces accept; crate::reasoning::store::admit turns it into the ReasoningBody that is stored, or refuses it.

use majordomus_cli::reasoning::record::ReasoningInput;

let input: ReasoningInput = serde_json::from_value(serde_json::json!({
    "kind": "plan",
    "assessment": "assessment-1",
}))
.unwrap();
assert!(matches!(input, ReasoningInput::Plan(ref p) if p.assessment == "assessment-1"));
let unknown = serde_json::json!({ "kind": "opinion", "text": "x" });
assert!(serde_json::from_value::<ReasoningInput>(unknown).is_err());
variant A material uncertainty.
subjectstringyesWhat is uncertain, as one line. Later assessments of the same subject find the conclusions of earlier ones through it.
materialityMateriality → MaterialitynoHow much the decision could matter.
confidenceReasoningConfidence → ReasoningConfidencenoHow sure the executor is of its hypothesis.
hypothesisstring | nullnoThe current hypothesis.
affectsarray of stringnoWhat the decision could affect: public_api, persistent_data, security, concurrency, architecture, ...
factsarray of stringnoWhat is known.
assumptionsarray of stringnoWhat is assumed.
questionsarray of stringnoWhat is still open.
evidencearray of ReasoningEvidence → ReasoningEvidencenoThe evidence gathered locally.
capabilitiesarray of stringnoThe advisory capabilities review would need; empty means independent_reasoning.
new_evidencebooleannoThe assessment brings evidence a standing conclusion on the subject did not have, so that conclusion does not answer it.
kind"assessment"yes
variant Make and record the plan for an assessment.
assessmentstringyesThe assessment the plan answers.
kind"plan"yes
variant One advisor exchange.
planstringyesThe recorded plan that selected this advisor.
advisorstringyesThe advisor asked.
statusConsultationStatus → ConsultationStatusyesHow it ended.
questionstring | nullnoThe question asked, in one line (the full prompt is not kept).
evidencearray of ReasoningEvidence → ReasoningEvidencenoThe evidence the question carried.
conclusionstring | nullnoThe advisor's conclusion, summarised. Required when completed, refused otherwise.
stanceStance | null → StancenoWhere it stands relative to the hypothesis.
assumptionsarray of stringnoAssumptions it identified.
risksarray of stringnoRisks it identified.
actionsarray of stringnoActions it recommended.
falsifiersarray of stringnoWhat would falsify its conclusion.
confidenceReasoningConfidence | null → ReasoningConfidencenoIts stated confidence.
duration_msinteger | nullnoHow long it took, milliseconds.
usageAdvisorUsage | null → AdvisorUsagenoWhat the transport reported it cost.
diagnosticstring | nullnoThe adapter's diagnostic for a failure: a class and a sentence, never a payload.
retry_after_secondsinteger | nullnoA rate limit's retry-after, seconds.
kind"consultation"yes
variant A disagreement.
assessmentstringyesThe assessment it is about.
positionsarray of DisagreementPosition → DisagreementPositionyesThe positions; at least two.
divergent_assumptionsarray of stringnoThe assumptions the positions divide on.
discriminating_questionstringyesThe question whose answer decides between them.
experimentstring | nullnoThe experiment proposed to answer it.
kind"disagreement"yes
variant A disagreement settled by evidence.
disagreementstringyesThe disagreement it settles.
experimentstringyesThe experiment performed.
outcomestringyesWhat it showed.
favoursstringyesThe position it favours: a source of the disagreement, or neither.
evidencearray of ReasoningEvidence → ReasoningEvidenceyesThe evidence it produced; at least one reference.
kind"resolution"yes
variant An engineering conclusion.
assessmentstringyesThe assessment it concludes.
decisionstringyesThe decision.
rationalestringyesWhy, from the evidence.
evidencearray of ReasoningEvidence → ReasoningEvidencenoThe evidence it rests on. Required for material uncertainty.
consultationsarray of stringnoThe completed consultations it weighed.
disagreementsarray of stringnoThe resolved disagreements it rests on.
rejectedarray of stringnoAlternatives considered and rejected, each with why.
assumptionsarray of stringnoAssumptions it still depends on.
risksarray of stringnoRisks that remain.
validation_planarray of stringnoHow the decision will be proven. Required for material uncertainty.
confidenceReasoningConfidence → ReasoningConfidencenoConfidence in it.
supersedesstring | nullnoAn earlier conclusion this one replaces, because new evidence overturned it.
kind"conclusion"yes
variant A validation step performed.
conclusionstringyesThe conclusion validated.
checkstringyesWhat was checked, in one line.
commandstring | nullnoThe command that checked it.
outcomeValidationOutcome → ValidationOutcomeyesHow it came out.
evidencearray of ReasoningEvidence → ReasoningEvidencenoWhat it produced.
kind"validation"yes
variant A failed attempt.
assessmentstring | nullnoThe assessment it bears on, when there is one.
attemptstringyesWhat was tried.
observed_failurestringyesWhat failed, observed.
assumption_invalidatedstring | nullnoThe assumption the failure invalidated.
evidencearray of ReasoningEvidence → ReasoningEvidencenoEvidence the failure produced.
updated_hypothesisstring | nullnoThe hypothesis now.
kind"attempt"yes

Used by ReasoningRecordInput

ReasoningMode

"offline" | "ci" | "fast" | "standard" | "strict"
How much independent review a session may use, and where from. The policy reads it for the review budget; availability reads it to leave advisors out (ci all of them, offline every one that leaves the machine). standard is the default.

use majordomus_cli::reasoning::availability::ReasoningMode;

assert_eq!(ReasoningMode::default(), ReasoningMode::Standard);
assert_eq!(serde_json::to_value(ReasoningMode::Offline).unwrap(), "offline");
let m: ReasoningMode = serde_json::from_str("\"strict\"").unwrap();
assert_eq!(m, ReasoningMode::Strict);
  • offlineNo network: only advisors that stay on this machine.
  • ciDeterministic: no advisor at all, whatever is installed.
  • fastReview only where the blast radius is high.
  • standardReview where uncertainty is material.
  • strictReview wider, and complementary review where the stakes are highest.

Used by ModeResolution ReviewPlan

ReasoningPhase

"uncertainty" | "consulting" | "local" | "reuse" | "advisor_response" | "degradation" | "disagreement" | "experiment" | "conclusion" | "validation" | "attempt"
A phase of the reasoning timeline.

Each record maps to one phase: a plan to consulting, local or reuse by its outcome, a consultation to an advisor response or a degradation by its status, and every other kind to the phase of the same name.

#### use majordomus_cli::reasoning::record::ReasoningRecord;
#### use serde_json::json;
#### fn rec(id: &str, n: u32, body: serde_json::Value) -> ReasoningRecord {
####     serde_json::from_value(json!({
####         "schema": "majordomus/reasoning-record", "version": 1, "id": id, "task": "t",
####         "recorded_at": format!("2026-10-01T00:00:{n:02}Z"), "body": body,
####     }))
####     .unwrap()
#### }
use majordomus_cli::reasoning::state::{derive, ReasoningPhase};
use majordomus_cli::reasoning::store::Loaded;

let mut loaded = Loaded::default();
loaded.records.push(rec("consultation-1", 1, json!({
    "kind": "consultation", "plan": "plan-0", "advisor": "alpha", "status": "unavailable",
})));
let phases: Vec<ReasoningPhase> =
    derive(&loaded, None).timeline.iter().map(|s| s.phase).collect();
assert_eq!(phases, [ReasoningPhase::Degradation]);
let word = serde_json::to_value(ReasoningPhase::AdvisorResponse).unwrap();
assert_eq!(word, "advisor_response");
  • uncertaintyUncertainty stated with evidence.
  • consultingReview planned.
  • localDecided locally, by plan.
  • reuseA standing conclusion reused.
  • advisor_responseAn advisor answered.
  • degradationAn advisor could not answer.
  • disagreementPositions diverged.
  • experimentAn experiment settled a disagreement.
  • conclusionA conclusion.
  • validationA validation step.
  • attemptA failed attempt.

Used by ReasoningStep

ReasoningRecord

object
A stored record: a body with the envelope only the writer stamps.

The envelope carries the schema and version, a unique id, the task, the time it was recorded, the commit the checkout was on and the secret shapes redacted from its text. One record is one JSON file in the store; the store reads back only records whose schema and version match.

use majordomus_cli::reasoning::record::*;

let record = ReasoningRecord {
    schema: RECORD_SCHEMA.into(),
    version: RECORD_VERSION,
    id: "attempt-20261001T000000Z-abcdef".into(),
    task: "untasked".into(),
    recorded_at: "2026-10-01T00:00:00Z".into(),
    head: None,
    redacted: vec![],
    body: ReasoningBody::Attempt(AttemptInput {
        assessment: None,
        attempt: "inline the call".into(),
        observed_failure: "the test still fails".into(),
        assumption_invalidated: None,
        evidence: vec![],
        updated_hypothesis: None,
    }),
};
let text = serde_json::to_string(&record).unwrap();
assert!(!text.contains("\"head\""), "an absent head is not written");
let back: ReasoningRecord = serde_json::from_str(&text).unwrap();
assert_eq!(back.id, record.id);
assert_eq!(back.body.kind(), "attempt");
propertytyperequireddescription
schema string yes RECORD_SCHEMA.
version integer (uint32) yes RECORD_VERSION.
id string yes <kind>-<yyyymmddThhmmssZ>-<hex>, unique in the store.
task string yes The task it was recorded under, or untasked.
recorded_at string yes When it was recorded, RFC 3339; stamped by the writer, never supplied.
head string | null no The commit the checkout was on.
redacted array of string no Secret shapes the writer removed from the text, by shape name.
body ReasoningBody → ReasoningBody yes The record.

Used by ReasoningExplanation ReasoningRecorded

ReasoningRecordInput

object
The input of reasoning.record: one record, as authored, tagged by its kind. The writer computes everything else (id, task, stamp, a plan's selection) itself.

use majordomus_cli::capability::builtin::reasoning::ReasoningRecordInput;
use majordomus_cli::reasoning::record::ReasoningInput;

let input: ReasoningRecordInput = serde_json::from_value(serde_json::json!({
    "record": { "kind": "plan", "assessment": "assessment-1" },
})).unwrap();
assert!(matches!(input.record, ReasoningInput::Plan(ref p) if p.assessment == "assessment-1"));
propertytyperequireddescription
record ReasoningInput → ReasoningInput yes The record.

Used by reasoning.record

ReasoningRecorded

object
The answer of reasoning.record: the record as stored, where it was written, and the timeline step it adds.

use majordomus_cli::capability::builtin::reasoning::ReasoningRecorded;

let schema = serde_json::to_value(schemars::schema_for!(ReasoningRecorded)).unwrap();
for field in ["record", "path", "step"] {
    assert!(schema["required"].as_array().unwrap().iter().any(|f| f == field), "{field}");
}
propertytyperequireddescription
record ReasoningRecord → ReasoningRecord yes The stored record.
path string yes Where it was written, relative to the repository.
step ReasoningStep → ReasoningStep yes The step it adds to the timeline: the report a terminal or the Cockpit shows.

Used by reasoning.record

ReasoningState

object
The reasoning state of a task (or of every task).

It is what every surface reports: the assessments, consultations, disagreements and conclusions of the task, the timeline of every record, the totals, and the store files that could not be read. Derived by derive(), never stored.

#### use majordomus_cli::reasoning::record::ReasoningRecord;
#### use serde_json::json;
#### fn rec(id: &str, task: &str, body: serde_json::Value) -> ReasoningRecord {
####     serde_json::from_value(json!({
####         "schema": "majordomus/reasoning-record", "version": 1, "id": id, "task": task,
####         "recorded_at": "2026-10-01T00:00:00Z", "body": body,
####     }))
####     .unwrap()
#### }
use majordomus_cli::reasoning::state::{derive, ReasoningState};
use majordomus_cli::reasoning::store::Loaded;

let mut loaded = Loaded::default();
let assessment = |subject: &str| json!({ "kind": "assessment", "subject": subject });
loaded.records.push(rec("assessment-1", "t1", assessment("a")));
loaded.records.push(rec("assessment-2", "t2", assessment("b")));
loaded.unreadable.push("t2/broken.json".into());

let one: ReasoningState = derive(&loaded, Some("t1"));
assert_eq!((one.task.as_str(), one.assessments.len()), ("t1", 1));
let all = derive(&loaded, None);
assert_eq!((all.task.as_str(), all.totals.records), ("all", 2));
assert_eq!(all.unreadable, ["t2/broken.json"]);
propertytyperequireddescription
task string yes The task reported, or all.
assessments array of AssessmentView → AssessmentView yes The assessments.
consultations array of ConsultationView → ConsultationView yes The consultations.
disagreements array of DisagreementView → DisagreementView yes The disagreements.
conclusions array of ConclusionView → ConclusionView yes The conclusions.
timeline array of ReasoningStep → ReasoningStep yes The timeline.
totals ReasoningTotals → ReasoningTotals yes Totals.
unreadable array of string no Store files that could not be read.

Used by ReasoningStatus

ReasoningStatus

object
The answer of reasoning.status: the mode in force, the advisor counts, the state the records derive, and that state rendered as the Markdown a handover carries.

use majordomus_cli::capability::builtin::reasoning::ReasoningStatus;

let schema = serde_json::to_value(schemars::schema_for!(ReasoningStatus)).unwrap();
for field in ["mode", "advisors_available", "advisors_unavailable", "state", "report"] {
    assert!(schema["required"].as_array().unwrap().iter().any(|f| f == field), "{field}");
}
propertytyperequireddescription
mode ModeResolution → ModeResolution yes The mode in force.
advisors_available integer (uint) yes Advisors available now.
advisors_unavailable integer (uint) yes Advisors not available now, each optional.
state ReasoningState → ReasoningState yes The state.
report string yes The state as Markdown: what a handover carries.

Used by reasoning.status

ReasoningStep

object
One step of the timeline: when, which record, which phase, and one line saying what happened.

The summary is cut to a single line of at most 140 characters, so a timeline renders the same on a terminal, a page and a handover.

#### use majordomus_cli::reasoning::record::ReasoningRecord;
#### use serde_json::json;
#### fn rec(id: &str, n: u32, body: serde_json::Value) -> ReasoningRecord {
####     serde_json::from_value(json!({
####         "schema": "majordomus/reasoning-record", "version": 1, "id": id, "task": "t",
####         "recorded_at": format!("2026-10-01T00:00:{n:02}Z"), "body": body,
####     }))
####     .unwrap()
#### }
use majordomus_cli::reasoning::state::{derive, ReasoningPhase, ReasoningStep};
use majordomus_cli::reasoning::store::Loaded;

let mut loaded = Loaded::default();
loaded.records.push(rec("attempt-1", 1, json!({
    "kind": "attempt", "attempt": "share the client\nacross threads",
    "observed_failure": "the handle is not Sync",
})));
let state = derive(&loaded, None);
let step: &ReasoningStep = &state.timeline[0];
assert_eq!(step.at, "2026-10-01T00:00:01Z");
assert_eq!(step.phase, ReasoningPhase::Attempt);
assert_eq!(step.summary, "share the client failed: the handle is not Sync");
propertytyperequireddescription
at string yes When.
id string yes The record.
phase ReasoningPhase → ReasoningPhase yes The phase.
summary string yes One line.

Used by ReasoningExplanation ReasoningRecorded ReasoningState

ReasoningTotals

object
Totals over the records of the derived task: counts, reviewers and reported tokens.

Independent reviewers are distinct advisors with at least one completed consultation; token counts are summed only from what adapters reported, never estimated.

#### use majordomus_cli::reasoning::record::ReasoningRecord;
#### use serde_json::json;
#### fn rec(id: &str, n: u32, body: serde_json::Value) -> ReasoningRecord {
####     serde_json::from_value(json!({
####         "schema": "majordomus/reasoning-record", "version": 1, "id": id, "task": "t",
####         "recorded_at": format!("2026-10-01T00:00:{n:02}Z"), "body": body,
####     }))
####     .unwrap()
#### }
use majordomus_cli::reasoning::state::{derive, ReasoningTotals};
use majordomus_cli::reasoning::store::Loaded;

let answer = |plan: &str| {
    json!({
        "kind": "consultation", "plan": plan, "advisor": "alpha", "status": "completed",
        "conclusion": "A", "stance": "supports",
        "usage": { "input_tokens": 100, "output_tokens": 20 },
    })
};
let mut loaded = Loaded::default();
loaded.records.push(rec("consultation-1", 1, answer("plan-a")));
loaded.records.push(rec("consultation-2", 2, answer("plan-b")));
let totals: ReasoningTotals = derive(&loaded, None).totals;
assert_eq!(totals.consultations_completed, 2);
assert_eq!(totals.independent_reviewers, 1, "one advisor, however often asked");
assert_eq!((totals.input_tokens, totals.output_tokens), (200, 40));
propertytyperequireddescription
records integer (uint) yes Records.
consultations_completed integer (uint) yes Consultations that completed.
consultations_failed integer (uint) yes Consultations that did not.
independent_reviewers integer (uint) yes Distinct advisors that answered.
input_tokens integer (uint64) yes Input tokens the adapters reported.
output_tokens integer (uint64) yes Output tokens the adapters reported.

Used by ReasoningState

ReviewPlan

object
The plan: the decision with its reasons attached. It carries what it answered (mode, materiality, confidence, capabilities), the budget, the outcome with one sentence of why, every advisor selected or excluded, and the local review when one is due.

use majordomus_cli::reasoning::availability::{ModeResolution, ReasoningMode};
use majordomus_cli::reasoning::policy::{plan, Materiality, PlanRequest, ReviewPlan, LOCAL_REVIEW};

let mode = ModeResolution { mode: ReasoningMode::Offline, source: "MAJORDOMUS_REASONING_MODE".into() };
let request = PlanRequest { materiality: Materiality::Material, ..Default::default() };
let p: ReviewPlan = plan(&request, &mode, &[]);
assert_eq!(p.mode_source, "MAJORDOMUS_REASONING_MODE");
assert_eq!(p.capabilities, ["independent_reasoning"]);
assert_eq!((p.budget, p.available), (1, 0));
assert_eq!(p.local_review.len(), LOCAL_REVIEW.len());
assert!(p.reason.starts_with("no available advisor offers"));
propertytyperequireddescription
mode ReasoningMode → ReasoningMode yes The mode it was made under.
mode_source string yes What decided the mode.
materiality Materiality → Materiality yes The materiality it answered.
confidence ReasoningConfidence → ReasoningConfidence yes The confidence it answered.
capabilities array of string yes The capabilities review was asked for.
budget integer (uint) yes How many advisors the mode and materiality allow.
outcome PlanOutcome → PlanOutcome yes What to do.
reason string yes Why, in one sentence.
selected array of SelectedAdvisor → SelectedAdvisor yes The advisors to consult, in selection order.
excluded array of ExcludedAdvisor → ExcludedAdvisor yes Every advisor not selected, with its reason.
available integer (uint) yes Advisors available when the plan was made, whatever was selected.
local_review array of string no The structured local review to perform, when material uncertainty meets no advisor.

Used by reasoning.plan StoredPlan

SelectedAdvisor

object
One selected advisor, the requested capabilities it covers, and why the greedy selection chose it at its position.

use majordomus_cli::reasoning::availability::{AdvisorState, ModeResolution, ReasoningMode};
use majordomus_cli::reasoning::policy::{plan, Materiality, PlanRequest, SelectedAdvisor};

fn advisor(id: &str, adapter: &str, status: &str) -> AdvisorState {
    serde_json::from_value(serde_json::json!({
        "id": id, "title": id, "transport": "api", "adapter": adapter,
        "capabilities": ["independent_reasoning"], "status": status,
        "reason": "present", "probe": "",
    }))
    .unwrap()
}
let mode = ModeResolution { mode: ReasoningMode::Standard, source: "default".into() };
let request = PlanRequest { materiality: Materiality::Material, ..Default::default() };
let p = plan(&request, &mode, &[advisor("a", "x-api", "available")]);
let chosen: &SelectedAdvisor = &p.selected[0];
assert_eq!(chosen.advisor, "a");
assert_eq!(chosen.covers, ["independent_reasoning"]);
assert!(chosen.why.starts_with("the first available advisor"));
propertytyperequireddescription
advisor string yes The advisor id.
covers array of string yes The requested capabilities it covers.
why string yes Why it was chosen.

Used by ReviewPlan

Stance

"supports" | "opposes" | "alternative" | "inconclusive"
Where an advisor's answer stands relative to the executor's hypothesis.

A completed consultation must state its stance; the store refuses one without it. The stance is a label for the timeline, not a vote: agreement settles nothing.

use majordomus_cli::reasoning::record::Stance;

let s: Stance = serde_json::from_str("\"alternative\"").unwrap();
assert_eq!(s, Stance::Alternative);
assert!(serde_json::from_str::<Stance>("\"agrees\"").is_err());
  • supportsIt supports the hypothesis.
  • opposesIt argues against it.
  • alternativeIt proposes something else.
  • inconclusiveIt does not decide.

Used by ConsultationView ReasoningBody ReasoningInput

StoredConclusion

object
A conclusion as stored: the input and what only the writer computes.

The input is flattened, so a stored conclusion reads as the input plus two fields: the advisors whose completed consultations it cites, each once and in canonical order, and their count. Both are computed at admission and checked again by reasoning.check, so a hand-edited count is reported.

use majordomus_cli::reasoning::record::{ConclusionInput, StoredConclusion};

let stored = StoredConclusion {
    input: ConclusionInput {
        assessment: "assessment-1".into(),
        decision: "keep the lock".into(),
        rationale: "measured".into(),
        ..Default::default()
    },
    reviewed_by: vec!["alpha".into()],
    independent_review_count: 1,
};
let json = serde_json::to_value(&stored).unwrap();
assert_eq!(json["decision"], "keep the lock");
assert_eq!(json["independent_review_count"], 1);
propertytyperequireddescription
assessment string yes The assessment it concludes.
decision string yes The decision.
rationale string yes Why, from the evidence.
evidence array of ReasoningEvidence → ReasoningEvidence no The evidence it rests on. Required for material uncertainty.
consultations array of string no The completed consultations it weighed.
disagreements array of string no The resolved disagreements it rests on.
rejected array of string no Alternatives considered and rejected, each with why.
assumptions array of string no Assumptions it still depends on.
risks array of string no Risks that remain.
validation_plan array of string no How the decision will be proven. Required for material uncertainty.
confidence ReasoningConfidence → ReasoningConfidence no Confidence in it.
supersedes string | null no An earlier conclusion this one replaces, because new evidence overturned it.
reviewed_by array of string yes The advisors whose completed consultations it cites, each once. Computed.
independent_review_count integer (uint) yes How many independent advisors reviewed it: reviewed_by's length. Computed.

Used by ReasoningBody

StoredPlan

object
A plan as stored: the decision and the availability it was made against.

The store builds it when it admits a PlanInput; nothing else does. The snapshot of availability is what a later consultation is admitted against, so an answer that arrives after the advisor's circuit opened still belongs to the plan that asked for it.

use majordomus_cli::reasoning::availability::{ModeResolution, ReasoningMode};
use majordomus_cli::reasoning::policy::PlanOutcome;
use majordomus_cli::reasoning::record::*;
use majordomus_cli::reasoning::store::{admit, Admission, Loaded};

let mode = ModeResolution { mode: ReasoningMode::Standard, source: "default".into() };
let admission = Admission { mode: &mode, availability: &[] };
let mut loaded = Loaded::default();
loaded.records.push(ReasoningRecord {
    schema: RECORD_SCHEMA.into(),
    version: RECORD_VERSION,
    id: "assessment-1".into(),
    task: "t".into(),
    recorded_at: "2026-10-01T00:00:01Z".into(),
    head: None,
    redacted: vec![],
    body: ReasoningBody::Assessment(AssessmentInput {
        subject: "a name".into(),
        ..Default::default()
    }),
});
let input = ReasoningInput::Plan(PlanInput { assessment: "assessment-1".into() });
let ReasoningBody::Plan(stored) = admit(input, &loaded, &admission).unwrap() else {
    panic!("a plan input is stored as a plan");
};
let stored: StoredPlan = stored;
assert_eq!(stored.assessment, "assessment-1");
assert_eq!(stored.plan.outcome, PlanOutcome::DecideLocally);
assert!(stored.availability.is_empty());
propertytyperequireddescription
assessment string yes The assessment it answers.
plan ReviewPlan → ReviewPlan yes The decision.
availability array of AdvisorState → AdvisorState yes Every advisor's standing at the moment of the plan: the snapshot a later consultation is admitted against.

Used by ReasoningBody

ValidationOutcome

"pass" | "fail"
Whether a performed validation step passed or failed.

One failed validation is enough to mark the assessment's standing as validation_failed in the derived state, whatever else passed.

use majordomus_cli::reasoning::record::ValidationOutcome;

let o: ValidationOutcome = serde_json::from_str("\"fail\"").unwrap();
assert_eq!(o, ValidationOutcome::Fail);
assert_eq!(serde_json::to_string(&ValidationOutcome::Pass).unwrap(), "\"pass\"");
  • passIt passed.
  • failIt failed.

Used by ReasoningBody ReasoningInput

Searches the titles and descriptions of every page this site publishes.