Skip to main content

heddle_cli_contract/cli/commands/command_catalog/
mod.rs

1// SPDX-License-Identifier: Apache-2.0
2//! Machine-readable command catalog.
3
4use std::sync::OnceLock;
5
6use clap::{ArgAction, CommandFactory};
7pub use heddle_core::ActionTemplate;
8use heddle_core::{
9    DiffReport, FsckReport, MachineOutputKind, QueryReport, ReportContract as CoreReportContract,
10    StatusReport, VerifyReport,
11};
12use schemars::JsonSchema;
13use serde::Serialize;
14
15#[cfg(feature = "client")]
16use crate::cli::AuthCommands;
17#[cfg(feature = "semantic")]
18use crate::cli::SemanticCommands;
19#[cfg(feature = "git-overlay")]
20use crate::cli::cli_args::SyncCommands;
21use crate::cli::{
22    AgentCommands, Cli, Commands, ContextCommands, DaemonCommands, DoctorCommands, HookCommands,
23    IntegrationCommands, MaintenanceCommands, OplogCommands, PurgeCommands, RedactCommands,
24    RedactTrustCommands, RemoteCommands, ShellCommands, ThreadCommands, ThreadMarkerCommands,
25    TimelineCommands, VisibilityCommands,
26    cli_args::{
27        AgentFanoutCommands, AgentPresenceCommands, AgentProvenanceCommands, AgentTaskCommands,
28        DiscussCommands, ReviewCommands,
29    },
30    render::shell_quote,
31};
32#[cfg(feature = "git-overlay")]
33use crate::cli::{ExportCommands, ImportCommands};
34
35#[derive(Debug, Clone, Serialize, JsonSchema)]
36pub struct CommandCatalogOutput {
37    pub kind: String,
38    pub executable_path: String,
39    pub commands: Vec<CommandCatalogEntry>,
40    pub global_options: Vec<CommandCatalogOption>,
41    pub json_discriminators: Vec<CommandJsonDiscriminator>,
42    pub recommended_action_placeholders: Vec<String>,
43    pub recommended_action_templates: Vec<ActionTemplate>,
44}
45
46#[derive(Debug, Clone, Serialize, JsonSchema)]
47pub struct CommandCatalogEntry {
48    pub path: Vec<String>,
49    pub display: String,
50    pub aliases: Vec<String>,
51    pub tier: String,
52    pub surface: String,
53    pub help_visibility: String,
54    pub help_rank: u16,
55    pub canonical_command: Option<String>,
56    pub canonical_action: Option<CanonicalAction>,
57    pub command_action: Option<CommandAction>,
58    pub summary: String,
59    pub has_subcommands: bool,
60    pub supports_json: bool,
61    pub output_modes: Vec<String>,
62    pub mutates: bool,
63    pub supports_op_id: bool,
64    pub persists_op_id: bool,
65    pub op_id_behavior: String,
66    pub op_id_store_scope: String,
67    pub observe_only: bool,
68    pub may_initialize: bool,
69    pub may_import_git: bool,
70    pub may_write_worktree: bool,
71    pub may_move_ref: bool,
72    pub destructive_requires_force: bool,
73    pub writes_heddle_refs: bool,
74    pub writes_git_refs: bool,
75    pub writes_worktree: bool,
76    pub writes_config: bool,
77    pub writes_hooks: bool,
78    pub network_io: bool,
79    pub daemon_process: bool,
80    pub object_gc: bool,
81    pub external_command: bool,
82    pub requires_git_executable: bool,
83    pub destructive_data: bool,
84    pub side_effects: Vec<CommandSideEffect>,
85    pub side_effect_class: String,
86    pub first_run_behavior: String,
87    pub json_kind: String,
88    pub json_discriminators: Vec<CommandJsonDiscriminator>,
89    pub schema_verbs: Vec<String>,
90    pub documented_schema_verbs: Vec<String>,
91    pub options: Vec<CommandCatalogOption>,
92    pub arguments: Vec<CommandCatalogArgument>,
93    /// Sysexits-style codes this command may legitimately return, with a
94    /// one-line agent-facing reason. Empty for commands not yet swept. See
95    /// `docs/exit-codes.md` for the full taxonomy.
96    pub exit_codes: Vec<CommandCatalogExitCode>,
97}
98
99#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, JsonSchema)]
100#[serde(rename_all = "snake_case")]
101pub enum CommandSideEffect {
102    ObserveOnly,
103    Initialize,
104    ImportGit,
105    WritesHeddleRefs,
106    WritesGitRefs,
107    WritesWorktree,
108    MayWriteWorktree,
109    WritesMetadata,
110    WritesConfig,
111    WritesHooks,
112    NetworkIo,
113    DaemonProcess,
114    ObjectGc,
115    ExternalCommand,
116    DestructiveRequiresForce,
117    DestructiveData,
118}
119
120#[derive(Debug, Clone, Serialize, JsonSchema)]
121pub struct CommandCatalogExitCode {
122    pub code: u8,
123    pub reason: String,
124}
125
126#[derive(Debug, Clone, Serialize, JsonSchema)]
127pub struct CanonicalAction {
128    pub command: String,
129    pub kind: String,
130    pub executable: bool,
131    pub note: String,
132    pub argv: Option<Vec<String>>,
133    pub template: Option<ActionTemplate>,
134}
135
136#[derive(Debug, Clone, Serialize, JsonSchema)]
137pub struct CommandAction {
138    pub action: String,
139    pub executable: bool,
140    pub argv: Option<Vec<String>>,
141    pub template: Option<ActionTemplate>,
142}
143
144#[derive(Debug, Clone, Serialize, JsonSchema)]
145pub struct CommandCatalogOption {
146    pub id: String,
147    pub long: Option<String>,
148    pub aliases: Vec<String>,
149    pub short: Option<String>,
150    pub value_names: Vec<String>,
151    pub value_kind: String,
152    pub default_values: Vec<String>,
153    pub possible_values: Vec<String>,
154    pub help: Option<String>,
155    pub required: bool,
156    pub global: bool,
157    pub hidden: bool,
158}
159
160#[derive(Debug, Clone, Serialize, JsonSchema)]
161pub struct CommandCatalogArgument {
162    pub id: String,
163    pub value_names: Vec<String>,
164    pub help: Option<String>,
165    pub required: bool,
166}
167
168#[derive(Debug, Clone)]
169pub struct ActionFields {
170    pub action: Option<String>,
171    pub template: Option<ActionTemplate>,
172}
173
174impl ActionFields {
175    pub(crate) fn from_optional_action(action: Option<String>) -> Self {
176        let Some(action) = action.filter(|action| !action.trim().is_empty()) else {
177            return Self::none();
178        };
179        validate_recommended_action(&action)
180            .unwrap_or_else(|err| panic!("invalid recommended action `{action}`: {err}"));
181        Self {
182            template: recommended_action_template(&action),
183            action: Some(action),
184        }
185    }
186
187    pub fn from_optional_action_ref(action: Option<&str>) -> Self {
188        Self::from_optional_action(action.map(str::to_string))
189    }
190
191    pub fn from_action(action: &str) -> Self {
192        Self::from_optional_action_ref(Some(action))
193    }
194
195    pub fn none() -> Self {
196        Self {
197            action: None,
198            template: None,
199        }
200    }
201}
202
203pub(crate) fn checked_action_from_argv<I, S>(argv: I) -> String
204where
205    I: IntoIterator<Item = S>,
206    S: AsRef<str>,
207{
208    let action = argv
209        .into_iter()
210        .map(|arg| shell_quote(arg.as_ref()))
211        .collect::<Vec<_>>()
212        .join(" ");
213    validate_recommended_action(&action)
214        .unwrap_or_else(|err| panic!("invalid recommended action `{action}`: {err}"));
215    action
216}
217
218pub fn heddle_action<I, S>(args: I) -> String
219where
220    I: IntoIterator<Item = S>,
221    S: AsRef<str>,
222{
223    let argv = std::iter::once("heddle".to_string())
224        .chain(args.into_iter().map(|arg| arg.as_ref().to_string()))
225        .collect::<Vec<_>>();
226    checked_action_from_argv(argv)
227}
228
229/// Build the `--thread <id>` argv fragment for splicing into a [`heddle_action`]
230/// argv. A historical / `new_unchecked` thread id that starts with `-` is
231/// rendered as the combined `--thread=<id>` token, which clap binds as the
232/// flag's value; the plain `--thread`, `<id>` pair would otherwise be re-parsed
233/// (after `split_recommended_action`) with `-foo` as another option, and
234/// `checked_action_from_argv` would panic. (heddle#464 close-the-class.)
235pub(crate) fn thread_flag_args(thread_id: &str) -> Vec<String> {
236    if thread_id.starts_with('-') {
237        vec![format!("--thread={thread_id}")]
238    } else {
239        vec!["--thread".to_string(), thread_id.to_string()]
240    }
241}
242
243/// Build the canonical non-mutating merge-preview command for a thread.
244pub fn merge_preview_command(thread_id: &str) -> String {
245    let mut argv = vec!["ready".to_string()];
246    argv.extend(thread_flag_args(thread_id));
247    heddle_action(argv)
248}
249
250/// Build the canonical local land command for a thread.
251pub fn land_local_command(thread_id: &str) -> String {
252    let mut argv = vec!["land".to_string()];
253    argv.extend(thread_flag_args(thread_id));
254    heddle_action(argv)
255}
256
257#[derive(Debug, Clone, PartialEq, Eq, Serialize, JsonSchema)]
258pub struct CommandJsonDiscriminator {
259    pub path: Vec<String>,
260    pub display: String,
261    pub schema_verb: Option<String>,
262    pub field: String,
263    pub value: String,
264    pub no_schema_reason: Option<String>,
265}
266
267impl CommandCatalogOutput {
268    pub fn command_by_display(&self, display: &str) -> Option<&CommandCatalogEntry> {
269        self.commands.iter().find(|entry| entry.display == display)
270    }
271
272    pub fn command_by_path(&self, path: &[String]) -> Option<&CommandCatalogEntry> {
273        self.commands.iter().find(|entry| entry.path == path)
274    }
275
276    pub fn options_for_display(&self, display: &str) -> Option<Vec<&CommandCatalogOption>> {
277        let entry = self.command_by_display(display)?;
278        Some(self.options_for_entry(entry))
279    }
280
281    pub fn options_for_path(&self, path: &[String]) -> Option<Vec<&CommandCatalogOption>> {
282        let entry = self.command_by_path(path)?;
283        Some(self.options_for_entry(entry))
284    }
285
286    pub fn options_for_entry<'a>(
287        &'a self,
288        entry: &'a CommandCatalogEntry,
289    ) -> Vec<&'a CommandCatalogOption> {
290        self.global_options
291            .iter()
292            .chain(entry.options.iter())
293            .collect()
294    }
295}
296
297#[derive(Debug, Clone, Copy)]
298struct CommandContract {
299    supports_json: bool,
300    supports_json_compact: bool,
301    mutates: bool,
302    /// Whether the command's mutation target is the current/`--repo`
303    /// repository. Destination-scoped commands such as `clone` must not run
304    /// current-repository recovery before their own destination preflight.
305    targets_current_repository: bool,
306    supports_op_id: bool,
307    persists_op_id: bool,
308    observe_only: bool,
309    may_initialize: bool,
310    may_import_git: bool,
311    may_write_worktree: bool,
312    may_move_ref: bool,
313    destructive_requires_force: bool,
314    writes_heddle_refs: bool,
315    writes_git_refs: bool,
316    writes_worktree: bool,
317    writes_metadata: bool,
318    writes_config: bool,
319    writes_hooks: bool,
320    network_io: bool,
321    daemon_process: bool,
322    object_gc: bool,
323    external_command: bool,
324    requires_git_executable: bool,
325    destructive_data: bool,
326    operator_envelope: bool,
327    json_kind: &'static str,
328    report_contract: Option<CoreReportContract>,
329    json_discriminators: &'static [CommandJsonDiscriminatorSpec],
330    schema_verbs: &'static [&'static str],
331    documented_schema_verbs: &'static [&'static str],
332    opaque_schema_verbs: &'static [&'static str],
333    surface: &'static str,
334    help_visibility: &'static str,
335    help_rank: u16,
336    /// Area grouping for the `heddle help advanced` listing (heddle#652).
337    /// Only meaningful on native-surface root commands with an advanced
338    /// help visibility; commands on the `automation` / `admin` /
339    /// `git_projection` surfaces derive their group from the surface itself
340    /// (see [`advanced_help_groups`]). The
341    /// `advanced_help_groups_cover_every_advanced_verb` test forces
342    /// every advanced native root command to pick one, so the grouped
343    /// help can never silently grow an uncategorized verb.
344    help_category: Option<&'static str>,
345    canonical_command: Option<&'static str>,
346    canonical_kind: Option<&'static str>,
347    canonical_note: Option<&'static str>,
348    advertised_action: Option<AdvertisedAction>,
349    feature_gate: Option<&'static str>,
350    /// Sysexits-style codes this command may legitimately return, paired
351    /// with a one-line agent-facing reason. Empty slice means "0 on
352    /// success, generic IoErr (74) on failure" — the implicit default for
353    /// commands not yet swept. See `docs/exit-codes.md` and
354    /// `crates/cli/src/exit.rs::HeddleExitCode`.
355    exit_codes: &'static [(u8, &'static str)],
356}
357
358#[derive(Debug, Clone, Copy, PartialEq, Eq)]
359struct CommandJsonDiscriminatorSpec {
360    schema_verb: Option<&'static str>,
361    field: &'static str,
362    value: &'static str,
363    no_schema_reason: Option<&'static str>,
364}
365
366#[derive(Debug, Clone, Copy, PartialEq, Eq)]
367pub struct AdvertisedAction {
368    action: &'static str,
369    argv_template: &'static [&'static str],
370    required_inputs: &'static [&'static str],
371    agent_may_fill: bool,
372    executable: bool,
373}
374
375#[derive(Debug, Clone, PartialEq, Eq)]
376pub struct CommandRuntimeContract {
377    pub path: Vec<&'static str>,
378    pub display: String,
379    pub supports_json: bool,
380    pub supports_json_compact: bool,
381    pub supports_op_id: bool,
382    pub persists_op_id: bool,
383    pub mutates: bool,
384    pub targets_current_repository: bool,
385    pub uses_bootstrap_op_id_store: bool,
386    pub help_visibility: &'static str,
387    pub help_rank: u16,
388    pub surface: &'static str,
389    pub canonical_command: Option<&'static str>,
390    pub json_kind: &'static str,
391}
392
393#[derive(Debug, Clone, Copy)]
394struct CommandContractEntry {
395    path: &'static [&'static str],
396    contract: CommandContract,
397}
398
399const RECOMMENDED_ACTION_PLACEHOLDERS: &[&str] = &[
400    // Message templates are display-only until the caller supplies a
401    // real message. They must not be exposed as directly executable
402    // argv because the literal ellipsis would create bad history.
403    "heddle capture -m \"...\"",
404    "heddle capture -m \"...\" --confidence <confidence>",
405    "heddle commit -m \"...\"",
406    "heddle init --principal-name <name> --principal-email <email>",
407    "heddle ready -m \"...\"",
408    "heddle context get --path <path>",
409    "heddle context set --path <path> --scope file -m \"...\"",
410    "heddle agent provenance begin",
411    "heddle start <name> --path <empty-path>",
412    "heddle start <name> --path ../<name>",
413    "heddle agent presence show <session>",
414    "heddle thread show <THREAD>",
415    // Remote setup requires filling in a real name and URL after
416    // inspecting current configuration.
417    "heddle remote add <name> <url>",
418    "heddle remote set-default <name>",
419    // Clone recovery commands must name a real remote and an empty
420    // destination path chosen by the operator.
421    "heddle clone <local-path> <path>",
422    "heddle clone <remote> <path>",
423    "heddle clone <remote> <new-path>",
424    "heddle clone <remote> <fresh-path>",
425    "heddle clone <remote> <path> --thread <thread>",
426    // Shallow Git import recovery requires choosing a complete checkout.
427    "heddle import git --path <full-git-repo>",
428    "heddle import git --path <full-git-repo> --ref <ref>",
429    "heddle thread switch <branch>",
430    "heddle ready --thread <thread>",
431    "heddle land --thread <thread>",
432];
433
434const RECOMMENDED_ACTION_TEMPLATES: &[(&str, &[&str], &[&str], bool)] = &[
435    (
436        "heddle capture -m \"...\"",
437        &["heddle", "capture", "-m", "<message>"],
438        &["message"],
439        true,
440    ),
441    (
442        "heddle capture -m \"...\" --confidence <confidence>",
443        &[
444            "heddle",
445            "capture",
446            "-m",
447            "<message>",
448            "--confidence",
449            "<confidence>",
450        ],
451        &["message", "confidence"],
452        true,
453    ),
454    (
455        "heddle commit -m \"...\"",
456        &["heddle", "commit", "-m", "<message>"],
457        &["message"],
458        true,
459    ),
460    (
461        "heddle init --principal-name <name> --principal-email <email>",
462        &[
463            "heddle",
464            "init",
465            "--principal-name",
466            "<name>",
467            "--principal-email",
468            "<email>",
469        ],
470        &["name", "email"],
471        true,
472    ),
473    (
474        "heddle ready -m \"...\"",
475        &["heddle", "ready", "-m", "<message>"],
476        &["message"],
477        true,
478    ),
479    (
480        "heddle context get --path <path>",
481        &["heddle", "context", "get", "--path", "<path>"],
482        &["path"],
483        true,
484    ),
485    (
486        "heddle context set --path <path> --scope file -m \"...\"",
487        &[
488            "heddle",
489            "context",
490            "set",
491            "--path",
492            "<path>",
493            "--scope",
494            "file",
495            "-m",
496            "<message>",
497        ],
498        &["path", "message"],
499        true,
500    ),
501    (
502        "heddle agent provenance begin",
503        &[
504            "heddle",
505            "agent",
506            "provenance",
507            "begin",
508            "--provider",
509            "<provider>",
510            "--model",
511            "<model>",
512        ],
513        &["provider", "model"],
514        true,
515    ),
516    (
517        "heddle start <name> --path <empty-path>",
518        &["heddle", "start", "<name>", "--path", "<empty-path>"],
519        &["name", "path"],
520        true,
521    ),
522    (
523        "heddle start <name> --path ../<name>",
524        &["heddle", "start", "<name>", "--path", "../<name>"],
525        &["name", "path"],
526        true,
527    ),
528    (
529        "heddle agent presence show <session>",
530        &["heddle", "agent", "presence", "show", "<session>"],
531        &["session"],
532        true,
533    ),
534    (
535        "heddle ready --thread <name>",
536        &["heddle", "ready", "--thread", "<thread>"],
537        &["thread"],
538        true,
539    ),
540    (
541        "heddle land --thread <name>",
542        &["heddle", "land", "--thread", "<thread>"],
543        &["thread"],
544        true,
545    ),
546    (
547        "heddle sync --thread <name>",
548        &["heddle", "sync", "--thread", "<thread>"],
549        &["thread"],
550        true,
551    ),
552    (
553        "heddle run --thread <name> -- <cmd...>",
554        &["heddle", "run", "--thread", "<thread>", "--", "<cmd...>"],
555        &["thread", "command"],
556        false,
557    ),
558    (
559        "heddle thread switch <name>",
560        &["heddle", "thread", "switch", "<thread>"],
561        &["thread"],
562        true,
563    ),
564    // Current-thread drop recovery (heddle#258): switch to a sibling
565    // thread first (or create one) instead of the circular `thread list`.
566    // The `<other>` slot is agent-fillable so a JSON caller can run it
567    // after picking a real sibling thread name.
568    (
569        "heddle thread switch <other>",
570        &["heddle", "thread", "switch", "<other>"],
571        &["other"],
572        true,
573    ),
574    (
575        "heddle thread create <other>",
576        &["heddle", "thread", "create", "<other>"],
577        &["other"],
578        true,
579    ),
580    (
581        "heddle thread show <THREAD>",
582        &["heddle", "thread", "show", "<thread>"],
583        &["thread"],
584        true,
585    ),
586    (
587        "heddle start <task> --parent-thread <THREAD>",
588        &["heddle", "start", "<task>", "--parent-thread", "<thread>"],
589        &["thread", "task"],
590        false,
591    ),
592    (
593        "heddle remote add <name> <url>",
594        &["heddle", "remote", "add", "<name>", "<url>"],
595        &["name", "url"],
596        false,
597    ),
598    (
599        "heddle remote set-default <name>",
600        &["heddle", "remote", "set-default", "<name>"],
601        &["name"],
602        false,
603    ),
604    (
605        "heddle ready --thread <thread>",
606        &["heddle", "ready", "--thread", "<thread>"],
607        &["thread"],
608        true,
609    ),
610    (
611        "heddle land --thread <thread>",
612        &["heddle", "land", "--thread", "<thread>"],
613        &["thread"],
614        false,
615    ),
616    (
617        "heddle clone <local-path> <path>",
618        &["heddle", "clone", "<local-path>", "<path>"],
619        &["local_path", "path"],
620        false,
621    ),
622    (
623        "heddle clone <remote> <path>",
624        &["heddle", "clone", "<remote>", "<path>"],
625        &["remote", "path"],
626        false,
627    ),
628    (
629        "heddle clone <remote> <new-path>",
630        &["heddle", "clone", "<remote>", "<new-path>"],
631        &["remote", "path"],
632        false,
633    ),
634    (
635        "heddle clone <remote> <path> --thread <thread>",
636        &[
637            "heddle", "clone", "<remote>", "<path>", "--thread", "<thread>",
638        ],
639        &["remote", "path", "thread"],
640        false,
641    ),
642    (
643        "heddle clone <remote> <fresh-path>",
644        &["heddle", "clone", "<remote>", "<fresh-path>"],
645        &["remote", "path"],
646        false,
647    ),
648    (
649        "heddle import git --path <full-git-repo>",
650        &["heddle", "import", "git", "--path", "<full-git-repo>"],
651        &["path"],
652        false,
653    ),
654    (
655        "heddle import git --path <full-git-repo> --ref <ref>",
656        &[
657            "heddle",
658            "import",
659            "git",
660            "--path",
661            "<full-git-repo>",
662            "--ref",
663            "<ref>",
664        ],
665        &["path", "ref"],
666        false,
667    ),
668    (
669        "heddle thread switch <branch>",
670        &["heddle", "thread", "switch", "<branch>"],
671        &["branch"],
672        false,
673    ),
674];
675
676const READ_JSON: CommandContract = CommandContract {
677    supports_json: true,
678    supports_json_compact: false,
679    mutates: false,
680    targets_current_repository: true,
681    supports_op_id: false,
682    persists_op_id: false,
683    observe_only: true,
684    may_initialize: false,
685    may_import_git: false,
686    may_write_worktree: false,
687    may_move_ref: false,
688    destructive_requires_force: false,
689    writes_heddle_refs: false,
690    writes_git_refs: false,
691    writes_worktree: false,
692    writes_metadata: false,
693    writes_config: false,
694    writes_hooks: false,
695    network_io: false,
696    daemon_process: false,
697    object_gc: false,
698    external_command: false,
699    requires_git_executable: false,
700    destructive_data: false,
701    operator_envelope: false,
702    json_kind: "json",
703    report_contract: None,
704    json_discriminators: &[],
705    schema_verbs: &[],
706    documented_schema_verbs: &[],
707    opaque_schema_verbs: &[],
708    surface: "native",
709    help_visibility: "advanced",
710    help_rank: 1000,
711    help_category: None,
712    canonical_command: None,
713    canonical_kind: None,
714    canonical_note: None,
715    advertised_action: None,
716    feature_gate: None,
717    exit_codes: &[],
718};
719
720const READ_TEXT: CommandContract = CommandContract {
721    supports_json: false,
722    json_kind: "none",
723    ..READ_JSON
724};
725
726const GROUP: CommandContract = CommandContract {
727    supports_json: false,
728    json_kind: "none",
729    ..READ_JSON
730};
731
732const READ_JSONL: CommandContract = CommandContract {
733    json_kind: "jsonl",
734    ..READ_JSON
735};
736
737const READ_JSON_OR_JSONL: CommandContract = CommandContract {
738    json_kind: "json_or_jsonl",
739    ..READ_JSON
740};
741
742const MUTATION_BASE: CommandContract = CommandContract {
743    mutates: true,
744    supports_op_id: true,
745    observe_only: false,
746    ..READ_JSON
747};
748
749const REF_MUTATION: CommandContract = CommandContract {
750    may_move_ref: true,
751    writes_heddle_refs: true,
752    ..MUTATION_BASE
753};
754
755const METADATA_MUTATION: CommandContract = CommandContract {
756    writes_metadata: true,
757    ..MUTATION_BASE
758};
759
760const REF_AND_METADATA_MUTATION: CommandContract = CommandContract {
761    writes_metadata: true,
762    ..REF_MUTATION
763};
764
765const NETWORK_METADATA_MUTATION: CommandContract = CommandContract {
766    network_io: true,
767    ..METADATA_MUTATION
768};
769
770const METADATA_MUTATION_NO_OP_ID: CommandContract = CommandContract {
771    supports_op_id: false,
772    ..METADATA_MUTATION
773};
774
775const NETWORK_METADATA_MUTATION_NO_OP_ID: CommandContract = CommandContract {
776    network_io: true,
777    ..METADATA_MUTATION_NO_OP_ID
778};
779
780const CONFIG_MUTATION_NO_OP_ID: CommandContract = CommandContract {
781    supports_op_id: false,
782    ..CONFIG_MUTATION
783};
784
785const CONFIG_MUTATION_TEXT: CommandContract = CommandContract {
786    supports_json: false,
787    supports_op_id: false,
788    json_kind: "none",
789    ..CONFIG_MUTATION
790};
791
792const NETWORK_CONFIG_MUTATION_TEXT: CommandContract = CommandContract {
793    supports_json: false,
794    supports_op_id: false,
795    network_io: true,
796    json_kind: "none",
797    ..CONFIG_MUTATION
798};
799
800const INIT: CommandContract = CommandContract {
801    may_initialize: true,
802    may_move_ref: false,
803    writes_heddle_refs: false,
804    writes_config: true,
805    ..MUTATION_BASE
806};
807
808const CAPTURE: CommandContract = CommandContract { ..REF_MUTATION };
809
810const fn compact_json(contract: CommandContract) -> CommandContract {
811    CommandContract {
812        supports_json_compact: true,
813        ..contract
814    }
815}
816
817const fn operator_envelope(contract: CommandContract) -> CommandContract {
818    CommandContract {
819        operator_envelope: true,
820        ..contract
821    }
822}
823
824const WORKTREE_MUTATION: CommandContract = CommandContract {
825    may_write_worktree: true,
826    writes_worktree: true,
827    ..REF_MUTATION
828};
829
830const REF_METADATA_WORKTREE_MUTATION: CommandContract = CommandContract {
831    writes_metadata: true,
832    ..WORKTREE_MUTATION
833};
834
835const DESTRUCTIVE_WORKTREE_MUTATION: CommandContract = CommandContract {
836    destructive_requires_force: true,
837    destructive_data: true,
838    ..WORKTREE_MUTATION
839};
840
841const DESTRUCTIVE_DATA_MUTATION: CommandContract = CommandContract {
842    destructive_data: true,
843    ..METADATA_MUTATION
844};
845
846const DESTRUCTIVE_REF_MUTATION: CommandContract = CommandContract {
847    destructive_data: true,
848    ..REF_MUTATION
849};
850
851const IMPORTING_MUTATION: CommandContract = CommandContract {
852    may_import_git: true,
853    ..REF_MUTATION
854};
855
856const FSCK_GIT_REPAIR: CommandContract = CommandContract {
857    may_import_git: true,
858    may_write_worktree: true,
859    writes_git_refs: true,
860    writes_worktree: true,
861    writes_metadata: true,
862    ..REF_MUTATION
863};
864
865const ADOPT: CommandContract = CommandContract {
866    may_initialize: true,
867    may_import_git: true,
868    writes_config: true,
869    ..REF_MUTATION
870};
871
872const CONFIG_MUTATION: CommandContract = CommandContract {
873    writes_config: true,
874    ..MUTATION_BASE
875};
876
877const HOOK_MUTATION: CommandContract = CommandContract {
878    writes_hooks: true,
879    ..CONFIG_MUTATION
880};
881
882const INTEGRATION_INSTALL_MUTATION: CommandContract = CommandContract {
883    writes_metadata: true,
884    ..HOOK_MUTATION
885};
886
887const DAEMON_MUTATION: CommandContract = CommandContract {
888    daemon_process: true,
889    ..METADATA_MUTATION_NO_OP_ID
890};
891
892const GC_MUTATION: CommandContract = CommandContract {
893    object_gc: true,
894    ..MUTATION_BASE
895};
896
897const EXTERNAL_COMMAND_MUTATION: CommandContract = CommandContract {
898    external_command: true,
899    supports_json: false,
900    supports_op_id: false,
901    json_kind: "none",
902    ..MUTATION_BASE
903};
904
905const EXTERNAL_WORKTREE_COMMAND: CommandContract = CommandContract {
906    may_write_worktree: true,
907    external_command: true,
908    ..EXTERNAL_COMMAND_MUTATION
909};
910
911const EXTERNAL_WORKTREE_MUTATION: CommandContract = CommandContract {
912    external_command: true,
913    ..WORKTREE_MUTATION
914};
915
916const fn documented_schemas(
917    contract: CommandContract,
918    schema_verbs: &'static [&'static str],
919) -> CommandContract {
920    CommandContract {
921        schema_verbs,
922        documented_schema_verbs: schema_verbs,
923        ..contract
924    }
925}
926
927const fn documented_core_report_schema(
928    contract: CommandContract,
929    report_contract: CoreReportContract,
930) -> CommandContract {
931    CommandContract {
932        supports_json: true,
933        json_kind: machine_output_kind_json_kind(report_contract.machine_output_kind),
934        report_contract: Some(report_contract),
935        ..contract
936    }
937}
938
939const fn opaque_schemas(
940    contract: CommandContract,
941    schema_verbs: &'static [&'static str],
942) -> CommandContract {
943    CommandContract {
944        schema_verbs,
945        documented_schema_verbs: schema_verbs,
946        opaque_schema_verbs: schema_verbs,
947        ..contract
948    }
949}
950
951const fn json_discriminators(
952    contract: CommandContract,
953    discriminators: &'static [CommandJsonDiscriminatorSpec],
954) -> CommandContract {
955    CommandContract {
956        json_discriminators: discriminators,
957        ..contract
958    }
959}
960
961const fn json_discriminator(
962    schema_verb: Option<&'static str>,
963    field: &'static str,
964    value: &'static str,
965) -> CommandJsonDiscriminatorSpec {
966    CommandJsonDiscriminatorSpec {
967        schema_verb,
968        field,
969        value,
970        no_schema_reason: None,
971    }
972}
973
974fn contract_schema_verbs(contract: CommandContract) -> impl Iterator<Item = &'static str> {
975    contract
976        .report_contract
977        .into_iter()
978        .map(|report_contract| report_contract.schema_name)
979        .chain(contract.schema_verbs.iter().copied())
980}
981
982fn contract_documented_schema_verbs(
983    contract: CommandContract,
984) -> impl Iterator<Item = &'static str> {
985    contract
986        .report_contract
987        .into_iter()
988        .map(|report_contract| report_contract.schema_name)
989        .chain(contract.documented_schema_verbs.iter().copied())
990}
991
992fn contract_json_discriminators(
993    contract: CommandContract,
994) -> impl Iterator<Item = CommandJsonDiscriminatorSpec> {
995    contract
996        .report_contract
997        .into_iter()
998        .filter_map(report_json_discriminator_from_contract)
999        .chain(contract.json_discriminators.iter().copied())
1000}
1001
1002fn report_json_discriminator_from_contract(
1003    report_contract: CoreReportContract,
1004) -> Option<CommandJsonDiscriminatorSpec> {
1005    report_contract
1006        .output_discriminator
1007        .map(|discriminator| CommandJsonDiscriminatorSpec {
1008            schema_verb: Some(report_contract.schema_name),
1009            field: discriminator.field,
1010            value: discriminator.value,
1011            no_schema_reason: None,
1012        })
1013}
1014
1015const fn machine_output_kind_json_kind(kind: MachineOutputKind) -> &'static str {
1016    match kind {
1017        MachineOutputKind::Json => "json",
1018        MachineOutputKind::JsonLines => "jsonl",
1019        MachineOutputKind::JsonOrJsonLines => "json_or_jsonl",
1020    }
1021}
1022
1023/// Helper for advertising a JSON discriminator value that is *not*
1024/// backed by a documented schema verb — used for preliminary /
1025/// transport-envelope records emitted alongside the primary payload
1026/// (e.g. `clone_connection` ahead of the final `clone` object on
1027/// hosted clones). The `reason` is required by the metadata invariant
1028/// test so the catalog can't carry orphan discriminators.
1029const fn json_discriminator_no_schema(
1030    reason: &'static str,
1031    field: &'static str,
1032    value: &'static str,
1033) -> CommandJsonDiscriminatorSpec {
1034    CommandJsonDiscriminatorSpec {
1035        schema_verb: None,
1036        field,
1037        value,
1038        no_schema_reason: Some(reason),
1039    }
1040}
1041
1042const fn front_door(contract: CommandContract, help_rank: u16) -> CommandContract {
1043    CommandContract {
1044        help_visibility: "everyday",
1045        help_rank,
1046        ..contract
1047    }
1048}
1049
1050const fn hidden(contract: CommandContract) -> CommandContract {
1051    CommandContract {
1052        surface: "internal",
1053        help_visibility: "hidden",
1054        ..contract
1055    }
1056}
1057
1058const fn surface(contract: CommandContract, surface: &'static str) -> CommandContract {
1059    CommandContract {
1060        surface,
1061        ..contract
1062    }
1063}
1064
1065const fn user_scoped(contract: CommandContract) -> CommandContract {
1066    CommandContract {
1067        targets_current_repository: false,
1068        ..contract
1069    }
1070}
1071
1072/// Assign the `heddle help advanced` area group for a native-surface
1073/// advanced root command (heddle#652). See [`advanced_help_groups`] for
1074/// the recognized ids and their display titles.
1075const fn category(contract: CommandContract, help_category: &'static str) -> CommandContract {
1076    CommandContract {
1077        help_category: Some(help_category),
1078        ..contract
1079    }
1080}
1081
1082const fn feature_gated(contract: CommandContract, feature_gate: &'static str) -> CommandContract {
1083    CommandContract {
1084        feature_gate: Some(feature_gate),
1085        ..contract
1086    }
1087}
1088
1089const QUERY_ATTRIBUTION_SCHEMA_VERBS: &[&str] = &["query --attribution"];
1090const QUERY_JSON_DISCRIMINATORS: &[CommandJsonDiscriminatorSpec] = &[json_discriminator(
1091    Some("query --attribution"),
1092    "output_kind",
1093    "query_attribution",
1094)];
1095
1096const fn exits(
1097    contract: CommandContract,
1098    exit_codes: &'static [(u8, &'static str)],
1099) -> CommandContract {
1100    CommandContract {
1101        exit_codes,
1102        ..contract
1103    }
1104}
1105
1106const fn git_projection_action(
1107    contract: CommandContract,
1108    canonical_command: &'static str,
1109    canonical_kind: &'static str,
1110    canonical_note: &'static str,
1111) -> CommandContract {
1112    CommandContract {
1113        surface: "git_projection",
1114        help_visibility: "git_projection",
1115        canonical_command: Some(canonical_command),
1116        canonical_kind: Some(canonical_kind),
1117        canonical_note: Some(canonical_note),
1118        ..contract
1119    }
1120}
1121
1122const fn advertised_action(
1123    contract: CommandContract,
1124    action: &'static str,
1125    argv_template: &'static [&'static str],
1126    required_inputs: &'static [&'static str],
1127    agent_may_fill: bool,
1128    executable: bool,
1129) -> CommandContract {
1130    CommandContract {
1131        advertised_action: Some(AdvertisedAction {
1132            action,
1133            argv_template,
1134            required_inputs,
1135            agent_may_fill,
1136            executable,
1137        }),
1138        ..contract
1139    }
1140}
1141
1142const CONTRACTS: &[CommandContractEntry] = &[
1143    entry(
1144        &["abort"],
1145        category(
1146            json_discriminators(
1147                documented_schemas(operator_envelope(compact_json(REF_MUTATION)), &["abort"]),
1148                &[json_discriminator(Some("abort"), "output_kind", "abort")],
1149            ),
1150            "recovery",
1151        ),
1152    ),
1153    entry(
1154        &["adopt"],
1155        front_door(
1156            advertised_action(
1157                json_discriminators(
1158                    documented_schemas(ADOPT, &["adopt"]),
1159                    &[json_discriminator(Some("adopt"), "output_kind", "adopt")],
1160                ),
1161                "heddle adopt --ref <branch>",
1162                &["heddle", "adopt", "--ref", "<branch>"],
1163                &["branch"],
1164                true,
1165                false,
1166            ),
1167            210,
1168        ),
1169    ),
1170    entry(&["agent"], surface(GROUP, "automation")),
1171    entry(
1172        &["agent", "reserve"],
1173        surface(
1174            documented_schemas(REF_AND_METADATA_MUTATION, &["agent reserve"]),
1175            "automation",
1176        ),
1177    ),
1178    entry(
1179        &["agent", "heartbeat"],
1180        surface(
1181            documented_schemas(METADATA_MUTATION, &["agent heartbeat"]),
1182            "automation",
1183        ),
1184    ),
1185    entry(
1186        &["agent", "capture"],
1187        surface(
1188            json_discriminators(
1189                documented_schemas(CAPTURE, &["agent capture"]),
1190                &[json_discriminator(
1191                    Some("agent capture"),
1192                    "output_kind",
1193                    "capture",
1194                )],
1195            ),
1196            "automation",
1197        ),
1198    ),
1199    entry(
1200        &["agent", "ready"],
1201        surface(
1202            json_discriminators(
1203                documented_schemas(CAPTURE, &["agent ready"]),
1204                &[json_discriminator(
1205                    Some("agent ready"),
1206                    "output_kind",
1207                    "ready",
1208                )],
1209            ),
1210            "automation",
1211        ),
1212    ),
1213    entry(
1214        &["agent", "release"],
1215        surface(
1216            documented_schemas(METADATA_MUTATION, &["agent release"]),
1217            "automation",
1218        ),
1219    ),
1220    entry(
1221        &["agent", "list"],
1222        surface(documented_schemas(READ_JSON, &["agent list"]), "automation"),
1223    ),
1224    entry(&["agent", "task"], surface(GROUP, "automation")),
1225    entry(
1226        &["agent", "task", "create"],
1227        surface(
1228            json_discriminators(
1229                documented_schemas(METADATA_MUTATION, &["agent task create"]),
1230                &[json_discriminator(
1231                    Some("agent task create"),
1232                    "output_kind",
1233                    "agent_task_create",
1234                )],
1235            ),
1236            "automation",
1237        ),
1238    ),
1239    entry(
1240        &["agent", "task", "list"],
1241        surface(
1242            json_discriminators(
1243                documented_schemas(READ_JSON, &["agent task list"]),
1244                &[json_discriminator(
1245                    Some("agent task list"),
1246                    "output_kind",
1247                    "agent_task_list",
1248                )],
1249            ),
1250            "automation",
1251        ),
1252    ),
1253    entry(
1254        &["agent", "task", "show"],
1255        surface(
1256            json_discriminators(
1257                documented_schemas(READ_JSON, &["agent task show"]),
1258                &[json_discriminator(
1259                    Some("agent task show"),
1260                    "output_kind",
1261                    "agent_task_show",
1262                )],
1263            ),
1264            "automation",
1265        ),
1266    ),
1267    entry(
1268        &["agent", "task", "update"],
1269        surface(
1270            json_discriminators(
1271                documented_schemas(METADATA_MUTATION, &["agent task update"]),
1272                &[json_discriminator(
1273                    Some("agent task update"),
1274                    "output_kind",
1275                    "agent_task_update",
1276                )],
1277            ),
1278            "automation",
1279        ),
1280    ),
1281    entry(&["agent", "fanout"], surface(GROUP, "automation")),
1282    entry(
1283        &["agent", "fanout", "plan"],
1284        surface(
1285            json_discriminators(
1286                documented_schemas(READ_JSON, &["agent fanout plan"]),
1287                &[json_discriminator(
1288                    Some("agent fanout plan"),
1289                    "output_kind",
1290                    "agent_fanout_plan",
1291                )],
1292            ),
1293            "automation",
1294        ),
1295    ),
1296    entry(
1297        &["agent", "fanout", "start"],
1298        surface(
1299            json_discriminators(
1300                documented_schemas(WORKTREE_MUTATION, &["agent fanout start"]),
1301                &[json_discriminator(
1302                    Some("agent fanout start"),
1303                    "output_kind",
1304                    "agent_fanout_start",
1305                )],
1306            ),
1307            "automation",
1308        ),
1309    ),
1310    entry(&["agent", "presence"], surface(GROUP, "automation")),
1311    entry(
1312        &["agent", "presence", "list"],
1313        surface(
1314            json_discriminators(
1315                documented_schemas(READ_JSON, &["agent presence list"]),
1316                &[json_discriminator(
1317                    Some("agent presence list"),
1318                    "output_kind",
1319                    "agent_presence_list",
1320                )],
1321            ),
1322            "automation",
1323        ),
1324    ),
1325    entry(
1326        &["agent", "presence", "show"],
1327        surface(
1328            json_discriminators(
1329                documented_schemas(READ_JSON, &["agent presence show"]),
1330                &[json_discriminator(
1331                    Some("agent presence show"),
1332                    "output_kind",
1333                    "agent_presence_show",
1334                )],
1335            ),
1336            "automation",
1337        ),
1338    ),
1339    entry(
1340        &["agent", "presence", "explain"],
1341        surface(
1342            json_discriminators(
1343                documented_schemas(READ_JSON, &["agent presence explain"]),
1344                &[json_discriminator(
1345                    Some("agent presence explain"),
1346                    "output_kind",
1347                    "agent_presence_explain",
1348                )],
1349            ),
1350            "automation",
1351        ),
1352    ),
1353    entry(
1354        &["agent", "presence", "complete"],
1355        surface(
1356            json_discriminators(
1357                documented_schemas(METADATA_MUTATION, &["agent presence complete"]),
1358                &[json_discriminator(
1359                    Some("agent presence complete"),
1360                    "output_kind",
1361                    "agent_presence_complete",
1362                )],
1363            ),
1364            "automation",
1365        ),
1366    ),
1367    entry(&["agent", "provenance"], surface(GROUP, "automation")),
1368    entry(
1369        &["agent", "provenance", "begin"],
1370        surface(
1371            documented_schemas(METADATA_MUTATION, &["agent provenance begin"]),
1372            "automation",
1373        ),
1374    ),
1375    entry(
1376        &["agent", "provenance", "segment"],
1377        surface(
1378            documented_schemas(METADATA_MUTATION, &["agent provenance segment"]),
1379            "automation",
1380        ),
1381    ),
1382    entry(
1383        &["agent", "provenance", "end"],
1384        surface(
1385            documented_schemas(METADATA_MUTATION, &["agent provenance end"]),
1386            "automation",
1387        ),
1388    ),
1389    entry(
1390        &["agent", "provenance", "show"],
1391        surface(
1392            documented_schemas(READ_JSON, &["agent provenance show"]),
1393            "automation",
1394        ),
1395    ),
1396    entry(
1397        &["agent", "provenance", "list"],
1398        surface(
1399            documented_schemas(READ_JSON, &["agent provenance list"]),
1400            "automation",
1401        ),
1402    ),
1403    entry(
1404        &["auth"],
1405        category(feature_gated(user_scoped(GROUP), "client"), "repo"),
1406    ),
1407    entry(
1408        &["auth", "login"],
1409        feature_gated(user_scoped(NETWORK_CONFIG_MUTATION_TEXT), "client"),
1410    ),
1411    entry(
1412        &["auth", "logout"],
1413        feature_gated(
1414            json_discriminators(
1415                documented_schemas(user_scoped(CONFIG_MUTATION_NO_OP_ID), &["auth logout"]),
1416                &[json_discriminator(
1417                    Some("auth logout"),
1418                    "output_kind",
1419                    "auth_logout",
1420                )],
1421            ),
1422            "client",
1423        ),
1424    ),
1425    entry(
1426        &["auth", "status"],
1427        feature_gated(
1428            json_discriminators(
1429                documented_schemas(user_scoped(READ_JSON), &["auth status"]),
1430                &[json_discriminator(
1431                    Some("auth status"),
1432                    "output_kind",
1433                    "auth_status",
1434                )],
1435            ),
1436            "client",
1437        ),
1438    ),
1439    entry(
1440        &["auth", "trust"],
1441        category(feature_gated(user_scoped(GROUP), "client"), "repo"),
1442    ),
1443    entry(
1444        &["auth", "trust", "show"],
1445        feature_gated(
1446            json_discriminators(
1447                documented_schemas(user_scoped(READ_JSON), &["auth trust show"]),
1448                &[json_discriminator(
1449                    Some("auth trust show"),
1450                    "output_kind",
1451                    "auth_trust_show",
1452                )],
1453            ),
1454            "client",
1455        ),
1456    ),
1457    entry(
1458        &["auth", "trust", "replace"],
1459        feature_gated(
1460            json_discriminators(
1461                documented_schemas(
1462                    user_scoped(CONFIG_MUTATION_NO_OP_ID),
1463                    &["auth trust replace"],
1464                ),
1465                &[json_discriminator(
1466                    Some("auth trust replace"),
1467                    "output_kind",
1468                    "auth_trust_replace",
1469                )],
1470            ),
1471            "client",
1472        ),
1473    ),
1474    entry(
1475        &["auth", "derive-agent"],
1476        feature_gated(user_scoped(CONFIG_MUTATION_TEXT), "client"),
1477    ),
1478    entry(
1479        &["auth", "create-service-token"],
1480        feature_gated(
1481            json_discriminators(
1482                documented_schemas(
1483                    user_scoped(NETWORK_METADATA_MUTATION_NO_OP_ID),
1484                    &["auth create-service-token"],
1485                ),
1486                &[json_discriminator(
1487                    Some("auth create-service-token"),
1488                    "output_kind",
1489                    "auth_create_service_token",
1490                )],
1491            ),
1492            "client",
1493        ),
1494    ),
1495    entry(
1496        &["whoami"],
1497        category(
1498            feature_gated(
1499                json_discriminators(
1500                    documented_schemas(READ_JSON, &["whoami"]),
1501                    &[json_discriminator(Some("whoami"), "output_kind", "whoami")],
1502                ),
1503                "client",
1504            ),
1505            // Groups under "Repo and environment" in `heddle help advanced`,
1506            // alongside the `auth` identity commands.
1507            "repo",
1508        ),
1509    ),
1510    entry(&["import"], surface(GROUP, "git_projection")),
1511    entry(
1512        &["import", "git"],
1513        exits(
1514            json_discriminators(
1515                documented_schemas(IMPORTING_MUTATION, &["import git"]),
1516                &[json_discriminator(
1517                    Some("import git"),
1518                    "output_kind",
1519                    "import_git",
1520                )],
1521            ),
1522            &[
1523                (0, "ok"),
1524                (65, "malformed git repo or unimportable refs"),
1525                (74, "io reading git refs"),
1526            ],
1527        ),
1528    ),
1529    entry(&["export"], surface(GROUP, "git_projection")),
1530    entry(
1531        &["export", "git"],
1532        json_discriminators(
1533            documented_schemas(
1534                CommandContract {
1535                    writes_git_refs: true,
1536                    ..REF_MUTATION
1537                },
1538                &["export git"],
1539            ),
1540            &[json_discriminator(
1541                Some("export git"),
1542                "output_kind",
1543                "export_git",
1544            )],
1545        ),
1546    ),
1547    entry(
1548        &["sync", "git"],
1549        exits(
1550            git_projection_action(
1551                json_discriminators(
1552                    documented_schemas(IMPORTING_MUTATION, &["sync git"]),
1553                    &[json_discriminator(
1554                        Some("sync git"),
1555                        "output_kind",
1556                        "sync_git",
1557                    )],
1558                ),
1559                "adopt",
1560                "workflow",
1561                "Use adopt to initialize Heddle from an existing Git repository and import its history.",
1562            ),
1563            &[
1564                (0, "ok"),
1565                (75, "remote unreachable; safe to retry"),
1566                (76, "remote rejected payload"),
1567            ],
1568        ),
1569    ),
1570    entry(
1571        &["capture"],
1572        front_door(
1573            category(
1574                json_discriminators(
1575                    documented_schemas(compact_json(CAPTURE), &["capture"]),
1576                    &[json_discriminator(
1577                        Some("capture"),
1578                        "output_kind",
1579                        "capture",
1580                    )],
1581                ),
1582                "states",
1583            ),
1584            30,
1585        ),
1586    ),
1587    entry(
1588        &["clone"],
1589        front_door(
1590            surface(
1591                json_discriminators(
1592                    documented_schemas(
1593                        CommandContract {
1594                            targets_current_repository: false,
1595                            may_initialize: true,
1596                            may_import_git: true,
1597                            may_write_worktree: true,
1598                            may_move_ref: true,
1599                            writes_git_refs: true,
1600                            writes_worktree: true,
1601                            writes_config: true,
1602                            network_io: true,
1603                            ..REF_MUTATION
1604                        },
1605                        &["clone"],
1606                    ),
1607                    &[
1608                        json_discriminator(Some("clone"), "output_kind", "clone"),
1609                        // `clone --output json` on a hosted/network remote
1610                        // emits a preliminary connection envelope before the
1611                        // final clone payload. Both records carry
1612                        // `output_kind` so agents that route on the
1613                        // discriminator (per heddle#272) can classify each
1614                        // line without falling back to text parsing. The
1615                        // envelope has no separate schema verb — it's a
1616                        // small inline object, not a Serialize struct in
1617                        // `schemas`. Source-of-truth value:
1618                        // `cli::cli::commands::CLONE_CONNECTION_OUTPUT_KIND`.
1619                        json_discriminator_no_schema(
1620                            "preliminary connection envelope emitted by hosted clones \
1621                         before the final clone payload (no separate schema)",
1622                            "output_kind",
1623                            "clone_connection",
1624                        ),
1625                        // `clone --recursive --output json` (Spool epic P9) emits a
1626                        // monorepo summary instead of the single-spool `clone`
1627                        // payload: the placed per-spool ops + the skipped (EdgeSkip)
1628                        // child edges. Inline object, no separate schema verb.
1629                        // Source: `clone::monorepo_clone_output_json`.
1630                        json_discriminator_no_schema(
1631                            "monorepo clone summary emitted by `clone --recursive` \
1632                         (placed spools + skipped child edges; no separate schema)",
1633                            "output_kind",
1634                            "clone_monorepo",
1635                        ),
1636                    ],
1637                ),
1638                "source_authority",
1639            ),
1640            220,
1641        ),
1642    ),
1643    entry(
1644        &["collapse"],
1645        category(opaque_schemas(REF_MUTATION, &["collapse"]), "states"),
1646    ),
1647    entry(
1648        &["commit"],
1649        exits(
1650            front_door(
1651                advertised_action(
1652                    surface(
1653                        json_discriminators(
1654                            documented_schemas(
1655                                CommandContract {
1656                                    writes_git_refs: true,
1657                                    writes_metadata: true,
1658                                    ..REF_MUTATION
1659                                },
1660                                &["commit"],
1661                            ),
1662                            &[json_discriminator(Some("commit"), "output_kind", "commit")],
1663                        ),
1664                        "source_authority",
1665                    ),
1666                    "heddle commit",
1667                    &["heddle", "commit"],
1668                    &[],
1669                    true,
1670                    true,
1671                ),
1672                28,
1673            ),
1674            &[
1675                (0, "Git checkpoint written or already current"),
1676                (65, "repository mode or worktree preflight refused"),
1677                (74, "io while writing Git state"),
1678            ],
1679        ),
1680    ),
1681    entry(
1682        &["expand"],
1683        category(
1684            json_discriminators(
1685                documented_schemas(READ_JSON, &["expand"]),
1686                &[json_discriminator(Some("expand"), "output_kind", "expand")],
1687            ),
1688            "states",
1689        ),
1690    ),
1691    entry(
1692        &["continue"],
1693        category(
1694            json_discriminators(
1695                documented_schemas(operator_envelope(compact_json(REF_MUTATION)), &["continue"]),
1696                &[json_discriminator(
1697                    Some("continue"),
1698                    "output_kind",
1699                    "continue",
1700                )],
1701            ),
1702            "recovery",
1703        ),
1704    ),
1705    entry(&["context"], category(GROUP, "collab")),
1706    entry(
1707        &["context", "set"],
1708        json_discriminators(
1709            opaque_schemas(REF_AND_METADATA_MUTATION, &["context set"]),
1710            &[json_discriminator(
1711                Some("context set"),
1712                "output_kind",
1713                "context_set",
1714            )],
1715        ),
1716    ),
1717    entry(
1718        &["context", "get"],
1719        json_discriminators(
1720            opaque_schemas(READ_JSON, &["context get"]),
1721            &[json_discriminator(
1722                Some("context get"),
1723                "output_kind",
1724                "context_get",
1725            )],
1726        ),
1727    ),
1728    entry(
1729        &["context", "list"],
1730        json_discriminators(
1731            opaque_schemas(READ_JSON, &["context list"]),
1732            &[json_discriminator(
1733                Some("context list"),
1734                "output_kind",
1735                "context_list",
1736            )],
1737        ),
1738    ),
1739    entry(
1740        &["context", "history"],
1741        json_discriminators(
1742            opaque_schemas(READ_JSON, &["context history"]),
1743            &[json_discriminator(
1744                Some("context history"),
1745                "output_kind",
1746                "context_history",
1747            )],
1748        ),
1749    ),
1750    entry(
1751        &["context", "edit"],
1752        json_discriminators(
1753            opaque_schemas(REF_AND_METADATA_MUTATION, &["context edit"]),
1754            &[json_discriminator(
1755                Some("context edit"),
1756                "output_kind",
1757                "context_edit",
1758            )],
1759        ),
1760    ),
1761    entry(
1762        &["context", "supersede"],
1763        json_discriminators(
1764            opaque_schemas(REF_AND_METADATA_MUTATION, &["context supersede"]),
1765            &[json_discriminator(
1766                Some("context supersede"),
1767                "output_kind",
1768                "context_supersede",
1769            )],
1770        ),
1771    ),
1772    entry(
1773        &["context", "rm"],
1774        json_discriminators(
1775            opaque_schemas(REF_AND_METADATA_MUTATION, &["context rm"]),
1776            &[json_discriminator(
1777                Some("context rm"),
1778                "output_kind",
1779                "context_rm",
1780            )],
1781        ),
1782    ),
1783    entry(
1784        &["context", "check"],
1785        json_discriminators(
1786            opaque_schemas(READ_JSON, &["context check"]),
1787            &[json_discriminator(
1788                Some("context check"),
1789                "output_kind",
1790                "context_check",
1791            )],
1792        ),
1793    ),
1794    entry(
1795        &["context", "suggest"],
1796        json_discriminators(
1797            opaque_schemas(READ_JSON, &["context suggest"]),
1798            &[json_discriminator(
1799                Some("context suggest"),
1800                "output_kind",
1801                "context_suggest",
1802            )],
1803        ),
1804    ),
1805    entry(
1806        &["context", "audit"],
1807        json_discriminators(
1808            opaque_schemas(READ_JSON, &["context audit"]),
1809            &[json_discriminator(
1810                Some("context audit"),
1811                "output_kind",
1812                "context_audit",
1813            )],
1814        ),
1815    ),
1816    #[cfg(all(feature = "git-overlay", feature = "ingest"))]
1817    entry(&["context", "reason"], category(GROUP, "context")),
1818    #[cfg(all(feature = "git-overlay", feature = "ingest"))]
1819    entry(
1820        &["context", "reason", "git"],
1821        surface(
1822            opaque_schemas(METADATA_MUTATION, &["context reason git"]),
1823            "git_projection",
1824        ),
1825    ),
1826    entry(&["daemon"], surface(GROUP, "admin")),
1827    entry(
1828        &["daemon", "serve"],
1829        surface(opaque_schemas(DAEMON_MUTATION, &["daemon serve"]), "admin"),
1830    ),
1831    entry(
1832        &["daemon", "status"],
1833        surface(opaque_schemas(READ_JSON, &["daemon status"]), "admin"),
1834    ),
1835    entry(
1836        &["daemon", "stop"],
1837        surface(
1838            json_discriminators(
1839                opaque_schemas(DAEMON_MUTATION, &["daemon stop"]),
1840                &[json_discriminator(
1841                    Some("daemon stop"),
1842                    "output_kind",
1843                    "daemon_stop",
1844                )],
1845            ),
1846            "admin",
1847        ),
1848    ),
1849    entry(
1850        &["diff"],
1851        front_door(
1852            documented_core_report_schema(READ_JSON, DiffReport::CONTRACT),
1853            20,
1854        ),
1855    ),
1856    entry(&["discuss"], category(GROUP, "collab")),
1857    entry(
1858        &["discuss", "open"],
1859        json_discriminators(
1860            documented_schemas(METADATA_MUTATION, &["discuss open"]),
1861            &[json_discriminator(
1862                Some("discuss open"),
1863                "output_kind",
1864                "discuss_open",
1865            )],
1866        ),
1867    ),
1868    entry(
1869        &["discuss", "append"],
1870        json_discriminators(
1871            documented_schemas(METADATA_MUTATION, &["discuss append"]),
1872            &[json_discriminator(
1873                Some("discuss append"),
1874                "output_kind",
1875                "discuss_append",
1876            )],
1877        ),
1878    ),
1879    entry(
1880        &["discuss", "resolve"],
1881        json_discriminators(
1882            documented_schemas(METADATA_MUTATION, &["discuss resolve"]),
1883            &[json_discriminator(
1884                Some("discuss resolve"),
1885                "output_kind",
1886                "discuss_resolve",
1887            )],
1888        ),
1889    ),
1890    entry(
1891        &["discuss", "reopen"],
1892        json_discriminators(
1893            documented_schemas(METADATA_MUTATION, &["discuss reopen"]),
1894            &[json_discriminator(
1895                Some("discuss reopen"),
1896                "output_kind",
1897                "discuss_reopen",
1898            )],
1899        ),
1900    ),
1901    entry(
1902        &["discuss", "list"],
1903        json_discriminators(
1904            documented_schemas(READ_JSON, &["discuss list"]),
1905            &[json_discriminator(
1906                Some("discuss list"),
1907                "output_kind",
1908                "discuss_list",
1909            )],
1910        ),
1911    ),
1912    entry(
1913        &["discuss", "show"],
1914        json_discriminators(
1915            documented_schemas(READ_JSON, &["discuss show"]),
1916            &[json_discriminator(
1917                Some("discuss show"),
1918                "output_kind",
1919                "discuss_show",
1920            )],
1921        ),
1922    ),
1923    entry(
1924        &["doctor"],
1925        front_door(
1926            json_discriminators(
1927                documented_schemas(READ_JSON, &["doctor"]),
1928                &[json_discriminator(Some("doctor"), "output_kind", "doctor")],
1929            ),
1930            120,
1931        ),
1932    ),
1933    entry(
1934        &["doctor", "docs"],
1935        json_discriminators(
1936            documented_schemas(READ_JSON, &["doctor docs"]),
1937            &[json_discriminator(
1938                Some("doctor docs"),
1939                "output_kind",
1940                "doctor_docs",
1941            )],
1942        ),
1943    ),
1944    entry(
1945        &["doctor", "schemas"],
1946        json_discriminators(
1947            documented_schemas(READ_JSON, &["doctor schemas"]),
1948            &[json_discriminator(
1949                Some("doctor schemas"),
1950                "output_kind",
1951                "doctor_schemas",
1952            )],
1953        ),
1954    ),
1955    entry(
1956        &["fsck"],
1957        category(
1958            documented_core_report_schema(READ_JSON, FsckReport::CONTRACT),
1959            "recovery",
1960        ),
1961    ),
1962    entry(&["fsck", "repair"], category(GROUP, "recovery")),
1963    entry(
1964        &["fsck", "repair", "git"],
1965        category(
1966            documented_schemas(
1967                documented_core_report_schema(FSCK_GIT_REPAIR, FsckReport::CONTRACT),
1968                &["fsck repair git"],
1969            ),
1970            "recovery",
1971        ),
1972    ),
1973    entry(&["oplog"], category(GROUP, "recovery")),
1974    entry(
1975        &["oplog", "recover"],
1976        category(
1977            json_discriminators(
1978                opaque_schemas(METADATA_MUTATION_NO_OP_ID, &["oplog recover"]),
1979                &[json_discriminator(
1980                    Some("oplog recover"),
1981                    "output_kind",
1982                    "oplog_recover",
1983                )],
1984            ),
1985            "recovery",
1986        ),
1987    ),
1988    entry(
1989        &["help"],
1990        category(
1991            json_discriminators(
1992                opaque_schemas(READ_JSON, &["help"]),
1993                &[json_discriminator(Some("help"), "kind", "command_catalog")],
1994            ),
1995            "repo",
1996        ),
1997    ),
1998    entry(&["hook"], surface(GROUP, "automation")),
1999    entry(
2000        &["hook", "list"],
2001        surface(opaque_schemas(READ_JSON, &["hook list"]), "automation"),
2002    ),
2003    entry(
2004        &["hook", "install"],
2005        surface(
2006            opaque_schemas(HOOK_MUTATION, &["hook install"]),
2007            "automation",
2008        ),
2009    ),
2010    entry(
2011        &["hook", "uninstall"],
2012        surface(
2013            opaque_schemas(HOOK_MUTATION, &["hook uninstall"]),
2014            "automation",
2015        ),
2016    ),
2017    entry(
2018        &["hook", "events"],
2019        surface(opaque_schemas(READ_JSON, &["hook events"]), "automation"),
2020    ),
2021    entry(
2022        &["init"],
2023        exits(
2024            front_door(
2025                json_discriminators(
2026                    documented_schemas(INIT, &["init"]),
2027                    &[json_discriminator(Some("init"), "output_kind", "init")],
2028                ),
2029                200,
2030            ),
2031            &[
2032                (0, "ok"),
2033                (73, "cannot create state directory"),
2034                (78, "workspace config invalid"),
2035            ],
2036        ),
2037    ),
2038    entry(&["integration"], surface(GROUP, "admin")),
2039    entry(
2040        &["integration", "list"],
2041        surface(
2042            documented_schemas(READ_JSON, &["integration list"]),
2043            "admin",
2044        ),
2045    ),
2046    entry(
2047        &["integration", "install"],
2048        surface(
2049            opaque_schemas(INTEGRATION_INSTALL_MUTATION, &["integration install"]),
2050            "admin",
2051        ),
2052    ),
2053    entry(
2054        &["integration", "doctor"],
2055        surface(
2056            documented_schemas(READ_JSON, &["integration doctor"]),
2057            "admin",
2058        ),
2059    ),
2060    entry(
2061        &["integration", "uninstall"],
2062        surface(
2063            opaque_schemas(INTEGRATION_INSTALL_MUTATION, &["integration uninstall"]),
2064            "admin",
2065        ),
2066    ),
2067    entry(
2068        &["integration", "upgrade"],
2069        surface(
2070            opaque_schemas(INTEGRATION_INSTALL_MUTATION, &["integration upgrade"]),
2071            "admin",
2072        ),
2073    ),
2074    entry(
2075        &["integration", "relay"],
2076        hidden(surface(
2077            opaque_schemas(REF_METADATA_WORKTREE_MUTATION, &["integration relay"]),
2078            "admin",
2079        )),
2080    ),
2081    entry(
2082        &["log"],
2083        front_door(
2084            json_discriminators(
2085                documented_schemas(READ_JSON, &["log", "log --reflog", "log --timeline"]),
2086                &[
2087                    json_discriminator(Some("log"), "output_kind", "log"),
2088                    json_discriminator(Some("log --reflog"), "output_kind", "log_reflog"),
2089                    json_discriminator(Some("log --timeline"), "output_kind", "timeline_log"),
2090                ],
2091            ),
2092            130,
2093        ),
2094    ),
2095    entry(&["maintenance"], surface(GROUP, "admin")),
2096    entry(
2097        &["maintenance", "inspect"],
2098        surface(
2099            json_discriminators(
2100                documented_schemas(READ_JSON, &["maintenance inspect"]),
2101                &[json_discriminator(
2102                    Some("maintenance inspect"),
2103                    "output_kind",
2104                    "maintenance_inspect",
2105                )],
2106            ),
2107            "admin",
2108        ),
2109    ),
2110    entry(
2111        &["maintenance", "refresh"],
2112        surface(
2113            json_discriminators(
2114                documented_schemas(
2115                    CommandContract {
2116                        object_gc: true,
2117                        ..METADATA_MUTATION
2118                    },
2119                    &["maintenance refresh"],
2120                ),
2121                &[json_discriminator(
2122                    Some("maintenance refresh"),
2123                    "output_kind",
2124                    "maintenance_refresh",
2125                )],
2126            ),
2127            "admin",
2128        ),
2129    ),
2130    entry(
2131        &["maintenance", "gc"],
2132        surface(
2133            json_discriminators(
2134                opaque_schemas(GC_MUTATION, &["maintenance gc"]),
2135                &[json_discriminator(
2136                    Some("maintenance gc"),
2137                    "output_kind",
2138                    "gc",
2139                )],
2140            ),
2141            "admin",
2142        ),
2143    ),
2144    entry(
2145        &["pull"],
2146        exits(
2147            front_door(
2148                json_discriminators(
2149                    surface(
2150                        documented_schemas(
2151                            CommandContract {
2152                                may_import_git: true,
2153                                writes_git_refs: true,
2154                                network_io: true,
2155                                ..WORKTREE_MUTATION
2156                            },
2157                            &["pull"],
2158                        ),
2159                        "source_authority",
2160                    ),
2161                    &[json_discriminator(Some("pull"), "output_kind", "pull")],
2162                ),
2163                90,
2164            ),
2165            &[
2166                (0, "ok"),
2167                (75, "remote unreachable; safe to retry"),
2168                (76, "upstream protocol error"),
2169                (78, "no upstream configured"),
2170            ],
2171        ),
2172    ),
2173    entry(
2174        &["push"],
2175        exits(
2176            front_door(
2177                advertised_action(
2178                    json_discriminators(
2179                        surface(
2180                            documented_schemas(
2181                                CommandContract {
2182                                    writes_git_refs: true,
2183                                    writes_config: true,
2184                                    network_io: true,
2185                                    ..REF_MUTATION
2186                                },
2187                                &["push"],
2188                            ),
2189                            "source_authority",
2190                        ),
2191                        &[json_discriminator(Some("push"), "output_kind", "push")],
2192                    ),
2193                    "heddle push",
2194                    &["heddle", "push"],
2195                    &[],
2196                    true,
2197                    true,
2198                ),
2199                80,
2200            ),
2201            &[
2202                (0, "ok"),
2203                (75, "remote unreachable; safe to retry"),
2204                (
2205                    76,
2206                    "remote rejected payload; do not retry without changing inputs",
2207                ),
2208                (78, "no upstream configured"),
2209            ],
2210        ),
2211    ),
2212    entry(
2213        &["query"],
2214        category(
2215            json_discriminators(
2216                documented_schemas(
2217                    documented_core_report_schema(READ_JSON, QueryReport::CONTRACT),
2218                    QUERY_ATTRIBUTION_SCHEMA_VERBS,
2219                ),
2220                QUERY_JSON_DISCRIMINATORS,
2221            ),
2222            "states",
2223        ),
2224    ),
2225    entry(
2226        &["ready"],
2227        front_door(
2228            json_discriminators(
2229                documented_schemas(compact_json(CAPTURE), &["ready"]),
2230                &[json_discriminator(Some("ready"), "output_kind", "ready")],
2231            ),
2232            50,
2233        ),
2234    ),
2235    entry(&["redact"], category(GROUP, "recovery")),
2236    entry(
2237        &["redact", "apply"],
2238        json_discriminators(
2239            opaque_schemas(METADATA_MUTATION, &["redact apply"]),
2240            &[json_discriminator(
2241                Some("redact apply"),
2242                "output_kind",
2243                "redact_apply",
2244            )],
2245        ),
2246    ),
2247    entry(
2248        &["redact", "list"],
2249        json_discriminators(
2250            opaque_schemas(READ_JSON, &["redact list"]),
2251            &[json_discriminator(
2252                Some("redact list"),
2253                "output_kind",
2254                "redact_list",
2255            )],
2256        ),
2257    ),
2258    entry(
2259        &["redact", "show"],
2260        json_discriminators(
2261            opaque_schemas(READ_JSON, &["redact show"]),
2262            &[json_discriminator(
2263                Some("redact show"),
2264                "output_kind",
2265                "redact_show",
2266            )],
2267        ),
2268    ),
2269    entry(&["redact", "purge"], GROUP),
2270    entry(
2271        &["redact", "purge", "apply"],
2272        json_discriminators(
2273            opaque_schemas(
2274                CommandContract {
2275                    destructive_requires_force: true,
2276                    ..DESTRUCTIVE_DATA_MUTATION
2277                },
2278                &["redact purge apply"],
2279            ),
2280            &[json_discriminator(
2281                Some("redact purge apply"),
2282                "output_kind",
2283                "purge_apply",
2284            )],
2285        ),
2286    ),
2287    entry(
2288        &["redact", "purge", "list"],
2289        json_discriminators(
2290            opaque_schemas(READ_JSON, &["redact purge list"]),
2291            &[json_discriminator(
2292                Some("redact purge list"),
2293                "output_kind",
2294                "purge_list",
2295            )],
2296        ),
2297    ),
2298    entry(&["redact", "trust"], GROUP),
2299    entry(
2300        &["redact", "trust", "add"],
2301        json_discriminators(
2302            opaque_schemas(CONFIG_MUTATION, &["redact trust add"]),
2303            &[json_discriminator(
2304                Some("redact trust add"),
2305                "output_kind",
2306                "redact_trust_add",
2307            )],
2308        ),
2309    ),
2310    entry(
2311        &["redact", "trust", "list"],
2312        json_discriminators(
2313            opaque_schemas(READ_JSON, &["redact trust list"]),
2314            &[json_discriminator(
2315                Some("redact trust list"),
2316                "output_kind",
2317                "redact_trust_list",
2318            )],
2319        ),
2320    ),
2321    entry(
2322        &["redact", "trust", "remove"],
2323        json_discriminators(
2324            opaque_schemas(CONFIG_MUTATION, &["redact trust remove"]),
2325            &[json_discriminator(
2326                Some("redact trust remove"),
2327                "output_kind",
2328                "redact_trust_remove",
2329            )],
2330        ),
2331    ),
2332    entry(
2333        &["remote"],
2334        category(surface(GROUP, "source_authority"), "repo"),
2335    ),
2336    entry(
2337        &["remote", "list"],
2338        surface(
2339            json_discriminators(
2340                documented_schemas(READ_JSON, &["remote list"]),
2341                &[json_discriminator(
2342                    Some("remote list"),
2343                    "output_kind",
2344                    "remote_list",
2345                )],
2346            ),
2347            "source_authority",
2348        ),
2349    ),
2350    entry(
2351        &["remote", "add"],
2352        surface(
2353            json_discriminators(
2354                documented_schemas(CONFIG_MUTATION, &["remote add"]),
2355                &[json_discriminator(
2356                    Some("remote add"),
2357                    "output_kind",
2358                    "remote_add",
2359                )],
2360            ),
2361            "source_authority",
2362        ),
2363    ),
2364    entry(
2365        &["remote", "remove"],
2366        surface(
2367            json_discriminators(
2368                documented_schemas(CONFIG_MUTATION, &["remote remove"]),
2369                &[json_discriminator(
2370                    Some("remote remove"),
2371                    "output_kind",
2372                    "remote_remove",
2373                )],
2374            ),
2375            "source_authority",
2376        ),
2377    ),
2378    entry(
2379        &["remote", "set-default"],
2380        surface(
2381            json_discriminators(
2382                documented_schemas(CONFIG_MUTATION, &["remote set-default"]),
2383                &[json_discriminator(
2384                    Some("remote set-default"),
2385                    "output_kind",
2386                    "remote_set_default",
2387                )],
2388            ),
2389            "source_authority",
2390        ),
2391    ),
2392    entry(
2393        &["remote", "show"],
2394        surface(
2395            json_discriminators(
2396                documented_schemas(READ_JSON, &["remote show"]),
2397                &[json_discriminator(
2398                    Some("remote show"),
2399                    "output_kind",
2400                    "remote_show",
2401                )],
2402            ),
2403            "source_authority",
2404        ),
2405    ),
2406    entry(
2407        &["resolve"],
2408        front_door(
2409            json_discriminators(
2410                documented_schemas(REF_MUTATION, &["resolve"]),
2411                &[json_discriminator(
2412                    Some("resolve"),
2413                    "output_kind",
2414                    "resolve",
2415                )],
2416            ),
2417            300,
2418        ),
2419    ),
2420    entry(
2421        &["retro"],
2422        category(documented_schemas(READ_JSON, &["retro"]), "states"),
2423    ),
2424    entry(
2425        &["revert"],
2426        category(
2427            json_discriminators(
2428                documented_schemas(WORKTREE_MUTATION, &["revert"]),
2429                &[json_discriminator(Some("revert"), "output_kind", "revert")],
2430            ),
2431            "states",
2432        ),
2433    ),
2434    entry(&["review"], category(GROUP, "collab")),
2435    entry(
2436        &["review", "show"],
2437        json_discriminators(
2438            documented_schemas(READ_JSON, &["review show"]),
2439            &[json_discriminator(
2440                Some("review show"),
2441                "output_kind",
2442                "review_show",
2443            )],
2444        ),
2445    ),
2446    entry(
2447        &["review", "sign"],
2448        json_discriminators(
2449            documented_schemas(METADATA_MUTATION, &["review sign"]),
2450            &[json_discriminator(
2451                Some("review sign"),
2452                "output_kind",
2453                "review_sign",
2454            )],
2455        ),
2456    ),
2457    entry(
2458        &["review", "next"],
2459        json_discriminators(
2460            documented_schemas(READ_JSON, &["review next"]),
2461            &[json_discriminator(
2462                Some("review next"),
2463                "output_kind",
2464                "review_next",
2465            )],
2466        ),
2467    ),
2468    entry(
2469        &["review", "health"],
2470        json_discriminators(
2471            documented_schemas(READ_JSON, &["review health"]),
2472            &[json_discriminator(
2473                Some("review health"),
2474                "output_kind",
2475                "review_health",
2476            )],
2477        ),
2478    ),
2479    entry(&["run"], surface(EXTERNAL_WORKTREE_COMMAND, "automation")),
2480    entry(
2481        &["schemas"],
2482        surface(
2483            json_discriminators(
2484                documented_schemas(READ_JSON, &["schemas"]),
2485                &[json_discriminator(
2486                    Some("schemas"),
2487                    "output_kind",
2488                    "schemas",
2489                )],
2490            ),
2491            "automation",
2492        ),
2493    ),
2494    entry(&["semantic"], category(GROUP, "states")),
2495    entry(
2496        &["semantic", "hot"],
2497        opaque_schemas(READ_JSON, &["semantic hot"]),
2498    ),
2499    // `semantic index` backfills the merkle semantic index, attaching a
2500    // SemanticIndex sidecar to states that lack one. Metadata mutation, text
2501    // progress output, no op-id (idempotent + restartable).
2502    entry(
2503        &["semantic", "index"],
2504        CommandContract {
2505            supports_json: false,
2506            supports_op_id: false,
2507            json_kind: "none",
2508            writes_metadata: true,
2509            ..MUTATION_BASE
2510        },
2511    ),
2512    entry(&["shell"], category(READ_TEXT, "repo")),
2513    entry(&["shell", "init"], READ_TEXT),
2514    entry(&["shell", "completion"], READ_TEXT),
2515    entry(&["shell", "prompt"], READ_TEXT),
2516    entry(&["complete"], hidden(READ_TEXT)),
2517    entry(
2518        &["land"],
2519        front_door(
2520            json_discriminators(
2521                documented_schemas(
2522                    CommandContract {
2523                        writes_git_refs: true,
2524                        network_io: true,
2525                        ..compact_json(REF_MUTATION)
2526                    },
2527                    &["land", "land --threads"],
2528                ),
2529                &[
2530                    json_discriminator(Some("land"), "output_kind", "land"),
2531                    json_discriminator(Some("land --threads"), "output_kind", "land_batch"),
2532                ],
2533            ),
2534            70,
2535        ),
2536    ),
2537    entry(
2538        &["show"],
2539        front_door(
2540            json_discriminators(
2541                documented_schemas(READ_JSON, &["show"]),
2542                &[json_discriminator(Some("show"), "output_kind", "show")],
2543            ),
2544            140,
2545        ),
2546    ),
2547    entry(
2548        &["start"],
2549        front_door(
2550            json_discriminators(
2551                documented_schemas(WORKTREE_MUTATION, &["start"]),
2552                &[json_discriminator(
2553                    Some("start"),
2554                    "output_kind",
2555                    "thread_start",
2556                )],
2557            ),
2558            40,
2559        ),
2560    ),
2561    entry(
2562        &["status"],
2563        exits(
2564            front_door(
2565                documented_core_report_schema(
2566                    compact_json(READ_JSON_OR_JSONL),
2567                    StatusReport::CONTRACT,
2568                ),
2569                10,
2570            ),
2571            &[(0, "ok"), (74, "io reading workspace state")],
2572        ),
2573    ),
2574    entry(
2575        &["sync"],
2576        category(
2577            json_discriminators(
2578                documented_schemas(operator_envelope(compact_json(REF_MUTATION)), &["sync"]),
2579                &[json_discriminator(Some("sync"), "output_kind", "sync")],
2580            ),
2581            "threads",
2582        ),
2583    ),
2584    entry(&["thread"], category(surface(GROUP, "native"), "threads")),
2585    entry(
2586        &["thread", "create"],
2587        json_discriminators(
2588            documented_schemas(REF_MUTATION, &["thread create"]),
2589            &[json_discriminator(
2590                Some("thread create"),
2591                "output_kind",
2592                "thread_create",
2593            )],
2594        ),
2595    ),
2596    entry(
2597        &["thread", "current"],
2598        documented_schemas(READ_JSON, &["thread current"]),
2599    ),
2600    entry(
2601        &["thread", "switch"],
2602        json_discriminators(
2603            documented_schemas(WORKTREE_MUTATION, &["thread switch"]),
2604            &[json_discriminator(
2605                Some("thread switch"),
2606                "output_kind",
2607                "thread_switch",
2608            )],
2609        ),
2610    ),
2611    entry(&["thread", "cd"], READ_TEXT),
2612    entry(
2613        &["thread", "list"],
2614        json_discriminators(
2615            documented_schemas(READ_JSON, &["thread list"]),
2616            &[json_discriminator(
2617                Some("thread list"),
2618                "output_kind",
2619                "thread_list",
2620            )],
2621        ),
2622    ),
2623    entry(
2624        &["thread", "show"],
2625        json_discriminators(
2626            documented_schemas(READ_JSON_OR_JSONL, &["thread show"]),
2627            &[json_discriminator(
2628                Some("thread show"),
2629                "output_kind",
2630                "thread_show",
2631            )],
2632        ),
2633    ),
2634    entry(
2635        &["thread", "captures"],
2636        documented_schemas(READ_JSON, &["thread captures"]),
2637    ),
2638    entry(
2639        &["thread", "rename"],
2640        json_discriminators(
2641            documented_schemas(REF_MUTATION, &["thread rename"]),
2642            &[json_discriminator(
2643                Some("thread rename"),
2644                "output_kind",
2645                "thread_rename",
2646            )],
2647        ),
2648    ),
2649    entry(
2650        &["thread", "refresh"],
2651        json_discriminators(
2652            documented_schemas(WORKTREE_MUTATION, &["thread refresh"]),
2653            &[json_discriminator(
2654                Some("thread refresh"),
2655                "output_kind",
2656                "thread_refresh",
2657            )],
2658        ),
2659    ),
2660    entry(
2661        &["thread", "move"],
2662        documented_schemas(REF_MUTATION, &["thread move"]),
2663    ),
2664    entry(
2665        &["thread", "absorb"],
2666        documented_schemas(REF_MUTATION, &["thread absorb"]),
2667    ),
2668    entry(
2669        &["thread", "resolve"],
2670        json_discriminators(
2671            documented_schemas(REF_MUTATION, &["thread resolve"]),
2672            &[json_discriminator(
2673                Some("thread resolve"),
2674                "output_kind",
2675                "thread_resolve",
2676            )],
2677        ),
2678    ),
2679    entry(
2680        &["thread", "promote"],
2681        json_discriminators(
2682            documented_schemas(WORKTREE_MUTATION, &["thread promote"]),
2683            &[json_discriminator(
2684                Some("thread promote"),
2685                "output_kind",
2686                "thread_promote",
2687            )],
2688        ),
2689    ),
2690    entry(
2691        &["thread", "drop"],
2692        json_discriminators(
2693            documented_schemas(DESTRUCTIVE_WORKTREE_MUTATION, &["thread drop"]),
2694            &[json_discriminator(
2695                Some("thread drop"),
2696                "output_kind",
2697                "thread_drop",
2698            )],
2699        ),
2700    ),
2701    entry(
2702        &["thread", "approve"],
2703        documented_schemas(NETWORK_METADATA_MUTATION, &["thread approve"]),
2704    ),
2705    entry(
2706        &["thread", "approvals"],
2707        documented_schemas(READ_JSON, &["thread approvals"]),
2708    ),
2709    entry(
2710        &["thread", "revoke-approval"],
2711        json_discriminators(
2712            documented_schemas(NETWORK_METADATA_MUTATION, &["thread revoke-approval"]),
2713            &[json_discriminator(
2714                Some("thread revoke-approval"),
2715                "output_kind",
2716                "thread_revoke_approval",
2717            )],
2718        ),
2719    ),
2720    entry(
2721        &["thread", "check-merge"],
2722        documented_schemas(READ_JSON, &["thread check-merge"]),
2723    ),
2724    entry(
2725        &["thread", "cleanup"],
2726        json_discriminators(
2727            documented_schemas(DESTRUCTIVE_WORKTREE_MUTATION, &["thread cleanup"]),
2728            &[json_discriminator(
2729                Some("thread cleanup"),
2730                "output_kind",
2731                "thread_cleanup",
2732            )],
2733        ),
2734    ),
2735    entry(&["thread", "marker"], GROUP),
2736    entry(
2737        &["thread", "marker", "list"],
2738        json_discriminators(
2739            documented_schemas(READ_JSON, &["thread marker list"]),
2740            &[json_discriminator(
2741                Some("thread marker list"),
2742                "output_kind",
2743                "thread_marker_list",
2744            )],
2745        ),
2746    ),
2747    entry(
2748        &["thread", "marker", "create"],
2749        json_discriminators(
2750            documented_schemas(REF_MUTATION, &["thread marker create"]),
2751            &[json_discriminator(
2752                Some("thread marker create"),
2753                "output_kind",
2754                "thread_marker_create",
2755            )],
2756        ),
2757    ),
2758    entry(
2759        &["thread", "marker", "delete"],
2760        json_discriminators(
2761            documented_schemas(DESTRUCTIVE_REF_MUTATION, &["thread marker delete"]),
2762            &[json_discriminator(
2763                Some("thread marker delete"),
2764                "output_kind",
2765                "thread_marker_delete",
2766            )],
2767        ),
2768    ),
2769    entry(
2770        &["thread", "marker", "show"],
2771        json_discriminators(
2772            documented_schemas(READ_JSON, &["thread marker show"]),
2773            &[json_discriminator(
2774                Some("thread marker show"),
2775                "output_kind",
2776                "thread_marker_show",
2777            )],
2778        ),
2779    ),
2780    entry(&["timeline"], surface(GROUP, "automation")),
2781    entry(
2782        &["timeline", "status"],
2783        surface(
2784            json_discriminators(
2785                documented_schemas(READ_JSON, &["timeline status"]),
2786                &[json_discriminator(
2787                    Some("timeline status"),
2788                    "output_kind",
2789                    "timeline_status",
2790                )],
2791            ),
2792            "automation",
2793        ),
2794    ),
2795    entry(
2796        &["timeline", "record-start"],
2797        surface(
2798            json_discriminators(
2799                documented_schemas(METADATA_MUTATION, &["timeline record-start"]),
2800                &[json_discriminator(
2801                    Some("timeline record-start"),
2802                    "output_kind",
2803                    "timeline_record_start",
2804                )],
2805            ),
2806            "automation",
2807        ),
2808    ),
2809    entry(
2810        &["timeline", "record-finish"],
2811        surface(
2812            json_discriminators(
2813                documented_schemas(METADATA_MUTATION, &["timeline record-finish"]),
2814                &[json_discriminator(
2815                    Some("timeline record-finish"),
2816                    "output_kind",
2817                    "timeline_record_finish",
2818                )],
2819            ),
2820            "automation",
2821        ),
2822    ),
2823    entry(
2824        &["timeline", "fork"],
2825        surface(
2826            json_discriminators(
2827                documented_schemas(METADATA_MUTATION, &["timeline fork"]),
2828                &[json_discriminator(
2829                    Some("timeline fork"),
2830                    "output_kind",
2831                    "timeline_action",
2832                )],
2833            ),
2834            "automation",
2835        ),
2836    ),
2837    entry(
2838        &["timeline", "reset"],
2839        surface(
2840            json_discriminators(
2841                documented_schemas(REF_METADATA_WORKTREE_MUTATION, &["timeline reset"]),
2842                &[json_discriminator(
2843                    Some("timeline reset"),
2844                    "output_kind",
2845                    "timeline_action",
2846                )],
2847            ),
2848            "automation",
2849        ),
2850    ),
2851    entry(
2852        &["timeline", "recover"],
2853        surface(
2854            json_discriminators(
2855                documented_schemas(METADATA_MUTATION, &["timeline recover"]),
2856                &[json_discriminator(
2857                    Some("timeline recover"),
2858                    "output_kind",
2859                    "timeline_action",
2860                )],
2861            ),
2862            "automation",
2863        ),
2864    ),
2865    entry(
2866        &["verify"],
2867        exits(
2868            front_door(
2869                documented_core_report_schema(READ_JSON, VerifyReport::CONTRACT),
2870                110,
2871            ),
2872            &[
2873                (0, "verified clean"),
2874                (65, "verification reports blocked state"),
2875                (74, "io reading state"),
2876            ],
2877        ),
2878    ),
2879    entry(&["visibility"], category(GROUP, "collab")),
2880    entry(
2881        &["visibility", "set"],
2882        json_discriminators(
2883            opaque_schemas(METADATA_MUTATION, &["visibility set"]),
2884            &[json_discriminator(
2885                Some("visibility set"),
2886                "output_kind",
2887                "visibility_set",
2888            )],
2889        ),
2890    ),
2891    entry(
2892        &["visibility", "promote"],
2893        json_discriminators(
2894            opaque_schemas(METADATA_MUTATION, &["visibility promote"]),
2895            &[json_discriminator(
2896                Some("visibility promote"),
2897                "output_kind",
2898                "visibility_promote",
2899            )],
2900        ),
2901    ),
2902    entry(
2903        &["visibility", "show"],
2904        json_discriminators(
2905            opaque_schemas(READ_JSON, &["visibility show"]),
2906            &[json_discriminator(
2907                Some("visibility show"),
2908                "output_kind",
2909                "visibility_show",
2910            )],
2911        ),
2912    ),
2913    entry(
2914        &["visibility", "list"],
2915        json_discriminators(
2916            opaque_schemas(READ_JSON, &["visibility list"]),
2917            &[json_discriminator(
2918                Some("visibility list"),
2919                "output_kind",
2920                "visibility_list",
2921            )],
2922        ),
2923    ),
2924    entry(
2925        &["try"],
2926        category(
2927            documented_schemas(EXTERNAL_WORKTREE_MUTATION, &["try"]),
2928            "threads",
2929        ),
2930    ),
2931    entry(
2932        &["undo"],
2933        front_door(
2934            json_discriminators(
2935                // `undo` keeps its own history, redo, and recovery modes.
2936                // Every kind the handler can emit must be advertised or an agent
2937                // validating responses via `heddle help --output json` rejects
2938                // the off-contract record. `undo --list` has its own
2939                // `UndoListSchema`.
2940                documented_schemas(
2941                    WORKTREE_MUTATION,
2942                    &["undo", "undo --list", "undo --redo", "undo --recover"],
2943                ),
2944                &[
2945                    json_discriminator(Some("undo"), "output_kind", "undo"),
2946                    json_discriminator(Some("undo --list"), "output_kind", "undo_list"),
2947                    json_discriminator(Some("undo --redo"), "output_kind", "redo"),
2948                    json_discriminator(Some("undo --recover"), "output_kind", "undo_recover"),
2949                ],
2950            ),
2951            100,
2952        ),
2953    ),
2954    entry(
2955        &["watch"],
2956        surface(documented_schemas(READ_JSONL, &["watch"]), "automation"),
2957    ),
2958];
2959
2960static ACTIVE_COMMAND_CONTRACT_ENTRIES: OnceLock<Vec<&'static CommandContractEntry>> =
2961    OnceLock::new();
2962
2963static ADVERTISED_COMMAND_CONTRACT_ENTRIES: OnceLock<Vec<&'static CommandContractEntry>> =
2964    OnceLock::new();
2965
2966const fn entry(path: &'static [&'static str], contract: CommandContract) -> CommandContractEntry {
2967    CommandContractEntry { path, contract }
2968}
2969
2970pub fn build_command_catalog() -> CommandCatalogOutput {
2971    debug_assert!(
2972        RECOMMENDED_ACTION_PLACEHOLDERS
2973            .iter()
2974            .all(|action| validate_recommended_action(action).is_ok())
2975    );
2976
2977    let command = Cli::command();
2978    let mut global_options: Vec<_> = command
2979        .get_arguments()
2980        .filter(|arg| arg.is_global_set())
2981        .map(catalog_option)
2982        .collect();
2983    if !command.is_disable_help_flag_set() {
2984        global_options.push(generated_help_option());
2985    }
2986
2987    let mut commands = Vec::new();
2988    let op_id_option = command
2989        .get_arguments()
2990        .find(|arg| arg.get_long() == Some("op-id"))
2991        .map(catalog_option);
2992    walk_commands(&command, &mut Vec::new(), &mut commands, &op_id_option);
2993    append_feature_gated_command_entries(&command, &mut commands, &op_id_option);
2994    CommandCatalogOutput {
2995        kind: "command_catalog".to_string(),
2996        executable_path: heddle_argv0(),
2997        commands,
2998        global_options,
2999        json_discriminators: command_json_discriminators(),
3000        recommended_action_placeholders: RECOMMENDED_ACTION_PLACEHOLDERS
3001            .iter()
3002            .map(|action| (*action).to_string())
3003            .collect(),
3004        recommended_action_templates: RECOMMENDED_ACTION_TEMPLATES
3005            .iter()
3006            .map(|(action, argv_template, required_inputs, agent_may_fill)| {
3007                action_template_from_parts(action, argv_template, required_inputs, *agent_may_fill)
3008            })
3009            .collect(),
3010    }
3011}
3012
3013pub(crate) fn heddle_argv0() -> String {
3014    match std::env::current_exe() {
3015        Ok(path) => {
3016            let file_name = path.file_name().and_then(|name| name.to_str());
3017            if matches!(file_name, Some("heddle") | Some("heddle.exe")) {
3018                path.display().to_string()
3019            } else {
3020                "heddle".to_string()
3021            }
3022        }
3023        Err(_) => "heddle".to_string(),
3024    }
3025}
3026
3027pub(crate) fn normalize_heddle_argv(mut argv: Vec<String>) -> Vec<String> {
3028    if argv.first().is_some_and(|first| first == "heddle") {
3029        argv[0] = heddle_argv0();
3030    }
3031    argv
3032}
3033
3034fn walk_commands(
3035    command: &clap::Command,
3036    prefix: &mut Vec<String>,
3037    out: &mut Vec<CommandCatalogEntry>,
3038    op_id_option: &Option<CommandCatalogOption>,
3039) {
3040    for subcommand in command.get_subcommands() {
3041        prefix.push(subcommand.get_name().to_string());
3042        out.push(catalog_entry(subcommand, prefix, op_id_option));
3043        walk_commands(subcommand, prefix, out, op_id_option);
3044        prefix.pop();
3045    }
3046}
3047
3048/// Append catalog entries for `feature_gated` contracts whose clap
3049/// subcommand is compiled out of THIS build (e.g. the `client`-gated
3050/// `auth`/`support`/`presence` surfaces in a default `cargo install`).
3051///
3052/// `walk_commands` only sees the live clap tree, so without this the
3053/// hosted verbs would be missing from the catalog `commands` list even
3054/// though their contract (schema verbs, `output_kind` discriminators)
3055/// is advertised — breaking the wire-format-stable promise agents read.
3056/// Entries already produced by the walk (the feature IS on) are skipped.
3057fn append_feature_gated_command_entries(
3058    root: &clap::Command,
3059    out: &mut Vec<CommandCatalogEntry>,
3060    op_id_option: &Option<CommandCatalogOption>,
3061) {
3062    let present: std::collections::BTreeSet<Vec<String>> =
3063        out.iter().map(|entry| entry.path.clone()).collect();
3064    for entry in CONTRACTS.iter() {
3065        if entry.contract.feature_gate.is_none() {
3066            continue;
3067        }
3068        let owned_path: Vec<String> = entry.path.iter().map(|s| (*s).to_string()).collect();
3069        if present.contains(&owned_path) {
3070            continue;
3071        }
3072        // Defensive: never synthesize an entry that the clap tree
3073        // actually carries (the feature is enabled in this build).
3074        if clap_command_path_exists(root, entry.path) {
3075            continue;
3076        }
3077        out.push(feature_gated_catalog_entry(
3078            &owned_path,
3079            entry.contract,
3080            op_id_option,
3081        ));
3082    }
3083}
3084
3085/// Build a [`CommandCatalogEntry`] for a contract with no live clap node.
3086/// Clap-derived fields (aliases, summary, options, arguments,
3087/// subcommand-ness) are empty/false; every behavioral field comes from
3088/// the contract, identically to [`catalog_entry`].
3089fn feature_gated_catalog_entry(
3090    path: &[String],
3091    contract: CommandContract,
3092    op_id_option: &Option<CommandCatalogOption>,
3093) -> CommandCatalogEntry {
3094    let mut options = Vec::new();
3095    if contract.supports_op_id
3096        && let Some(op_id_option) = op_id_option
3097    {
3098        options.push(op_id_option.clone());
3099    }
3100    CommandCatalogEntry {
3101        path: path.to_vec(),
3102        display: path.join(" "),
3103        aliases: Vec::new(),
3104        tier: help_visibility_to_tier(contract.help_visibility).to_string(),
3105        surface: contract.surface.to_string(),
3106        help_visibility: contract.help_visibility.to_string(),
3107        help_rank: contract.help_rank,
3108        canonical_command: contract
3109            .canonical_command
3110            .map(std::string::ToString::to_string),
3111        canonical_action: canonical_action(contract),
3112        command_action: contract
3113            .advertised_action
3114            .map(command_action_from_advertised),
3115        summary: String::new(),
3116        has_subcommands: false,
3117        supports_json: contract.supports_json,
3118        output_modes: supported_output_modes(contract),
3119        mutates: contract.mutates,
3120        supports_op_id: contract.supports_op_id,
3121        persists_op_id: contract.persists_op_id,
3122        op_id_behavior: op_id_behavior(contract).to_string(),
3123        op_id_store_scope: op_id_store_scope(contract).to_string(),
3124        observe_only: contract.observe_only,
3125        may_initialize: contract.may_initialize,
3126        may_import_git: contract.may_import_git,
3127        may_write_worktree: contract.may_write_worktree,
3128        may_move_ref: contract.may_move_ref,
3129        destructive_requires_force: contract.destructive_requires_force,
3130        writes_heddle_refs: contract.writes_heddle_refs,
3131        writes_git_refs: contract.writes_git_refs,
3132        writes_worktree: contract.writes_worktree,
3133        writes_config: contract.writes_config,
3134        writes_hooks: contract.writes_hooks,
3135        network_io: contract.network_io,
3136        daemon_process: contract.daemon_process,
3137        object_gc: contract.object_gc,
3138        external_command: contract.external_command,
3139        requires_git_executable: contract.requires_git_executable,
3140        destructive_data: contract.destructive_data,
3141        side_effects: side_effects(contract),
3142        side_effect_class: side_effect_class(contract).to_string(),
3143        first_run_behavior: first_run_behavior(contract).to_string(),
3144        json_kind: contract.json_kind.to_string(),
3145        json_discriminators: json_discriminators_for_path(path.iter().map(String::as_str)),
3146        schema_verbs: contract_schema_verbs(contract)
3147            .map(str::to_string)
3148            .collect(),
3149        documented_schema_verbs: contract_documented_schema_verbs(contract)
3150            .map(str::to_string)
3151            .collect(),
3152        options,
3153        arguments: Vec::new(),
3154        exit_codes: contract
3155            .exit_codes
3156            .iter()
3157            .map(|(code, reason)| CommandCatalogExitCode {
3158                code: *code,
3159                reason: (*reason).to_string(),
3160            })
3161            .collect(),
3162    }
3163}
3164
3165fn catalog_entry(
3166    command: &clap::Command,
3167    path: &[String],
3168    op_id_option: &Option<CommandCatalogOption>,
3169) -> CommandCatalogEntry {
3170    let mut options = Vec::new();
3171    let mut arguments = Vec::new();
3172    for arg in command.get_arguments() {
3173        if arg.get_long().is_some() || arg.get_short().is_some() {
3174            options.push(catalog_option(arg));
3175        } else {
3176            arguments.push(catalog_argument(arg));
3177        }
3178    }
3179
3180    let contract = command_contract(path);
3181    if contract.supports_op_id
3182        && let Some(op_id_option) = op_id_option
3183    {
3184        options.push(op_id_option.clone());
3185    }
3186    CommandCatalogEntry {
3187        path: path.to_vec(),
3188        display: path.join(" "),
3189        aliases: command
3190            .get_all_aliases()
3191            .map(std::string::ToString::to_string)
3192            .collect(),
3193        tier: help_visibility_to_tier(contract.help_visibility).to_string(),
3194        surface: contract.surface.to_string(),
3195        help_visibility: contract.help_visibility.to_string(),
3196        help_rank: contract.help_rank,
3197        canonical_command: contract
3198            .canonical_command
3199            .map(std::string::ToString::to_string),
3200        canonical_action: canonical_action(contract),
3201        command_action: command_action(command, path, contract),
3202        summary: clean_catalog_summary(
3203            command
3204                .get_about()
3205                .or_else(|| command.get_long_about())
3206                .map(|about| about.to_string().lines().next().unwrap_or("").to_string())
3207                .unwrap_or_default(),
3208        ),
3209        has_subcommands: command.get_subcommands().next().is_some(),
3210        supports_json: contract.supports_json,
3211        output_modes: supported_output_modes(contract),
3212        mutates: contract.mutates,
3213        supports_op_id: contract.supports_op_id,
3214        persists_op_id: contract.persists_op_id,
3215        op_id_behavior: op_id_behavior(contract).to_string(),
3216        op_id_store_scope: op_id_store_scope(contract).to_string(),
3217        observe_only: contract.observe_only,
3218        may_initialize: contract.may_initialize,
3219        may_import_git: contract.may_import_git,
3220        may_write_worktree: contract.may_write_worktree,
3221        may_move_ref: contract.may_move_ref,
3222        destructive_requires_force: contract.destructive_requires_force,
3223        writes_heddle_refs: contract.writes_heddle_refs,
3224        writes_git_refs: contract.writes_git_refs,
3225        writes_worktree: contract.writes_worktree,
3226        writes_config: contract.writes_config,
3227        writes_hooks: contract.writes_hooks,
3228        network_io: contract.network_io,
3229        daemon_process: contract.daemon_process,
3230        object_gc: contract.object_gc,
3231        external_command: contract.external_command,
3232        requires_git_executable: contract.requires_git_executable,
3233        destructive_data: contract.destructive_data,
3234        side_effects: side_effects(contract),
3235        side_effect_class: side_effect_class(contract).to_string(),
3236        first_run_behavior: first_run_behavior(contract).to_string(),
3237        json_kind: contract.json_kind.to_string(),
3238        json_discriminators: json_discriminators_for_path(path.iter().map(String::as_str)),
3239        schema_verbs: contract_schema_verbs(contract)
3240            .map(str::to_string)
3241            .collect(),
3242        documented_schema_verbs: contract_documented_schema_verbs(contract)
3243            .map(str::to_string)
3244            .collect(),
3245        options,
3246        arguments,
3247        exit_codes: contract
3248            .exit_codes
3249            .iter()
3250            .map(|(code, reason)| CommandCatalogExitCode {
3251                code: *code,
3252                reason: (*reason).to_string(),
3253            })
3254            .collect(),
3255    }
3256}
3257
3258fn supported_output_modes(contract: CommandContract) -> Vec<String> {
3259    let mut modes = vec!["text".to_string()];
3260    if contract.supports_json {
3261        modes.push("json".to_string());
3262    }
3263    if contract.supports_json_compact {
3264        modes.push("json-compact".to_string());
3265    }
3266    modes
3267}
3268
3269fn canonical_action(contract: CommandContract) -> Option<CanonicalAction> {
3270    let command = contract.canonical_command?;
3271    let kind = contract.canonical_kind.unwrap_or("direct_command");
3272    let (argv, template) = canonical_action_metadata(command, kind);
3273    Some(CanonicalAction {
3274        command: command.to_string(),
3275        kind: kind.to_string(),
3276        executable: argv.is_some(),
3277        note: contract.canonical_note.unwrap_or_default().to_string(),
3278        argv,
3279        template,
3280    })
3281}
3282
3283fn canonical_action_metadata(
3284    command: &str,
3285    kind: &str,
3286) -> (Option<Vec<String>>, Option<ActionTemplate>) {
3287    match (command, kind) {
3288        ("adopt", "workflow") => (
3289            None,
3290            Some(action_template_from_parts(
3291                "heddle adopt --ref <branch>",
3292                &["heddle", "adopt", "--ref", "<branch>"],
3293                &["branch"],
3294                true,
3295            )),
3296        ),
3297        ("capture", "workflow") => (
3298            None,
3299            Some(action_template_from_parts(
3300                "heddle capture -m <message>",
3301                &["heddle", "capture", "-m", "<message>"],
3302                &["message"],
3303                true,
3304            )),
3305        ),
3306        ("thread switch", "direct_command") => (
3307            None,
3308            Some(action_template_from_parts(
3309                "heddle thread switch <thread>",
3310                &["heddle", "thread", "switch", "<thread>"],
3311                &["thread"],
3312                true,
3313            )),
3314        ),
3315        (_, "direct_command") => {
3316            let argv = std::iter::once("heddle".to_string())
3317                .chain(command.split_whitespace().map(str::to_string))
3318                .collect::<Vec<_>>();
3319            (Some(normalize_heddle_argv(argv)), None)
3320        }
3321        _ => (None, None),
3322    }
3323}
3324
3325fn command_action(
3326    command: &clap::Command,
3327    path: &[String],
3328    contract: CommandContract,
3329) -> Option<CommandAction> {
3330    if let Some(action) = contract.advertised_action {
3331        return Some(command_action_from_advertised(action));
3332    }
3333    if command.get_subcommands().next().is_some() {
3334        return None;
3335    }
3336
3337    let mut argv_template = std::iter::once("heddle".to_string())
3338        .chain(path.iter().cloned())
3339        .collect::<Vec<_>>();
3340    let mut required_inputs = Vec::new();
3341    for arg in command.get_arguments().filter(|arg| !arg.is_hide_set()) {
3342        if !arg.is_required_set() {
3343            continue;
3344        }
3345        if let Some(long) = arg.get_long() {
3346            argv_template.push(format!("--{long}"));
3347        } else if let Some(short) = arg.get_short() {
3348            argv_template.push(format!("-{short}"));
3349        }
3350        let names = value_names(arg);
3351        if names.is_empty() {
3352            required_inputs.push(arg.get_id().as_str().to_string());
3353            if arg.get_long().is_none() && arg.get_short().is_none() {
3354                argv_template.push(format!("<{}>", arg.get_id().as_str()));
3355            }
3356        } else {
3357            for name in names {
3358                let input = name.to_ascii_lowercase();
3359                required_inputs.push(input.clone());
3360                argv_template.push(format!("<{input}>"));
3361            }
3362        }
3363    }
3364
3365    let action = argv_template.join(" ");
3366    if required_inputs.is_empty() {
3367        Some(CommandAction {
3368            action,
3369            executable: true,
3370            argv: Some(normalize_heddle_argv(argv_template)),
3371            template: None,
3372        })
3373    } else {
3374        Some(CommandAction {
3375            action: action.clone(),
3376            executable: false,
3377            argv: None,
3378            template: Some(action_template_from_owned(
3379                action,
3380                argv_template,
3381                required_inputs,
3382                true,
3383            )),
3384        })
3385    }
3386}
3387
3388fn command_action_from_advertised(action: AdvertisedAction) -> CommandAction {
3389    let argv_template = action
3390        .argv_template
3391        .iter()
3392        .map(|part| (*part).to_string())
3393        .collect::<Vec<_>>();
3394    if action.executable {
3395        CommandAction {
3396            action: action.action.to_string(),
3397            executable: true,
3398            argv: Some(normalize_heddle_argv(argv_template)),
3399            template: None,
3400        }
3401    } else {
3402        CommandAction {
3403            action: action.action.to_string(),
3404            executable: false,
3405            argv: None,
3406            template: Some(action_template_from_parts(
3407                action.action,
3408                action.argv_template,
3409                action.required_inputs,
3410                action.agent_may_fill,
3411            )),
3412        }
3413    }
3414}
3415
3416fn action_template_from_parts(
3417    action: &str,
3418    argv_template: &[&str],
3419    required_inputs: &[&str],
3420    agent_may_fill: bool,
3421) -> ActionTemplate {
3422    action_template_from_owned(
3423        action.to_string(),
3424        argv_template
3425            .iter()
3426            .map(|part| (*part).to_string())
3427            .collect(),
3428        required_inputs
3429            .iter()
3430            .map(|input| (*input).to_string())
3431            .collect(),
3432        agent_may_fill,
3433    )
3434}
3435
3436fn action_template_from_owned(
3437    action: String,
3438    argv_template: Vec<String>,
3439    required_inputs: Vec<String>,
3440    agent_may_fill: bool,
3441) -> ActionTemplate {
3442    ActionTemplate {
3443        action,
3444        argv_template: normalize_heddle_argv(argv_template),
3445        required_inputs,
3446        agent_may_fill,
3447    }
3448}
3449
3450fn clean_catalog_summary(summary: String) -> String {
3451    let stripped = summary
3452        .trim_start_matches("Automation/workflow command:")
3453        .trim_start();
3454    let mut chars = stripped.chars();
3455    match chars.next() {
3456        Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
3457        None => String::new(),
3458    }
3459}
3460
3461fn side_effect_class(contract: CommandContract) -> &'static str {
3462    if contract.observe_only {
3463        "observe_only"
3464    } else if contract.destructive_requires_force && contract.writes_worktree {
3465        "destructive_worktree_mutation"
3466    } else if contract.destructive_data {
3467        "destructive_data"
3468    } else if contract.object_gc {
3469        "object_gc"
3470    } else if contract.writes_worktree {
3471        "worktree_mutation"
3472    } else if contract.may_import_git {
3473        "git_import"
3474    } else if contract.may_initialize {
3475        "initialize"
3476    } else if contract.writes_hooks {
3477        "hook_mutation"
3478    } else if contract.network_io {
3479        "network_mutation"
3480    } else if contract.writes_config {
3481        "config_mutation"
3482    } else if contract.daemon_process {
3483        "daemon_process"
3484    } else if contract.external_command {
3485        "external_command"
3486    } else if contract.writes_heddle_refs || contract.writes_git_refs || contract.may_move_ref {
3487        "ref_mutation"
3488    } else if contract.writes_metadata {
3489        "metadata_mutation"
3490    } else {
3491        "none"
3492    }
3493}
3494
3495fn side_effects(contract: CommandContract) -> Vec<CommandSideEffect> {
3496    if contract.observe_only {
3497        return vec![CommandSideEffect::ObserveOnly];
3498    }
3499
3500    let mut effects = Vec::new();
3501    if contract.may_initialize {
3502        effects.push(CommandSideEffect::Initialize);
3503    }
3504    if contract.may_import_git {
3505        effects.push(CommandSideEffect::ImportGit);
3506    }
3507    if contract.writes_heddle_refs {
3508        effects.push(CommandSideEffect::WritesHeddleRefs);
3509    }
3510    if contract.writes_git_refs {
3511        effects.push(CommandSideEffect::WritesGitRefs);
3512    }
3513    if contract.writes_worktree {
3514        effects.push(CommandSideEffect::WritesWorktree);
3515    } else if contract.may_write_worktree {
3516        effects.push(CommandSideEffect::MayWriteWorktree);
3517    }
3518    if contract.writes_metadata {
3519        effects.push(CommandSideEffect::WritesMetadata);
3520    }
3521    if contract.writes_config {
3522        effects.push(CommandSideEffect::WritesConfig);
3523    }
3524    if contract.writes_hooks {
3525        effects.push(CommandSideEffect::WritesHooks);
3526    }
3527    if contract.network_io {
3528        effects.push(CommandSideEffect::NetworkIo);
3529    }
3530    if contract.daemon_process {
3531        effects.push(CommandSideEffect::DaemonProcess);
3532    }
3533    if contract.object_gc {
3534        effects.push(CommandSideEffect::ObjectGc);
3535    }
3536    if contract.external_command {
3537        effects.push(CommandSideEffect::ExternalCommand);
3538    }
3539    if contract.destructive_requires_force {
3540        effects.push(CommandSideEffect::DestructiveRequiresForce);
3541    }
3542    if contract.destructive_data {
3543        effects.push(CommandSideEffect::DestructiveData);
3544    }
3545    effects
3546}
3547
3548fn op_id_behavior(contract: CommandContract) -> &'static str {
3549    if contract.persists_op_id {
3550        "generated_resume"
3551    } else if contract.supports_op_id {
3552        "explicit_replay"
3553    } else {
3554        "none"
3555    }
3556}
3557
3558fn uses_bootstrap_op_id_store(contract: CommandContract) -> bool {
3559    contract.supports_op_id && contract.may_initialize
3560}
3561
3562fn op_id_store_scope(contract: CommandContract) -> &'static str {
3563    if !contract.supports_op_id {
3564        "none"
3565    } else if uses_bootstrap_op_id_store(contract) {
3566        "bootstrap"
3567    } else {
3568        "repository"
3569    }
3570}
3571
3572fn first_run_behavior(contract: CommandContract) -> &'static str {
3573    if contract.observe_only {
3574        "observe_only_no_init"
3575    } else if contract.may_initialize && contract.may_import_git {
3576        "may_initialize_and_import_git"
3577    } else if contract.may_initialize {
3578        "may_initialize"
3579    } else if contract.may_import_git {
3580        "may_import_git"
3581    } else if contract.mutates {
3582        "requires_initialized_repo"
3583    } else {
3584        "no_repo_required"
3585    }
3586}
3587
3588fn catalog_option(arg: &clap::Arg) -> CommandCatalogOption {
3589    CommandCatalogOption {
3590        id: arg.get_id().as_str().to_string(),
3591        long: arg.get_long().map(str::to_string),
3592        aliases: option_aliases(arg),
3593        short: arg.get_short().map(|short| short.to_string()),
3594        value_names: value_names(arg),
3595        value_kind: value_kind(arg).to_string(),
3596        default_values: arg
3597            .get_default_values()
3598            .iter()
3599            .map(|value| value.to_string_lossy().into_owned())
3600            .collect(),
3601        possible_values: arg
3602            .get_possible_values()
3603            .iter()
3604            .map(|value| value.get_name().to_string())
3605            .collect(),
3606        help: arg.get_help().map(|help| help.to_string()),
3607        required: arg.is_required_set(),
3608        global: arg.is_global_set(),
3609        hidden: arg.is_hide_set(),
3610    }
3611}
3612
3613fn generated_help_option() -> CommandCatalogOption {
3614    CommandCatalogOption {
3615        id: "help".to_string(),
3616        long: Some("help".to_string()),
3617        aliases: Vec::new(),
3618        short: Some("h".to_string()),
3619        value_names: Vec::new(),
3620        value_kind: "boolean".to_string(),
3621        default_values: Vec::new(),
3622        possible_values: Vec::new(),
3623        help: Some("Print help".to_string()),
3624        required: false,
3625        global: true,
3626        hidden: false,
3627    }
3628}
3629
3630fn option_aliases(arg: &clap::Arg) -> Vec<String> {
3631    let mut aliases = std::collections::BTreeSet::new();
3632    for alias in arg.get_all_aliases().unwrap_or_default() {
3633        aliases.insert(alias.to_string());
3634    }
3635    for alias in arg.get_visible_aliases().unwrap_or_default() {
3636        aliases.insert(alias.to_string());
3637    }
3638    aliases.into_iter().collect()
3639}
3640
3641fn catalog_argument(arg: &clap::Arg) -> CommandCatalogArgument {
3642    CommandCatalogArgument {
3643        id: arg.get_id().as_str().to_string(),
3644        value_names: value_names(arg),
3645        help: arg.get_help().map(|help| help.to_string()),
3646        required: arg.is_required_set(),
3647    }
3648}
3649
3650fn value_kind(arg: &clap::Arg) -> &'static str {
3651    match arg.get_action() {
3652        ArgAction::SetTrue | ArgAction::SetFalse => "boolean",
3653        ArgAction::Count => "count",
3654        ArgAction::Append => "list",
3655        ArgAction::Set if !arg.get_possible_values().is_empty() => "enum",
3656        ArgAction::Set => "string",
3657        _ => "unknown",
3658    }
3659}
3660
3661fn value_names(arg: &clap::Arg) -> Vec<String> {
3662    arg.get_value_names()
3663        .map(|names| names.iter().map(|name| name.to_string()).collect())
3664        .unwrap_or_default()
3665}
3666
3667fn command_contract(path: &[String]) -> CommandContract {
3668    command_contract_for_path(path.iter().map(String::as_str))
3669        .unwrap_or_else(|| panic!("missing command contract for `{}`", path.join(" ")))
3670}
3671
3672fn command_contract_for_path<'a>(
3673    path: impl IntoIterator<Item = &'a str>,
3674) -> Option<CommandContract> {
3675    let path = path.into_iter().collect::<Vec<_>>();
3676    active_command_contract_entries()
3677        .iter()
3678        .copied()
3679        .find(|entry| entry.path == path.as_slice())
3680        .map(|entry| entry.contract)
3681}
3682
3683#[cfg(test)]
3684fn raw_command_contract_for_path<'a>(
3685    path: impl IntoIterator<Item = &'a str>,
3686) -> Option<CommandContract> {
3687    let path = path.into_iter().collect::<Vec<_>>();
3688    CONTRACTS
3689        .iter()
3690        .find(|entry| entry.path == path.as_slice())
3691        .map(|entry| entry.contract)
3692}
3693
3694#[cfg(test)]
3695pub(crate) fn sibling_documented_schema_verbs(schema_verb: &str) -> Vec<&'static str> {
3696    active_command_contract_entries()
3697        .iter()
3698        .filter(|entry| {
3699            contract_documented_schema_verbs(entry.contract)
3700                .any(|documented| documented == schema_verb)
3701        })
3702        .flat_map(|entry| contract_documented_schema_verbs(entry.contract))
3703        .filter(|documented| *documented != schema_verb)
3704        .collect()
3705}
3706
3707fn active_command_contract_entries() -> &'static [&'static CommandContractEntry] {
3708    ACTIVE_COMMAND_CONTRACT_ENTRIES
3709        .get_or_init(|| {
3710            let command = Cli::command();
3711            CONTRACTS
3712                .iter()
3713                .filter(|entry| clap_command_path_exists(&command, entry.path))
3714                .collect()
3715        })
3716        .as_slice()
3717}
3718
3719/// The contracts advertised to agents: every [`active_command_contract_entries`]
3720/// entry (present in the compiled clap tree) PLUS any `feature_gated` contract
3721/// whose clap subcommand is compiled out of this build. The hosted surfaces
3722/// (`auth`, `support`, `presence`) are `client`-gated, so a default
3723/// `cargo install heddle-cli` omits their clap nodes — but their schema verbs
3724/// and `output_kind` discriminators are a wire-format-stable promise that must
3725/// stay advertised in the catalog regardless of build features. Distinct from
3726/// the active set, which the clap-tree-equivalence invariants pin exactly.
3727fn advertised_command_contract_entries() -> &'static [&'static CommandContractEntry] {
3728    ADVERTISED_COMMAND_CONTRACT_ENTRIES
3729        .get_or_init(|| {
3730            let command = Cli::command();
3731            let mut entries = active_command_contract_entries().to_vec();
3732            for entry in CONTRACTS.iter() {
3733                if entry.contract.feature_gate.is_some()
3734                    && !clap_command_path_exists(&command, entry.path)
3735                {
3736                    entries.push(entry);
3737                }
3738            }
3739            entries
3740        })
3741        .as_slice()
3742}
3743
3744fn clap_command_path_exists(command: &clap::Command, path: &[&str]) -> bool {
3745    let mut current = command;
3746    for part in path {
3747        let Some(next) = current
3748            .get_subcommands()
3749            .find(|subcommand| subcommand.get_name() == *part)
3750        else {
3751            return false;
3752        };
3753        current = next;
3754    }
3755    true
3756}
3757
3758pub fn command_json_discriminators() -> Vec<CommandJsonDiscriminator> {
3759    advertised_command_contract_entries()
3760        .iter()
3761        .copied()
3762        .flat_map(|entry| {
3763            contract_json_discriminators(entry.contract)
3764                .map(move |discriminator| json_discriminator_metadata(entry.path, &discriminator))
3765        })
3766        .collect()
3767}
3768
3769pub fn operator_envelope_verbs() -> Vec<String> {
3770    active_command_contract_entries()
3771        .iter()
3772        .copied()
3773        .filter(|entry| entry.contract.operator_envelope)
3774        .map(|entry| entry.path.join(" "))
3775        .collect()
3776}
3777
3778#[cfg(test)]
3779pub fn command_json_discriminator_for_schema_verb(
3780    schema_verb: &str,
3781) -> Option<CommandJsonDiscriminator> {
3782    command_json_discriminators_for_schema_verb(schema_verb)
3783        .into_iter()
3784        .next()
3785}
3786
3787pub fn command_json_discriminators_for_schema_verb(
3788    schema_verb: &str,
3789) -> Vec<CommandJsonDiscriminator> {
3790    advertised_command_contract_entries()
3791        .iter()
3792        .copied()
3793        .filter(|entry| {
3794            contract_json_discriminators(entry.contract)
3795                .any(|discriminator| discriminator.schema_verb == Some(schema_verb))
3796        })
3797        .flat_map(|entry| {
3798            let schema_verbs = contract_schema_verbs(entry.contract).collect::<Vec<_>>();
3799            let include_same_command_siblings =
3800                schema_verbs.len() == 1 && schema_verbs[0] == schema_verb;
3801            contract_json_discriminators(entry.contract).filter_map(move |discriminator| {
3802                if discriminator.schema_verb == Some(schema_verb)
3803                    || (include_same_command_siblings && discriminator.schema_verb.is_none())
3804                {
3805                    Some(json_discriminator_metadata(entry.path, &discriminator))
3806                } else {
3807                    None
3808                }
3809            })
3810        })
3811        .collect()
3812}
3813
3814fn json_discriminators_for_path<'a>(
3815    path: impl IntoIterator<Item = &'a str>,
3816) -> Vec<CommandJsonDiscriminator> {
3817    let path = path.into_iter().collect::<Vec<_>>();
3818    advertised_command_contract_entries()
3819        .iter()
3820        .copied()
3821        .filter(|entry| entry.path == path.as_slice())
3822        .flat_map(|entry| {
3823            contract_json_discriminators(entry.contract)
3824                .map(move |discriminator| json_discriminator_metadata(entry.path, &discriminator))
3825        })
3826        .collect()
3827}
3828
3829fn json_discriminator_metadata(
3830    path: &'static [&'static str],
3831    discriminator: &CommandJsonDiscriminatorSpec,
3832) -> CommandJsonDiscriminator {
3833    CommandJsonDiscriminator {
3834        path: path.iter().map(|part| (*part).to_string()).collect(),
3835        display: path.join(" "),
3836        schema_verb: discriminator.schema_verb.map(str::to_string),
3837        field: discriminator.field.to_string(),
3838        value: discriminator.value.to_string(),
3839        no_schema_reason: discriminator.no_schema_reason.map(str::to_string),
3840    }
3841}
3842
3843pub fn command_runtime_contract_for_command(command: &Commands) -> CommandRuntimeContract {
3844    let path = command_path(command);
3845    runtime_contract_for_path(path.iter().copied())
3846        .unwrap_or_else(|| panic!("missing command contract for `{}`", path.join(" ")))
3847}
3848
3849pub fn command_runtime_contract(command_name: &str) -> Option<CommandRuntimeContract> {
3850    runtime_contract_for_path(command_name.split_whitespace())
3851}
3852
3853/// The catalog's declared mutation surface for a command, surfaced to
3854/// callers such as `--dry-run` so the plan report cites the single
3855/// source of truth (`heddle help --output json`) rather than
3856/// re-deriving side effects at each call site.
3857#[derive(Debug, Clone, Copy, Serialize)]
3858pub struct CommandSideEffectFlags {
3859    pub may_move_ref: bool,
3860    pub destructive_requires_force: bool,
3861    pub network_io: bool,
3862    pub writes_heddle_refs: bool,
3863    pub writes_git_refs: bool,
3864}
3865
3866/// Look up the declared side-effect flags for a command path (e.g.
3867/// `"push"`, `"land"`, `"ready"`). Returns `None` for an unknown command.
3868pub fn command_side_effect_flags(command_name: &str) -> Option<CommandSideEffectFlags> {
3869    let path = command_name.split_whitespace().collect::<Vec<_>>();
3870    active_command_contract_entries()
3871        .iter()
3872        .copied()
3873        .find(|entry| entry.path == path.as_slice())
3874        .map(|entry| CommandSideEffectFlags {
3875            may_move_ref: entry.contract.may_move_ref,
3876            destructive_requires_force: entry.contract.destructive_requires_force,
3877            network_io: entry.contract.network_io,
3878            writes_heddle_refs: entry.contract.writes_heddle_refs,
3879            writes_git_refs: entry.contract.writes_git_refs,
3880        })
3881}
3882
3883pub(crate) fn command_runtime_contract_for_schema_verb(
3884    schema_verb: &str,
3885) -> Option<CommandRuntimeContract> {
3886    command_runtime_contract(schema_verb)
3887        .or_else(|| command_runtime_contract(&schema_verb_without_flags(schema_verb)))
3888}
3889
3890pub(crate) fn schema_verb_without_flags(schema_verb: &str) -> String {
3891    schema_verb
3892        .split_whitespace()
3893        .filter(|part| !part.starts_with('-'))
3894        .collect::<Vec<_>>()
3895        .join(" ")
3896}
3897
3898fn runtime_contract_for_path<'a>(
3899    path: impl IntoIterator<Item = &'a str>,
3900) -> Option<CommandRuntimeContract> {
3901    let path = path.into_iter().collect::<Vec<_>>();
3902    active_command_contract_entries()
3903        .iter()
3904        .copied()
3905        .find(|entry| entry.path == path.as_slice())
3906        .map(|entry| runtime_contract(entry.path, entry.contract))
3907}
3908
3909fn runtime_contract(
3910    path: &'static [&'static str],
3911    contract: CommandContract,
3912) -> CommandRuntimeContract {
3913    CommandRuntimeContract {
3914        path: path.to_vec(),
3915        display: path.join(" "),
3916        supports_json: contract.supports_json,
3917        supports_json_compact: contract.supports_json_compact,
3918        mutates: contract.mutates,
3919        targets_current_repository: contract.targets_current_repository,
3920        supports_op_id: contract.supports_op_id,
3921        persists_op_id: contract.persists_op_id,
3922        uses_bootstrap_op_id_store: uses_bootstrap_op_id_store(contract),
3923        help_visibility: contract.help_visibility,
3924        help_rank: contract.help_rank,
3925        surface: contract.surface,
3926        canonical_command: contract.canonical_command,
3927        json_kind: contract.json_kind,
3928    }
3929}
3930
3931pub fn command_supports_op_id(command_name: &str) -> bool {
3932    command_runtime_contract(command_name)
3933        .map(|contract| contract.supports_op_id)
3934        .unwrap_or(false)
3935}
3936
3937pub fn command_persists_op_id(command_name: &str) -> bool {
3938    command_runtime_contract(command_name)
3939        .map(|contract| contract.persists_op_id)
3940        .unwrap_or(false)
3941}
3942
3943pub fn command_uses_bootstrap_op_id_store(command_name: &str) -> bool {
3944    command_runtime_contract(command_name)
3945        .map(|contract| contract.uses_bootstrap_op_id_store)
3946        .unwrap_or(false)
3947}
3948
3949pub(crate) fn feature_gated_command_roots() -> Vec<&'static str> {
3950    let mut roots = CONTRACTS
3951        .iter()
3952        .filter(|entry| entry.path.len() == 1 && entry.contract.feature_gate.is_some())
3953        .map(|entry| entry.path[0])
3954        .collect::<Vec<_>>();
3955    roots.sort_unstable();
3956    roots.dedup();
3957    roots
3958}
3959
3960pub fn command_supports_op_id_for_command(command: &Commands) -> bool {
3961    command_runtime_contract_for_command(command).supports_op_id
3962}
3963
3964pub fn command_supports_json_for_command(command: &Commands) -> bool {
3965    command_runtime_contract_for_command(command).supports_json
3966}
3967
3968pub fn command_help_tier(command_name: &str) -> &'static str {
3969    command_runtime_contract(command_name)
3970        .map(|contract| help_visibility_to_tier(contract.help_visibility))
3971        .unwrap_or("advanced")
3972}
3973
3974pub fn command_surface(command_name: &str) -> &'static str {
3975    command_runtime_contract(command_name)
3976        .map(|contract| contract.surface)
3977        .unwrap_or("native")
3978}
3979
3980pub fn command_help_visibility(command_name: &str) -> &'static str {
3981    command_runtime_contract(command_name)
3982        .map(|contract| contract.help_visibility)
3983        .unwrap_or("advanced")
3984}
3985
3986pub fn command_canonical_command(command_name: &str) -> Option<&'static str> {
3987    command_runtime_contract(command_name).and_then(|contract| contract.canonical_command)
3988}
3989
3990pub fn root_commands_for_help_visibility(visibility: &str) -> Vec<&'static str> {
3991    let mut entries = active_command_contract_entries()
3992        .iter()
3993        .copied()
3994        .filter(|entry| entry.path.len() == 1 && entry.contract.help_visibility == visibility)
3995        .collect::<Vec<_>>();
3996    entries.sort_by_key(|entry| (entry.contract.help_rank, entry.path[0]));
3997    entries.into_iter().map(|entry| entry.path[0]).collect()
3998}
3999
4000pub fn root_commands_for_advanced_help() -> Vec<&'static str> {
4001    let mut entries = advanced_help_root_entries();
4002    entries.sort_by_key(|entry| (entry.contract.help_rank, entry.path[0]));
4003    entries.into_iter().map(|entry| entry.path[0]).collect()
4004}
4005
4006/// Root commands on the advanced help surface, unsorted.
4007fn advanced_help_root_entries() -> Vec<&'static CommandContractEntry> {
4008    active_command_contract_entries()
4009        .iter()
4010        .copied()
4011        .filter(|entry| {
4012            entry.path.len() == 1
4013                && !matches!(entry.contract.help_visibility, "everyday" | "hidden")
4014        })
4015        .collect()
4016}
4017
4018/// Area groups for the `heddle help advanced` listing, in render order,
4019/// as `(display title, verbs)` pairs (heddle#652). The grouping is
4020/// contract-table data, not a hand-maintained help string: native
4021/// advanced commands carry an explicit `help_category` on their
4022/// registration, while `automation` / `admin` / `git_projection` commands
4023/// derive their group from the surface they already declare. Verbs keep
4024/// contract order (help_rank, then name) within each group — the same
4025/// ordering the flat list used. Feature-gated verbs absent from the
4026/// current build simply don't appear; a group may come back empty.
4027pub fn advanced_help_groups() -> Vec<(&'static str, Vec<&'static str>)> {
4028    const GROUPS: &[(&str, &str)] = &[
4029        ("threads", "Threads and integration"),
4030        ("states", "States and history"),
4031        ("collab", "Collaboration and review"),
4032        ("recovery", "Recovery and integrity"),
4033        ("repo", "Repo and environment"),
4034        ("automation", "Agents and automation"),
4035        ("git-interop", "Git interop"),
4036        ("admin", "Admin and maintenance"),
4037    ];
4038    let mut entries = advanced_help_root_entries();
4039    entries.sort_by_key(|entry| (entry.contract.help_rank, entry.path[0]));
4040    GROUPS
4041        .iter()
4042        .map(|(id, title)| {
4043            (
4044                *title,
4045                entries
4046                    .iter()
4047                    .filter(|entry| advanced_help_group_id(&entry.contract) == *id)
4048                    .map(|entry| entry.path[0])
4049                    .collect(),
4050            )
4051        })
4052        .collect()
4053}
4054
4055/// Resolve which advanced-help group a root command belongs to. The
4056/// non-native surfaces are themselves the grouping; native commands use
4057/// the `help_category` set at registration. An unset category on a
4058/// native advanced command maps to "" (member of no group) — the
4059/// `advanced_help_groups_cover_every_advanced_verb` test turns that into
4060/// a build failure rather than a silently missing help line.
4061fn advanced_help_group_id(contract: &CommandContract) -> &'static str {
4062    match contract.surface {
4063        "automation" => "automation",
4064        "admin" => "admin",
4065        "git_projection" => "git-interop",
4066        _ => contract.help_category.unwrap_or(""),
4067    }
4068}
4069
4070pub fn recommended_action_template(action: &str) -> Option<ActionTemplate> {
4071    let trimmed = action.trim();
4072    if trimmed.is_empty() {
4073        return None;
4074    }
4075    RECOMMENDED_ACTION_TEMPLATES
4076        .iter()
4077        .find(|(template_action, _, _, _)| *template_action == trimmed)
4078        .map(
4079            |(template_action, argv_template, required_inputs, agent_may_fill)| {
4080                action_template_from_parts(
4081                    template_action,
4082                    argv_template,
4083                    required_inputs,
4084                    *agent_may_fill,
4085                )
4086            },
4087        )
4088        .or_else(|| dynamic_recommended_action_template(trimmed))
4089        .or_else(|| concrete_recommended_action_template(trimmed))
4090}
4091
4092/// Fallback template for a concrete, placeholder-free recommended action
4093/// (e.g. `heddle status`). The template *is* the parsed argv with no inputs
4094/// left to fill — agents run `argv_template` verbatim. This makes
4095/// `recommended_action_template` total over every valid action so the
4096/// fillable `_template` is the single canonical machine shape and the
4097/// always-null `_argv` sibling could be dropped (HeddleCo/heddle#254).
4098///
4099/// Returns `None` for placeholder/display-only actions that lack a
4100/// registered structured template (they are invalid and surface upstream),
4101/// matching the previous parsed-argv contract.
4102fn concrete_recommended_action_template(action: &str) -> Option<ActionTemplate> {
4103    if RECOMMENDED_ACTION_PLACEHOLDERS.contains(&action) || is_display_only_template(action) {
4104        return None;
4105    }
4106    if validate_recommended_action(action).is_err() {
4107        return None;
4108    }
4109    let argv = split_recommended_action(action).ok()?;
4110    Some(action_template_from_owned(
4111        action.to_string(),
4112        argv,
4113        Vec::new(),
4114        false,
4115    ))
4116}
4117
4118fn dynamic_recommended_action_template(action: &str) -> Option<ActionTemplate> {
4119    let argv = split_recommended_action(action).ok()?;
4120    if let Some(template) = dynamic_message_recommended_action_template(action, &argv) {
4121        return Some(template);
4122    }
4123    match argv.as_slice() {
4124        [heddle, clone, remote, path]
4125            if heddle == "heddle" && clone == "clone" && is_placeholder_arg(path) =>
4126        {
4127            Some(action_template_from_owned(
4128                action.to_string(),
4129                vec![
4130                    "heddle".to_string(),
4131                    "clone".to_string(),
4132                    remote.clone(),
4133                    path.clone(),
4134                ],
4135                vec![placeholder_input_name(path)],
4136                false,
4137            ))
4138        }
4139        [heddle, clone, remote, path, flag, thread]
4140            if heddle == "heddle"
4141                && clone == "clone"
4142                && flag == "--thread"
4143                && is_placeholder_arg(path) =>
4144        {
4145            Some(action_template_from_owned(
4146                action.to_string(),
4147                vec![
4148                    "heddle".to_string(),
4149                    "clone".to_string(),
4150                    remote.clone(),
4151                    path.clone(),
4152                    "--thread".to_string(),
4153                    thread.clone(),
4154                ],
4155                vec![placeholder_input_name(path)],
4156                false,
4157            ))
4158        }
4159        [heddle, start, thread_name, path_flag, path]
4160            if heddle == "heddle"
4161                && start == "start"
4162                && path_flag == "--path"
4163                && is_placeholder_arg(path) =>
4164        {
4165            let required_inputs = [thread_name, path]
4166                .iter()
4167                .filter(|arg| is_placeholder_arg(arg))
4168                .map(|arg| placeholder_input_name(arg))
4169                .collect();
4170            Some(action_template_from_owned(
4171                action.to_string(),
4172                vec![
4173                    "heddle".to_string(),
4174                    "start".to_string(),
4175                    thread_name.clone(),
4176                    "--path".to_string(),
4177                    path.clone(),
4178                ],
4179                required_inputs,
4180                true,
4181            ))
4182        }
4183        [heddle, thread_cmd, absorb, thread_name, into_flag, parent]
4184            if heddle == "heddle"
4185                && thread_cmd == "thread"
4186                && absorb == "absorb"
4187                && into_flag == "--into"
4188                && is_placeholder_arg(parent) =>
4189        {
4190            Some(action_template_from_owned(
4191                action.to_string(),
4192                vec![
4193                    "heddle".to_string(),
4194                    "thread".to_string(),
4195                    "absorb".to_string(),
4196                    thread_name.clone(),
4197                    "--into".to_string(),
4198                    parent.clone(),
4199                ],
4200                vec![placeholder_input_name(parent)],
4201                true,
4202            ))
4203        }
4204        _ => None,
4205    }
4206}
4207
4208fn dynamic_message_recommended_action_template(
4209    action: &str,
4210    argv: &[String],
4211) -> Option<ActionTemplate> {
4212    match argv {
4213        [heddle, command, message_flag, message]
4214            if heddle == "heddle"
4215                && matches!(command.as_str(), "capture" | "ready")
4216                && is_message_flag(message_flag)
4217                && is_message_placeholder_arg(message) =>
4218        {
4219            Some(action_template_from_owned(
4220                action.to_string(),
4221                vec![
4222                    "heddle".to_string(),
4223                    command.clone(),
4224                    "-m".to_string(),
4225                    "<message>".to_string(),
4226                ],
4227                vec!["message".to_string()],
4228                true,
4229            ))
4230        }
4231        [
4232            heddle,
4233            capture,
4234            message_flag,
4235            message,
4236            confidence_flag,
4237            confidence,
4238        ] if heddle == "heddle"
4239            && capture == "capture"
4240            && is_message_flag(message_flag)
4241            && is_message_placeholder_arg(message)
4242            && confidence_flag == "--confidence"
4243            && is_placeholder_arg(confidence) =>
4244        {
4245            Some(action_template_from_owned(
4246                action.to_string(),
4247                vec![
4248                    "heddle".to_string(),
4249                    "capture".to_string(),
4250                    "-m".to_string(),
4251                    "<message>".to_string(),
4252                    "--confidence".to_string(),
4253                    confidence.clone(),
4254                ],
4255                vec!["message".to_string(), placeholder_input_name(confidence)],
4256                true,
4257            ))
4258        }
4259        // The confidence/verification policy-blocker recovery scopes itself to
4260        // the thread's checkout via the global `--repo <path>` flag (heddle#464).
4261        // `<path>` is a concrete worktree path, not a placeholder, so the only
4262        // fillable inputs remain the message and confidence.
4263        [
4264            heddle,
4265            repo_flag,
4266            repo_path,
4267            command,
4268            message_flag,
4269            message,
4270            confidence_flag,
4271            confidence,
4272        ] if heddle == "heddle"
4273            && repo_flag == "--repo"
4274            && command == "capture"
4275            && is_message_flag(message_flag)
4276            && is_message_placeholder_arg(message)
4277            && confidence_flag == "--confidence"
4278            && is_placeholder_arg(confidence) =>
4279        {
4280            Some(action_template_from_owned(
4281                action.to_string(),
4282                vec![
4283                    "heddle".to_string(),
4284                    "--repo".to_string(),
4285                    repo_path.clone(),
4286                    command.clone(),
4287                    "-m".to_string(),
4288                    "<message>".to_string(),
4289                    "--confidence".to_string(),
4290                    confidence.clone(),
4291                ],
4292                vec!["message".to_string(), placeholder_input_name(confidence)],
4293                true,
4294            ))
4295        }
4296        _ => None,
4297    }
4298}
4299
4300fn is_message_flag(value: &str) -> bool {
4301    value == "-m" || value == "--message"
4302}
4303
4304fn is_message_placeholder_arg(value: &str) -> bool {
4305    matches!(value, "..." | "…") || value == "<message>"
4306}
4307
4308fn is_placeholder_arg(value: &str) -> bool {
4309    value.starts_with('<') && value.ends_with('>') && value.len() > 2
4310}
4311
4312fn placeholder_input_name(value: &str) -> String {
4313    value
4314        .trim_start_matches('<')
4315        .trim_end_matches('>')
4316        .replace('-', "_")
4317}
4318
4319fn help_visibility_to_tier(help_visibility: &str) -> &'static str {
4320    match help_visibility {
4321        "everyday" => "everyday",
4322        "hidden" => "hidden",
4323        _ => "advanced",
4324    }
4325}
4326
4327pub fn observe_only_root_commands() -> Vec<&'static str> {
4328    active_command_contract_entries()
4329        .iter()
4330        .copied()
4331        .filter(|entry| {
4332            entry.path.len() == 1 && entry.contract.observe_only && !entry.contract.mutates
4333        })
4334        .map(|entry| entry.path[0])
4335        .collect()
4336}
4337
4338pub fn command_contract_root_commands() -> Vec<&'static str> {
4339    active_command_contract_entries()
4340        .iter()
4341        .copied()
4342        .filter(|entry| entry.path.len() == 1)
4343        .map(|entry| entry.path[0])
4344        .collect()
4345}
4346
4347pub fn validate_recommended_action(action: &str) -> std::result::Result<(), String> {
4348    let trimmed = action.trim();
4349    if trimmed.is_empty() {
4350        return Ok(());
4351    }
4352    if RECOMMENDED_ACTION_PLACEHOLDERS.contains(&trimmed) {
4353        return recommended_action_template(trimmed)
4354            .map(|_| ())
4355            .ok_or_else(|| {
4356                format!(
4357                    "recommended action placeholder `{trimmed}` must have a structured template"
4358                )
4359            });
4360    }
4361    if is_display_only_template(trimmed) {
4362        return recommended_action_template(trimmed).map(|_| ()).ok_or_else(|| {
4363            format!(
4364                "display-only recommended action `{trimmed}` must be registered as a structured template"
4365            )
4366        });
4367    }
4368
4369    let argv = split_recommended_action(trimmed)?;
4370    match argv.first().map(String::as_str) {
4371        Some("heddle") => Cli::command()
4372            .try_get_matches_from(argv)
4373            .map(|_| ())
4374            .map_err(|err| err.to_string()),
4375        Some(other) => Err(format!(
4376            "recommended action must start with `heddle`, found `{other}`"
4377        )),
4378        None => Ok(()),
4379    }
4380}
4381
4382fn is_display_only_template(action: &str) -> bool {
4383    action.contains("...") || action.contains('…') || (action.contains('<') && action.contains('>'))
4384}
4385
4386pub fn split_recommended_action(action: &str) -> std::result::Result<Vec<String>, String> {
4387    let mut args = Vec::new();
4388    let mut current = String::new();
4389    let mut chars = action.chars().peekable();
4390    let mut in_single_quote = false;
4391    let mut in_double_quote = false;
4392
4393    while let Some(ch) = chars.next() {
4394        match (ch, in_single_quote, in_double_quote) {
4395            ('\'', false, false) => in_single_quote = true,
4396            ('\'', true, false) => in_single_quote = false,
4397            ('"', false, false) => in_double_quote = true,
4398            ('"', false, true) => in_double_quote = false,
4399            ('\\', false, _) => match chars.next() {
4400                Some(next) => current.push(next),
4401                None => current.push('\\'),
4402            },
4403            (ch, false, false) if ch.is_whitespace() => {
4404                if !current.is_empty() {
4405                    args.push(std::mem::take(&mut current));
4406                }
4407            }
4408            (ch, _, _) => current.push(ch),
4409        }
4410    }
4411
4412    if in_single_quote {
4413        return Err("unterminated single quote in recommended action".to_string());
4414    }
4415    if in_double_quote {
4416        return Err("unterminated double quote in recommended action".to_string());
4417    }
4418    if !current.is_empty() {
4419        args.push(current);
4420    }
4421    Ok(args)
4422}
4423
4424pub(crate) fn schema_verbs() -> Vec<&'static str> {
4425    let mut verbs = collect_schema_verbs(contract_schema_verbs);
4426    verbs.push("error");
4427    verbs
4428}
4429
4430pub(crate) fn documented_schema_verbs() -> Vec<&'static str> {
4431    let mut verbs = collect_schema_verbs(contract_documented_schema_verbs);
4432    verbs.push("error");
4433    verbs
4434}
4435
4436pub(crate) fn opaque_schema_verbs() -> Vec<&'static str> {
4437    collect_schema_verbs(|contract| contract.opaque_schema_verbs.iter().copied())
4438}
4439
4440fn collect_schema_verbs<I>(select: impl Fn(CommandContract) -> I) -> Vec<&'static str>
4441where
4442    I: IntoIterator<Item = &'static str>,
4443{
4444    let mut verbs = Vec::new();
4445    for entry in advertised_command_contract_entries().iter().copied() {
4446        for verb in select(entry.contract) {
4447            if !verbs.contains(&verb) {
4448                verbs.push(verb);
4449            }
4450        }
4451    }
4452    verbs
4453}
4454
4455pub fn command_path(command: &Commands) -> Vec<&'static str> {
4456    match command {
4457        Commands::Init(_) => vec!["init"],
4458        Commands::Adopt(_) => vec!["adopt"],
4459        Commands::Help { .. } => vec!["help"],
4460        Commands::Status { .. } => vec!["status"],
4461        Commands::Watch(_) => vec!["watch"],
4462        Commands::Verify => vec!["verify"],
4463        Commands::Doctor(args) => match &args.command {
4464            None => vec!["doctor"],
4465            Some(DoctorCommands::Docs(_)) => vec!["doctor", "docs"],
4466            Some(DoctorCommands::Schemas(_)) => vec!["doctor", "schemas"],
4467        },
4468        Commands::Schemas { .. } => vec!["schemas"],
4469        Commands::Start(_) => vec!["start"],
4470        Commands::Try(_) => vec!["try"],
4471        Commands::Run(_) => vec!["run"],
4472        Commands::Sync(args) => {
4473            #[cfg(feature = "git-overlay")]
4474            {
4475                if matches!(args.command, Some(SyncCommands::Git { .. })) {
4476                    return vec!["sync", "git"];
4477                }
4478            }
4479            #[cfg(not(feature = "git-overlay"))]
4480            let _ = args;
4481            vec!["sync"]
4482        }
4483        Commands::Continue => vec!["continue"],
4484        Commands::Abort => vec!["abort"],
4485        Commands::Land(_) => vec!["land"],
4486        Commands::Ready(_) => vec!["ready"],
4487        Commands::Capture(_) => vec!["capture"],
4488        Commands::Commit(_) => vec!["commit"],
4489        Commands::Log(_) => vec!["log"],
4490        Commands::Show { .. } => vec!["show"],
4491        Commands::Retro(_) => vec!["retro"],
4492        Commands::Diff(_) => vec!["diff"],
4493        Commands::Discuss { command } => match command {
4494            DiscussCommands::Open(_) => vec!["discuss", "open"],
4495            DiscussCommands::Append(_) => vec!["discuss", "append"],
4496            DiscussCommands::Resolve(_) => vec!["discuss", "resolve"],
4497            DiscussCommands::Reopen(_) => vec!["discuss", "reopen"],
4498            DiscussCommands::List(_) => vec!["discuss", "list"],
4499            DiscussCommands::Show(_) => vec!["discuss", "show"],
4500        },
4501        Commands::Query(_) => vec!["query"],
4502        Commands::Review { command } => match command {
4503            ReviewCommands::Show(_) => vec!["review", "show"],
4504            ReviewCommands::Sign(_) => vec!["review", "sign"],
4505            ReviewCommands::Next(_) => vec!["review", "next"],
4506            ReviewCommands::Health(_) => vec!["review", "health"],
4507        },
4508        Commands::Redact { command } => match command {
4509            RedactCommands::Apply(_) => vec!["redact", "apply"],
4510            RedactCommands::List(_) => vec!["redact", "list"],
4511            RedactCommands::Show(_) => vec!["redact", "show"],
4512            RedactCommands::Trust(command) => match command {
4513                RedactTrustCommands::Add(_) => vec!["redact", "trust", "add"],
4514                RedactTrustCommands::List(_) => vec!["redact", "trust", "list"],
4515                RedactTrustCommands::Remove(_) => vec!["redact", "trust", "remove"],
4516            },
4517            RedactCommands::Purge(command) => match command {
4518                PurgeCommands::Apply(_) => vec!["redact", "purge", "apply"],
4519                PurgeCommands::List(_) => vec!["redact", "purge", "list"],
4520            },
4521        },
4522        Commands::Visibility { command } => match command {
4523            VisibilityCommands::Set(_) => vec!["visibility", "set"],
4524            VisibilityCommands::Promote(_) => vec!["visibility", "promote"],
4525            VisibilityCommands::Show(_) => vec!["visibility", "show"],
4526            VisibilityCommands::List(_) => vec!["visibility", "list"],
4527        },
4528        Commands::Revert(_) => vec!["revert"],
4529        Commands::Undo(_) => vec!["undo"],
4530        Commands::Collapse(_) => vec!["collapse"],
4531        Commands::Expand(_) => vec!["expand"],
4532        Commands::Thread { command } => match command {
4533            ThreadCommands::Create { .. } => vec!["thread", "create"],
4534            ThreadCommands::Current => vec!["thread", "current"],
4535            ThreadCommands::Switch { .. } => vec!["thread", "switch"],
4536            ThreadCommands::Cd { .. } => vec!["thread", "cd"],
4537            ThreadCommands::List(_) => vec!["thread", "list"],
4538            ThreadCommands::Show(_) => vec!["thread", "show"],
4539            ThreadCommands::Captures(_) => vec!["thread", "captures"],
4540            ThreadCommands::Rename(_) => vec!["thread", "rename"],
4541            ThreadCommands::Refresh(_) => vec!["thread", "refresh"],
4542            ThreadCommands::Move(_) => vec!["thread", "move"],
4543            ThreadCommands::Absorb(_) => vec!["thread", "absorb"],
4544            ThreadCommands::Resolve(_) => vec!["thread", "resolve"],
4545            ThreadCommands::Promote(_) => vec!["thread", "promote"],
4546            ThreadCommands::Drop(_) => vec!["thread", "drop"],
4547            ThreadCommands::Approve(_) => vec!["thread", "approve"],
4548            ThreadCommands::Approvals(_) => vec!["thread", "approvals"],
4549            ThreadCommands::RevokeApproval(_) => vec!["thread", "revoke-approval"],
4550            ThreadCommands::CheckMerge(_) => vec!["thread", "check-merge"],
4551            ThreadCommands::Cleanup(_) => vec!["thread", "cleanup"],
4552            ThreadCommands::Marker { command } => match command {
4553                ThreadMarkerCommands::List { .. } => vec!["thread", "marker", "list"],
4554                ThreadMarkerCommands::Create { .. } => {
4555                    vec!["thread", "marker", "create"]
4556                }
4557                ThreadMarkerCommands::Delete { .. } => {
4558                    vec!["thread", "marker", "delete"]
4559                }
4560                ThreadMarkerCommands::Show { .. } => vec!["thread", "marker", "show"],
4561            },
4562        },
4563        Commands::Timeline(args) => match &args.command {
4564            TimelineCommands::Status(_) => vec!["timeline", "status"],
4565            TimelineCommands::RecordStart(_) => vec!["timeline", "record-start"],
4566            TimelineCommands::RecordFinish(_) => vec!["timeline", "record-finish"],
4567            TimelineCommands::Fork(_) => vec!["timeline", "fork"],
4568            TimelineCommands::Reset(_) => vec!["timeline", "reset"],
4569            TimelineCommands::Recover(_) => vec!["timeline", "recover"],
4570        },
4571        Commands::Shell { command } => match command {
4572            ShellCommands::Init { .. } => vec!["shell", "init"],
4573            ShellCommands::Completion { .. } => vec!["shell", "completion"],
4574            ShellCommands::Prompt => vec!["shell", "prompt"],
4575        },
4576        Commands::Complete { .. } => vec!["complete"],
4577        Commands::Resolve(_) => vec!["resolve"],
4578        Commands::Fsck(args) => match &args.command {
4579            None => vec!["fsck"],
4580            Some(crate::cli::FsckCommands::Repair { target }) => match target {
4581                crate::cli::FsckRepairCommands::Git(_) => vec!["fsck", "repair", "git"],
4582            },
4583        },
4584        #[cfg(feature = "git-overlay")]
4585        Commands::Import { command } => match command {
4586            ImportCommands::Git { .. } => vec!["import", "git"],
4587        },
4588        #[cfg(feature = "git-overlay")]
4589        Commands::Export { command } => match command {
4590            ExportCommands::Git { .. } => vec!["export", "git"],
4591        },
4592        Commands::Oplog { command } => match command {
4593            OplogCommands::Recover => vec!["oplog", "recover"],
4594        },
4595        Commands::Push(_) => vec!["push"],
4596        Commands::Pull(_) => vec!["pull"],
4597        Commands::Remote { command } => match command {
4598            RemoteCommands::List => vec!["remote", "list"],
4599            RemoteCommands::Add { .. } => vec!["remote", "add"],
4600            RemoteCommands::Remove { .. } => vec!["remote", "remove"],
4601            RemoteCommands::SetDefault { .. } => vec!["remote", "set-default"],
4602            RemoteCommands::Show { .. } => vec!["remote", "show"],
4603        },
4604        #[cfg(feature = "client")]
4605        Commands::Auth { command } => match command {
4606            AuthCommands::Login { .. } => vec!["auth", "login"],
4607            AuthCommands::Logout { .. } => vec!["auth", "logout"],
4608            AuthCommands::Status { .. } => vec!["auth", "status"],
4609            AuthCommands::Trust { command } => match command {
4610                crate::cli::AuthTrustCommands::Show(_) => vec!["auth", "trust", "show"],
4611                crate::cli::AuthTrustCommands::Replace(_) => vec!["auth", "trust", "replace"],
4612            },
4613            AuthCommands::DeriveAgent { .. } => vec!["auth", "derive-agent"],
4614            AuthCommands::CreateServiceToken { .. } => vec!["auth", "create-service-token"],
4615        },
4616        #[cfg(feature = "client")]
4617        Commands::Whoami { .. } => vec!["whoami"],
4618        Commands::Context { command } => match command {
4619            ContextCommands::Set(_) => vec!["context", "set"],
4620            ContextCommands::Get(_) => vec!["context", "get"],
4621            ContextCommands::List(_) => vec!["context", "list"],
4622            ContextCommands::History(_) => vec!["context", "history"],
4623            ContextCommands::Edit(_) => vec!["context", "edit"],
4624            ContextCommands::Supersede(_) => vec!["context", "supersede"],
4625            ContextCommands::Rm(_) => vec!["context", "rm"],
4626            ContextCommands::Check(_) => vec!["context", "check"],
4627            ContextCommands::Suggest(_) => vec!["context", "suggest"],
4628            ContextCommands::Audit(_) => vec!["context", "audit"],
4629            #[cfg(all(feature = "git-overlay", feature = "ingest"))]
4630            ContextCommands::Reason { command } => match command {
4631                crate::cli::cli_args::ContextReasonCommands::Git(_) => {
4632                    vec!["context", "reason", "git"]
4633                }
4634            },
4635        },
4636        Commands::Integration { command } => match command {
4637            IntegrationCommands::List => vec!["integration", "list"],
4638            IntegrationCommands::Install(_) => vec!["integration", "install"],
4639            IntegrationCommands::Doctor => vec!["integration", "doctor"],
4640            IntegrationCommands::Uninstall(_) => vec!["integration", "uninstall"],
4641            IntegrationCommands::Upgrade(_) => vec!["integration", "upgrade"],
4642            IntegrationCommands::Relay(_) => vec!["integration", "relay"],
4643        },
4644        #[cfg(feature = "semantic")]
4645        Commands::Semantic { command } => match command {
4646            SemanticCommands::Hot { .. } => vec!["semantic", "hot"],
4647            SemanticCommands::Index { .. } => vec!["semantic", "index"],
4648        },
4649        Commands::Daemon { command } => match command {
4650            DaemonCommands::Serve => vec!["daemon", "serve"],
4651            DaemonCommands::Status => vec!["daemon", "status"],
4652            DaemonCommands::Stop => vec!["daemon", "stop"],
4653        },
4654        Commands::Agent { command } => match command {
4655            AgentCommands::Reserve(_) => vec!["agent", "reserve"],
4656            AgentCommands::Heartbeat(_) => vec!["agent", "heartbeat"],
4657            AgentCommands::Capture(_) => vec!["agent", "capture"],
4658            AgentCommands::Ready(_) => vec!["agent", "ready"],
4659            AgentCommands::Release(_) => vec!["agent", "release"],
4660            AgentCommands::List(_) => vec!["agent", "list"],
4661            AgentCommands::Task(command) => match command {
4662                AgentTaskCommands::Create(_) => vec!["agent", "task", "create"],
4663                AgentTaskCommands::List(_) => vec!["agent", "task", "list"],
4664                AgentTaskCommands::Show(_) => vec!["agent", "task", "show"],
4665                AgentTaskCommands::Update(_) => vec!["agent", "task", "update"],
4666            },
4667            AgentCommands::Fanout(command) => match command {
4668                AgentFanoutCommands::Plan(_) => vec!["agent", "fanout", "plan"],
4669                AgentFanoutCommands::Start(_) => vec!["agent", "fanout", "start"],
4670            },
4671            AgentCommands::Presence(command) => match command {
4672                AgentPresenceCommands::List(_) => vec!["agent", "presence", "list"],
4673                AgentPresenceCommands::Show(_) => vec!["agent", "presence", "show"],
4674                AgentPresenceCommands::Explain(_) => vec!["agent", "presence", "explain"],
4675                AgentPresenceCommands::Complete(_) => vec!["agent", "presence", "complete"],
4676            },
4677            AgentCommands::Provenance(command) => match command {
4678                AgentProvenanceCommands::Begin(_) => vec!["agent", "provenance", "begin"],
4679                AgentProvenanceCommands::Segment(_) => vec!["agent", "provenance", "segment"],
4680                AgentProvenanceCommands::End(_) => vec!["agent", "provenance", "end"],
4681                AgentProvenanceCommands::Show(_) => vec!["agent", "provenance", "show"],
4682                AgentProvenanceCommands::List(_) => vec!["agent", "provenance", "list"],
4683            },
4684        },
4685        Commands::Maintenance { command } => match command {
4686            MaintenanceCommands::Inspect => vec!["maintenance", "inspect"],
4687            MaintenanceCommands::Refresh => vec!["maintenance", "refresh"],
4688            MaintenanceCommands::Gc { .. } => vec!["maintenance", "gc"],
4689        },
4690        Commands::Clone(_) => vec!["clone"],
4691        Commands::Hook { command } => match command {
4692            HookCommands::List => vec!["hook", "list"],
4693            HookCommands::Install { .. } => vec!["hook", "install"],
4694            HookCommands::Uninstall { .. } => vec!["hook", "uninstall"],
4695            HookCommands::Events { .. } => vec!["hook", "events"],
4696        },
4697    }
4698}
4699
4700#[cfg(test)]
4701mod tests;