Skip to main content

heddle_cli_contract/cli/commands/
schemas.rs

1// SPDX-License-Identifier: Apache-2.0
2//! `heddle schemas <verb>` — runtime introspection for CLI JSON output shapes.
3//!
4//! This module owns the JSON schema mirrors for `--output json`-emitting
5//! verbs. Schema registration metadata comes from the active command
6//! catalog; the schemas themselves are schemars-derived mirror structs
7//! rather than threading
8//! `JsonSchema` through every output struct in the workspace
9//! (`repo`, `objects`, etc.). The cost: when a real output struct
10//! changes, the mirror here must change too. The benefit:
11//! `heddle doctor schemas` validates the documented samples against
12//! these schemas, catching the mirror drift the same way it catches
13//! doc drift.
14//!
15//! See [`super::doctor_schemas`] for the drift checker.
16
17use std::{collections::BTreeMap, sync::OnceLock};
18
19use anyhow::{Result, anyhow};
20use heddle_core::{DiffReport, FsckReport, QueryReport, StatusReport, VerifyReport};
21use schemars::{JsonSchema, schema_for};
22use serde::Serialize;
23use serde_json::Value;
24
25use super::{RecoveryAdvice, command_catalog};
26use crate::cli::{Cli, should_output_json};
27
28static SCHEMA_VERBS: OnceLock<Vec<&'static str>> = OnceLock::new();
29static DOCUMENTED_SCHEMA_VERBS: OnceLock<Vec<&'static str>> = OnceLock::new();
30static OPAQUE_SCHEMA_VERBS: OnceLock<Vec<&'static str>> = OnceLock::new();
31
32macro_rules! schema_registry {
33    ($(($verbs:expr, $schema:ty)),+ $(,)?) => {
34        fn schema_for_registered_verb(verb: &str) -> Option<Value> {
35            $(
36                if $verbs.contains(&verb) {
37                    let root = schema_for!($schema);
38                    return serde_json::to_value(&root).ok();
39                }
40            )+
41            None
42        }
43
44        #[cfg(test)]
45        fn schema_implementation_verbs() -> Vec<&'static str> {
46            let mut verbs = report_contract_schema_verbs().to_vec();
47            $(
48                for verb in $verbs {
49                    if !verbs.contains(verb) {
50                        verbs.push(*verb);
51                    }
52                }
53            )+
54            verbs
55        }
56    };
57}
58
59#[cfg(test)]
60fn report_contract_schema_verbs() -> &'static [&'static str] {
61    &[
62        QueryReport::CONTRACT.schema_name,
63        DiffReport::CONTRACT.schema_name,
64        FsckReport::CONTRACT.schema_name,
65        StatusReport::CONTRACT.schema_name,
66        VerifyReport::CONTRACT.schema_name,
67    ]
68}
69
70schema_registry! {
71    (&["fsck repair git"], FsckReport),
72    (&["init"], InitSchema),
73    (&["adopt"], AdoptSchema),
74    (&["capture"], CaptureSchema),
75    (&["commit"], CommitSchema),
76    (&["undo", "undo --redo", "undo --recover"], UndoSchema),
77    (&["undo --list"], UndoListSchema),
78    (&["ready"], ReadySchema),
79    (&["land"], LandSchema),
80    (&["land --threads"], LandBatchSchema),
81    (&["sync"], SyncSchema),
82    (&["continue", "abort"], OperatorCommandSchema),
83    (&["start"], ThreadStartSchema),
84    (&["thread create", "thread switch", "thread rename"], ThreadStartSchema),
85    (&["thread current"], ThreadCurrentSchema),
86    (&["thread captures"], ThreadCapturesSchema),
87    (&["thread refresh", "thread drop"], ThreadCommandSchema),
88    (&["thread promote"], ThreadCommandSchema),
89    (&["thread move"], ThreadMoveSchema),
90    (&["thread absorb"], ThreadAbsorbSchema),
91    (&["thread resolve"], ThreadResolveSchema),
92    (&["thread approve"], ThreadApprovalSchema),
93    (&["thread approvals"], ThreadApprovalListSchema),
94    (&["thread revoke-approval"], ThreadRevokeApprovalSchema),
95    (&["thread check-merge"], ThreadMergeEligibilitySchema),
96    (&["thread cleanup"], ThreadCleanupSchema),
97    (&["thread marker list"], ThreadMarkerListSchema),
98    (&["thread marker create", "thread marker delete", "thread marker show"], ThreadMarkerOpSchema),
99    (&["thread show"], ThreadShowSchema),
100    (&["clone"], CloneSchema),
101    (&["remote list"], RemoteListSchema),
102    (&["remote show"], RemoteInfoSchema),
103    (&["remote add", "remote remove", "remote set-default"], RemoteMutationSchema),
104    (&["pull"], PullSchema),
105    (&["push"], PushSchema),
106    (&["expand"], ExpandSchema),
107    (&["log"], LogSchema),
108    (&["log --reflog"], LogReflogSchema),
109    (&["log --timeline"], TimelineLogSchema),
110    (&["timeline status"], TimelineStatusSchema),
111    (&["timeline record-start", "timeline record-finish"], TimelineRecordingSchema),
112    (&["timeline fork", "timeline reset", "timeline recover"], TimelineActionSchema),
113    (&["show"], ShowSchema),
114    (&["thread list"], ThreadListSchema),
115    (&["schemas"], SchemasListSchema),
116    (&["review show"], ReviewShowSchema),
117    (&["review sign"], ReviewSignSchema),
118    (&["review next"], ReviewNextSchema),
119    (&["review health"], ReviewHealthSchema),
120    (&["retro"], RetroSchema),
121    (&["discuss open", "discuss append", "discuss resolve", "discuss reopen"], DiscussionWriteSchema),
122    (&["discuss show"], DiscussionShowSchema),
123    (&["discuss list"], DiscussionListSchema),
124    (&["query --attribution"], BlameSchema),
125    (&["export git"], ExportGitSchema),
126    (&["import git"], ImportGitSchema),
127    (&["sync git"], SyncGitSchema),
128    (&["revert"], RevertSchema),
129    (&["doctor"], DoctorSchema),
130    (&["doctor docs"], DoctorDocsSchema),
131    (&["doctor schemas"], DoctorSchemasSchema),
132    (&["agent presence show"], AgentPresenceSingleSchema),
133    (&["agent presence list"], AgentPresenceListSchema),
134    (&["agent presence complete"], AgentPresenceCompleteSchema),
135    (&["agent presence explain"], AgentPresenceExplainSchema),
136    (&["agent reserve", "agent heartbeat", "agent release"], AgentReservationEnvelopeSchema),
137    (&["agent capture"], CaptureSchema),
138    (&["agent ready"], ReadySchema),
139    (&["agent list"], AgentReservationListSchema),
140    (&["agent task create", "agent task show", "agent task update"], AgentTaskEnvelopeSchema),
141    (&["agent task list"], AgentTaskListSchema),
142    (&["agent fanout plan", "agent fanout start"], AgentFanoutSchema),
143    (&["auth logout"], AuthLogoutSchema),
144    (&["auth status"], AuthStatusSchema),
145    (&["whoami"], WhoamiSchema),
146    (&["auth create-service-token"], AuthCreateServiceTokenSchema),
147    (&["agent provenance begin", "agent provenance end", "agent provenance show"], AgentProvenanceEnvelopeSchema),
148    (&["agent provenance segment"], AgentProvenanceSegmentEnvelopeSchema),
149    (&["agent provenance list"], AgentProvenanceListSchema),
150    (&["watch"], WatchLineSchema),
151    (&["integration list", "integration doctor"], IntegrationStatusListSchema),
152    (&["try"], TrySchema),
153    (&["resolve"], ResolveSchema),
154    (&["maintenance inspect"], MaintenanceInspectSchema),
155    (&["maintenance refresh"], MaintenanceRefreshSchema),
156    (&["error"], ErrorEnvelopeSchema),
157}
158
159/// All verbs whose `--output json` output has a schema mirror, derived from
160/// the active command catalog.
161pub fn schema_verbs() -> &'static [&'static str] {
162    SCHEMA_VERBS
163        .get_or_init(command_catalog::schema_verbs)
164        .as_slice()
165}
166
167/// Schema verbs that `heddle doctor schemas` must check against
168/// `docs/json-schemas.md`, derived from the active command catalog.
169pub fn documented_schema_verbs() -> &'static [&'static str] {
170    DOCUMENTED_SCHEMA_VERBS
171        .get_or_init(command_catalog::documented_schema_verbs)
172        .as_slice()
173}
174
175/// Runtime schema verbs that intentionally expose only an opaque JSON
176/// object shape. Coverage reports count these separately from
177/// concrete schema mirrors.
178pub(crate) fn opaque_schema_verbs() -> &'static [&'static str] {
179    OPAQUE_SCHEMA_VERBS
180        .get_or_init(command_catalog::opaque_schema_verbs)
181        .as_slice()
182}
183
184/// Generate the schema for `verb`. Returns `None` if no schema is registered.
185pub fn schema_for_verb(verb: &str) -> Option<Value> {
186    let verb = verb.trim();
187    if !schema_verbs().contains(&verb) {
188        return None;
189    }
190    let mut schema = schema_for_registered_verb(verb)
191        .or_else(|| schema_for_report_contract_verb(verb))
192        .or_else(|| {
193            opaque_schema_verbs()
194                .contains(&verb)
195                .then(|| serde_json::to_value(schema_for!(GenericJsonObjectSchema)).ok())
196                .flatten()
197        })?;
198    add_op_id_replay_fields_if_supported(verb, &mut schema);
199    add_json_discriminator_if_advertised(verb, &mut schema);
200    stabilize_land_output_shapes(verb, &mut schema);
201    Some(schema)
202}
203
204fn require_object_fields(schema: &mut Value, fields: &[&str]) {
205    let Some(object) = schema.as_object_mut() else {
206        return;
207    };
208    let required = object
209        .entry("required".to_string())
210        .or_insert_with(|| Value::Array(Vec::new()));
211    let Some(required) = required.as_array_mut() else {
212        return;
213    };
214    for field in fields {
215        if !required.iter().any(|value| value.as_str() == Some(field)) {
216            required.push(Value::String((*field).to_string()));
217        }
218    }
219}
220
221fn stabilize_land_output_shapes(verb: &str, schema: &mut Value) {
222    match verb {
223        "land" => require_object_fields(schema, &["siblings_restacked", "siblings_restack_failed"]),
224        "land --threads" => {
225            require_object_fields(
226                schema,
227                &[
228                    "stopped_at",
229                    "git_head",
230                    "recommended_action",
231                    "verification",
232                ],
233            );
234            if let Some(peer) = schema
235                .get_mut("$defs")
236                .and_then(Value::as_object_mut)
237                .and_then(|defs| defs.get_mut("LandBatchPeerSchema"))
238            {
239                require_object_fields(
240                    peer,
241                    &[
242                        "siblings_restacked",
243                        "siblings_restack_failed",
244                        "blockers",
245                        "warnings",
246                        "recovery_commands",
247                    ],
248                );
249            }
250        }
251        _ => {}
252    }
253}
254
255fn schema_for_report_contract_verb(verb: &str) -> Option<Value> {
256    match verb {
257        verb if verb == QueryReport::CONTRACT.schema_name => Some((QueryReport::CONTRACT.schema)()),
258        verb if verb == DiffReport::CONTRACT.schema_name => Some((DiffReport::CONTRACT.schema)()),
259        verb if verb == FsckReport::CONTRACT.schema_name => Some((FsckReport::CONTRACT.schema)()),
260        verb if verb == StatusReport::CONTRACT.schema_name => {
261            Some((StatusReport::CONTRACT.schema)())
262        }
263        verb if verb == VerifyReport::CONTRACT.schema_name => {
264            Some((VerifyReport::CONTRACT.schema)())
265        }
266        _ => None,
267    }
268}
269
270#[cfg(test)]
271const OP_ID_REPLAY_FIELD_NAMES: &[&str] = &[
272    "op_id",
273    "operation_record",
274    "idempotency_status",
275    "replayed",
276];
277
278fn add_op_id_replay_fields_if_supported(verb: &str, schema: &mut Value) {
279    if !schema_verb_supports_op_id(verb) {
280        return;
281    }
282
283    let Some(object) = schema.as_object_mut() else {
284        return;
285    };
286    let properties = object
287        .entry("properties".to_string())
288        .or_insert_with(|| serde_json::json!({}));
289    let Some(properties) = properties.as_object_mut() else {
290        return;
291    };
292
293    properties
294        .entry("op_id".to_string())
295        .or_insert_with(|| serde_json::json!({ "type": ["string", "null"] }));
296    properties
297        .entry("idempotency_status".to_string())
298        .or_insert_with(|| serde_json::json!({ "type": ["string", "null"] }));
299    properties
300        .entry("replayed".to_string())
301        .or_insert_with(|| serde_json::json!({ "type": ["boolean", "null"] }));
302    properties
303        .entry("operation_record".to_string())
304        .or_insert_with(|| {
305            serde_json::json!({
306                "anyOf": [
307                    {
308                        "type": "object",
309                        "properties": {
310                            "op_id": { "type": "string" },
311                            "command": { "type": "string" },
312                            "idempotency_status": { "type": "string" },
313                            "replayed": { "type": "boolean" }
314                        },
315                        "required": [
316                            "command",
317                            "idempotency_status",
318                            "op_id",
319                            "replayed"
320                        ]
321                    },
322                    { "type": "null" }
323                ]
324            })
325        });
326}
327
328fn add_json_discriminator_if_advertised(verb: &str, schema: &mut Value) {
329    let mut discriminators = command_catalog::command_json_discriminators_for_schema_verb(verb);
330    if schema.get("anyOf").is_some() {
331        for discriminator in command_catalog::command_json_discriminators()
332            .into_iter()
333            .filter(|discriminator| {
334                discriminator.display == verb && discriminator.schema_verb.as_deref() != Some(verb)
335            })
336        {
337            discriminators.push(discriminator);
338        }
339    }
340    discriminators.sort_by(|left, right| {
341        (&left.field, &left.value, &left.display).cmp(&(&right.field, &right.value, &right.display))
342    });
343    discriminators.dedup_by(|left, right| left.field == right.field && left.value == right.value);
344
345    if discriminators.is_empty() {
346        return;
347    };
348
349    if add_json_discriminators_to_union_branches(verb, schema, &discriminators) {
350        return;
351    }
352
353    let field = discriminators[0].field.as_str();
354    let values = discriminators
355        .iter()
356        .filter(|discriminator| discriminator.field == field)
357        .map(|discriminator| discriminator.value.as_str())
358        .collect::<Vec<_>>();
359    add_json_discriminator_to_schema_object(schema, field, &values);
360}
361
362fn add_json_discriminators_to_union_branches(
363    verb: &str,
364    schema: &mut Value,
365    discriminators: &[command_catalog::CommandJsonDiscriminator],
366) -> bool {
367    let Some(branches) = schema
368        .get_mut("anyOf")
369        .and_then(|value| value.as_array_mut())
370    else {
371        return false;
372    };
373
374    let mut injected = 0usize;
375    for branch in branches {
376        let Some(branch_ref) = branch
377            .get("$ref")
378            .and_then(|value| value.as_str())
379            .map(str::to_string)
380        else {
381            continue;
382        };
383        let Some(discriminator) = discriminator_for_union_branch(verb, &branch_ref, discriminators)
384        else {
385            continue;
386        };
387        let original_branch = branch.clone();
388        let mut discriminator_schema = serde_json::json!({ "type": "object" });
389        add_json_discriminator_to_schema_object(
390            &mut discriminator_schema,
391            &discriminator.field,
392            &[&discriminator.value],
393        );
394        *branch = serde_json::json!({
395            "allOf": [original_branch, discriminator_schema],
396        });
397        injected += 1;
398    }
399
400    injected > 0
401}
402
403fn discriminator_for_union_branch<'a>(
404    verb: &str,
405    branch_ref: &str,
406    discriminators: &'a [command_catalog::CommandJsonDiscriminator],
407) -> Option<&'a command_catalog::CommandJsonDiscriminator> {
408    if discriminators.len() == 1 {
409        return discriminators.first();
410    }
411
412    let def_name = schema_ref_name(branch_ref)?;
413    if verb == "inspect" {
414        let value = match def_name {
415            "ShowSchema" => "inspect_state",
416            "ThreadShowSchema" => "thread_show",
417            _ => return None,
418        };
419        return discriminators
420            .iter()
421            .find(|discriminator| discriminator.value == value);
422    }
423
424    None
425}
426
427fn schema_ref_name(reference: &str) -> Option<&str> {
428    reference
429        .strip_prefix("#/$defs/")
430        .or_else(|| reference.strip_prefix("#/definitions/"))
431}
432
433fn add_json_discriminator_to_schema_object(schema: &mut Value, field: &str, values: &[&str]) {
434    let enum_values = values
435        .iter()
436        .map(|value| Value::String((*value).to_string()))
437        .collect::<Vec<_>>();
438
439    let Some(object) = schema.as_object_mut() else {
440        return;
441    };
442    let properties = object
443        .entry("properties".to_string())
444        .or_insert_with(|| serde_json::json!({}));
445    let Some(properties) = properties.as_object_mut() else {
446        return;
447    };
448    properties.insert(
449        field.to_string(),
450        serde_json::json!({
451            "type": "string",
452            "enum": enum_values,
453        }),
454    );
455
456    let required = object
457        .entry("required".to_string())
458        .or_insert_with(|| serde_json::json!([]));
459    let Some(required) = required.as_array_mut() else {
460        return;
461    };
462    if !required
463        .iter()
464        .any(|required_field| required_field.as_str() == Some(field))
465    {
466        required.push(Value::String(field.to_string()));
467    }
468}
469
470fn schema_verb_supports_op_id(verb: &str) -> bool {
471    command_catalog::command_runtime_contract_for_schema_verb(verb)
472        .is_some_and(|contract| contract.supports_op_id)
473}
474
475/// Public entrypoint for `heddle schemas [<verb>]`.
476///
477/// `verb_parts` are the raw trailing tokens after `schemas`.
478/// Lookup is a flat string match after filtering global flags that
479/// clap cannot see because the schema command intentionally accepts
480/// literal command flags like `--preview` and `--reflog`.
481pub fn cmd_schemas(cli: &Cli, verb_parts: &[String]) -> Result<()> {
482    let verb_parts = normalize_schema_verb_parts(verb_parts)?;
483    if verb_parts.is_empty() {
484        let out = serde_json::json!({
485            "output_kind": "schemas",
486            "status": "completed",
487            "schema_verbs": schema_verbs(),
488            "documented_schema_verbs": documented_schema_verbs(),
489        });
490        return render_schema_json(&out);
491    }
492
493    let verb = verb_parts.join(" ");
494    let schema = schema_for_verb(&verb)
495        .ok_or_else(|| anyhow!(schema_not_registered_advice(&verb, schema_verbs())))?;
496
497    // `heddle schemas` always emits machine-readable JSON.
498    let _json = should_output_json(cli, None);
499    render_schema_json(&schema)
500}
501
502fn render_schema_json(value: &serde_json::Value) -> Result<()> {
503    println!("{}", serde_json::to_string_pretty(value)?);
504    Ok(())
505}
506
507fn schema_not_registered_advice(verb: &str, known_verbs: &[&str]) -> RecoveryAdvice {
508    let matches = suggested_schema_verbs(verb, known_verbs);
509    let primary_command = matches
510        .first()
511        .map(|matched| format!("heddle schemas {matched}"))
512        .unwrap_or_else(|| "heddle schemas".to_string());
513    let hint = if matches.is_empty() {
514        "Run `heddle schemas` to list schema-backed verbs, or inspect the command catalog with `heddle help --output json`.".to_string()
515    } else {
516        format!(
517            "`{verb}` is not exact; available schema verb{}: {}.",
518            if matches.len() == 1 { "" } else { "s" },
519            matches
520                .iter()
521                .map(|matched| format!("`{matched}`"))
522                .collect::<Vec<_>>()
523                .join(", ")
524        )
525    };
526    let mut recovery_commands = Vec::new();
527    push_unique_command(&mut recovery_commands, primary_command.clone());
528    push_unique_command(&mut recovery_commands, "heddle schemas".to_string());
529    push_unique_command(
530        &mut recovery_commands,
531        "heddle help --output json".to_string(),
532    );
533    for matched in matches.iter().skip(1) {
534        push_unique_command(&mut recovery_commands, format!("heddle schemas {matched}"));
535    }
536
537    RecoveryAdvice::safety_refusal(
538        "schema_not_registered",
539        format!("No JSON schema is registered for `{verb}`"),
540        hint,
541        format!("`{verb}` is not in the runtime schema registry"),
542        "schema lookup does not change repository state; retrying the same unknown verb will fail until a schema is registered",
543        "no repository objects, refs, metadata, or worktree files were changed",
544        primary_command,
545        recovery_commands,
546    )
547}
548
549fn suggested_schema_verbs<'a>(verb: &str, known_verbs: &'a [&'a str]) -> Vec<&'a str> {
550    let exactish = matching_schema_verbs(verb, known_verbs);
551    if !exactish.is_empty() {
552        return exactish;
553    }
554
555    let normalized = verb.trim();
556    if normalized.is_empty() {
557        return Vec::new();
558    }
559    let bare = command_catalog::schema_verb_without_flags(normalized);
560    if bare.is_empty() {
561        return Vec::new();
562    }
563
564    known_verbs
565        .iter()
566        .copied()
567        .filter(|known| {
568            known.starts_with(normalized)
569                || command_catalog::schema_verb_without_flags(known).starts_with(&bare)
570        })
571        .take(5)
572        .collect()
573}
574
575fn push_unique_command(commands: &mut Vec<String>, command: String) {
576    if !commands.iter().any(|existing| existing == &command) {
577        commands.push(command);
578    }
579}
580
581fn matching_schema_verbs<'a>(verb: &str, known_verbs: &'a [&'a str]) -> Vec<&'a str> {
582    let normalized = verb.trim();
583    if normalized.is_empty() {
584        return Vec::new();
585    }
586    let bare = command_catalog::schema_verb_without_flags(normalized);
587    let prefix = format!("{bare} ");
588    known_verbs
589        .iter()
590        .copied()
591        .filter(|known| {
592            *known != normalized
593                && (*known == bare
594                    || known.starts_with(&prefix)
595                    || command_catalog::schema_verb_without_flags(known) == bare)
596        })
597        .collect()
598}
599
600fn normalize_schema_verb_parts(parts: &[String]) -> Result<Vec<String>> {
601    let mut normalized = Vec::new();
602    let mut iter = parts.iter();
603    while let Some(part) = iter.next() {
604        match part.as_str() {
605            "--no-color" | "-q" | "--quiet" | "-v" | "--verbose" => {}
606            "--output" | "--repo" => {
607                iter.next()
608                    .ok_or_else(|| anyhow!("missing value for `{part}`"))?;
609            }
610            _ if part.starts_with("--output=") || part.starts_with("--repo=") => {}
611            _ => normalized.push(part.clone()),
612        }
613    }
614    Ok(normalized)
615}
616
617// ---------------------------------------------------------------------------
618// Mirror types
619// ---------------------------------------------------------------------------
620//
621// Each mirror struct mirrors the JSON wire shape of a single
622// `--output json`-emitting verb. The struct's `serde` attributes match the
623// real serializer; the `schemars` derive produces a JSON Schema we
624// emit verbatim.
625//
626// When you add or rename a field on a real output struct, update the
627// matching mirror here and the entry in `docs/json-schemas.md`. CI
628// runs `heddle doctor schemas` which validates the doc samples
629// against these schemas.
630
631// ---- shared sub-types ------------------------------------------------------
632//
633// Variants here are referenced only through the schemars derive,
634// which the dead-code lint can't see. The annotation keeps the
635// surface honest without polluting downstream warnings.
636#[allow(dead_code)]
637#[derive(Debug, Serialize, JsonSchema)]
638#[serde(rename_all = "snake_case")]
639pub enum ThreadModeSchema {
640    Materialized,
641    Virtualized,
642    Solid,
643}
644
645#[allow(dead_code)]
646#[derive(Debug, Serialize, JsonSchema)]
647#[serde(rename_all = "snake_case")]
648pub enum ThreadStateSchema {
649    Draft,
650    Active,
651    Ready,
652    Blocked,
653    Merged,
654    Abandoned,
655    Promoted,
656}
657
658#[allow(dead_code)]
659#[derive(Debug, Serialize, JsonSchema)]
660#[serde(rename_all = "snake_case")]
661pub enum ThreadFreshnessSchema {
662    Current,
663    Stale,
664    Unknown,
665}
666
667#[allow(dead_code)]
668#[derive(Debug, Serialize, JsonSchema)]
669#[serde(rename_all = "snake_case")]
670pub enum ThreadImpactCategorySchema {
671    DependencyGraph,
672    BuildRuntimeConfig,
673    GeneratedOutputs,
674    RepoWideRefactor,
675    PublicApiSurface,
676}
677
678#[allow(dead_code)]
679#[derive(Debug, Serialize, JsonSchema)]
680#[serde(rename_all = "kebab-case")]
681pub enum CoordinationStatusSchema {
682    Clean,
683    Ahead,
684    Diverged,
685    Blocked,
686    MergeReady,
687}
688
689#[derive(Debug, Serialize, JsonSchema)]
690pub struct ActorInfoSchema {
691    pub provider: Option<String>,
692    pub model: Option<String>,
693}
694
695#[derive(Debug, Serialize, JsonSchema)]
696pub struct GenericJsonObjectSchema {
697    #[serde(flatten)]
698    pub fields: BTreeMap<String, Value>,
699}
700
701#[derive(Debug, Serialize, JsonSchema)]
702pub struct AuthLogoutSchema {
703    pub output_kind: String,
704    pub server: String,
705    pub removed: bool,
706    pub device_identity_removed: bool,
707}
708
709#[derive(Debug, Serialize, JsonSchema)]
710pub struct AuthStatusSchema {
711    pub output_kind: String,
712    pub server: String,
713    pub authenticated: bool,
714    pub proof_key_available: bool,
715    pub subject: Option<String>,
716    pub credential_id: Option<String>,
717    pub expires_at: Option<String>,
718    pub recommended_action: Option<String>,
719}
720
721#[derive(Debug, Serialize, JsonSchema)]
722pub struct WhoamiSchema {
723    pub output_kind: String,
724    pub server: String,
725    /// A usable credential is stored locally for this server.
726    pub authenticated: bool,
727    /// The server answered `WhoAmI`; `identity` below is authoritative.
728    pub reachable: bool,
729    /// `root`, `agent`, or `service-account`; null when unauthenticated.
730    pub token_kind: Option<String>,
731    /// Resource scopes as `kind:path` (empty ⇒ full resource authority).
732    pub scopes: Vec<String>,
733    /// Intersected hosted-operation ceiling; null ⇒ full authority.
734    pub operation_ceiling: Option<Vec<String>>,
735    pub expires_at: Option<String>,
736    /// Seconds until expiry; negative when already expired.
737    pub ttl_seconds_remaining: Option<i64>,
738    /// The device proof key required to sign hosted requests is present.
739    pub proof_key_available: bool,
740    pub identity: Option<WhoamiIdentitySchema>,
741    pub recommended_action: Option<String>,
742}
743
744#[derive(Debug, Serialize, JsonSchema)]
745pub struct WhoamiIdentitySchema {
746    pub subject: String,
747    pub actor_subject: String,
748    pub is_staff: bool,
749    pub is_service_account: bool,
750    pub is_biscuit: bool,
751    pub session_id: String,
752    pub amr: Vec<String>,
753    pub server_scope: String,
754    pub credential_id: String,
755    pub device_id: Option<String>,
756    pub agent_provider: Option<String>,
757    pub agent_model: Option<String>,
758    pub roles: Vec<WhoamiRoleSchema>,
759}
760
761#[derive(Debug, Serialize, JsonSchema)]
762pub struct WhoamiRoleSchema {
763    pub resource_path: String,
764    pub resource_kind: String,
765    /// One of `reader`, `developer`, `maintainer`, `admin`, `owner`.
766    pub role: String,
767}
768
769#[derive(Debug, Serialize, JsonSchema)]
770pub struct AuthCreateServiceTokenSchema {
771    pub output_kind: String,
772    pub name: String,
773    pub namespace: String,
774    pub scope: String,
775    /// Path to the `.hcred` credential file written with mode 0600. The token
776    /// and proof key live inside this file and never appear in the contract.
777    pub credential_path: String,
778    pub expires_in_days: u32,
779}
780
781#[derive(Debug, Serialize, JsonSchema)]
782pub struct IntegrationStatusListSchema(pub Vec<IntegrationStatusSchema>);
783
784#[derive(Debug, Serialize, JsonSchema)]
785pub struct IntegrationStatusSchema {
786    pub harness: String,
787    pub scope: String,
788    pub method: String,
789    pub status: String,
790    pub healthy: bool,
791    pub paths: Vec<String>,
792    pub capabilities: Vec<String>,
793    pub capability_paths: Vec<String>,
794    pub path_mode: String,
795}
796
797#[derive(Debug, Serialize, JsonSchema)]
798pub struct MaintenanceInspectSchema {
799    pub output_kind: String,
800    pub commit_graph: CommitGraphInspectionSchema,
801    pub worktree_index: WorktreeIndexInspectionSchema,
802    pub change_monitor: ChangeMonitorInspectionSchema,
803    pub refs: RefCountsInspectionSchema,
804    pub ref_summary_index: RefSummaryIndexInspectionSchema,
805    pub pack_files: PackFilesInspectionSchema,
806    pub partial_fetch: PartialFetchInspectionSchema,
807    pub pull_planner_cache: PullPlannerCacheInspectionSchema,
808}
809
810#[derive(Debug, Serialize, JsonSchema)]
811pub struct CommitGraphInspectionSchema {
812    pub present: bool,
813    pub node_count: usize,
814    pub bloom_covered_nodes: usize,
815    pub bytes: u64,
816    pub error: Option<String>,
817}
818
819#[derive(Debug, Serialize, JsonSchema)]
820pub struct WorktreeIndexInspectionSchema {
821    pub present: bool,
822    pub file_entries: usize,
823    pub directory_entries: usize,
824    pub untracked_directory_entries: usize,
825    pub snapshot_bytes: u64,
826    pub journal_bytes: u64,
827    pub journal_ops: usize,
828    pub journal_replay_ms: u128,
829    pub error: Option<String>,
830}
831
832#[derive(Debug, Serialize, JsonSchema)]
833pub struct ChangeMonitorInspectionSchema {
834    pub backend: String,
835    pub status: String,
836    pub reason: Option<String>,
837    pub changed_path_count: usize,
838}
839
840#[derive(Debug, Serialize, JsonSchema)]
841pub struct RefCountsInspectionSchema {
842    pub total: usize,
843    pub threads: usize,
844    pub markers: usize,
845    pub remotes: usize,
846    pub remote_threads: usize,
847    pub packed_refs_present: bool,
848    pub packed_refs_bytes: u64,
849}
850
851#[derive(Debug, Serialize, JsonSchema)]
852pub struct RefSummaryIndexInspectionSchema {
853    pub present: bool,
854    pub valid: bool,
855    pub bytes: u64,
856    pub threads: usize,
857    pub markers: usize,
858    pub remotes: usize,
859    pub remote_threads: usize,
860    pub packed_threads: usize,
861    pub packed_markers: usize,
862    pub error: Option<String>,
863}
864
865#[derive(Debug, Serialize, JsonSchema)]
866pub struct PackFilesInspectionSchema {
867    pub pack_count: usize,
868    pub index_count: usize,
869    pub unpaired_pack_count: usize,
870    pub pending_install_intents: usize,
871}
872
873#[derive(Debug, Serialize, JsonSchema)]
874pub struct PartialFetchInspectionSchema {
875    pub count: usize,
876    pub missing_blob_count: usize,
877}
878
879#[derive(Debug, Serialize, JsonSchema)]
880pub struct PullPlannerCacheInspectionSchema {
881    pub status: String,
882    pub present: bool,
883    pub manifest_count: usize,
884    pub planner_entry_count: usize,
885    pub total_bytes: u64,
886}
887
888#[derive(Debug, Serialize, JsonSchema)]
889pub struct MaintenanceRefreshSchema {
890    pub output_kind: String,
891    pub rebuilt_commit_graph: bool,
892    pub rebuilt_ref_summary_index: bool,
893    pub rebuilt_worktree_index: bool,
894    pub refreshed_change_monitor: bool,
895    pub rebuilt_pull_planner_cache: bool,
896    pub pruned_pull_planner_entries: usize,
897    pub pack_install_intents_recovered_completed: u64,
898    pub pack_install_intents_aborted: u64,
899    pub pack_install_intents_skipped_in_progress: u64,
900    pub pack_install_intents_quarantined: u64,
901    pub pack_install_metrics: PackInstallMetricsSchema,
902    pub unpaired_packs_pruned: u64,
903    pub unpaired_pack_bytes_freed: u64,
904    pub report: MaintenanceInspectReportSchema,
905}
906
907#[derive(Debug, Serialize, JsonSchema)]
908pub struct PackInstallMetricsSchema {
909    pub installs_ok: u64,
910    pub installs_err: u64,
911    pub recover_completed: u64,
912    pub recover_aborted: u64,
913    pub recover_skipped_in_progress: u64,
914    pub recover_quarantined: u64,
915}
916
917#[derive(Debug, Serialize, JsonSchema)]
918pub struct MaintenanceInspectReportSchema {
919    pub commit_graph: CommitGraphInspectionSchema,
920    pub worktree_index: WorktreeIndexInspectionSchema,
921    pub change_monitor: ChangeMonitorInspectionSchema,
922    pub refs: RefCountsInspectionSchema,
923    pub ref_summary_index: RefSummaryIndexInspectionSchema,
924    pub pack_files: PackFilesInspectionSchema,
925    pub partial_fetch: PartialFetchInspectionSchema,
926    pub pull_planner_cache: PullPlannerCacheInspectionSchema,
927}
928
929#[derive(Debug, Serialize, JsonSchema)]
930pub struct BlameSchema {
931    pub output_kind: Option<String>,
932    pub status: Option<String>,
933    pub file: String,
934    pub context: Vec<BlameContextSnippetSchema>,
935    pub lines: Vec<BlameLineSchema>,
936}
937
938#[derive(Debug, Serialize, JsonSchema)]
939pub struct BlameLineSchema {
940    pub line_number: usize,
941    pub content: String,
942    pub state_id: String,
943    pub principal: BlamePrincipalSchema,
944    pub agent: Option<BlameAgentSchema>,
945    pub timestamp: String,
946    pub origins: Option<Vec<BlameOriginSchema>>,
947}
948
949#[derive(Debug, Serialize, JsonSchema)]
950pub struct BlameOriginSchema {
951    pub state_id: String,
952    pub principal: BlamePrincipalSchema,
953    pub agent: Option<BlameAgentSchema>,
954    pub timestamp: String,
955}
956
957#[derive(Debug, Serialize, JsonSchema)]
958pub struct BlamePrincipalSchema {
959    pub name: String,
960    pub email: String,
961}
962
963#[derive(Debug, Serialize, JsonSchema)]
964pub struct BlameAgentSchema {
965    pub provider: String,
966    pub model: String,
967    #[serde(default, skip_serializing_if = "Option::is_none")]
968    pub session_id: Option<String>,
969    #[serde(default, skip_serializing_if = "Option::is_none")]
970    pub policy_id: Option<String>,
971}
972
973#[derive(Debug, Serialize, JsonSchema)]
974pub struct BlameContextSnippetSchema {
975    pub annotation_id: String,
976    pub kind: String,
977    pub content: String,
978    pub revision_count: usize,
979}
980
981#[derive(Debug, Serialize, JsonSchema)]
982pub struct ResolveSchema {
983    pub output_kind: String,
984    pub message: Option<String>,
985    pub resolved: Option<Vec<String>>,
986    pub remaining: Option<Vec<String>>,
987    pub conflicts: Option<Vec<String>>,
988    pub continued: Option<bool>,
989    pub continuation_status: Option<String>,
990    pub continuation_message: Option<String>,
991    pub next_action: Option<String>,
992    pub recommended_action: Option<String>,
993}
994
995#[allow(dead_code, clippy::large_enum_variant)]
996#[derive(Debug, Serialize, JsonSchema)]
997#[serde(untagged)]
998pub enum InspectSchema {
999    #[allow(dead_code)]
1000    State(ShowSchema),
1001    #[allow(dead_code)]
1002    Thread(ThreadShowSchema),
1003}
1004
1005#[derive(Debug, Serialize, JsonSchema)]
1006pub struct RetroSchema {
1007    pub since: Option<String>,
1008    pub until: Option<String>,
1009    pub duration_secs: Option<i64>,
1010    pub states_captured: Vec<RetroStateEntrySchema>,
1011    pub agents_active: Vec<RetroAgentEntrySchema>,
1012    pub agent_tasks: Vec<RetroAgentTaskEntrySchema>,
1013    pub timeline_steps: Vec<RetroTimelineStepEntrySchema>,
1014    pub markers_created: Vec<RetroMarkerEntrySchema>,
1015    pub context_annotations: Vec<RetroContextAnnotationEntrySchema>,
1016    pub verify_signals: Vec<RetroVerifySignalSchema>,
1017    pub merges: Vec<RetroOperationEntrySchema>,
1018    pub undos: Vec<RetroOperationEntrySchema>,
1019}
1020
1021#[derive(Debug, Serialize, JsonSchema)]
1022pub struct RetroStateEntrySchema {
1023    pub state_id: String,
1024    pub intent: Option<String>,
1025    pub confidence: Option<f32>,
1026    pub agent: Option<String>,
1027    pub principal: String,
1028    pub timestamp: String,
1029}
1030
1031#[derive(Debug, Serialize, JsonSchema)]
1032pub struct RetroAgentEntrySchema {
1033    pub session_id: String,
1034    pub provider: Option<String>,
1035    pub model: Option<String>,
1036    pub status: String,
1037    pub started_at: String,
1038    pub completed_at: Option<String>,
1039    pub tokens: RetroAgentTokensSchema,
1040}
1041
1042#[derive(Debug, Serialize, JsonSchema)]
1043pub struct RetroAgentTokensSchema {
1044    pub input: Option<u64>,
1045    pub output: Option<u64>,
1046    pub reasoning: Option<u64>,
1047    pub tool_calls: Option<u32>,
1048}
1049
1050#[derive(Debug, Serialize, JsonSchema)]
1051pub struct RetroAgentTaskEntrySchema {
1052    pub task_id: String,
1053    pub title: String,
1054    pub status: String,
1055    pub target_thread: String,
1056    pub updated_at: String,
1057    pub completed_at: Option<String>,
1058    pub coordination_discussion_id: Option<String>,
1059}
1060
1061#[derive(Debug, Serialize, JsonSchema)]
1062pub struct RetroTimelineStepEntrySchema {
1063    pub thread: String,
1064    pub step_id: String,
1065    pub branch_id: String,
1066    pub parent_step_id: Option<String>,
1067    pub tool_name: Option<String>,
1068    pub tool_status: Option<String>,
1069    pub changed: Option<bool>,
1070    pub payload_summary: Option<String>,
1071    pub payload_hash: Option<String>,
1072    pub before_state: Option<String>,
1073    pub after_state: Option<String>,
1074    pub capture_state: Option<String>,
1075    pub started_at_ms: Option<i64>,
1076    pub finished_at_ms: Option<i64>,
1077}
1078
1079#[derive(Debug, Serialize, JsonSchema)]
1080pub struct RetroMarkerEntrySchema {
1081    pub name: String,
1082    pub state: String,
1083    pub timestamp: String,
1084}
1085
1086#[derive(Debug, Serialize, JsonSchema)]
1087pub struct RetroContextAnnotationEntrySchema {
1088    pub path: String,
1089    pub scope: String,
1090    pub kind: String,
1091    pub content_excerpt: String,
1092    pub attribution: String,
1093    pub created_at: String,
1094}
1095
1096#[derive(Debug, Serialize, JsonSchema)]
1097pub struct RetroVerifySignalSchema {
1098    pub kind: String,
1099    pub label: String,
1100    pub timestamp: String,
1101}
1102
1103#[derive(Debug, Serialize, JsonSchema)]
1104pub struct RetroOperationEntrySchema {
1105    pub description: String,
1106    pub timestamp: String,
1107}
1108
1109#[derive(Debug, Serialize, JsonSchema)]
1110pub struct DiscussionSchema {
1111    pub id: String,
1112    pub title: String,
1113    pub anchor: DiscussionAnchorSchema,
1114    pub visibility: String,
1115    pub status: String,
1116    pub resolution: Option<DiscussionResolutionSchema>,
1117    pub conflict_operation_ids: Vec<String>,
1118    pub head_operation_ids: Vec<String>,
1119    pub display_head_operation_id: String,
1120    pub turns: Vec<DiscussionTurnSchema>,
1121}
1122
1123#[derive(Debug, Serialize, JsonSchema)]
1124pub struct DiscussionWriteSchema {
1125    pub output_kind: String,
1126    pub operation_id: String,
1127    pub disposition: String,
1128    pub discussion: DiscussionSchema,
1129}
1130
1131#[derive(Debug, Serialize, JsonSchema)]
1132pub struct DiscussionShowSchema {
1133    pub output_kind: String,
1134    pub discussion: DiscussionSchema,
1135}
1136
1137#[derive(Debug, Serialize, JsonSchema)]
1138pub struct DiscussionResolutionSchema {
1139    pub kind: String,
1140    pub annotation_id: Option<String>,
1141    pub state_id: Option<String>,
1142    pub change_id: Option<String>,
1143    pub reason: Option<String>,
1144}
1145
1146#[derive(Debug, Serialize, JsonSchema)]
1147pub struct DiscussionAnchorSchema {
1148    pub kind: String,
1149    pub state_id: Option<String>,
1150    pub change_id: Option<String>,
1151    pub path: Option<String>,
1152    pub symbol: Option<String>,
1153}
1154
1155#[derive(Debug, Serialize, JsonSchema)]
1156pub struct DiscussionTurnSchema {
1157    pub operation_id: String,
1158    pub author_name: String,
1159    pub author_email: String,
1160    pub agent: Option<String>,
1161    pub occurred_at_ms: i64,
1162    pub body: String,
1163    pub content_hash: String,
1164}
1165
1166#[derive(Debug, Serialize, JsonSchema)]
1167pub struct DiscussionListSchema {
1168    pub output_kind: String,
1169    pub discussions: Vec<DiscussionSchema>,
1170}
1171
1172// ---- core loop write/read helpers -----------------------------------------
1173
1174#[derive(Debug, Serialize, JsonSchema)]
1175pub struct InitSchema {
1176    pub output_kind: String,
1177    pub status: String,
1178    pub action: String,
1179    pub path: String,
1180    pub repository_mode: String,
1181    pub git_detected: bool,
1182    pub heddle_initialized: bool,
1183    pub installed_heddleignore: bool,
1184    pub principal_configured: bool,
1185    pub principal_status: String,
1186    pub principal_source: Option<String>,
1187    pub principal: Option<InitPrincipalSchema>,
1188    pub principal_recommended_action: Option<String>,
1189    pub side_effects: Vec<String>,
1190    pub message: String,
1191    pub next_action: Option<String>,
1192    pub recommended_action: Option<String>,
1193}
1194
1195#[derive(Debug, Serialize, JsonSchema)]
1196pub struct InitPrincipalSchema {
1197    pub name: String,
1198    pub email: String,
1199}
1200
1201#[derive(Debug, Serialize, JsonSchema)]
1202pub struct CaptureSchema {
1203    pub output_kind: Option<String>,
1204    pub status: String,
1205    pub action: String,
1206    pub state_id: String,
1207    pub content_hash: String,
1208    pub intent: Option<String>,
1209    pub confidence: Option<f32>,
1210    pub task_assignment_id: Option<String>,
1211    pub principal: CommitPrincipalSchema,
1212    pub agent: Option<CommitAgentSchema>,
1213    pub promotion_suggested: bool,
1214    pub heavy_impact_paths: Vec<String>,
1215    pub signed: bool,
1216    pub message: String,
1217    pub next_action: Option<String>,
1218    pub next_action_template: Option<ActionTemplateSchema>,
1219    pub recommended_action: Option<String>,
1220    pub recommended_action_template: Option<ActionTemplateSchema>,
1221}
1222
1223#[derive(Debug, Serialize, JsonSchema)]
1224pub struct CommitPrincipalSchema {
1225    pub name: String,
1226    pub email: String,
1227}
1228
1229#[derive(Debug, Serialize, JsonSchema)]
1230pub struct CommitAgentSchema {
1231    pub provider: String,
1232    pub model: String,
1233    #[serde(default, skip_serializing_if = "Option::is_none")]
1234    pub session_id: Option<String>,
1235    #[serde(default, skip_serializing_if = "Option::is_none")]
1236    pub segment_id: Option<String>,
1237    #[serde(default, skip_serializing_if = "Option::is_none")]
1238    pub policy_id: Option<String>,
1239}
1240
1241#[derive(Debug, Serialize, JsonSchema)]
1242pub struct CommitSchema {
1243    pub output_kind: String,
1244    pub action: String,
1245    pub status: String,
1246    pub state_id: String,
1247    pub git_commit: String,
1248    pub summary: String,
1249    pub recommended_action: Option<String>,
1250    pub recommended_action_template: Option<ActionTemplateSchema>,
1251    pub verification: RepositoryVerificationStateSchema,
1252}
1253
1254#[derive(Debug, Serialize, JsonSchema)]
1255pub struct OperatorCommandSchema {
1256    pub output_kind: Option<String>,
1257    pub status: String,
1258    pub action: String,
1259    pub message: String,
1260    pub blockers: Vec<String>,
1261    pub warnings: Vec<String>,
1262    pub next_action: Option<String>,
1263    pub next_action_template: Option<ActionTemplateSchema>,
1264    pub recommended_action: Option<String>,
1265    pub recommended_action_template: Option<ActionTemplateSchema>,
1266}
1267
1268#[derive(Debug, Serialize, JsonSchema)]
1269pub struct UndoSchema {
1270    pub output_kind: Option<String>,
1271    pub status: Option<String>,
1272    pub action: String,
1273    pub message: String,
1274    pub batches: Vec<Value>,
1275    pub next_action: Option<String>,
1276    pub next_action_template: Option<ActionTemplateSchema>,
1277    pub recommended_action: Option<String>,
1278    pub recommended_action_template: Option<ActionTemplateSchema>,
1279    /// The checkout-local state preserved for recovery, and its internal
1280    /// handle. Present on completed undo and recovery operations.
1281    #[serde(default, skip_serializing_if = "Option::is_none")]
1282    pub recovery_state: Option<String>,
1283    #[serde(default, skip_serializing_if = "Option::is_none")]
1284    pub recovery_marker: Option<String>,
1285}
1286
1287/// `heddle undo --list --output json` history view. Distinct from
1288/// [`UndoSchema`] (the rewind/redo payload): the list view carries only the
1289/// discriminator and the oplog batches, with none of the action/status/
1290/// recovery fields a real undo emits. Mirrors `OpListOutput` in `undo.rs`.
1291#[derive(Debug, Serialize, JsonSchema)]
1292pub struct UndoListSchema {
1293    pub output_kind: Option<String>,
1294    pub batches: Vec<Value>,
1295}
1296
1297#[derive(Debug, Serialize, JsonSchema)]
1298pub struct ReadySchema {
1299    pub output_kind: Option<String>,
1300    pub status: String,
1301    pub action: String,
1302    pub message: String,
1303    pub blockers: Vec<String>,
1304    pub warnings: Vec<String>,
1305    pub next_action: Option<String>,
1306    pub next_action_template: Option<ActionTemplateSchema>,
1307    pub recommended_action: Option<String>,
1308    pub recommended_action_template: Option<ActionTemplateSchema>,
1309    pub captured: bool,
1310    pub captured_state: Option<String>,
1311    pub thread_state: Option<String>,
1312    pub readiness: ReadyReadinessSchema,
1313    pub report: Value,
1314    #[serde(rename = "verification")]
1315    pub verification: RepositoryVerificationStateSchema,
1316}
1317
1318#[derive(Debug, Serialize, JsonSchema)]
1319pub struct ReadyReadinessSchema {
1320    pub status: String,
1321    pub captured: bool,
1322    pub captured_state: Option<String>,
1323    pub checks: ReadyChecksSchema,
1324    pub integration: String,
1325    pub freshness: String,
1326    pub merge_type: String,
1327    pub changed_path_count: usize,
1328    pub changed_paths: Vec<String>,
1329    pub conflict_count: usize,
1330    pub conflicts: Vec<String>,
1331    pub impact: String,
1332    pub impact_categories: Vec<String>,
1333    pub blockers: Vec<String>,
1334}
1335
1336#[derive(Debug, Serialize, JsonSchema)]
1337pub struct ReadyChecksSchema {
1338    pub status: String,
1339    pub reason: String,
1340}
1341
1342#[derive(Debug, Serialize, JsonSchema)]
1343pub struct SyncSchema {
1344    #[serde(flatten)]
1345    pub operator: OperatorCommandSchema,
1346    pub thread: Option<String>,
1347    pub current_state: Option<String>,
1348    pub chosen_path: Option<String>,
1349}
1350
1351#[derive(Debug, Serialize, JsonSchema)]
1352pub struct LandSchema {
1353    pub output_kind: Option<String>,
1354    pub status: String,
1355    pub action: String,
1356    pub message: String,
1357    pub blockers: Option<Vec<String>>,
1358    pub warnings: Option<Vec<String>>,
1359    pub next_action: Option<String>,
1360    pub next_action_template: Option<ActionTemplateSchema>,
1361    pub recommended_action: Option<String>,
1362    pub recommended_action_template: Option<ActionTemplateSchema>,
1363    pub thread: String,
1364    pub captured: bool,
1365    pub checkpointed: bool,
1366    pub git_commit: Option<String>,
1367    pub synced: bool,
1368    pub integrated: bool,
1369    pub performed_steps: Vec<String>,
1370    pub skipped_steps: Vec<String>,
1371    pub merge_state: Option<String>,
1372    pub chosen_path: String,
1373    pub siblings_restacked: Vec<String>,
1374    pub siblings_restack_failed: Vec<SiblingRestackFailureSchema>,
1375}
1376
1377#[derive(Debug, Serialize, JsonSchema)]
1378pub struct SiblingRestackFailureSchema {
1379    pub thread: String,
1380    pub message: String,
1381}
1382
1383#[derive(Debug, Serialize, JsonSchema)]
1384pub struct LandBatchPeerSchema {
1385    pub thread: String,
1386    pub status: String,
1387    pub message: String,
1388    pub captured: bool,
1389    pub checkpointed: bool,
1390    pub git_commit: Option<String>,
1391    pub integrated: bool,
1392    pub synced: bool,
1393    pub siblings_restacked: Vec<String>,
1394    pub siblings_restack_failed: Vec<SiblingRestackFailureSchema>,
1395    pub blockers: Vec<String>,
1396    pub warnings: Vec<String>,
1397    pub primary_command: Option<String>,
1398    pub recovery_commands: Vec<String>,
1399}
1400
1401#[derive(Debug, Serialize, JsonSchema)]
1402pub struct LandBatchSchema {
1403    pub output_kind: String,
1404    pub status: String,
1405    pub action: String,
1406    pub message: String,
1407    pub threads: Vec<String>,
1408    pub landed: Vec<String>,
1409    pub stopped_at: Option<String>,
1410    pub peers: Vec<LandBatchPeerSchema>,
1411    pub git_head: Option<String>,
1412    pub recommended_action: Option<String>,
1413    pub verification: Option<RepositoryVerificationStateSchema>,
1414}
1415
1416#[derive(Debug, Serialize, JsonSchema)]
1417pub struct ThreadStartSchema {
1418    pub output_kind: Option<String>,
1419    pub status: Option<String>,
1420    pub action: Option<String>,
1421    pub name: String,
1422    pub message: String,
1423    pub next_action: Option<String>,
1424    pub next_action_template: Option<ActionTemplateSchema>,
1425    pub recommended_action: Option<String>,
1426    pub recommended_action_template: Option<ActionTemplateSchema>,
1427    pub thread: Option<ThreadSummarySchema>,
1428    pub path: Option<String>,
1429    pub execution_path: Option<String>,
1430    pub fskit_readiness: Option<FsKitReadinessSchema>,
1431}
1432
1433#[derive(Debug, Serialize, JsonSchema)]
1434pub struct FsKitReadinessSchema {
1435    pub state: String,
1436    pub backend: String,
1437    pub action: String,
1438    pub settings_url: Option<String>,
1439}
1440
1441#[derive(Debug, Serialize, JsonSchema)]
1442pub struct ThreadCurrentSchema {
1443    pub thread: String,
1444}
1445
1446#[derive(Debug, Serialize, JsonSchema)]
1447#[serde(transparent)]
1448pub struct ThreadCapturesSchema(pub Vec<ThreadCaptureEntrySchema>);
1449
1450#[derive(Debug, Serialize, JsonSchema)]
1451pub struct ThreadCaptureEntrySchema {
1452    pub state_id: String,
1453    pub created_at: String,
1454    pub intent: Option<String>,
1455    pub confidence: Option<f32>,
1456    pub agent: Option<String>,
1457    pub message: String,
1458    pub summary: Option<ThreadCaptureSummarySchema>,
1459}
1460
1461#[derive(Debug, Serialize, JsonSchema)]
1462pub struct ThreadCaptureSummarySchema {
1463    pub added: usize,
1464    pub modified: usize,
1465    pub deleted: usize,
1466    pub total: usize,
1467}
1468
1469#[derive(Debug, Serialize, JsonSchema)]
1470pub struct ThreadCommandSchema {
1471    pub output_kind: String,
1472    pub status: String,
1473    pub action: String,
1474    pub name: String,
1475    pub message: String,
1476    pub next_action: Option<String>,
1477    pub next_action_template: Option<ActionTemplateSchema>,
1478    pub recommended_action: Option<String>,
1479    pub recommended_action_template: Option<ActionTemplateSchema>,
1480    pub thread: Option<ThreadSummarySchema>,
1481    pub path: Option<String>,
1482    pub execution_path: Option<String>,
1483}
1484
1485#[derive(Debug, Serialize, JsonSchema)]
1486pub struct ThreadMoveSchema {
1487    pub from_thread: String,
1488    pub to_thread: String,
1489    pub moved_paths: Vec<String>,
1490    pub source_state_id: Option<String>,
1491    pub target_state_id: String,
1492    pub message: String,
1493}
1494
1495#[derive(Debug, Serialize, JsonSchema)]
1496pub struct ThreadAbsorbSchema {
1497    pub thread: String,
1498    pub into: String,
1499    pub preview_only: bool,
1500    pub conflicts: Vec<String>,
1501    pub merge_state: Option<String>,
1502    pub message: String,
1503}
1504
1505#[derive(Debug, Serialize, JsonSchema)]
1506pub struct ThreadResolveSchema {
1507    #[serde(flatten)]
1508    pub operator: OperatorCommandSchema,
1509    pub thread: String,
1510}
1511
1512#[derive(Debug, Serialize, JsonSchema)]
1513pub struct ThreadApprovalSchema {
1514    pub id: String,
1515    pub repo_path: String,
1516    pub source_thread: String,
1517    pub target_thread: String,
1518    pub source_state: String,
1519    pub approver_user_id: String,
1520    pub note: String,
1521    pub approved_at: u64,
1522    pub expires_at: u64,
1523}
1524
1525#[derive(Debug, Serialize, JsonSchema)]
1526#[serde(transparent)]
1527pub struct ThreadApprovalListSchema(pub Vec<ThreadApprovalSchema>);
1528
1529#[derive(Debug, Serialize, JsonSchema)]
1530pub struct ThreadRevokeApprovalSchema {
1531    pub output_kind: String,
1532    pub deleted: bool,
1533    pub id: String,
1534}
1535
1536#[derive(Debug, Serialize, JsonSchema)]
1537pub struct ThreadMergeEligibilitySchema {
1538    pub allowed: bool,
1539    pub unmet: Vec<ThreadMergeRequirementSchema>,
1540    pub valid_approvals: Vec<ThreadApprovalSchema>,
1541}
1542
1543#[derive(Debug, Serialize, JsonSchema)]
1544pub struct ThreadMergeRequirementSchema {
1545    pub policy_id: String,
1546    pub kind: String,
1547    pub group_id: String,
1548    pub reason: String,
1549    pub needed: u32,
1550    pub have: u32,
1551}
1552
1553#[derive(Debug, Serialize, JsonSchema)]
1554pub struct ThreadCleanupSchema {
1555    #[serde(flatten)]
1556    pub operator: OperatorCommandSchema,
1557    pub dry_run: bool,
1558    pub merged: Vec<ThreadDroppedSchema>,
1559    pub auto: Vec<ThreadDroppedSchema>,
1560    pub reclaimed_bytes: u64,
1561    pub would_reclaim_bytes: u64,
1562    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1563    pub skipped: Vec<ThreadCleanupSkippedSchema>,
1564}
1565
1566#[derive(Debug, Serialize, JsonSchema)]
1567pub struct ThreadDroppedSchema {
1568    pub thread: String,
1569    pub id: String,
1570    pub reason: String,
1571    pub age_seconds: i64,
1572    pub bytes: u64,
1573    pub execution_path: Option<String>,
1574}
1575
1576#[derive(Debug, Serialize, JsonSchema)]
1577pub struct ThreadCleanupSkippedSchema {
1578    pub thread: String,
1579    pub id: String,
1580    pub reason: String,
1581    pub note: String,
1582}
1583
1584#[derive(Debug, Serialize, JsonSchema)]
1585pub struct ThreadMarkerListSchema {
1586    pub output_kind: String,
1587    pub markers: Vec<ThreadMarkerEntrySchema>,
1588}
1589
1590#[derive(Debug, Serialize, JsonSchema)]
1591pub struct ThreadMarkerEntrySchema {
1592    pub name: String,
1593    pub state_id: String,
1594}
1595
1596#[derive(Debug, Serialize, JsonSchema)]
1597pub struct ThreadMarkerOpSchema {
1598    pub output_kind: String,
1599    pub name: Option<String>,
1600    pub state_id: Option<String>,
1601    pub deleted: Option<Vec<ThreadMarkerEntrySchema>>,
1602    pub count: Option<usize>,
1603    pub message: String,
1604}
1605
1606#[derive(Debug, Serialize, JsonSchema)]
1607pub struct ThreadShowSchema {
1608    pub output_kind: Option<String>,
1609    pub repository_label: String,
1610    pub repository_context: Option<RepositoryContextInfoSchema>,
1611    #[serde(flatten)]
1612    pub summary: ThreadSummarySchema,
1613    pub next_action: Option<String>,
1614    pub next_action_template: Option<ActionTemplateSchema>,
1615    pub recommended_action: Option<String>,
1616    pub recommended_action_template: Option<ActionTemplateSchema>,
1617    #[serde(rename = "verification")]
1618    pub trust: RepositoryVerificationStateSchema,
1619    pub recovery_commands: Vec<String>,
1620}
1621
1622#[derive(Debug, Serialize, JsonSchema)]
1623pub struct ThreadSummarySchema {
1624    pub name: String,
1625    pub operation: OpaqueObject,
1626    pub remote_tracking: OpaqueObject,
1627    pub base_state: Option<String>,
1628    pub base_root: Option<String>,
1629    pub current_state: Option<String>,
1630    pub path: Option<String>,
1631    pub execution_path: Option<String>,
1632    #[serde(default, skip_serializing_if = "Option::is_none")]
1633    pub session_id: Option<String>,
1634    #[serde(default, skip_serializing_if = "Option::is_none")]
1635    pub heddle_session_id: Option<String>,
1636    pub actor: Option<ActorInfoSchema>,
1637    pub harness: Option<String>,
1638    pub thinking_level: Option<String>,
1639    #[serde(default, skip_serializing_if = "Option::is_none")]
1640    pub native_actor_key: Option<String>,
1641    #[serde(default, skip_serializing_if = "Option::is_none")]
1642    pub native_parent_actor_key: Option<String>,
1643    #[serde(default, skip_serializing_if = "Option::is_none")]
1644    pub probe_source: Option<String>,
1645    #[serde(default, skip_serializing_if = "Option::is_none")]
1646    pub probe_confidence: Option<f32>,
1647    pub usage_summary: OpaqueObject,
1648    pub last_progress_at: Option<String>,
1649    pub last_activity_at: Option<String>,
1650    pub report_flush_state: Option<String>,
1651    pub attach_reason: Option<String>,
1652    pub thread_mode: Option<ThreadModeSchema>,
1653    pub thread_state: Option<ThreadStateSchema>,
1654    pub freshness: Option<ThreadFreshnessSchema>,
1655    pub visibility: String,
1656    pub target_thread: Option<String>,
1657    pub parent_thread: Option<String>,
1658    pub child_threads: Vec<String>,
1659    pub sibling_threads: Vec<String>,
1660    pub stack_depth: usize,
1661    pub stale_from_parent: bool,
1662    pub task: Option<String>,
1663    pub task_assignment_id: Option<String>,
1664    pub task_summary: Option<ThreadTaskSummarySchema>,
1665    pub changed_paths: Vec<String>,
1666    pub promotion_suggested: bool,
1667    pub impact_categories: Vec<ThreadImpactCategorySchema>,
1668    pub heavy_impact_paths: Vec<String>,
1669    pub verification_summary: Value,
1670    pub confidence_summary: Value,
1671    pub integration_policy_result: Value,
1672    pub coordination_status: CoordinationStatusSchema,
1673    pub is_current: bool,
1674    pub is_isolated: bool,
1675    pub thread_health: String,
1676    pub blockers: Vec<String>,
1677    // Runtime `ThreadSummary.recommended_action` is a `String` serialized
1678    // through `serialize_empty_action_as_null`, so the wire value is
1679    // `string | null` (HeddleCo/heddle#645 presence contract).
1680    pub recommended_action: Option<String>,
1681    pub recommended_action_template: Option<ActionTemplateSchema>,
1682    pub git_branch_tip: Option<String>,
1683    pub history_imported: bool,
1684    pub auto: bool,
1685    pub shared_target_dir: Option<String>,
1686}
1687
1688#[derive(Debug, Serialize, JsonSchema)]
1689pub struct ThreadTaskSummarySchema {
1690    pub task_id: String,
1691    pub title: String,
1692    pub status: String,
1693    pub target_thread: String,
1694    pub updated_at: String,
1695    pub completed_at: Option<String>,
1696    pub coordination_discussion_id: Option<String>,
1697}
1698
1699#[derive(Debug, Serialize, JsonSchema)]
1700#[serde(untagged)]
1701#[allow(dead_code)]
1702pub enum CloneSchema {
1703    Git(CloneGitSchema),
1704    Heddle(CloneHeddleSchema),
1705}
1706
1707#[derive(Debug, Serialize, JsonSchema)]
1708pub struct CloneGitSchema {
1709    pub output_kind: String,
1710    pub action: String,
1711    pub status: String,
1712    pub success: bool,
1713    pub cloned: bool,
1714    pub transport: GitTransportSchema,
1715    pub remote: String,
1716    pub local: String,
1717    pub branch: String,
1718    pub repository_capability: String,
1719    pub commits_imported: usize,
1720    pub states_created: usize,
1721    pub verification: RepositoryVerificationStateSchema,
1722}
1723
1724#[derive(Debug, Serialize, JsonSchema)]
1725pub struct CloneHeddleSchema {
1726    pub output_kind: String,
1727    pub action: String,
1728    pub status: String,
1729    pub success: bool,
1730    pub cloned: bool,
1731    pub transport: HeddleTransportSchema,
1732    pub remote: String,
1733    pub local: String,
1734    pub branch: String,
1735    pub repository_capability: String,
1736    pub objects: Option<usize>,
1737    pub state: Option<String>,
1738    pub verification: RepositoryVerificationStateSchema,
1739}
1740
1741#[derive(Debug, Serialize, JsonSchema)]
1742pub struct AdoptSchema {
1743    pub output_kind: Option<String>,
1744    pub status: Option<String>,
1745    pub action: Option<String>,
1746    pub adopted: bool,
1747    pub initialized: bool,
1748    pub path: String,
1749    pub refs: Vec<String>,
1750    pub commits_imported: usize,
1751    pub states_created: usize,
1752    pub branches_synced: usize,
1753    pub tags_synced: usize,
1754    pub skipped_non_commit_refs: usize,
1755    pub already_in_sync: bool,
1756    pub recommended_action: Option<String>,
1757    pub recommended_action_template: Option<ActionTemplateSchema>,
1758    #[serde(rename = "verification")]
1759    pub trust: RepositoryVerificationStateSchema,
1760}
1761
1762#[derive(Debug, Serialize, JsonSchema)]
1763pub struct RemoteListSchema {
1764    pub output_kind: Option<String>,
1765    pub remotes: Vec<RemoteInfoSchema>,
1766}
1767
1768#[derive(Debug, Serialize, JsonSchema)]
1769pub struct RemoteInfoSchema {
1770    pub output_kind: Option<String>,
1771    pub name: String,
1772    pub url: String,
1773    pub source: String,
1774    pub is_default: bool,
1775}
1776
1777#[derive(Debug, Serialize, JsonSchema)]
1778pub struct RemoteMutationSchema {
1779    pub output_kind: Option<String>,
1780    pub status: String,
1781    pub action: String,
1782    pub name: String,
1783    pub url: Option<String>,
1784    pub default: Option<String>,
1785    pub message: String,
1786    pub verification: RepositoryVerificationStateSchema,
1787}
1788
1789#[derive(Debug, Serialize, JsonSchema)]
1790pub struct AgentPresenceSingleSchema {
1791    pub output_kind: String,
1792    pub presence: ActorEntrySchema,
1793    #[serde(rename = "verification")]
1794    pub trust: RepositoryVerificationStateSchema,
1795}
1796
1797#[derive(Debug, Serialize, JsonSchema)]
1798pub struct AgentPresenceListSchema {
1799    pub output_kind: String,
1800    pub presence: Vec<ActorEntrySchema>,
1801    pub active_only: bool,
1802    #[serde(rename = "verification")]
1803    pub trust: RepositoryVerificationStateSchema,
1804}
1805
1806#[derive(Debug, Serialize, JsonSchema)]
1807pub struct AgentPresenceCompleteSchema {
1808    pub output_kind: String,
1809    pub session_id: String,
1810    pub status: String,
1811    pub thread: String,
1812    #[serde(default, skip_serializing_if = "Option::is_none")]
1813    pub coordination_status: Option<String>,
1814    #[serde(default, skip_serializing_if = "Option::is_none")]
1815    pub recommended_action: Option<String>,
1816    #[serde(default, skip_serializing_if = "Option::is_none")]
1817    pub recommended_action_template: Option<ActionTemplateSchema>,
1818    #[serde(rename = "verification")]
1819    pub trust: RepositoryVerificationStateSchema,
1820}
1821
1822#[derive(Debug, Serialize, JsonSchema)]
1823pub struct AgentPresenceExplainSchema {
1824    pub output_kind: String,
1825    #[serde(default, skip_serializing_if = "Option::is_none")]
1826    pub attached: Option<bool>,
1827    #[serde(default, skip_serializing_if = "Option::is_none")]
1828    pub active_presence: Option<Value>,
1829    #[serde(default, skip_serializing_if = "Option::is_none")]
1830    pub reason: Option<String>,
1831    #[serde(default, skip_serializing_if = "Option::is_none")]
1832    pub repository: Option<String>,
1833    #[serde(default, skip_serializing_if = "Option::is_none")]
1834    pub detected: Option<Value>,
1835    #[serde(default, skip_serializing_if = "Option::is_none")]
1836    pub environment: Option<Value>,
1837    #[serde(default, skip_serializing_if = "Option::is_none")]
1838    pub recommended_action: Option<String>,
1839    #[serde(default, skip_serializing_if = "Option::is_none")]
1840    pub recommended_action_template: Option<ActionTemplateSchema>,
1841    #[serde(default, skip_serializing_if = "Option::is_none")]
1842    pub session_id: Option<String>,
1843    #[serde(default, skip_serializing_if = "Option::is_none")]
1844    pub thread: Option<String>,
1845    #[serde(default, skip_serializing_if = "Option::is_none")]
1846    pub heddle_session_id: Option<String>,
1847    #[serde(default, skip_serializing_if = "Option::is_none")]
1848    pub client_instance_id: Option<String>,
1849    #[serde(default, skip_serializing_if = "Option::is_none")]
1850    pub native_actor_key: Option<String>,
1851    #[serde(default, skip_serializing_if = "Option::is_none")]
1852    pub native_parent_actor_key: Option<String>,
1853    #[serde(default, skip_serializing_if = "Option::is_none")]
1854    pub native_instance_key: Option<String>,
1855    #[serde(default, skip_serializing_if = "Option::is_none")]
1856    pub probe_source: Option<String>,
1857    #[serde(default, skip_serializing_if = "Option::is_none")]
1858    pub probe_confidence: Option<f32>,
1859    #[serde(default, skip_serializing_if = "Option::is_none")]
1860    pub attach_reason: Option<String>,
1861    #[serde(default, skip_serializing_if = "Option::is_none")]
1862    pub attach_precedence: Option<Vec<String>>,
1863    #[serde(default, skip_serializing_if = "Option::is_none")]
1864    pub winning_rule: Option<String>,
1865    #[serde(rename = "verification")]
1866    pub trust: RepositoryVerificationStateSchema,
1867}
1868
1869#[derive(Debug, Serialize, JsonSchema)]
1870pub struct ActorEntrySchema {
1871    pub session_id: String,
1872    #[serde(default, skip_serializing_if = "Option::is_none")]
1873    pub client_instance_id: Option<String>,
1874    #[serde(default, skip_serializing_if = "Option::is_none")]
1875    pub native_actor_key: Option<String>,
1876    #[serde(default, skip_serializing_if = "Option::is_none")]
1877    pub native_parent_actor_key: Option<String>,
1878    #[serde(default, skip_serializing_if = "Option::is_none")]
1879    pub native_instance_key: Option<String>,
1880    #[serde(default, skip_serializing_if = "Option::is_none")]
1881    pub heddle_session_id: Option<String>,
1882    pub thread: String,
1883    #[serde(default, skip_serializing_if = "Option::is_none")]
1884    pub thread_id: Option<String>,
1885    pub base_state: String,
1886    #[serde(default, skip_serializing_if = "Option::is_none")]
1887    pub path: Option<String>,
1888    #[serde(default, skip_serializing_if = "Option::is_none")]
1889    pub provider: Option<String>,
1890    #[serde(default, skip_serializing_if = "Option::is_none")]
1891    pub model: Option<String>,
1892    #[serde(default, skip_serializing_if = "Option::is_none")]
1893    pub harness: Option<String>,
1894    #[serde(default, skip_serializing_if = "Option::is_none")]
1895    pub thinking_level: Option<String>,
1896    pub usage_summary: Value,
1897    #[serde(default, skip_serializing_if = "Option::is_none")]
1898    pub last_progress_at: Option<String>,
1899    #[serde(default, skip_serializing_if = "Option::is_none")]
1900    pub report_flush_state: Option<String>,
1901    #[serde(default, skip_serializing_if = "Option::is_none")]
1902    pub attach_reason: Option<String>,
1903    pub attach_precedence: Vec<String>,
1904    #[serde(default, skip_serializing_if = "Option::is_none")]
1905    pub winning_attach_rule: Option<String>,
1906    #[serde(default, skip_serializing_if = "Option::is_none")]
1907    pub probe_source: Option<String>,
1908    #[serde(default, skip_serializing_if = "Option::is_none")]
1909    pub probe_confidence: Option<f32>,
1910    pub status: String,
1911    pub started_at: String,
1912    pub actor_chain: Vec<ActorChainEntrySchema>,
1913}
1914
1915#[derive(Debug, Serialize, JsonSchema)]
1916pub struct ActorChainEntrySchema {
1917    pub session_id: String,
1918    #[serde(default, skip_serializing_if = "Option::is_none")]
1919    pub native_actor_key: Option<String>,
1920    #[serde(default, skip_serializing_if = "Option::is_none")]
1921    pub native_parent_actor_key: Option<String>,
1922    pub thread: String,
1923    pub status: String,
1924    #[serde(default, skip_serializing_if = "Option::is_none")]
1925    pub provider: Option<String>,
1926    #[serde(default, skip_serializing_if = "Option::is_none")]
1927    pub model: Option<String>,
1928    #[serde(default, skip_serializing_if = "Option::is_none")]
1929    pub harness: Option<String>,
1930}
1931
1932#[derive(Debug, Serialize, JsonSchema)]
1933pub struct AgentReservationEnvelopeSchema {
1934    pub reservation: AgentReservationSchema,
1935    pub token: Option<String>,
1936    #[serde(rename = "verification")]
1937    pub trust: RepositoryVerificationStateSchema,
1938}
1939
1940#[derive(Debug, Serialize, JsonSchema)]
1941pub struct AgentReservationListSchema {
1942    pub reservations: Vec<AgentReservationSchema>,
1943    pub alive_only: bool,
1944    pub thread: Option<String>,
1945    #[serde(rename = "verification")]
1946    pub trust: RepositoryVerificationStateSchema,
1947}
1948
1949#[derive(Debug, Serialize, JsonSchema)]
1950pub struct AgentReservationSchema {
1951    pub lease_id: String,
1952    pub actor_session_id: Option<String>,
1953    pub thread: String,
1954    pub anchor_state: Option<String>,
1955    pub anchor_root: Option<String>,
1956    pub task_assignment_id: Option<String>,
1957    pub status: String,
1958    pub path: Option<String>,
1959    pub heartbeat_at: String,
1960    pub lease_expires_at: String,
1961    pub liveness: String,
1962}
1963
1964#[derive(Debug, Serialize, JsonSchema)]
1965pub struct AgentTaskEnvelopeSchema {
1966    pub output_kind: String,
1967    pub task: AgentTaskSchema,
1968    #[serde(rename = "verification")]
1969    pub trust: RepositoryVerificationStateSchema,
1970}
1971
1972#[derive(Debug, Serialize, JsonSchema)]
1973pub struct AgentTaskListSchema {
1974    pub output_kind: String,
1975    pub tasks: Vec<AgentTaskSchema>,
1976    pub thread: Option<String>,
1977    pub status: Option<String>,
1978    #[serde(rename = "verification")]
1979    pub trust: RepositoryVerificationStateSchema,
1980}
1981
1982#[derive(Debug, Serialize, JsonSchema)]
1983pub struct AgentTaskSchema {
1984    pub schema_version: u32,
1985    pub task_id: String,
1986    pub title: String,
1987    pub body: String,
1988    pub status: String,
1989    pub target_thread: String,
1990    pub base_state: Option<String>,
1991    pub base_root: Option<String>,
1992    pub parent_task_id: Option<String>,
1993    pub coordination_discussion_id: Option<String>,
1994    pub allow_offline: bool,
1995    pub delegated_by: Option<String>,
1996    pub created_at: String,
1997    pub updated_at: String,
1998    pub completed_at: Option<String>,
1999}
2000
2001#[derive(Debug, Serialize, JsonSchema)]
2002pub struct AgentFanoutSchema {
2003    pub output_kind: String,
2004    pub title: String,
2005    pub parent_thread: String,
2006    pub base_state: String,
2007    pub base_root: String,
2008    pub coordination_discussion_id: Option<String>,
2009    pub parent_task: Option<AgentTaskSchema>,
2010    pub lanes: Vec<AgentFanoutLaneSchema>,
2011    pub commands: Vec<AgentFanoutCommandSchema>,
2012    #[serde(rename = "verification")]
2013    pub trust: RepositoryVerificationStateSchema,
2014}
2015
2016#[derive(Debug, Serialize, JsonSchema)]
2017pub struct AgentFanoutLaneSchema {
2018    pub thread: String,
2019    pub path: String,
2020    pub title: String,
2021    pub task: Option<AgentTaskSchema>,
2022    pub session_id: Option<String>,
2023    pub status: String,
2024}
2025
2026#[derive(Debug, Serialize, JsonSchema)]
2027pub struct AgentFanoutCommandSchema {
2028    pub lane_thread: String,
2029    pub command: String,
2030    pub argv: Vec<String>,
2031}
2032
2033#[derive(Debug, Serialize, JsonSchema)]
2034pub struct AgentProvenanceEnvelopeSchema {
2035    pub session: SessionEntrySchema,
2036}
2037
2038#[derive(Debug, Serialize, JsonSchema)]
2039pub struct AgentProvenanceSegmentEnvelopeSchema {
2040    pub segment: SessionSegmentSchema,
2041}
2042
2043#[derive(Debug, Serialize, JsonSchema)]
2044pub struct AgentProvenanceListSchema {
2045    pub sessions: Vec<SessionEntrySchema>,
2046    pub active_only: bool,
2047    #[serde(rename = "verification")]
2048    pub trust: RepositoryVerificationStateSchema,
2049}
2050
2051#[derive(Debug, Serialize, JsonSchema)]
2052pub struct SessionEntrySchema {
2053    pub id: String,
2054    pub principal: String,
2055    pub created_at: String,
2056    #[serde(default, skip_serializing_if = "Option::is_none")]
2057    pub ended_at: Option<String>,
2058    pub active: bool,
2059    pub segments: Vec<SessionSegmentSchema>,
2060}
2061
2062#[derive(Debug, Serialize, JsonSchema)]
2063pub struct SessionSegmentSchema {
2064    pub id: String,
2065    pub provider: String,
2066    pub model: String,
2067    pub started_at: String,
2068    #[serde(default, skip_serializing_if = "Option::is_none")]
2069    pub policy_id: Option<String>,
2070}
2071
2072#[derive(Debug, Serialize, JsonSchema)]
2073#[serde(untagged)]
2074#[allow(dead_code)]
2075pub enum PullSchema {
2076    Git(PullGitSchema),
2077    Heddle(PullHeddleSchema),
2078}
2079
2080#[derive(Debug, Serialize, JsonSchema)]
2081pub struct PullGitSchema {
2082    pub output_kind: String,
2083    pub action: String,
2084    pub status: String,
2085    pub pulled: bool,
2086    pub changed: bool,
2087    pub success: bool,
2088    pub transport: GitTransportSchema,
2089    pub remote: String,
2090    pub branch: String,
2091    pub old_git_head: Option<String>,
2092    pub new_git_head: String,
2093    pub old_state: Option<String>,
2094    pub new_state: String,
2095    pub states_created: usize,
2096    pub commits_seen: usize,
2097    pub commits_seen_scope: String,
2098    pub materialized_checkout: bool,
2099    pub changed_path_count: usize,
2100    pub changed_paths: Vec<String>,
2101    pub verification: RepositoryVerificationStateSchema,
2102}
2103
2104#[derive(Debug, Serialize, JsonSchema)]
2105pub struct PullHeddleSchema {
2106    pub output_kind: String,
2107    pub action: String,
2108    pub status: String,
2109    pub pulled: bool,
2110    pub changed: bool,
2111    pub success: bool,
2112    pub transport: HeddleTransportSchema,
2113    pub remote: String,
2114    pub thread: String,
2115    pub state: Option<String>,
2116    pub objects: Option<usize>,
2117    pub verification: RepositoryVerificationStateSchema,
2118}
2119
2120#[derive(Debug, Serialize, JsonSchema)]
2121#[serde(untagged)]
2122#[allow(dead_code)]
2123pub enum PushSchema {
2124    Git(PushGitSchema),
2125    Heddle(PushHeddleSchema),
2126}
2127
2128#[derive(Debug, Serialize, JsonSchema)]
2129pub struct PushGitSchema {
2130    pub output_kind: String,
2131    pub action: String,
2132    pub status: String,
2133    pub pushed: bool,
2134    pub changed: bool,
2135    pub success: bool,
2136    pub transport: GitTransportSchema,
2137    pub remote: String,
2138    pub push_scope: String,
2139    pub ref_scope: String,
2140    pub refs_written: Vec<String>,
2141    pub git_tracking_remote: Option<String>,
2142    pub git_remote_configured: Option<GitRemoteConfiguredSchema>,
2143    pub git_upstream_configured: Option<GitUpstreamConfiguredSchema>,
2144    pub tags_included: bool,
2145    pub force: bool,
2146    pub force_discard_warning: Option<String>,
2147    pub thread: Option<String>,
2148    pub next_action: NullableStringSchema,
2149    pub next_action_template: NullableActionTemplateSchema,
2150    pub recommended_action: NullableStringSchema,
2151    pub recommended_action_template: NullableActionTemplateSchema,
2152    pub verification: RepositoryVerificationStateSchema,
2153}
2154
2155#[derive(Debug, Serialize, JsonSchema)]
2156pub struct PushHeddleSchema {
2157    pub output_kind: String,
2158    pub action: String,
2159    pub status: String,
2160    pub pushed: bool,
2161    pub changed: bool,
2162    pub success: bool,
2163    pub transport: HeddleTransportSchema,
2164    pub remote: Option<String>,
2165    pub push_scope: Option<String>,
2166    pub refs_written: Option<Vec<String>>,
2167    pub force: Option<bool>,
2168    pub thread: Option<String>,
2169    pub state: Option<String>,
2170    pub objects: Option<usize>,
2171    pub next_action: NullableStringSchema,
2172    pub next_action_template: NullableActionTemplateSchema,
2173    pub recommended_action: NullableStringSchema,
2174    pub recommended_action_template: NullableActionTemplateSchema,
2175    pub verification: RepositoryVerificationStateSchema,
2176}
2177
2178#[derive(Debug, Serialize, JsonSchema)]
2179#[serde(rename_all = "lowercase")]
2180#[allow(dead_code)]
2181pub enum GitTransportSchema {
2182    Git,
2183}
2184
2185#[derive(Debug, Serialize, JsonSchema)]
2186#[serde(rename_all = "lowercase")]
2187#[allow(dead_code)]
2188pub enum HeddleTransportSchema {
2189    Heddle,
2190}
2191
2192#[derive(Debug, Serialize, JsonSchema)]
2193pub struct GitRemoteConfiguredSchema {
2194    pub name: String,
2195    pub url: String,
2196}
2197
2198#[derive(Debug, Serialize, JsonSchema)]
2199pub struct GitUpstreamConfiguredSchema {
2200    pub branch: String,
2201    pub remote: String,
2202}
2203
2204/// Operation banner — kept opaque because the underlying
2205/// [`repo::RepositoryOperationStatus`] is a workspace type and its
2206/// shape is internal. `Value` here means "any JSON object or null".
2207type OpaqueObject = Option<Value>;
2208
2209#[derive(Debug, Serialize, JsonSchema)]
2210pub struct RepositoryContextInfoSchema {
2211    pub kind: String,
2212    pub parent_repository: Option<String>,
2213    pub target_thread: Option<String>,
2214    pub parent_thread: Option<String>,
2215}
2216
2217// ---- verify ---------------------------------------------------------------
2218
2219#[derive(Debug, Serialize, JsonSchema)]
2220pub struct RepositoryVerificationStateSchema {
2221    #[serde(rename = "verified")]
2222    pub verified: bool,
2223    pub status: String,
2224    pub repository_mode: String,
2225    pub heddle_initialized: bool,
2226    pub git_branch: Option<String>,
2227    pub heddle_thread: Option<String>,
2228    pub worktree_dirty: bool,
2229    pub worktree_state: String,
2230    pub import_state: String,
2231    pub mapping_state: String,
2232    pub remote_drift: String,
2233    pub active_operation: Option<String>,
2234    pub default_remote: Option<String>,
2235    pub clone_verification: String,
2236    pub machine_contract: String,
2237    pub machine_contract_coverage: MachineContractCoverageSchema,
2238    pub workflow_status: String,
2239    pub workflow_summary: String,
2240    pub summary: String,
2241    pub recommended_action: Option<String>,
2242    pub recommended_action_template: Option<ActionTemplateSchema>,
2243    pub recovery_commands: Vec<String>,
2244    pub recovery_action_templates: Vec<ActionTemplateSchema>,
2245    pub checks: Vec<VerificationCheckSchema>,
2246}
2247
2248#[derive(Debug, Serialize, JsonSchema)]
2249pub struct MachineContractCoverageSchema {
2250    pub status: String,
2251    #[serde(rename = "verified_scope")]
2252    pub verified_scope: String,
2253    pub advanced_scope: String,
2254    pub summary: String,
2255    pub catalog_commands_total: usize,
2256    pub catalog_mutating_commands_total: usize,
2257    pub json_commands_total: usize,
2258    pub json_mutating_commands_total: usize,
2259    pub json_commands_with_schema: usize,
2260    pub json_commands_with_accepted_opaque_schema: usize,
2261    pub json_commands_without_schema: usize,
2262    #[serde(rename = "verified_scope_json_commands_total")]
2263    pub verified_scope_json_commands_total: usize,
2264    #[serde(rename = "verified_scope_json_commands_with_schema")]
2265    pub verified_scope_json_commands_with_schema: usize,
2266    #[serde(rename = "verified_scope_json_commands_with_accepted_opaque_schema")]
2267    pub verified_scope_json_commands_with_accepted_opaque_schema: usize,
2268    #[serde(rename = "verified_scope_json_commands_without_schema")]
2269    pub verified_scope_json_commands_without_schema: usize,
2270    pub advanced_scope_json_commands_total: usize,
2271    pub advanced_scope_json_commands_with_accepted_opaque_schema: usize,
2272    pub mutating_commands_total: usize,
2273    pub mutating_commands_with_schema: usize,
2274    pub mutating_commands_with_accepted_opaque_schema: usize,
2275    pub mutating_commands_without_schema: usize,
2276    #[serde(rename = "verified_scope_mutating_commands_total")]
2277    pub verified_scope_mutating_commands_total: usize,
2278    #[serde(rename = "verified_scope_mutating_commands_with_schema")]
2279    pub verified_scope_mutating_commands_with_schema: usize,
2280    #[serde(rename = "verified_scope_mutating_commands_with_accepted_opaque_schema")]
2281    pub verified_scope_mutating_commands_with_accepted_opaque_schema: usize,
2282    #[serde(rename = "verified_scope_mutating_commands_without_schema")]
2283    pub verified_scope_mutating_commands_without_schema: usize,
2284    pub advanced_scope_mutating_commands_total: usize,
2285    pub advanced_scope_mutating_commands_with_accepted_opaque_schema: usize,
2286    pub schema_verbs_total: usize,
2287    pub documented_schema_verbs_total: usize,
2288    pub undocumented_schema_verbs_total: usize,
2289    pub opaque_schema_verbs_total: usize,
2290    pub accepted_opaque_schema_verbs_total: usize,
2291    pub unaccepted_opaque_schema_verbs_total: usize,
2292    pub supports_op_id_total: usize,
2293    pub jsonl_commands_total: usize,
2294    pub missing_schema_examples: Vec<String>,
2295    pub missing_mutating_schema_examples: Vec<String>,
2296    #[serde(rename = "verified_scope_missing_schema_examples")]
2297    pub verified_scope_missing_schema_examples: Vec<String>,
2298    #[serde(rename = "verified_scope_accepted_opaque_schema_examples")]
2299    pub verified_scope_accepted_opaque_schema_examples: Vec<String>,
2300    pub advanced_scope_accepted_opaque_schema_examples: Vec<String>,
2301    pub accepted_opaque_schema_examples: Vec<String>,
2302    pub unaccepted_opaque_schema_examples: Vec<String>,
2303    pub undocumented_schema_examples: Vec<String>,
2304}
2305
2306#[derive(Debug, Serialize, JsonSchema)]
2307pub struct VerificationCheckSchema {
2308    pub name: String,
2309    pub status: String,
2310    pub clean: bool,
2311    pub summary: String,
2312    pub recommended_action: Option<String>,
2313    pub recommended_action_template: Option<ActionTemplateSchema>,
2314    pub recovery_commands: Vec<String>,
2315    pub recovery_action_templates: Vec<ActionTemplateSchema>,
2316    pub details: std::collections::BTreeMap<String, String>,
2317}
2318
2319#[derive(Debug, Serialize, JsonSchema)]
2320pub struct ActionTemplateSchema {
2321    pub action: String,
2322    pub argv_template: Vec<String>,
2323    pub required_inputs: Vec<String>,
2324    /// Whether an agent may replace placeholders in `argv_template`.
2325    ///
2326    /// When `agent_may_fill` is false, treat `action` and `argv_template` as
2327    /// display-only: do not substitute `<name>`/`<url>` placeholders. Surface
2328    /// the template to a human or discard it. Substituting and running it will
2329    /// pass literal `<name>` to Heddle and fail.
2330    pub agent_may_fill: bool,
2331}
2332
2333#[derive(Debug, Serialize, JsonSchema)]
2334pub struct LogSchema {
2335    pub output_kind: Option<String>,
2336    pub status: Option<String>,
2337    pub repository_capability: String,
2338    pub storage_model: String,
2339    pub states: Vec<StateEntrySchema>,
2340}
2341
2342#[derive(Debug, Serialize, JsonSchema)]
2343pub struct StateEntrySchema {
2344    pub state_id: String,
2345    pub content_hash: String,
2346    pub intent: Option<String>,
2347    pub principal: String,
2348    pub agent: Option<String>,
2349    pub confidence: Option<f32>,
2350    pub created_at: String,
2351    pub parents: Vec<String>,
2352    pub git_checkpoint: Option<String>,
2353    pub collapsed: Option<CollapsedEntrySchema>,
2354}
2355
2356#[derive(Debug, Serialize, JsonSchema)]
2357pub struct CollapsedEntrySchema {
2358    pub expandable: bool,
2359    pub source_count: usize,
2360}
2361
2362#[derive(Debug, Serialize, JsonSchema)]
2363pub struct TimelineLogSchema {
2364    pub output_kind: String,
2365    pub status: String,
2366    pub repository_capability: String,
2367    pub storage_model: String,
2368    pub thread: String,
2369    pub cursor: TimelineCursorSchema,
2370    pub branches: Vec<TimelineBranchSchema>,
2371    pub steps: Vec<TimelineStepSchema>,
2372    pub active_branch_path: Vec<String>,
2373    pub actions: TimelineActionsSchema,
2374    pub recovery: Option<TimelineRecoverySchema>,
2375}
2376
2377#[derive(Debug, Serialize, JsonSchema)]
2378pub struct TimelineCursorSchema {
2379    pub branch_id: Option<String>,
2380    pub step_id: Option<String>,
2381    pub state: Option<String>,
2382    pub state_full: Option<String>,
2383}
2384
2385#[derive(Debug, Serialize, JsonSchema)]
2386pub struct TimelineBranchSchema {
2387    pub branch_id: String,
2388    pub parent_branch_id: Option<String>,
2389    pub forked_from_step_id: Option<String>,
2390    pub forked_from_state: Option<String>,
2391    pub reason: Option<String>,
2392    pub created_at_ms: Option<i64>,
2393    pub step_ids: Vec<String>,
2394    pub is_active: bool,
2395    pub is_on_active_path: bool,
2396}
2397
2398#[derive(Debug, Serialize, JsonSchema)]
2399pub struct TimelineStepSchema {
2400    pub step_id: String,
2401    pub branch_id: String,
2402    pub parent_step_id: Option<String>,
2403    pub native: Option<TimelineNativeSchema>,
2404    pub tool_name: Option<String>,
2405    pub status: Option<String>,
2406    pub changed: Option<bool>,
2407    pub touched_paths: Vec<String>,
2408    pub labels: Vec<String>,
2409    pub before_state: Option<String>,
2410    pub after_state: Option<String>,
2411    pub capture_state: Option<String>,
2412    pub cursor_state: Option<String>,
2413    pub cursor_state_full: Option<String>,
2414    pub payload_summary: Option<String>,
2415    pub payload_hash: Option<String>,
2416    pub capture_oplog_batch_id: Option<u64>,
2417    pub started_at_ms: Option<i64>,
2418    pub finished_at_ms: Option<i64>,
2419    pub operation_ids: Vec<String>,
2420    pub is_current: bool,
2421    pub is_on_active_branch_path: bool,
2422    pub can_seek: bool,
2423    pub can_fork: bool,
2424    pub can_reset: bool,
2425    pub can_materialize: bool,
2426    pub has_boundary_warning: bool,
2427}
2428
2429#[derive(Debug, Serialize, JsonSchema)]
2430pub struct TimelineNativeSchema {
2431    pub harness: String,
2432    pub session_id: Option<String>,
2433    pub message_id: Option<String>,
2434    pub tool_call_id: String,
2435}
2436
2437#[derive(Debug, Serialize, JsonSchema)]
2438pub struct TimelineActionsSchema {
2439    pub can_undo: bool,
2440    pub can_redo: bool,
2441}
2442
2443#[derive(Debug, Serialize, JsonSchema)]
2444pub struct TimelineRecoverySchema {
2445    pub status: String,
2446    pub branch_id: String,
2447    pub from_step_id: Option<String>,
2448    pub to_step_id: Option<String>,
2449    pub from_state: String,
2450    pub to_state: String,
2451    pub reason: String,
2452    pub moved_at_ms: i64,
2453    pub checkout_state: Option<String>,
2454}
2455
2456#[derive(Debug, Serialize, JsonSchema)]
2457pub struct TimelineStatusSchema {
2458    pub output_kind: String,
2459    pub status: String,
2460    pub thread: String,
2461    pub cursor_branch_id: Option<String>,
2462    pub cursor_step_id: Option<String>,
2463    pub cursor_state: Option<String>,
2464    pub current_step: Option<TimelineStatusStepSchema>,
2465    pub active_branch_path: Vec<String>,
2466    pub can_undo: bool,
2467    pub can_redo: bool,
2468    pub branch_count: usize,
2469    pub step_count: usize,
2470    pub recovery: Option<TimelineStatusRecoverySchema>,
2471}
2472
2473#[derive(Debug, Serialize, JsonSchema)]
2474pub struct TimelineStatusStepSchema {
2475    pub step_id: String,
2476    pub branch_id: String,
2477    pub parent_step_id: Option<String>,
2478    pub tool_name: Option<String>,
2479    pub tool_status: Option<String>,
2480    pub changed: Option<bool>,
2481    pub payload_summary: Option<String>,
2482    pub payload_hash: Option<String>,
2483    pub labels: Vec<String>,
2484    pub started_at_ms: Option<i64>,
2485    pub finished_at_ms: Option<i64>,
2486    pub can_seek: bool,
2487    pub can_fork: bool,
2488    pub can_reset: bool,
2489    pub can_materialize: bool,
2490    pub has_boundary_warning: bool,
2491}
2492
2493#[derive(Debug, Serialize, JsonSchema)]
2494pub struct TimelineStatusRecoverySchema {
2495    pub status: String,
2496    pub branch_id: String,
2497    pub from_step_id: Option<String>,
2498    pub to_step_id: Option<String>,
2499    pub from_state: String,
2500    pub to_state: String,
2501    pub reason: String,
2502    pub moved_at_ms: i64,
2503    pub checkout_state: Option<String>,
2504}
2505
2506#[derive(Debug, Serialize, JsonSchema)]
2507pub struct TimelineRecordingSchema {
2508    pub output_kind: String,
2509    pub status: String,
2510    pub action: String,
2511    pub thread: String,
2512    pub step_id: String,
2513    pub branch_id: String,
2514    pub parent_step_id: Option<String>,
2515    pub operation_id: String,
2516    pub before_state: Option<String>,
2517    pub after_state: Option<String>,
2518    pub changed: Option<bool>,
2519    pub tool_status: Option<String>,
2520    pub payload_summary: Option<String>,
2521    pub payload_hash: Option<String>,
2522    pub branch_count: usize,
2523    pub step_count: usize,
2524}
2525
2526#[derive(Debug, Serialize, JsonSchema)]
2527pub struct TimelineActionSchema {
2528    pub output_kind: String,
2529    pub status: String,
2530    pub action: String,
2531    pub thread: String,
2532    pub branch_id: Option<String>,
2533    pub parent_branch_id: Option<String>,
2534    pub from_step_id: Option<String>,
2535    pub cursor_branch_id: Option<String>,
2536    pub cursor_step_id: Option<String>,
2537    pub operation_id: Option<String>,
2538    pub recovered_operation_id: Option<String>,
2539    pub materialized: Option<bool>,
2540    pub materialization_status: Option<String>,
2541    pub recovery_status: Option<String>,
2542    pub blocker_count: usize,
2543    pub branch_count: usize,
2544    pub step_count: usize,
2545}
2546
2547#[derive(Debug, Serialize, JsonSchema)]
2548pub struct ExpandSchema {
2549    pub output_kind: String,
2550    pub status: String,
2551    pub requested: String,
2552    pub collapsed: ExpandedCollapseSchema,
2553    pub captures: Vec<ExpandedCaptureSchema>,
2554}
2555
2556#[derive(Debug, Serialize, JsonSchema)]
2557pub struct ExpandedCollapseSchema {
2558    pub state_id: String,
2559    pub state_id_full: String,
2560    pub git_commit: Option<String>,
2561    pub thread: Option<String>,
2562    pub source_count: usize,
2563}
2564
2565#[derive(Debug, Serialize, JsonSchema)]
2566pub struct ExpandedCaptureSchema {
2567    pub state_id: String,
2568    pub state_id_full: String,
2569    pub content_hash: String,
2570    pub intent: Option<String>,
2571    pub principal: String,
2572    pub agent: Option<String>,
2573    pub confidence: Option<f32>,
2574    pub created_at: String,
2575    pub parents: Vec<String>,
2576}
2577
2578#[derive(Debug, Serialize, JsonSchema)]
2579pub struct LogReflogSchema {
2580    pub output_kind: Option<String>,
2581    pub status: Option<String>,
2582    pub repository_capability: String,
2583    pub storage_model: String,
2584    pub entries: Vec<ReflogEntrySchema>,
2585}
2586
2587#[derive(Debug, Serialize, JsonSchema)]
2588pub struct ReflogEntrySchema {
2589    pub source: String,
2590    pub reference: String,
2591    pub old_oid: String,
2592    pub new_oid: String,
2593    pub actor: String,
2594    pub timestamp: Option<String>,
2595    pub message: String,
2596}
2597
2598// ---- show -----------------------------------------------------------------
2599
2600#[derive(Debug, Serialize, JsonSchema)]
2601pub struct ShowSchema {
2602    pub output_kind: String,
2603    pub repository_capability: String,
2604    pub storage_model: String,
2605    pub state_id: String,
2606    pub state_id_full: String,
2607    pub content_hash: String,
2608    pub tree: String,
2609    pub parents: Vec<String>,
2610    pub intent: Option<String>,
2611    pub confidence: Option<f32>,
2612    pub principal: ShowPrincipalSchema,
2613    pub agent: Option<ShowAgentSchema>,
2614    pub created_at: String,
2615    pub status: String,
2616    pub verification: OpaqueObject,
2617    pub git_checkpoint: Option<String>,
2618}
2619
2620#[derive(Debug, Serialize, JsonSchema)]
2621pub struct ShowPrincipalSchema {
2622    pub name: String,
2623    pub email: String,
2624}
2625
2626#[derive(Debug, Serialize, JsonSchema)]
2627pub struct ShowAgentSchema {
2628    pub provider: Option<String>,
2629    pub model: Option<String>,
2630    #[serde(default, skip_serializing_if = "Option::is_none")]
2631    pub session_id: Option<String>,
2632    #[serde(default, skip_serializing_if = "Option::is_none")]
2633    pub policy_id: Option<String>,
2634}
2635
2636// ---- thread list ----------------------------------------------------------
2637
2638#[derive(Debug, Serialize, JsonSchema)]
2639pub struct ThreadListSchema {
2640    pub output_kind: Option<String>,
2641    pub repository_capability: String,
2642    pub repository_label: String,
2643    pub repository_context: Option<RepositoryContextInfoSchema>,
2644    pub storage_model: String,
2645    pub hosted_enabled: bool,
2646    pub threads: Vec<ThreadSummarySchema>,
2647    pub available_git_refs: Vec<AvailableGitRefSchema>,
2648    pub current: Option<String>,
2649    #[serde(rename = "verification")]
2650    pub trust: RepositoryVerificationStateSchema,
2651    pub recommended_action: Option<String>,
2652    pub recommended_action_template: Option<ActionTemplateSchema>,
2653    pub recovery_commands: Vec<String>,
2654    pub recovery_action_templates: Vec<ActionTemplateSchema>,
2655}
2656
2657#[derive(Debug, Serialize, JsonSchema)]
2658pub struct AvailableGitRefSchema {
2659    pub name: String,
2660    pub git_commit: String,
2661    pub recommended_action: Option<String>,
2662    pub recommended_action_template: Option<ActionTemplateSchema>,
2663}
2664
2665// ---- review ---------------------------------------------------------------
2666
2667#[derive(Debug, Serialize, JsonSchema)]
2668pub struct ReviewShowSchema {
2669    pub output_kind: String,
2670    pub state_id: String,
2671    pub headline: String,
2672    pub agent_narrative: Option<String>,
2673    pub files_changed: usize,
2674    pub in_budget_signals: Vec<Value>,
2675    pub all_signals: Vec<Value>,
2676    pub discussions: Vec<Value>,
2677    pub signing_kinds: Vec<String>,
2678    pub signatures: Vec<Value>,
2679}
2680
2681#[derive(Debug, Serialize, JsonSchema)]
2682pub struct ReviewSignSchema {
2683    pub output_kind: String,
2684    pub signature_id: String,
2685    pub state_id: String,
2686}
2687
2688/// `heddle review next --output json` emits a stable envelope keyed by
2689/// `output_kind: "review_next"`. When the scan window holds a pending
2690/// review, the pending state's view is flattened alongside `output_kind`
2691/// (`state_id`, `headline`, `existing_signatures`) and the same view is
2692/// echoed under `next`. When no pending review is found, only
2693/// `output_kind` and `next: null` are emitted — there is no top-level
2694/// `null`. Mirrors the envelope built in `review::run_next`.
2695#[derive(Debug, Serialize, JsonSchema)]
2696pub struct ReviewNextSchema {
2697    pub output_kind: String,
2698    pub state_id: Option<String>,
2699    pub headline: Option<String>,
2700    pub existing_signatures: Option<u32>,
2701    pub next: RequiredNullableNextState,
2702}
2703
2704/// `next` is ALWAYS present in the runtime envelope — either the pending
2705/// review state or an explicit JSON `null`. Modeling it as
2706/// `Option<ReviewNextStateSchema>` directly would let schemars drop the
2707/// field from the schema's `required` set, advertising a shape the command
2708/// never emits. This wrapper keeps the value nullable (its schema delegates
2709/// to `Option`'s nullable form) while reporting `_schemars_private_is_option
2710/// == false`, so the derive marks `next` required (heddle#272 Codex r7).
2711#[derive(Debug, Serialize)]
2712#[serde(transparent)]
2713pub struct RequiredNullableNextState(pub Option<ReviewNextStateSchema>);
2714
2715impl JsonSchema for RequiredNullableNextState {
2716    fn schema_name() -> std::borrow::Cow<'static, str> {
2717        std::borrow::Cow::Borrowed("RequiredNullableNextState")
2718    }
2719
2720    fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
2721        <Option<ReviewNextStateSchema> as JsonSchema>::json_schema(generator)
2722    }
2723}
2724
2725/// The pending review state echoed under `review next`'s `next` field.
2726#[derive(Debug, Serialize, JsonSchema)]
2727pub struct ReviewNextStateSchema {
2728    pub state_id: String,
2729    pub headline: String,
2730    pub existing_signatures: u32,
2731}
2732
2733#[derive(Debug, Serialize, JsonSchema)]
2734pub struct ReviewHealthSchema {
2735    pub output_kind: String,
2736    pub entries: Vec<ReviewHealthEntrySchema>,
2737    pub window_states: usize,
2738}
2739
2740#[derive(Debug, Serialize, JsonSchema)]
2741pub struct ReviewHealthEntrySchema {
2742    pub module_id: String,
2743    pub fire_rate: f64,
2744    pub warn: bool,
2745}
2746
2747// ---- command/schema introspection ----------------------------------------
2748
2749#[derive(Debug, Serialize, JsonSchema)]
2750pub struct SchemasListSchema {
2751    pub output_kind: Option<String>,
2752    pub status: Option<String>,
2753    pub schema_verbs: Vec<String>,
2754    pub documented_schema_verbs: Vec<String>,
2755}
2756
2757#[derive(Debug, Serialize, JsonSchema)]
2758pub struct DoctorDocsSchema {
2759    pub output_kind: String,
2760    pub status: String,
2761    #[serde(rename = "verified")]
2762    pub verified: bool,
2763    pub recommended_action: Option<String>,
2764    pub files_scanned: usize,
2765    pub issues: Vec<DoctorDocsIssueSchema>,
2766}
2767
2768#[derive(Debug, Serialize, JsonSchema)]
2769pub struct DoctorDocsIssueSchema {
2770    pub file: String,
2771    pub line: usize,
2772    pub invocation: String,
2773    pub kind: String,
2774    pub detail: String,
2775    pub suggestion: Option<String>,
2776}
2777
2778#[derive(Debug, Serialize, JsonSchema)]
2779pub struct DoctorSchemasSchema {
2780    pub output_kind: String,
2781    pub status: String,
2782    #[serde(rename = "verified")]
2783    pub verified: bool,
2784    pub summary: String,
2785    pub recommended_action: Option<String>,
2786    pub recovery_commands: Vec<String>,
2787    pub registered_verbs: Vec<String>,
2788    pub documented_verbs: Vec<String>,
2789    pub undocumented_verbs: Vec<String>,
2790    pub unmatched_verbs: Vec<String>,
2791    pub passing_verbs: Vec<String>,
2792    pub issues: Vec<DoctorSchemaIssueSchema>,
2793    pub command_contract_schema_coverage: CommandContractSchemaCoverageSchema,
2794    pub doc_path: String,
2795}
2796
2797#[derive(Debug, Serialize, JsonSchema)]
2798pub struct DoctorSchemaIssueSchema {
2799    pub verb: String,
2800    pub line: usize,
2801    pub unknown_key: String,
2802    pub detail: String,
2803}
2804
2805#[derive(Debug, Serialize, JsonSchema)]
2806pub struct CommandContractSchemaCoverageSchema {
2807    pub status: String,
2808    #[serde(rename = "verified_scope")]
2809    pub verified_scope: String,
2810    pub advanced_scope: String,
2811    pub summary: String,
2812    pub catalog_commands_total: usize,
2813    pub catalog_mutating_commands_total: usize,
2814    pub json_commands_total: usize,
2815    pub json_mutating_commands_total: usize,
2816    pub json_commands_with_schema: usize,
2817    pub json_commands_with_accepted_opaque_schema: usize,
2818    pub json_commands_without_schema: usize,
2819    #[serde(rename = "verified_scope_json_commands_total")]
2820    pub verified_scope_json_commands_total: usize,
2821    #[serde(rename = "verified_scope_json_commands_with_schema")]
2822    pub verified_scope_json_commands_with_schema: usize,
2823    #[serde(rename = "verified_scope_json_commands_with_accepted_opaque_schema")]
2824    pub verified_scope_json_commands_with_accepted_opaque_schema: usize,
2825    #[serde(rename = "verified_scope_json_commands_without_schema")]
2826    pub verified_scope_json_commands_without_schema: usize,
2827    pub advanced_scope_json_commands_total: usize,
2828    pub advanced_scope_json_commands_with_accepted_opaque_schema: usize,
2829    pub mutating_commands_total: usize,
2830    pub mutating_commands_with_schema: usize,
2831    pub mutating_commands_with_accepted_opaque_schema: usize,
2832    pub mutating_commands_without_schema: usize,
2833    #[serde(rename = "verified_scope_mutating_commands_total")]
2834    pub verified_scope_mutating_commands_total: usize,
2835    #[serde(rename = "verified_scope_mutating_commands_with_schema")]
2836    pub verified_scope_mutating_commands_with_schema: usize,
2837    #[serde(rename = "verified_scope_mutating_commands_with_accepted_opaque_schema")]
2838    pub verified_scope_mutating_commands_with_accepted_opaque_schema: usize,
2839    #[serde(rename = "verified_scope_mutating_commands_without_schema")]
2840    pub verified_scope_mutating_commands_without_schema: usize,
2841    pub advanced_scope_mutating_commands_total: usize,
2842    pub advanced_scope_mutating_commands_with_accepted_opaque_schema: usize,
2843    pub undocumented_schema_verbs_total: usize,
2844    pub opaque_schema_verbs_total: usize,
2845    pub accepted_opaque_schema_verbs_total: usize,
2846    pub unaccepted_opaque_schema_verbs_total: usize,
2847    pub missing_schema_examples: Vec<String>,
2848    pub missing_mutating_schema_examples: Vec<String>,
2849    #[serde(rename = "verified_scope_missing_schema_examples")]
2850    pub verified_scope_missing_schema_examples: Vec<String>,
2851    #[serde(rename = "verified_scope_accepted_opaque_schema_examples")]
2852    pub verified_scope_accepted_opaque_schema_examples: Vec<String>,
2853    pub advanced_scope_accepted_opaque_schema_examples: Vec<String>,
2854    pub accepted_opaque_schema_examples: Vec<String>,
2855    pub unaccepted_opaque_schema_examples: Vec<String>,
2856    pub undocumented_schema_examples: Vec<String>,
2857}
2858
2859#[derive(Debug, Serialize, JsonSchema)]
2860pub struct WatchLineSchema {
2861    pub ts: String,
2862    pub thread: Option<String>,
2863    pub kind: String,
2864    pub state_id: Option<String>,
2865    pub intent: Option<String>,
2866    pub confidence: Option<f32>,
2867    pub actor: Option<ActorInfoSchema>,
2868    pub id: u64,
2869}
2870
2871#[derive(Debug, Serialize, JsonSchema)]
2872pub struct TrySchema {
2873    pub status: String,
2874    pub action: String,
2875    pub message: String,
2876    pub thread: String,
2877    pub thread_dropped: bool,
2878    pub cleanup_error: Option<String>,
2879    pub exit_code: Option<i32>,
2880    pub duration_ms: u128,
2881    pub captured_state: Option<String>,
2882    pub merge_state: Option<String>,
2883    pub next_action: Option<String>,
2884    pub next_action_template: Option<ActionTemplateSchema>,
2885    pub recommended_action: Option<String>,
2886    pub recommended_action_template: Option<ActionTemplateSchema>,
2887    pub recovery_commands: Vec<String>,
2888    pub recovery_action_templates: Vec<ActionTemplateSchema>,
2889}
2890
2891// ---- git projection ops -----------------------------------------------------------
2892
2893#[derive(Debug, Serialize, JsonSchema)]
2894pub struct ExportedRefSchema {
2895    pub name: String,
2896    pub tip: String,
2897}
2898
2899#[derive(Debug, Serialize, JsonSchema)]
2900pub struct ExportGitSchema {
2901    pub output_kind: Option<String>,
2902    pub states_exported: u64,
2903    pub commits_total: u64,
2904    pub threads_synced: u64,
2905    pub markers_synced: u64,
2906    pub branches: Vec<ExportedRefSchema>,
2907    pub tags: Vec<ExportedRefSchema>,
2908    pub destination: String,
2909}
2910
2911#[derive(Debug, Serialize, JsonSchema)]
2912pub struct ImportGitSchema {
2913    pub output_kind: Option<String>,
2914    pub status: String,
2915    pub action: Option<String>,
2916    pub summary: String,
2917    pub commits_imported: u64,
2918    pub states_created: u64,
2919    pub branches_synced: u64,
2920    pub tags_synced: u64,
2921    pub skipped_non_commit_refs: u64,
2922    pub lossy_entries: Vec<LossyImportEntrySchema>,
2923    pub already_in_sync: bool,
2924    pub recommended_action: Option<String>,
2925    pub recommended_action_template: Option<ActionTemplateSchema>,
2926    pub recovery_commands: Vec<String>,
2927}
2928
2929#[derive(Debug, Serialize, JsonSchema)]
2930pub struct LossyImportEntrySchema {
2931    pub path: String,
2932    pub action: String,
2933    pub reason: String,
2934    pub git_object: Option<String>,
2935}
2936
2937#[derive(Debug, Serialize, JsonSchema)]
2938pub struct SyncGitSchema {
2939    pub output_kind: Option<String>,
2940    pub status: String,
2941    pub action: Option<String>,
2942    pub summary: String,
2943    pub states_exported: u64,
2944    pub commits_exported_total: u64,
2945    pub commits_imported: u64,
2946    pub threads_synced: u64,
2947    pub markers_synced: u64,
2948    pub recommended_action: Option<String>,
2949    pub recommended_action_template: Option<ActionTemplateSchema>,
2950    pub recovery_commands: Vec<String>,
2951}
2952
2953#[derive(Debug, Serialize, JsonSchema)]
2954pub struct RevertSchema {
2955    pub output_kind: String,
2956    pub state_id: Option<String>,
2957    pub reverted_state: String,
2958    pub files_affected: Vec<String>,
2959    pub message: String,
2960}
2961
2962// ---- git overlay diagnostics ---------------------------------------------
2963
2964// ---- doctor ---------------------------------------------------------------
2965
2966#[derive(Debug, Serialize, JsonSchema)]
2967pub struct DoctorSchema {
2968    pub output_kind: String,
2969    pub repository: String,
2970    pub repository_capability: String,
2971    pub storage_model: String,
2972    pub hosted_enabled: bool,
2973    #[serde(rename = "verification")]
2974    pub trust: RepositoryVerificationStateSchema,
2975    pub operation: OpaqueObject,
2976    pub remote_tracking: OpaqueObject,
2977    pub thread: Option<Value>,
2978    pub state: Option<Value>,
2979    pub changes: Value,
2980    pub workspace: Value,
2981    pub health: Value,
2982    pub recommended_action: Option<String>,
2983    pub recommended_action_template: Option<ActionTemplateSchema>,
2984    pub recovery_commands: Vec<String>,
2985    pub profile: Option<Value>,
2986}
2987
2988// ---- error envelope (cross-cutting) ---------------------------------------
2989//
2990// Emitted to **stderr** (not stdout) by any state-changing verb that fails
2991// when JSON output is selected. The 21 verb schemas above describe the
2992// stdout success shape; this schema describes the stderr failure shape so
2993// scripts and agents can parse failures without scraping freeform text.
2994//
2995// Field contract:
2996//
2997// - `code` — stable machine code; currently mirrors `kind`.
2998// - `error` — human-readable message (the anyhow chain rendered via `{:#}`).
2999//   Always present, never empty.
3000// - `exit_code` — process exit code emitted for the failure.
3001// - `hint` — single-line next-step recommendation. Empty string when no
3002//   actionable hint applies. JSON-mode runtime errors use a non-empty
3003//   fallback hint when no specific recovery class applies.
3004// - `kind` — stable predicate name keying the hint family. JSON-mode
3005//   runtime errors use `runtime_error` when the error didn't match a
3006//   known class. Current values include:
3007//   `repository_not_found`, `repository_exists`, `state_not_found`,
3008//   `thread_not_found`, `out_of_space`, `permission_denied`,
3009//   `read_only_filesystem`, and `runtime_error`. New kinds may be added
3010//   (additive); existing ones are stable.
3011// - `unsafe_condition`, `would_change`, `preserved` — typed safety facts.
3012// - `primary_command`, `primary_command_template` — the main recovery
3013//   action as a human-readable command string plus a fillable template
3014//   (always present for a valid action). The `_argv` sidecar was dropped
3015//   (HeddleCo/heddle#254): it was null for every placeholder action and
3016//   silently read as "no action" to agents — use the template instead.
3017// - `recovery_commands`, `recovery_action_templates` — all recovery
3018//   actions the runtime can represent, as command strings or fillable
3019//   templates.
3020
3021#[derive(Debug, Serialize, JsonSchema)]
3022pub struct ErrorEnvelopeSchema {
3023    pub error: String,
3024    pub exit_code: u8,
3025    pub hint: String,
3026    pub kind: String,
3027    pub op_id: Option<String>,
3028    pub idempotency_status: Option<String>,
3029    pub replayed: Option<bool>,
3030    pub unsafe_condition: String,
3031    pub would_change: String,
3032    pub preserved: String,
3033    pub primary_command: String,
3034    pub primary_command_template: NullableActionTemplateSchema,
3035    pub recovery_commands: Vec<String>,
3036    pub recovery_action_templates: Vec<ActionTemplateSchema>,
3037}
3038
3039#[derive(Debug, Serialize, JsonSchema)]
3040#[serde(untagged)]
3041#[allow(dead_code)]
3042pub enum NullableActionTemplateSchema {
3043    Template(ActionTemplateSchema),
3044    Null(()),
3045}
3046
3047#[derive(Debug, Serialize, JsonSchema)]
3048#[serde(untagged)]
3049#[allow(dead_code)]
3050pub enum NullableStringSchema {
3051    Value(String),
3052    Null(()),
3053}
3054
3055// ---------------------------------------------------------------------------
3056// Tests
3057// ---------------------------------------------------------------------------
3058
3059#[cfg(test)]
3060mod tests {
3061    use super::*;
3062
3063    fn required_fields(schema: &Value) -> Vec<&str> {
3064        schema
3065            .get("required")
3066            .and_then(|value| value.as_array())
3067            .expect("schema has required fields")
3068            .iter()
3069            .map(|value| value.as_str().expect("required field is a string"))
3070            .collect()
3071    }
3072
3073    fn property_schema<'a>(schema: &'a Value, property: &str) -> &'a Value {
3074        schema
3075            .get("properties")
3076            .and_then(|p| p.as_object())
3077            .and_then(|properties| properties.get(property))
3078            .unwrap_or_else(|| panic!("schema has `{property}` property"))
3079    }
3080
3081    fn resolve_schema_ref<'a>(root: &'a Value, reference: &str) -> &'a Value {
3082        reference
3083            .strip_prefix("#/$defs/")
3084            .or_else(|| reference.strip_prefix("#/definitions/"))
3085            .and_then(|name| {
3086                root.get("$defs")
3087                    .or_else(|| root.get("definitions"))
3088                    .and_then(|defs| defs.get(name))
3089            })
3090            .unwrap_or_else(|| panic!("schema reference `{reference}` resolves"))
3091    }
3092
3093    fn schema_declares_property(root: &Value, schema: &Value, property: &str) -> bool {
3094        if let Some(reference) = schema.get("$ref").and_then(|value| value.as_str()) {
3095            return schema_declares_property(root, resolve_schema_ref(root, reference), property);
3096        }
3097
3098        if schema
3099            .get("properties")
3100            .and_then(|properties| properties.get(property))
3101            .is_some()
3102        {
3103            return true;
3104        }
3105
3106        for combinator in ["anyOf", "oneOf"] {
3107            if let Some(schemas) = schema.get(combinator).and_then(|value| value.as_array()) {
3108                return !schemas.is_empty()
3109                    && schemas
3110                        .iter()
3111                        .all(|schema| schema_declares_property(root, schema, property));
3112            }
3113        }
3114
3115        schema
3116            .get("allOf")
3117            .and_then(|value| value.as_array())
3118            .is_some_and(|schemas| {
3119                schemas
3120                    .iter()
3121                    .any(|schema| schema_declares_property(root, schema, property))
3122            })
3123    }
3124
3125    fn schema_allows_null(root: &Value, schema: &Value) -> bool {
3126        if let Some(reference) = schema.get("$ref").and_then(|value| value.as_str()) {
3127            return schema_allows_null(root, resolve_schema_ref(root, reference));
3128        }
3129
3130        if schema.get("type") == Some(&Value::String("null".to_string())) {
3131            return true;
3132        }
3133        if schema
3134            .get("type")
3135            .and_then(|value| value.as_array())
3136            .is_some_and(|types| types.contains(&Value::String("null".to_string())))
3137        {
3138            return true;
3139        }
3140
3141        ["anyOf", "oneOf", "allOf"].iter().any(|combinator| {
3142            schema
3143                .get(*combinator)
3144                .and_then(|value| value.as_array())
3145                .is_some_and(|schemas| {
3146                    schemas
3147                        .iter()
3148                        .any(|schema| schema_allows_null(root, schema))
3149                })
3150        })
3151    }
3152
3153    fn collect_string_enums<'a>(root: &'a Value, schema: &'a Value, values: &mut Vec<&'a str>) {
3154        if let Some(reference) = schema.get("$ref").and_then(|value| value.as_str()) {
3155            collect_string_enums(root, resolve_schema_ref(root, reference), values);
3156        }
3157
3158        if let Some(enum_values) = schema.get("enum").and_then(|value| value.as_array()) {
3159            for value in enum_values {
3160                if let Some(value) = value.as_str() {
3161                    values.push(value);
3162                }
3163            }
3164        }
3165
3166        for combinator in ["anyOf", "oneOf", "allOf"] {
3167            if let Some(schemas) = schema.get(combinator).and_then(|value| value.as_array()) {
3168                for schema in schemas {
3169                    collect_string_enums(root, schema, values);
3170                }
3171            }
3172        }
3173    }
3174
3175    fn collect_discriminator_values<'a>(
3176        root: &'a Value,
3177        schema: &'a Value,
3178        field: &str,
3179        values: &mut Vec<&'a str>,
3180    ) {
3181        if let Some(reference) = schema.get("$ref").and_then(|value| value.as_str()) {
3182            collect_discriminator_values(root, resolve_schema_ref(root, reference), field, values);
3183            return;
3184        }
3185
3186        if let Some(property) = schema
3187            .get("properties")
3188            .and_then(|properties| properties.get(field))
3189        {
3190            collect_string_enums(root, property, values);
3191        }
3192
3193        for combinator in ["anyOf", "oneOf", "allOf"] {
3194            if let Some(schemas) = schema.get(combinator).and_then(|value| value.as_array()) {
3195                for schema in schemas {
3196                    collect_discriminator_values(root, schema, field, values);
3197                }
3198            }
3199        }
3200    }
3201
3202    fn schema_requires_discriminator(root: &Value, schema: &Value, field: &str) -> bool {
3203        if let Some(reference) = schema.get("$ref").and_then(|value| value.as_str()) {
3204            return schema_requires_discriminator(root, resolve_schema_ref(root, reference), field);
3205        }
3206
3207        if schema
3208            .get("properties")
3209            .and_then(|properties| properties.get(field))
3210            .is_some()
3211        {
3212            return schema
3213                .get("required")
3214                .and_then(|value| value.as_array())
3215                .is_some_and(|required| {
3216                    required
3217                        .iter()
3218                        .any(|required_field| required_field.as_str() == Some(field))
3219                });
3220        }
3221
3222        for combinator in ["anyOf", "oneOf"] {
3223            if let Some(schemas) = schema.get(combinator).and_then(|value| value.as_array()) {
3224                return !schemas.is_empty()
3225                    && schemas
3226                        .iter()
3227                        .all(|schema| schema_requires_discriminator(root, schema, field));
3228            }
3229        }
3230
3231        schema
3232            .get("allOf")
3233            .and_then(|value| value.as_array())
3234            .is_some_and(|schemas| {
3235                schemas
3236                    .iter()
3237                    .any(|schema| schema_requires_discriminator(root, schema, field))
3238            })
3239    }
3240
3241    /// Every schema verb advertised by the command contract table must
3242    /// produce a schema.
3243    /// Otherwise `heddle doctor schemas` would silently miss drift on
3244    /// that verb.
3245    #[test]
3246    fn registry_covers_every_listed_verb() {
3247        for verb in schema_verbs() {
3248            assert!(
3249                schema_for_verb(verb).is_some(),
3250                "verb '{verb}' is advertised by command contracts but schema_for_verb returned None"
3251            );
3252        }
3253    }
3254
3255    #[test]
3256    fn documented_registry_is_subset_of_runtime_registry() {
3257        let all = schema_verbs();
3258        for verb in documented_schema_verbs() {
3259            assert!(
3260                all.contains(verb),
3261                "documented schema verb '{verb}' is not advertised as a runtime schema"
3262            );
3263        }
3264    }
3265
3266    /// Every documented (non-opaque) verb whose catalog advertises an
3267    /// `output_kind` discriminator must declare the `output_kind`
3268    /// property on its *registered schema struct*, not merely rely on the
3269    /// runtime injection in [`schema_for_verb`].
3270    ///
3271    /// heddle#272 r6 (Codex P2): `schema_for_verb` injects the
3272    /// discriminator from the catalog after deriving the struct schema,
3273    /// so `heddle schemas <verb>` already surfaces `output_kind`. That
3274    /// injection masks the fact that the Rust mirror struct (e.g.
3275    /// `DiffSchema`) never declares the field. The mirror
3276    /// is the source of truth a reader greps; it must be honest about the
3277    /// discriminator the runtime always emits. This check reads the
3278    /// *pre-injection* struct schema so a missing field fails CI rather
3279    /// than being papered over by the catalog.
3280    #[test]
3281    fn documented_swept_schema_structs_declare_output_kind() {
3282        let mut missing = Vec::new();
3283        for verb in documented_schema_verbs() {
3284            // Opaque verbs expose a generic object schema; their
3285            // discriminator is genuinely catalog-only (there is no
3286            // Serialize mirror struct to declare it on).
3287            if opaque_schema_verbs().contains(verb) {
3288                continue;
3289            }
3290            let Some(discriminator) =
3291                command_catalog::command_json_discriminator_for_schema_verb(verb)
3292            else {
3293                continue;
3294            };
3295            if discriminator.field != "output_kind" {
3296                continue;
3297            }
3298            let bare = schema_for_report_contract_verb(verb)
3299                .or_else(|| schema_for_registered_verb(verb))
3300                .unwrap_or_else(|| panic!("documented verb `{verb}` has no registered schema"));
3301            let declares = schema_declares_property(&bare, &bare, "output_kind");
3302            if !declares {
3303                missing.push(format!(
3304                    "{verb}: catalog advertises output_kind=`{}` but the schema struct declares no `output_kind` property",
3305                    discriminator.value
3306                ));
3307            }
3308        }
3309        assert!(
3310            missing.is_empty(),
3311            "Documented swept schema structs missing the `output_kind` property. Add \
3312             `pub output_kind: String` to each mirror struct so it matches the runtime \
3313             emission (the catalog injection masks this at the `heddle schemas` layer, \
3314             but the struct must be honest):\n  - {}",
3315            missing.join("\n  - ")
3316        );
3317    }
3318
3319    #[test]
3320    fn implementation_registry_matches_command_contract_registry() {
3321        let advertised = schema_verbs();
3322        let mut implemented = schema_implementation_verbs();
3323        for verb in opaque_schema_verbs() {
3324            if !implemented.contains(verb) {
3325                implemented.push(*verb);
3326            }
3327            assert!(
3328                advertised.contains(verb),
3329                "opaque schema verb '{verb}' must also be advertised by active command contracts"
3330            );
3331        }
3332        for verb in advertised {
3333            assert!(
3334                implemented.contains(verb),
3335                "verb '{verb}' is advertised by command contracts but the schema implementation registry does not handle it"
3336            );
3337        }
3338        for verb in &implemented {
3339            if cfg!(all(feature = "git-overlay", feature = "semantic")) {
3340                assert!(
3341                    advertised.contains(verb),
3342                    "verb '{verb}' has a schema implementation but is not advertised by active command contracts"
3343                );
3344            } else if !advertised.contains(verb) {
3345                assert!(
3346                    schema_for_verb(verb).is_none(),
3347                    "inactive schema implementation '{verb}' must not be publicly resolvable"
3348                );
3349            }
3350        }
3351    }
3352
3353    #[test]
3354    fn command_catalog_schema_verbs_match_schema_list_except_error_envelope() {
3355        let catalog = command_catalog::build_command_catalog();
3356        let mut catalog_verbs = catalog
3357            .commands
3358            .iter()
3359            .flat_map(|command| command.schema_verbs.iter().map(String::as_str))
3360            .collect::<Vec<_>>();
3361        catalog_verbs.sort_unstable();
3362        catalog_verbs.dedup();
3363
3364        let mut listed_verbs = schema_verbs().to_vec();
3365        listed_verbs.sort_unstable();
3366        listed_verbs.retain(|verb| *verb != "error");
3367
3368        assert_eq!(
3369            catalog_verbs, listed_verbs,
3370            "`heddle help --output json` command schema verbs must match `heddle schemas` except for the cross-cutting JSON error envelope"
3371        );
3372    }
3373
3374    #[cfg(not(feature = "git-overlay"))]
3375    #[test]
3376    fn native_only_schema_registry_excludes_git_overlay_verbs() {
3377        let catalog = command_catalog::build_command_catalog();
3378        for verb in [
3379            "import git",
3380            "export git",
3381            "sync git",
3382            "context reason git",
3383            "git-overlay",
3384        ] {
3385            assert!(
3386                !schema_verbs().contains(&verb),
3387                "native-only schema listing must not advertise git-overlay verb `{verb}`"
3388            );
3389            assert!(
3390                !documented_schema_verbs().contains(&verb),
3391                "native-only documented schema listing must not advertise git-overlay verb `{verb}`"
3392            );
3393            assert!(
3394                schema_for_verb(verb).is_none(),
3395                "native-only schema lookup must reject git-overlay verb `{verb}`"
3396            );
3397            assert!(
3398                catalog.commands.iter().all(|command| {
3399                    !command
3400                        .schema_verbs
3401                        .iter()
3402                        .any(|schema_verb| schema_verb == verb)
3403                        && !command
3404                            .documented_schema_verbs
3405                            .iter()
3406                            .any(|schema_verb| schema_verb == verb)
3407                }),
3408                "native-only command catalog must not advertise git-overlay schema verb `{verb}`"
3409            );
3410        }
3411    }
3412
3413    #[test]
3414    fn unknown_verb_returns_none() {
3415        assert!(schema_for_verb("nope").is_none());
3416    }
3417
3418    #[test]
3419    fn status_schema_has_expected_top_level_properties() {
3420        let schema = schema_for_verb("status").expect("status schema");
3421        let properties = schema
3422            .get("properties")
3423            .and_then(|p| p.as_object())
3424            .expect("status schema has properties");
3425        for required in &[
3426            "repository_capability",
3427            "storage_model",
3428            "hosted_enabled",
3429            "verification",
3430            "thread",
3431            "current_state",
3432            "actor",
3433            "blockers",
3434            "changes",
3435        ] {
3436            assert!(
3437                properties.contains_key(*required),
3438                "status schema missing property '{required}'"
3439            );
3440        }
3441        for legacy in &["git_overlay_import_hint", "git_overlay_health"] {
3442            assert!(
3443                !properties.contains_key(*legacy),
3444                "status schema must expose verification, not legacy Git overlay sidecar '{legacy}'"
3445            );
3446        }
3447    }
3448
3449    #[test]
3450    fn verify_schema_nests_repository_verification_state() {
3451        let schema = schema_for_verb("verify").expect("verify schema");
3452        let properties = schema
3453            .get("properties")
3454            .and_then(|p| p.as_object())
3455            .expect("verify schema has properties");
3456        assert!(
3457            properties.contains_key("verification"),
3458            "verify schema must expose nested verification state"
3459        );
3460        for flattened in ["verified", "status", "checks", "recommended_action"] {
3461            assert!(
3462                !properties.contains_key(flattened),
3463                "verify schema must not expose flattened verification property `{flattened}`"
3464            );
3465        }
3466    }
3467
3468    #[test]
3469    fn action_template_agent_may_fill_schema_describes_false_semantics() {
3470        let schema = schema_for_verb("verify").expect("verify schema");
3471        let action_template = schema
3472            .get("$defs")
3473            .or_else(|| schema.get("definitions"))
3474            .and_then(|defs| {
3475                defs.get("ActionTemplate")
3476                    .or_else(|| defs.get("ActionTemplateSchema"))
3477            })
3478            .expect("verify schema includes ActionTemplateSchema definition");
3479        let description = property_schema(action_template, "agent_may_fill")
3480            .get("description")
3481            .and_then(Value::as_str)
3482            .expect("agent_may_fill schema description is present");
3483
3484        assert!(
3485            description.contains("When `agent_may_fill` is false"),
3486            "agent_may_fill schema description must document false semantics: {description}"
3487        );
3488        assert!(
3489            description.contains("display-only"),
3490            "agent_may_fill schema description must warn agents not to execute display-only templates: {description}"
3491        );
3492        assert!(
3493            description.contains("do not substitute `<name>`/`<url>` placeholders"),
3494            "agent_may_fill schema description must prohibit placeholder substitution when false: {description}"
3495        );
3496    }
3497
3498    /// HeddleCo/heddle#645 conformance: the action-field presence contract.
3499    ///
3500    /// `next_action` / `recommended_action` encode "no action needed" as
3501    /// `null` and "not applicable to this output shape" as an absent
3502    /// field — never as `""` (the runtime maps empty selections to `None`
3503    /// via `next_action::normalized_action` /
3504    /// `serialize_empty_action_as_null`, and the serialization walker in
3505    /// `validate_next_actions_at_path` rejects any empty string that
3506    /// slips past). At the schema level that means: wherever one of these
3507    /// properties is *required*, its schema must allow `null` — a
3508    /// non-nullable required action field would force emitters to leak
3509    /// `""` for the no-action case.
3510    #[test]
3511    fn action_fields_follow_presence_contract_in_every_schema() {
3512        fn walk(root: &Value, schema: &Value, verb: &str, path: &str) {
3513            match schema {
3514                Value::Object(object) => {
3515                    if let Some(properties) = object.get("properties").and_then(|p| p.as_object()) {
3516                        let required: Vec<&str> = object
3517                            .get("required")
3518                            .and_then(|value| value.as_array())
3519                            .map(|fields| {
3520                                fields.iter().filter_map(|field| field.as_str()).collect()
3521                            })
3522                            .unwrap_or_default();
3523                        for (name, child) in properties {
3524                            if matches!(name.as_str(), "next_action" | "recommended_action")
3525                                && required.contains(&name.as_str())
3526                            {
3527                                assert!(
3528                                    schema_allows_null(root, child),
3529                                    "`{verb}` schema requires `{path}.{name}` without allowing \
3530                                     null; the action contract is null = no action, absent = \
3531                                     not applicable, never \"\": {child}"
3532                                );
3533                            }
3534                        }
3535                    }
3536                    for (key, child) in object {
3537                        walk(root, child, verb, &format!("{path}.{key}"));
3538                    }
3539                }
3540                Value::Array(items) => {
3541                    for (index, child) in items.iter().enumerate() {
3542                        walk(root, child, verb, &format!("{path}[{index}]"));
3543                    }
3544                }
3545                _ => {}
3546            }
3547        }
3548
3549        for verb in schema_verbs() {
3550            let schema =
3551                schema_for_verb(verb).unwrap_or_else(|| panic!("schema registered for `{verb}`"));
3552            walk(&schema, &schema, verb, "$");
3553        }
3554    }
3555
3556    #[test]
3557    fn status_schema_allows_null_recommended_action() {
3558        let schema = schema_for_verb("status").expect("status schema");
3559        let recommended_action = property_schema(&schema, "recommended_action");
3560        assert!(
3561            schema_allows_null(&schema, recommended_action),
3562            "status recommended_action must allow null because empty actions serialize as null: {recommended_action}"
3563        );
3564
3565        let required = required_fields(&schema);
3566        assert!(
3567            required.contains(&"recommended_action"),
3568            "status recommended_action should remain a stable emitted field: {schema}"
3569        );
3570    }
3571
3572    #[test]
3573    fn status_agent_context_fields_are_omittable() {
3574        let schema = schema_for_verb("status").expect("status schema");
3575        let required = required_fields(&schema);
3576        for field in [
3577            "path",
3578            "execution_path",
3579            "session_id",
3580            "heddle_session_id",
3581            "actor",
3582            "harness",
3583            "thinking_level",
3584            "usage_summary",
3585            "last_progress_at",
3586            "report_flush_state",
3587            "attach_reason",
3588            "target_thread",
3589            "parent_thread",
3590            "task",
3591        ] {
3592            assert!(
3593                !required.contains(&field),
3594                "status `{field}` is omitted when no agent/materialized context is recorded: {schema}"
3595            );
3596        }
3597    }
3598
3599    #[test]
3600    fn status_thread_mode_schema_matches_observed_modes() {
3601        let schema = schema_for_verb("status").expect("status schema");
3602        let mut values = Vec::new();
3603        collect_string_enums(
3604            &schema,
3605            property_schema(&schema, "thread_mode"),
3606            &mut values,
3607        );
3608
3609        for expected in ["materialized", "virtualized", "solid"] {
3610            assert!(
3611                values.contains(&expected),
3612                "status thread_mode schema missing observed mode `{expected}`: {values:?}"
3613            );
3614        }
3615        assert!(
3616            !values.contains(&"lightweight"),
3617            "status thread_mode schema must not advertise removed mode `lightweight`: {values:?}"
3618        );
3619    }
3620
3621    #[test]
3622    fn ready_schema_requires_stable_operator_and_readiness_fields() {
3623        let schema = schema_for_verb("ready").expect("ready schema");
3624        let properties = schema
3625            .get("properties")
3626            .and_then(|p| p.as_object())
3627            .expect("ready schema has properties");
3628        assert!(
3629            properties.contains_key("blockers"),
3630            "ready schema should still document blockers when emitted"
3631        );
3632        assert!(
3633            properties.contains_key("warnings"),
3634            "ready schema should still document warnings when emitted"
3635        );
3636        assert!(
3637            properties.contains_key("readiness"),
3638            "ready schema should document the stable readiness summary"
3639        );
3640        assert!(
3641            properties.contains_key("verification"),
3642            "ready schema should document the repository verification proof"
3643        );
3644
3645        let required = required_fields(&schema);
3646        for stable_field in ["blockers", "warnings", "readiness", "verification"] {
3647            assert!(
3648                required.contains(&stable_field),
3649                "ready schema must require `{stable_field}` because ready JSON always emits the stable field set: {schema}"
3650            );
3651        }
3652        assert!(
3653            properties.contains_key("captured_state"),
3654            "ready schema should document captured_state even though schemars models nullable Option fields as optional"
3655        );
3656    }
3657
3658    #[test]
3659    fn land_batch_peer_primary_command_remains_optional() {
3660        let schema = schema_for_verb("land --threads").expect("land batch schema");
3661        let peer = schema
3662            .get("$defs")
3663            .and_then(Value::as_object)
3664            .and_then(|defs| defs.get("LandBatchPeerSchema"))
3665            .expect("land batch peer schema");
3666        assert!(
3667            property_schema(peer, "primary_command").is_object(),
3668            "peer schema must still describe primary_command: {peer}"
3669        );
3670        assert!(
3671            !required_fields(peer).contains(&"primary_command"),
3672            "successful peers omit None primary_command values: {peer}"
3673        );
3674    }
3675
3676    #[test]
3677    fn push_schema_requires_stable_runtime_fields() {
3678        let schema = schema_for_verb("push").expect("push schema");
3679        let variants = schema
3680            .get("anyOf")
3681            .and_then(Value::as_array)
3682            .expect("push schema has transport variants")
3683            .iter()
3684            .map(|variant| {
3685                let reference = variant.get("$ref").and_then(Value::as_str).or_else(|| {
3686                    variant
3687                        .get("allOf")
3688                        .and_then(Value::as_array)
3689                        .and_then(|parts| {
3690                            parts
3691                                .iter()
3692                                .find_map(|part| part.get("$ref").and_then(Value::as_str))
3693                        })
3694                });
3695                reference
3696                    .map(|reference| resolve_schema_ref(&schema, reference))
3697                    .unwrap_or(variant)
3698            })
3699            .collect::<Vec<_>>();
3700        assert_eq!(variants.len(), 2, "Git and Heddle push variants");
3701
3702        for variant in &variants {
3703            let required = required_fields(variant);
3704            for stable_field in [
3705                "output_kind",
3706                "action",
3707                "status",
3708                "pushed",
3709                "changed",
3710                "success",
3711                "transport",
3712                "next_action",
3713                "next_action_template",
3714                "recommended_action",
3715                "recommended_action_template",
3716                "verification",
3717            ] {
3718                assert!(
3719                    required.contains(&stable_field),
3720                    "every push variant must require `{stable_field}`: {variant}"
3721                );
3722            }
3723        }
3724
3725        let git_required = variants
3726            .iter()
3727            .map(|variant| required_fields(variant))
3728            .find(|required| required.contains(&"ref_scope"))
3729            .expect("Git push variant");
3730        for git_field in [
3731            "remote",
3732            "push_scope",
3733            "refs_written",
3734            "tags_included",
3735            "force",
3736        ] {
3737            assert!(
3738                git_required.contains(&git_field),
3739                "Git push requires `{git_field}`"
3740            );
3741        }
3742
3743        let heddle_required = variants
3744            .iter()
3745            .map(|variant| required_fields(variant))
3746            .find(|required| !required.contains(&"ref_scope"))
3747            .expect("Heddle push variant");
3748        for conditional in [
3749            "remote",
3750            "push_scope",
3751            "refs_written",
3752            "force",
3753            "thread",
3754            "state",
3755            "objects",
3756        ] {
3757            assert!(
3758                !heddle_required.contains(&conditional),
3759                "Heddle push conditionally omits `{conditional}`"
3760            );
3761        }
3762    }
3763
3764    #[test]
3765    fn advertised_json_discriminators_are_reflected_in_schemas() {
3766        use std::collections::{BTreeMap, BTreeSet};
3767
3768        for schema_verb in schema_verbs() {
3769            let mut discriminators =
3770                command_catalog::command_json_discriminators_for_schema_verb(schema_verb);
3771            if discriminators.is_empty() {
3772                continue;
3773            };
3774            let schema =
3775                schema_for_verb(schema_verb).unwrap_or_else(|| panic!("{schema_verb} schema"));
3776            if schema.get("anyOf").is_some() {
3777                // A union schema published under this verb covers every schema
3778                // verb its catalog entry documents — the expected discriminator
3779                // set must include the siblings (e.g. inspect's union carries
3780                // the `thread show` branch's thread_show).
3781                for sibling in command_catalog::sibling_documented_schema_verbs(schema_verb) {
3782                    discriminators.extend(
3783                        command_catalog::command_json_discriminators_for_schema_verb(sibling),
3784                    );
3785                }
3786                for discriminator in command_catalog::command_json_discriminators()
3787                    .into_iter()
3788                    .filter(|discriminator| {
3789                        discriminator.display == *schema_verb
3790                            && discriminator.schema_verb.as_deref() != Some(schema_verb)
3791                    })
3792                {
3793                    discriminators.push(discriminator);
3794                }
3795            }
3796
3797            let mut expected_by_field = BTreeMap::<String, BTreeSet<String>>::new();
3798            for discriminator in discriminators {
3799                expected_by_field
3800                    .entry(discriminator.field)
3801                    .or_default()
3802                    .insert(discriminator.value);
3803            }
3804
3805            for (field, expected) in expected_by_field {
3806                let mut actual = Vec::new();
3807                collect_discriminator_values(&schema, &schema, &field, &mut actual);
3808                let actual = actual
3809                    .into_iter()
3810                    .map(str::to_string)
3811                    .collect::<BTreeSet<_>>();
3812                assert_eq!(
3813                    actual, expected,
3814                    "{schema_verb} schema must narrow `{field}` to every catalog-advertised value"
3815                );
3816                assert!(
3817                    schema_requires_discriminator(&schema, &schema, &field),
3818                    "{schema_verb} schema must require discriminator field `{field}`"
3819                );
3820            }
3821        }
3822    }
3823
3824    #[test]
3825    fn oss_recovery_surfaces_do_not_use_opaque_generic_schema() {
3826        for verb in [
3827            "fsck",
3828            "resolve",
3829            "retro",
3830            "discuss open",
3831            "discuss append",
3832            "discuss resolve",
3833            "discuss reopen",
3834            "discuss list",
3835            "discuss show",
3836            "query",
3837            "query --attribution",
3838        ] {
3839            assert!(
3840                !opaque_schema_verbs().contains(&verb),
3841                "`{verb}` should have a concrete machine-contract schema, not the opaque generic object"
3842            );
3843            let schema = schema_for_verb(verb).unwrap_or_else(|| panic!("{verb} schema exists"));
3844            assert_ne!(
3845                schema.get("additionalProperties"),
3846                Some(&Value::Bool(true)),
3847                "`{verb}` schema should not accept arbitrary top-level fields"
3848            );
3849        }
3850    }
3851
3852    #[test]
3853    fn op_id_supported_schema_verbs_declare_replay_fields() {
3854        let mut checked = 0;
3855        for verb in schema_verbs() {
3856            if !schema_verb_supports_op_id(verb) {
3857                continue;
3858            }
3859            checked += 1;
3860            let schema =
3861                schema_for_verb(verb).unwrap_or_else(|| panic!("schema for `{verb}` exists"));
3862            let properties = schema
3863                .get("properties")
3864                .and_then(|p| p.as_object())
3865                .unwrap_or_else(|| panic!("schema for `{verb}` should expose properties"));
3866            for required in OP_ID_REPLAY_FIELD_NAMES {
3867                assert!(
3868                    properties.contains_key(*required),
3869                    "schema for op-id-supported verb `{verb}` missing replay property `{required}`"
3870                );
3871            }
3872        }
3873        assert!(
3874            checked > 1,
3875            "op-id schema coverage test should exercise multiple verbs"
3876        );
3877    }
3878
3879    #[test]
3880    fn log_schema_has_states_array() {
3881        let schema = schema_for_verb("log").expect("log schema");
3882        let properties = schema
3883            .get("properties")
3884            .and_then(|p| p.as_object())
3885            .unwrap();
3886        assert!(properties.contains_key("states"));
3887        assert!(properties.contains_key("repository_capability"));
3888    }
3889}