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