pub struct MissionHost { /* private fields */ }Expand description
Registry of missions this server process hosts (see module docs).
Implementations§
Source§impl MissionHost
impl MissionHost
Sourcepub fn new(repo_root: PathBuf) -> Self
pub fn new(repo_root: PathBuf) -> Self
Host for repo_root, discovering the Claude backend on first use.
Sourcepub fn with_backend(repo_root: PathBuf, backend: Arc<dyn AgentBackend>) -> Self
pub fn with_backend(repo_root: PathBuf, backend: Arc<dyn AgentBackend>) -> Self
Host with an injected backend (tests drive the full lifecycle through
kranz_engine::backend_mock without a claude binary).
Sourcepub fn with_gate_executor<F>(repo_root: PathBuf, gate_executor: F) -> Self
pub fn with_gate_executor<F>(repo_root: PathBuf, gate_executor: F) -> Self
Host with an injected gate-suite executor (tests script CI gate
outcomes for MissionHost::merge hermetically, without ever
shelling out to cargo/npm). Mirrors MissionHost::with_backend’s
seam, for the gate suite instead of the agent backend.
Sourcepub async fn create(
&self,
goal: &str,
config_patch: Option<&Value>,
) -> Result<String, ApiError>
pub async fn create( &self, goal: &str, config_patch: Option<&Value>, ) -> Result<String, ApiError>
POST /api/missions: layered config + optional request patch →
validate → create the mission → hold its engine in the registry.
Public: the Slack bridge drives the same lifecycle through this host
(wired by kranz serve --slack), so these five operations are the
shared client surface, not axum-private plumbing.
Sourcepub async fn draft(
&self,
slug: &str,
then_enqueue: bool,
) -> Result<DraftOutcome, ApiError>
pub async fn draft( &self, slug: &str, then_enqueue: bool, ) -> Result<DraftOutcome, ApiError>
Entry point any surface (REST, Slack, CLI-over-HTTP) can call to draft
a backlog ticket non-interactively: validate the slug, load the ticket,
create its planning mission through this host — so the create path
registers it in missions and its lifecycle events stream over
GET /api/missions/:id/ws exactly like POST /api/missions — then run
drive_draft (roadmap f-1-1) to completion against that hosted
engine. The engine is dropped and the registry entry removed once the
draft turn ends (mirroring [run_to_end]’s drop-then-remove ordering)
so the mission stays observable/resumable afterward; no operator
checkout restoration happens here — a headless server has no checkout
to restore.
Sourcepub async fn draft_async(
&self,
slug: &str,
then_enqueue: bool,
) -> Result<String, ApiError>
pub async fn draft_async( &self, slug: &str, then_enqueue: bool, ) -> Result<String, ApiError>
POST /api/tickets/:slug/draft: fire-and-observe twin of
Self::draft for the REST surface — a draft can run for a while (a
planning conversation with the orchestrator), so this creates the
planning mission SYNCHRONOUSLY (registering it exactly like create,
so its lifecycle streams over GET /api/missions/:id/ws immediately),
then spawns drive_draft as a background task and returns the
mission id right away. The final outcome (Review vs NeedsContext) is
read back later via GET /api/tickets/:slug.
Sourcepub fn approve_ticket(
&self,
slug: &str,
force: bool,
) -> Result<ApprovedTicket, ApiError>
pub fn approve_ticket( &self, slug: &str, force: bool, ) -> Result<ApprovedTicket, ApiError>
POST /api/tickets/:slug/approve: the shared kranz_engine::deps
gate (cycle detection, unsatisfied-blocker refusal) plus the
enqueue side effects — the exact same core kranz_cli’s kranz ticket approve calls, so the CLI and REST surfaces can never drift.
Sourcepub async fn planning_turn(
&self,
id: &str,
text: &str,
) -> Result<String, ApiError>
pub async fn planning_turn( &self, id: &str, text: &str, ) -> Result<String, ApiError>
POST /api/missions/:id/planning/turn: one conversational turn. A
captured seed reply (fresh session / re-seed) is prepended — it
happened first in the conversation.
Sourcepub async fn request_plan(&self, id: &str) -> Result<Value, ApiError>
pub async fn request_plan(&self, id: &str) -> Result<Value, ApiError>
POST /api/missions/:id/planning/request-plan: demand the plan.
Ready → plan + cost estimate; NotReady → the orchestrator’s prose
(back to the conversation).
Sourcepub async fn approve(&self, id: &str, plan: Plan) -> Result<String, ApiError>
pub async fn approve(&self, id: &str, plan: Plan) -> Result<String, ApiError>
POST /api/missions/:id/approve: commit plan.json/plan.md/index.md on
the mission branch exactly like the CLI. Returns the mission branch.
Sourcepub async fn start(&self, id: &str) -> Result<(), ApiError>
pub async fn start(&self, id: &str) -> Result<(), ApiError>
POST /api/missions/:id/start: consume the hosted engine into a
background engine.run() task — or, for a mission not in the registry
(blocked earlier, server restarted, or CLI-created), resume it from
the event log and run that.
Sourcepub async fn merge(&self, id: &str) -> Result<Value, ApiError>
pub async fn merge(&self, id: &str) -> Result<Value, ApiError>
POST /api/missions/:id/merge: the human-triggered gated Merge
action (roadmap M6). Loads the mission’s base_branch/base_sha/
mission_branch from its event log (no engine needs to be hosted —
merge is independent of the planning/run-loop registry) and runs
kranz_engine::merge::merge_mission under spawn_blocking (git and
the gate suite are both blocking work). Never pushes.
Sourcepub async fn ask(&self, question: &str) -> Result<Value, ApiError>
pub async fn ask(&self, question: &str) -> Result<Value, ApiError>
Read-only, LLM-backed Q&A for /kranz ask: ground the model in current
mission/ticket state and return one answer plus usage. This deliberately
bypasses the hosted mission registry: it must never create a mission,
append mission events, enqueue work, approve, start, or merge.
Sourcepub fn release(&self, id: &str) -> Result<bool, ApiError>
pub fn release(&self, id: &str) -> Result<bool, ApiError>
Release a hosted idle engine: drop it from the registry (flushing its
log and freeing the single-writer lock) so an EXTERNAL runner — the
kranz work dispatcher, a terminal kranz plan/run — can take the
mission over. The approve-and-QUEUE path needs this: without it the
approved engine would sit attached here holding the lock, and the very
dispatcher the queue points at would be refused with LockHeld.
Returns true when the mission is now free of THIS host (released, or
was never hosted), false when it is actively running here (never
interrupted). A turn in flight is an error, mirroring the other
planning operations.
Sourcepub fn sweep_idle(&self, threshold: Duration) -> Vec<String>
pub fn sweep_idle(&self, threshold: Duration) -> Vec<String>
Release every Planning entry idle for at least threshold (a mission
touched more recently than that is left alone). A mid-turn cell can
never actually be released — release refuses it via
turn_in_flight, which this treats as “not idle yet” rather than an
error. Returns the ids this call actually released.
Sourcepub async fn abandon(&self, id: &str, reason: &str) -> Result<(), ApiError>
pub async fn abandon(&self, id: &str, reason: &str) -> Result<(), ApiError>
POST /api/missions/:id/abandon: retire a mission through the
engine’s canonical abandon path (terminal-refusing, event-recorded).
A mission hosted HERE is taken out of the registry first — an idle
planning engine is dropped (freeing the lock), a running task is
aborted and awaited (the engine’s Drop flushes the log and kills its
agent children) — so the abandon event lands on a quiet log. A lock
held by a FOREIGN process (a terminal kranz plan/run) surfaces as
the engine’s LockHeld → 409; the web never force-steals.
Sourcepub fn clean(&self, id: &str, all: bool) -> Result<(), ApiError>
pub fn clean(&self, id: &str, all: bool) -> Result<(), ApiError>
POST /api/missions/:id/delete: remove a TERMINAL mission’s directory,
mirroring kranz clean exactly — cleanable_class decides, all
opts in to deleting Complete missions (which otherwise stay: they feed
the cost-calibration corpus), and a live lock is re-checked immediately
before removal so nothing is ever deleted under a running engine.
Only the mission directory and its own missions/index.md line go;
branches, tags, and every other mission’s index line are left intact
(same contract as the CLI).
Sourcepub fn pending_plan(&self, id: &str) -> Option<Plan>
pub fn pending_plan(&self, id: &str) -> Option<Plan>
The reviewed plan awaiting approval, if any (clone). GET /api/missions/:id/pending-plan and the glasses PLAN page read this.
Sourcepub async fn try_approve_pending(
&self,
id: &str,
) -> Result<Option<String>, ApiError>
pub async fn try_approve_pending( &self, id: &str, ) -> Result<Option<String>, ApiError>
Approve the currently parked plan for an explicit untargeted command
(/kranz approve). Preview-based clients must use the matching variant.
Sourcepub async fn try_approve_pending_matching(
&self,
id: &str,
expected_identity: Option<&str>,
) -> Result<PendingApproval, ApiError>
pub async fn try_approve_pending_matching( &self, id: &str, expected_identity: Option<&str>, ) -> Result<PendingApproval, ApiError>
Approve only the plan the caller reviewed. The engine lock serializes replacement and approval; the pending-plan lock protects comparison and consumption. An approval failure leaves the original plan parked, with no restore that could overwrite a concurrent replacement.
Sourcepub async fn approve_pending(
&self,
id: &str,
expected_identity: Option<&str>,
) -> Result<String, ApiError>
pub async fn approve_pending( &self, id: &str, expected_identity: Option<&str>, ) -> Result<String, ApiError>
REST approval requires the identity returned with the reviewed preview.
Sourcepub async fn drain(&self) -> Result<Value, ApiError>
pub async fn drain(&self) -> Result<Value, ApiError>
POST /api/queue/drain: run the queue drain/claim/skip loop
(kranz_engine::work::drain_queue) as a background task on this
serve process. This is just ANOTHER dispatcher: it does not register
missions in the missions planning registry, and arbitrates against
an external kranz work process exactly as today — through the queue
claim files and the events.jsonl single-writer lock, no new locking.
IDEMPOTENT while a drain is live: a second call while the tracked drain task has not finished returns THAT drain’s current state instead of spawning a second one.
Sourcepub fn queue_state(&self) -> Value
pub fn queue_state(&self) -> Value
GET /api/queue: the queue front-to-back, who (if anyone) currently
holds the busy lock, and this host’s own drain tracker.
Readiness is probed for the front entry only (with a short TTL
cache). Deeper entries omit readiness so a long queue cannot turn
every dashboard poll into N CLI shells.