Skip to main content

mati_core/mcp/protocol/
inputs.rs

1use serde::{Deserialize, Serialize};
2
3use crate::hooks::decide::Action;
4use crate::store::{
5    PolicyMode as StorePolicyMode, PolicyRecord, PolicyStage as StorePolicyStage,
6    Priority as StorePriority, ReceiptSource as StoreReceiptSource,
7};
8
9#[derive(Debug, Serialize, Deserialize)]
10#[serde(deny_unknown_fields)]
11pub struct GetInput {
12    pub key: String,
13}
14
15#[derive(Debug, Serialize, Deserialize)]
16#[serde(deny_unknown_fields)]
17pub struct HookEvaluateInput {
18    pub file_key: String,
19    #[serde(default)]
20    pub include_recent: bool,
21    /// Actor scope for the consult-receipt lookup: `agent_id` for a subagent,
22    /// `None` (global) for the main thread. Drives per-actor enforcement.
23    #[serde(default)]
24    pub actor: Option<String>,
25}
26
27#[derive(Debug, Serialize, Deserialize)]
28#[serde(deny_unknown_fields)]
29pub struct PolicyEvaluateInput {
30    pub action: Action,
31    #[serde(default)]
32    pub actor: Option<String>,
33    /// Original command text used only for record-only bypass detection.
34    #[serde(default)]
35    pub raw_command: Option<String>,
36}
37
38#[derive(Debug, Serialize, Deserialize)]
39pub struct PolicyVerdict {
40    pub key: String,
41    pub stage: StorePolicyStage,
42    pub mode: StorePolicyMode,
43    pub rule: String,
44    pub reason: String,
45    pub severity: StorePriority,
46    pub requires_key: String,
47    #[serde(default)]
48    pub via: Vec<StoreReceiptSource>,
49    pub satisfied: bool,
50    /// Snapshot of the global enforcement mode used for block degradation.
51    #[serde(default)]
52    pub strict: bool,
53}
54
55#[derive(Debug, Serialize, Deserialize)]
56pub struct PolicyEvaluateResult {
57    pub verdicts: Vec<PolicyVerdict>,
58    /// Policy key whose literal was found in an otherwise unclassified command.
59    #[serde(default)]
60    pub bypass_key: Option<String>,
61}
62
63#[derive(Debug, Serialize, Deserialize)]
64#[serde(deny_unknown_fields)]
65pub struct ScanPrefixInput {
66    pub prefix: String,
67}
68
69#[derive(Debug, Serialize, Deserialize)]
70#[serde(deny_unknown_fields)]
71pub struct ScanKeysInput {
72    pub prefix: String,
73}
74
75#[derive(Debug, Serialize, Deserialize)]
76#[serde(deny_unknown_fields)]
77pub struct ScanEnforcementEventsInput {
78    #[serde(default)]
79    pub since_seq: u64,
80    #[serde(default = "default_until_seq")]
81    pub until_seq: u64,
82}
83
84#[derive(Debug, Serialize, Deserialize)]
85#[serde(deny_unknown_fields)]
86pub struct ScanEnforcementEventsSinceMsInput {
87    #[serde(default)]
88    pub since_ms: u64,
89    #[serde(default = "default_until_ms")]
90    pub until_ms: u64,
91}
92
93fn default_until_ms() -> u64 {
94    u64::MAX
95}
96
97fn default_until_seq() -> u64 {
98    u64::MAX
99}
100
101#[derive(Debug, Serialize, Deserialize)]
102#[serde(deny_unknown_fields)]
103pub struct HistoryInput {
104    pub key: String,
105    #[serde(default = "default_history_limit")]
106    pub limit: u64,
107}
108
109#[derive(Debug, Serialize, Deserialize)]
110#[serde(deny_unknown_fields)]
111pub struct HistorySinceInput {
112    pub key: String,
113    pub since_ts: u64,
114    #[serde(default = "default_history_limit")]
115    pub limit: u64,
116}
117
118fn default_history_limit() -> u64 {
119    50
120}
121
122#[derive(Debug, Serialize, Deserialize)]
123#[serde(deny_unknown_fields)]
124pub struct SessionCheckConsultedInput {
125    pub key: String,
126}
127
128#[derive(Debug, Serialize, Deserialize)]
129#[serde(deny_unknown_fields)]
130pub struct SessionCheckConsultedRecentInput {
131    pub key: String,
132    #[serde(default = "default_ttl_secs")]
133    pub ttl_secs: u64,
134}
135
136fn default_ttl_secs() -> u64 {
137    900
138}
139
140#[derive(Debug, Serialize, Deserialize)]
141#[serde(deny_unknown_fields)]
142pub struct MemQueryInput {
143    pub query: String,
144    #[serde(default = "default_query_mode")]
145    pub mode: QueryMode,
146    #[serde(default = "default_query_limit")]
147    pub limit: u32,
148    /// Look-back window in days for time-scoped modes (`policy_activity`).
149    /// Ignored by other modes; `None` or `0` falls back to the mode's own
150    /// default, and values above the retention horizon are capped.
151    #[serde(default)]
152    pub since: Option<u64>,
153}
154
155fn default_query_mode() -> QueryMode {
156    QueryMode::Text
157}
158
159fn default_query_limit() -> u32 {
160    20
161}
162
163/// Search mode for mem_query.
164#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
165#[serde(rename_all = "snake_case")]
166pub enum QueryMode {
167    /// BM25 full-text search over record keys, values, and tags.
168    Text,
169    /// Filter records by tag (substring, case-insensitive).
170    Tag,
171    /// 1-hop graph traversal from a seed key.
172    Graph,
173    /// Confirmed gotchas whose `affected_files` fall under a path prefix.
174    /// `query` is a repo-relative directory or file path; empty returns nothing.
175    /// Resolves against the canonical `gotcha:*` records, not the search index.
176    DirGotchas,
177    /// Semantic search (requires --features semantic).
178    Semantic,
179    /// Shadow observations for local policies (`analytics:policy_shadow_*`).
180    /// `query` selects one policy slug; empty means all.
181    PolicyObservations,
182    /// Activity report for active local policies over a look-back window.
183    /// `query` selects one policy slug (empty = all); `since` sets the days.
184    PolicyActivity,
185    /// Raw local analytics records (`analytics:*`). `query` names the aggregate
186    /// by key substring (e.g. `miss_`) and is required — an empty query returns
187    /// nothing rather than dumping every record. Bounded by `limit`.
188    Analytics,
189}
190
191// ── B. Read-with-side-effect inputs ─────────────────────────────────────────
192
193#[derive(Debug, Serialize, Deserialize)]
194#[serde(deny_unknown_fields)]
195pub struct MemGetInput {
196    pub key: String,
197    /// Worktree (and, when a subagent, subagent) scope for the consultation
198    /// receipt this call mints. Server-populated by `mati serve` from its own
199    /// process cwd — never client-supplied through the public tool schema,
200    /// which exposes only `key`.
201    #[serde(default)]
202    pub actor: Option<String>,
203}
204
205#[derive(Debug, Serialize, Deserialize)]
206#[serde(deny_unknown_fields)]
207pub struct MemBootstrapInput {
208    #[serde(default)]
209    pub context_files: Vec<String>,
210}
211
212// ── C. Semantic mutation inputs ─────────────────────────────────────────────
213
214/// Gotcha creation/update input. The client expresses intent only — the daemon
215/// derives confidence, quality, timestamps, and version.
216///
217/// `confirmed` is the one exception, and it is honoured only for a
218/// developer-originated write (`source: "developer_manual"`). Everything else,
219/// including every `mem_set` from an agent, is forced to `false` and must go
220/// through `GotchaConfirm`.
221#[derive(Debug, Serialize, Deserialize)]
222#[serde(deny_unknown_fields)]
223pub struct GotchaDraftInput {
224    /// Gotcha key, must match `gotcha:<slug>`.
225    pub key: String,
226    /// Actionable rule text (imperative verb).
227    pub rule: String,
228    /// Causality sentence explaining why this rule exists.
229    pub reason: String,
230    /// Severity level.
231    pub severity: Severity,
232    /// File paths this gotcha applies to.
233    #[serde(default)]
234    pub affected_files: Vec<String>,
235    /// Optional external reference URL.
236    #[serde(default)]
237    pub ref_url: Option<String>,
238    /// Optional tags.
239    #[serde(default)]
240    pub tags: Vec<String>,
241    /// Record-level priority.
242    #[serde(default)]
243    pub priority: Priority,
244    /// Record source — when set, the handler uses this instead of defaulting
245    /// to `ClaudeEnrich`. CLI `gotcha add` sends `DeveloperManual` here.
246    #[serde(default)]
247    pub source: Option<String>,
248    /// Developer-asserted confirmation, honoured only alongside
249    /// `source: "developer_manual"`. `mem_set` never sets either field, so an
250    /// agent cannot mint an enforcing gotcha without the `confirm` action.
251    #[serde(default)]
252    pub confirmed: bool,
253}
254
255#[derive(Debug, Serialize, Deserialize)]
256#[serde(deny_unknown_fields)]
257pub struct GotchaConfirmInput {
258    pub key: String,
259    /// True when the confirm came from an in-session elicitation accept (the
260    /// developer approved a prompt showing the rule), false for a CLI or
261    /// direct-mode confirm. Drives the enforcement event's reason code so the
262    /// audit chain records the strongest confirm channel distinctly. Defaults
263    /// to false so an older client that omits it deserializes cleanly.
264    #[serde(default)]
265    pub via_elicitation: bool,
266}
267
268#[derive(Debug, Serialize, Deserialize)]
269#[serde(deny_unknown_fields)]
270pub struct GotchaTombstoneInput {
271    pub key: String,
272}
273
274#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
275#[serde(rename_all = "snake_case")]
276pub enum PolicyWriteOp {
277    Create,
278    Edit,
279    Enable,
280    Disable,
281    Stage,
282    Delete,
283}
284
285#[derive(Debug, Serialize, Deserialize)]
286#[serde(deny_unknown_fields)]
287pub struct PolicyWriteInput {
288    pub op: PolicyWriteOp,
289    pub key: String,
290    #[serde(default)]
291    pub policy: Option<PolicyRecord>,
292    #[serde(default)]
293    pub stage: Option<StorePolicyStage>,
294}
295
296/// File enrichment input from LLM analysis (e.g., /mati-enrich workflow).
297/// The file record must already exist (created by init/reparse).
298///
299/// Fields that are daemon-managed and MUST NOT appear:
300/// - `gotcha_keys` (managed by gotcha lifecycle commands)
301/// - `imports` (derived from tree-sitter)
302/// - All structural/internal fields (unsafe_count, unwrap_count, etc.)
303#[derive(Debug, Serialize, Deserialize)]
304#[serde(deny_unknown_fields)]
305pub struct FileEnrichInput {
306    /// File path (maps to `file:<path>`).
307    pub path: String,
308    /// Purpose sentence (verb-led).
309    pub purpose: String,
310    /// Function/method entry points identified by enrichment.
311    #[serde(default)]
312    pub entry_points: Vec<String>,
313    /// Decision records that affect this file.
314    #[serde(default)]
315    pub decision_keys: Vec<String>,
316    /// TODO items found during enrichment.
317    #[serde(default)]
318    pub todos: Vec<String>,
319    /// Optional tags.
320    #[serde(default)]
321    pub tags: Vec<String>,
322    /// Record-level priority.
323    #[serde(default)]
324    pub priority: Priority,
325}
326
327#[derive(Debug, Serialize, Deserialize)]
328#[serde(deny_unknown_fields)]
329pub struct FileReparseInput {
330    pub path: String,
331}
332
333#[derive(Debug, Serialize, Deserialize)]
334#[serde(deny_unknown_fields)]
335pub struct FileEditHookInput {
336    pub path: String,
337}
338
339/// Path-only doc capture. The daemon reads the file from disk and extracts
340/// the doc comment — no content crosses the wire.
341#[derive(Debug, Serialize, Deserialize)]
342#[serde(deny_unknown_fields)]
343pub struct DocCaptureInput {
344    pub path: String,
345}
346
347#[derive(Debug, Serialize, Deserialize)]
348#[serde(deny_unknown_fields)]
349pub struct DecisionUpsertInput {
350    /// Key slug (daemon prepends `decision:`).
351    pub slug: String,
352    /// Human-readable summary ("We use X because Y").
353    pub value: String,
354    /// Concise decision summary (payload field).
355    pub summary: String,
356    /// Rationale text (payload field).
357    pub rationale: String,
358    /// Optional tags.
359    #[serde(default)]
360    pub tags: Vec<String>,
361    /// Record-level priority.
362    #[serde(default)]
363    pub priority: Priority,
364}
365
366#[derive(Debug, Serialize, Deserialize)]
367#[serde(deny_unknown_fields)]
368pub struct DevNoteUpsertInput {
369    /// If absent, daemon auto-generates `dev_note:<slug>-<timestamp>`.
370    /// If present, must match an existing `dev_note:*` key (update mode).
371    #[serde(default)]
372    pub key: Option<String>,
373    /// Freeform note text.
374    pub text: String,
375    /// Optional tags.
376    #[serde(default)]
377    pub tags: Vec<String>,
378    /// Record-level priority.
379    #[serde(default)]
380    pub priority: Priority,
381}
382
383#[derive(Debug, Serialize, Deserialize)]
384#[serde(deny_unknown_fields)]
385pub struct SessionLogInput {
386    /// The event type (closed enum, 14 variants).
387    pub event: SessionEvent,
388    /// The record key this event pertains to.
389    pub key: String,
390    /// The AI agent session (Claude Code `session_id`) that triggered this event,
391    /// for per-actor audit attribution (schema_version 2). Optional — absent for
392    /// older clients and agents that provide no session.
393    #[serde(default)]
394    pub session_id: Option<String>,
395    /// Receipt scope this event was decided at — the subagent `agent_id`, or
396    /// absent for the main thread. The daemon needs it to look up the receipt
397    /// that authorized an allow, since receipts are actor-scoped.
398    #[serde(default)]
399    pub actor: Option<String>,
400    /// SHA-256 of the gotcha state the hook decided on, for the enforcement
401    /// event's `decision_basis_hash`. Computed hook-side because that is where
402    /// the decision was made; a digest, never raw tool input.
403    #[serde(default)]
404    pub decision_basis_hash: Option<String>,
405}
406
407/// Exact payload received from Claude Code's InstructionsLoaded hook.
408#[derive(Debug, Serialize, Deserialize)]
409#[serde(deny_unknown_fields)]
410pub struct InstructionsLoadedInput {
411    pub payload: crate::hooks::decide::InstructionsLoadedPayload,
412}
413
414#[derive(Debug, Serialize, Deserialize)]
415#[serde(deny_unknown_fields)]
416pub struct PolicyShadowObserveInput {
417    pub policy_key: String,
418    pub action: Action,
419    pub would: crate::hooks::decide::ShadowOutcome,
420}
421
422/// Session analytics event types. Each maps to a daily aggregation key prefix.
423///
424/// `Hit` is NOT included — it has richer side effects and uses the separate
425/// `ConsultationHit` command.
426#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
427#[serde(rename_all = "snake_case")]
428pub enum SessionEvent {
429    Miss,
430    ComplianceMiss,
431    ComplianceHit,
432    /// Claude edit gate: an edit DEFERRED because a recent consultation receipt
433    /// exists. Records `AllowAfterReceipt` with an edit-specific reason code, so
434    /// the audit trail proves the edit (not just the read) was preceded by a
435    /// consult (Plane 2 evidence).
436    EditConsulted,
437    /// Claude edit gate: an edit was DENIED (stale or shell-evaded — no recent
438    /// consult). Records `Deny` with an edit-specific reason code.
439    EditBlocked,
440    /// Enterprise floor mandate: an unconsulted access to a consult-required path was DENIED.
441    /// Records `Deny` with reason `floor_consult_required` (distinct from gotcha denies).
442    FloorConsultMiss,
443    /// Local policy denied an unconsulted governed action.
444    PolicyConsultMiss,
445    /// Local policy allowed an action after its receipt was present.
446    PolicyConsultHit,
447    /// Local policy injected steering context without changing the decision.
448    PolicySteered,
449    CodexShellMiss,
450    /// Codex PRE-hook (`codex-pre-bash` / `codex-pre-apply-patch`) BLOCKED an
451    /// unconsulted access — the shell command or patch never ran. Records
452    /// `Deny`, unlike [`SessionEvent::CodexShellMiss`], which the POST-bash
453    /// hook fires after the fact when nothing was denied.
454    CodexShellBlocked,
455    /// An unclassified command mentioned a meaningful literal from an active
456    /// policy trigger; records a bypass signal without changing the decision.
457    UnclassifiedPolicyLiteralBypass,
458    Bootstrap,
459    PromptNudge,
460    /// Post-bash observed a command that passed `is_schema_introspection` but
461    /// whose leading word did not classify as `db_client` — an upstream
462    /// PreToolUse hook rewrote it (observed live: `rtk psql …`). The
463    /// consultation receipt this command should mint never mints, so a
464    /// `db_client` policy stays permanently unsatisfiable: a deadlock, not a
465    /// bypass. Diagnostic only; does not change any decision.
466    WrappedDbClientMiss,
467    /// The `FileDeleted` tombstone bypass fired on a caller-confirmed
468    /// deletion, suppressing a deny a qualifying confirmed gotcha would
469    /// otherwise have produced. Records `BypassDetected` — the decision
470    /// stayed `Tombstone` (allow), but the suppression is enforcement-
471    /// relevant and must reach the hash-chained log.
472    TombstoneBypassedDeny,
473}
474
475#[derive(Debug, Serialize, Deserialize)]
476#[serde(deny_unknown_fields)]
477pub struct ConsultationHitInput {
478    pub key: String,
479    /// Capture a content fingerprint when the target record exists.
480    #[serde(default = "default_capture_fingerprint")]
481    pub capture_fingerprint: bool,
482    #[serde(default)]
483    pub actor: Option<String>,
484    /// Claude session_id (the session) — for ReceiptMinted audit attribution.
485    #[serde(default)]
486    pub session_id: Option<String>,
487    /// Subagent agent_id when present (fallback attribution).
488    #[serde(default)]
489    pub agent_id: Option<String>,
490    /// How the consultation happened, recorded onto the receipt. `None` when
491    /// the caller's path is unattributed — see
492    /// `store::session::ConsultationReceipt::source`. Defaulted so an older
493    /// hook binary talking to a newer daemon still mints.
494    #[serde(default)]
495    pub source: Option<crate::store::ReceiptSource>,
496    /// SHA-256 of the gotcha state in force when the receipt was minted.
497    #[serde(default)]
498    pub decision_basis_hash: Option<String>,
499}
500
501fn default_capture_fingerprint() -> bool {
502    true
503}
504
505/// Input for `Command::SubagentHarvest`. Written by the `SubagentStop` hook
506/// when a Task subagent finishes: its `last_assistant_message` is the subagent's
507/// own prose summary, captured into `session:summary:latest` so `mem_bootstrap`
508/// can surface it as `recent_session`. All fields but `summary` default, so an
509/// older hook binary talking to a newer daemon still writes.
510#[derive(Debug, Serialize, Deserialize)]
511#[serde(deny_unknown_fields)]
512pub struct SubagentHarvestInput {
513    /// The subagent's final assistant message — its own summary of the work.
514    pub summary: String,
515    /// Claude session_id the subagent ran under (the spawning session).
516    #[serde(default)]
517    pub session_id: Option<String>,
518    /// The subagent's own agent_id.
519    #[serde(default)]
520    pub agent_id: Option<String>,
521    /// Subagent type (e.g. "general-purpose").
522    #[serde(default)]
523    pub agent_type: Option<String>,
524    /// Path to the subagent's transcript, for later retrieval.
525    #[serde(default)]
526    pub transcript_path: Option<String>,
527}
528
529/// Input for `Command::SubagentSpawned`. Written by the `SubagentStart` hook:
530/// records a subagent's presence as a hash-chained enforcement event so the audit
531/// can attribute a subagent that spawned and never consulted. `agent_id` is
532/// required in practice (the presence is meaningless without it); the recorder
533/// no-ops when it is absent. All fields default for hook/daemon version skew.
534#[derive(Debug, Serialize, Deserialize)]
535#[serde(deny_unknown_fields)]
536pub struct SubagentSpawnedInput {
537    /// The spawned subagent's own agent_id.
538    #[serde(default)]
539    pub agent_id: Option<String>,
540    /// The spawning session_id.
541    #[serde(default)]
542    pub session_id: Option<String>,
543    /// Subagent type (e.g. "general-purpose"), preserved for audit scoring.
544    #[serde(default)]
545    pub agent_type: Option<String>,
546}
547
548/// Input for `Command::SubagentEdge`. Written by the `Agent`-tool `PostToolUse`
549/// hook when one subagent spawns another: records the parent→child spawn edge as
550/// a hash-chained enforcement event so the audit can walk the tree past the leaf.
551/// `child_agent_id` (the spawned subagent) and `parent_agent_id` (its spawner)
552/// are both required in practice; the recorder no-ops when either is absent —
553/// a root-session spawn (no parent) is already covered by `SubagentSpawned`. All
554/// fields default for hook/daemon version skew.
555#[derive(Debug, Serialize, Deserialize)]
556#[serde(deny_unknown_fields)]
557pub struct SubagentEdgeInput {
558    /// The spawned child subagent's own agent_id (`tool_response.agentId`).
559    #[serde(default)]
560    pub child_agent_id: Option<String>,
561    /// The spawning parent subagent's agent_id (top-level `agent_id`).
562    #[serde(default)]
563    pub parent_agent_id: Option<String>,
564    /// The shared session_id.
565    #[serde(default)]
566    pub session_id: Option<String>,
567    /// Child subagent type (e.g. "general-purpose"), preserved for audit scoring.
568    #[serde(default)]
569    pub agent_type: Option<String>,
570}
571
572/// Input for `Command::RecordImport`. Records are written verbatim into the
573/// knowledge tree, preserving every field. The daemon validates each record's
574/// key prefix against the knowledge-namespace allowlist before writing.
575#[derive(Debug, Serialize, Deserialize)]
576#[serde(deny_unknown_fields)]
577pub struct RecordImportInput {
578    pub records: Vec<crate::store::Record>,
579}
580
581/// Input for `Command::ConfigGet`. `key` is the dotted config name
582/// (e.g. `audit.write_durability`, `enforcement.retention`).
583#[derive(Debug, Serialize, Deserialize)]
584#[serde(deny_unknown_fields)]
585pub struct ConfigGetInput {
586    pub key: String,
587}
588
589/// Input for `Command::ConfigSet`. Values are always sent as strings on the
590/// wire and parsed/validated by the dispatcher.
591#[derive(Debug, Serialize, Deserialize)]
592#[serde(deny_unknown_fields)]
593pub struct ConfigSetInput {
594    pub key: String,
595    pub value: String,
596}
597
598/// Input for `Command::SandboxAudit`. The dispatcher records an
599/// `EnforcementConfigChanged` event verbatim from these fields.
600#[derive(Debug, Serialize, Deserialize)]
601#[serde(deny_unknown_fields)]
602pub struct SandboxAuditInput {
603    pub setting: String,
604    /// Value mati expected before the observed configuration change. Older
605    /// clients may omit this because sandbox audits historically had no
606    /// before-value; the guard supplies it for ConfigChange events.
607    #[serde(default)]
608    pub old_value: String,
609    pub new_value: String,
610    pub reason: String,
611}
612
613// ── Shared enums ────────────────────────────────────────────────────────────
614
615/// Severity level for gotcha records. Closed enum.
616#[derive(Debug, Clone, Default, Deserialize, Serialize, PartialEq, Eq)]
617#[serde(rename_all = "snake_case")]
618pub enum Severity {
619    Critical,
620    High,
621    #[default]
622    Normal,
623    Low,
624}
625
626/// Record-level priority. Closed enum.
627#[derive(Debug, Clone, Default, Deserialize, Serialize, PartialEq, Eq)]
628#[serde(rename_all = "snake_case")]
629pub enum Priority {
630    Critical,
631    High,
632    #[default]
633    Normal,
634    Low,
635}
636
637// ── Conversions from store types ────────────────────────────────────────────
638
639impl From<crate::store::Priority> for Severity {
640    fn from(p: crate::store::Priority) -> Self {
641        match p {
642            crate::store::Priority::Low => Severity::Low,
643            crate::store::Priority::Normal => Severity::Normal,
644            crate::store::Priority::High => Severity::High,
645            crate::store::Priority::Critical => Severity::Critical,
646        }
647    }
648}
649
650impl From<crate::store::Priority> for Priority {
651    fn from(p: crate::store::Priority) -> Self {
652        match p {
653            crate::store::Priority::Low => Priority::Low,
654            crate::store::Priority::Normal => Priority::Normal,
655            crate::store::Priority::High => Priority::High,
656            crate::store::Priority::Critical => Priority::Critical,
657        }
658    }
659}
660
661// ── Command helpers ──────────────────────────────────────────────────────────