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", "derive-agent"],
1441        feature_gated(user_scoped(CONFIG_MUTATION_TEXT), "client"),
1442    ),
1443    entry(
1444        &["auth", "create-service-token"],
1445        feature_gated(
1446            json_discriminators(
1447                documented_schemas(
1448                    user_scoped(NETWORK_METADATA_MUTATION_NO_OP_ID),
1449                    &["auth create-service-token"],
1450                ),
1451                &[json_discriminator(
1452                    Some("auth create-service-token"),
1453                    "output_kind",
1454                    "auth_create_service_token",
1455                )],
1456            ),
1457            "client",
1458        ),
1459    ),
1460    entry(
1461        &["whoami"],
1462        category(
1463            feature_gated(
1464                json_discriminators(
1465                    documented_schemas(READ_JSON, &["whoami"]),
1466                    &[json_discriminator(Some("whoami"), "output_kind", "whoami")],
1467                ),
1468                "client",
1469            ),
1470            // Groups under "Repo and environment" in `heddle help advanced`,
1471            // alongside the `auth` identity commands.
1472            "repo",
1473        ),
1474    ),
1475    entry(&["import"], surface(GROUP, "git_projection")),
1476    entry(
1477        &["import", "git"],
1478        exits(
1479            json_discriminators(
1480                documented_schemas(IMPORTING_MUTATION, &["import git"]),
1481                &[json_discriminator(
1482                    Some("import git"),
1483                    "output_kind",
1484                    "import_git",
1485                )],
1486            ),
1487            &[
1488                (0, "ok"),
1489                (65, "malformed git repo or unimportable refs"),
1490                (74, "io reading git refs"),
1491            ],
1492        ),
1493    ),
1494    entry(&["export"], surface(GROUP, "git_projection")),
1495    entry(
1496        &["export", "git"],
1497        json_discriminators(
1498            documented_schemas(
1499                CommandContract {
1500                    writes_git_refs: true,
1501                    ..REF_MUTATION
1502                },
1503                &["export git"],
1504            ),
1505            &[json_discriminator(
1506                Some("export git"),
1507                "output_kind",
1508                "export_git",
1509            )],
1510        ),
1511    ),
1512    entry(
1513        &["sync", "git"],
1514        exits(
1515            git_projection_action(
1516                json_discriminators(
1517                    documented_schemas(IMPORTING_MUTATION, &["sync git"]),
1518                    &[json_discriminator(
1519                        Some("sync git"),
1520                        "output_kind",
1521                        "sync_git",
1522                    )],
1523                ),
1524                "adopt",
1525                "workflow",
1526                "Use adopt to initialize Heddle from an existing Git repository and import its history.",
1527            ),
1528            &[
1529                (0, "ok"),
1530                (75, "remote unreachable; safe to retry"),
1531                (76, "remote rejected payload"),
1532            ],
1533        ),
1534    ),
1535    entry(
1536        &["capture"],
1537        front_door(
1538            category(
1539                json_discriminators(
1540                    documented_schemas(compact_json(CAPTURE), &["capture"]),
1541                    &[json_discriminator(
1542                        Some("capture"),
1543                        "output_kind",
1544                        "capture",
1545                    )],
1546                ),
1547                "states",
1548            ),
1549            30,
1550        ),
1551    ),
1552    entry(
1553        &["clone"],
1554        front_door(
1555            surface(
1556                json_discriminators(
1557                    documented_schemas(
1558                        CommandContract {
1559                            targets_current_repository: false,
1560                            may_initialize: true,
1561                            may_import_git: true,
1562                            may_write_worktree: true,
1563                            may_move_ref: true,
1564                            writes_git_refs: true,
1565                            writes_worktree: true,
1566                            writes_config: true,
1567                            network_io: true,
1568                            ..REF_MUTATION
1569                        },
1570                        &["clone"],
1571                    ),
1572                    &[
1573                        json_discriminator(Some("clone"), "output_kind", "clone"),
1574                        // `clone --output json` on a hosted/network remote
1575                        // emits a preliminary connection envelope before the
1576                        // final clone payload. Both records carry
1577                        // `output_kind` so agents that route on the
1578                        // discriminator (per heddle#272) can classify each
1579                        // line without falling back to text parsing. The
1580                        // envelope has no separate schema verb — it's a
1581                        // small inline object, not a Serialize struct in
1582                        // `schemas`. Source-of-truth value:
1583                        // `cli::cli::commands::CLONE_CONNECTION_OUTPUT_KIND`.
1584                        json_discriminator_no_schema(
1585                            "preliminary connection envelope emitted by hosted clones \
1586                         before the final clone payload (no separate schema)",
1587                            "output_kind",
1588                            "clone_connection",
1589                        ),
1590                        // `clone --recursive --output json` (Spool epic P9) emits a
1591                        // monorepo summary instead of the single-spool `clone`
1592                        // payload: the placed per-spool ops + the skipped (EdgeSkip)
1593                        // child edges. Inline object, no separate schema verb.
1594                        // Source: `clone::monorepo_clone_output_json`.
1595                        json_discriminator_no_schema(
1596                            "monorepo clone summary emitted by `clone --recursive` \
1597                         (placed spools + skipped child edges; no separate schema)",
1598                            "output_kind",
1599                            "clone_monorepo",
1600                        ),
1601                    ],
1602                ),
1603                "source_authority",
1604            ),
1605            220,
1606        ),
1607    ),
1608    entry(
1609        &["collapse"],
1610        category(opaque_schemas(REF_MUTATION, &["collapse"]), "states"),
1611    ),
1612    entry(
1613        &["commit"],
1614        exits(
1615            front_door(
1616                advertised_action(
1617                    surface(
1618                        json_discriminators(
1619                            documented_schemas(
1620                                CommandContract {
1621                                    writes_git_refs: true,
1622                                    writes_metadata: true,
1623                                    ..REF_MUTATION
1624                                },
1625                                &["commit"],
1626                            ),
1627                            &[json_discriminator(Some("commit"), "output_kind", "commit")],
1628                        ),
1629                        "source_authority",
1630                    ),
1631                    "heddle commit",
1632                    &["heddle", "commit"],
1633                    &[],
1634                    true,
1635                    true,
1636                ),
1637                28,
1638            ),
1639            &[
1640                (0, "Git checkpoint written or already current"),
1641                (65, "repository mode or worktree preflight refused"),
1642                (74, "io while writing Git state"),
1643            ],
1644        ),
1645    ),
1646    entry(
1647        &["expand"],
1648        category(
1649            json_discriminators(
1650                documented_schemas(READ_JSON, &["expand"]),
1651                &[json_discriminator(Some("expand"), "output_kind", "expand")],
1652            ),
1653            "states",
1654        ),
1655    ),
1656    entry(
1657        &["continue"],
1658        category(
1659            json_discriminators(
1660                documented_schemas(operator_envelope(compact_json(REF_MUTATION)), &["continue"]),
1661                &[json_discriminator(
1662                    Some("continue"),
1663                    "output_kind",
1664                    "continue",
1665                )],
1666            ),
1667            "recovery",
1668        ),
1669    ),
1670    entry(&["context"], category(GROUP, "collab")),
1671    entry(
1672        &["context", "set"],
1673        json_discriminators(
1674            opaque_schemas(REF_AND_METADATA_MUTATION, &["context set"]),
1675            &[json_discriminator(
1676                Some("context set"),
1677                "output_kind",
1678                "context_set",
1679            )],
1680        ),
1681    ),
1682    entry(
1683        &["context", "get"],
1684        json_discriminators(
1685            opaque_schemas(READ_JSON, &["context get"]),
1686            &[json_discriminator(
1687                Some("context get"),
1688                "output_kind",
1689                "context_get",
1690            )],
1691        ),
1692    ),
1693    entry(
1694        &["context", "list"],
1695        json_discriminators(
1696            opaque_schemas(READ_JSON, &["context list"]),
1697            &[json_discriminator(
1698                Some("context list"),
1699                "output_kind",
1700                "context_list",
1701            )],
1702        ),
1703    ),
1704    entry(
1705        &["context", "history"],
1706        json_discriminators(
1707            opaque_schemas(READ_JSON, &["context history"]),
1708            &[json_discriminator(
1709                Some("context history"),
1710                "output_kind",
1711                "context_history",
1712            )],
1713        ),
1714    ),
1715    entry(
1716        &["context", "edit"],
1717        json_discriminators(
1718            opaque_schemas(REF_AND_METADATA_MUTATION, &["context edit"]),
1719            &[json_discriminator(
1720                Some("context edit"),
1721                "output_kind",
1722                "context_edit",
1723            )],
1724        ),
1725    ),
1726    entry(
1727        &["context", "supersede"],
1728        json_discriminators(
1729            opaque_schemas(REF_AND_METADATA_MUTATION, &["context supersede"]),
1730            &[json_discriminator(
1731                Some("context supersede"),
1732                "output_kind",
1733                "context_supersede",
1734            )],
1735        ),
1736    ),
1737    entry(
1738        &["context", "rm"],
1739        json_discriminators(
1740            opaque_schemas(REF_AND_METADATA_MUTATION, &["context rm"]),
1741            &[json_discriminator(
1742                Some("context rm"),
1743                "output_kind",
1744                "context_rm",
1745            )],
1746        ),
1747    ),
1748    entry(
1749        &["context", "check"],
1750        json_discriminators(
1751            opaque_schemas(READ_JSON, &["context check"]),
1752            &[json_discriminator(
1753                Some("context check"),
1754                "output_kind",
1755                "context_check",
1756            )],
1757        ),
1758    ),
1759    entry(
1760        &["context", "suggest"],
1761        json_discriminators(
1762            opaque_schemas(READ_JSON, &["context suggest"]),
1763            &[json_discriminator(
1764                Some("context suggest"),
1765                "output_kind",
1766                "context_suggest",
1767            )],
1768        ),
1769    ),
1770    entry(
1771        &["context", "audit"],
1772        json_discriminators(
1773            opaque_schemas(READ_JSON, &["context audit"]),
1774            &[json_discriminator(
1775                Some("context audit"),
1776                "output_kind",
1777                "context_audit",
1778            )],
1779        ),
1780    ),
1781    #[cfg(all(feature = "git-overlay", feature = "ingest"))]
1782    entry(&["context", "reason"], category(GROUP, "context")),
1783    #[cfg(all(feature = "git-overlay", feature = "ingest"))]
1784    entry(
1785        &["context", "reason", "git"],
1786        surface(
1787            opaque_schemas(METADATA_MUTATION, &["context reason git"]),
1788            "git_projection",
1789        ),
1790    ),
1791    entry(&["daemon"], surface(GROUP, "admin")),
1792    entry(
1793        &["daemon", "serve"],
1794        surface(opaque_schemas(DAEMON_MUTATION, &["daemon serve"]), "admin"),
1795    ),
1796    entry(
1797        &["daemon", "status"],
1798        surface(opaque_schemas(READ_JSON, &["daemon status"]), "admin"),
1799    ),
1800    entry(
1801        &["daemon", "stop"],
1802        surface(
1803            json_discriminators(
1804                opaque_schemas(DAEMON_MUTATION, &["daemon stop"]),
1805                &[json_discriminator(
1806                    Some("daemon stop"),
1807                    "output_kind",
1808                    "daemon_stop",
1809                )],
1810            ),
1811            "admin",
1812        ),
1813    ),
1814    entry(
1815        &["diff"],
1816        front_door(
1817            documented_core_report_schema(READ_JSON, DiffReport::CONTRACT),
1818            20,
1819        ),
1820    ),
1821    entry(&["discuss"], category(GROUP, "collab")),
1822    entry(
1823        &["discuss", "open"],
1824        json_discriminators(
1825            documented_schemas(METADATA_MUTATION, &["discuss open"]),
1826            &[json_discriminator(
1827                Some("discuss open"),
1828                "output_kind",
1829                "discuss_open",
1830            )],
1831        ),
1832    ),
1833    entry(
1834        &["discuss", "append"],
1835        json_discriminators(
1836            documented_schemas(METADATA_MUTATION, &["discuss append"]),
1837            &[json_discriminator(
1838                Some("discuss append"),
1839                "output_kind",
1840                "discuss_append",
1841            )],
1842        ),
1843    ),
1844    entry(
1845        &["discuss", "resolve"],
1846        json_discriminators(
1847            documented_schemas(METADATA_MUTATION, &["discuss resolve"]),
1848            &[json_discriminator(
1849                Some("discuss resolve"),
1850                "output_kind",
1851                "discuss_resolve",
1852            )],
1853        ),
1854    ),
1855    entry(
1856        &["discuss", "reopen"],
1857        json_discriminators(
1858            documented_schemas(METADATA_MUTATION, &["discuss reopen"]),
1859            &[json_discriminator(
1860                Some("discuss reopen"),
1861                "output_kind",
1862                "discuss_reopen",
1863            )],
1864        ),
1865    ),
1866    entry(
1867        &["discuss", "list"],
1868        json_discriminators(
1869            documented_schemas(READ_JSON, &["discuss list"]),
1870            &[json_discriminator(
1871                Some("discuss list"),
1872                "output_kind",
1873                "discuss_list",
1874            )],
1875        ),
1876    ),
1877    entry(
1878        &["discuss", "show"],
1879        json_discriminators(
1880            documented_schemas(READ_JSON, &["discuss show"]),
1881            &[json_discriminator(
1882                Some("discuss show"),
1883                "output_kind",
1884                "discuss_show",
1885            )],
1886        ),
1887    ),
1888    entry(
1889        &["doctor"],
1890        front_door(
1891            json_discriminators(
1892                documented_schemas(READ_JSON, &["doctor"]),
1893                &[json_discriminator(Some("doctor"), "output_kind", "doctor")],
1894            ),
1895            120,
1896        ),
1897    ),
1898    entry(
1899        &["doctor", "docs"],
1900        json_discriminators(
1901            documented_schemas(READ_JSON, &["doctor docs"]),
1902            &[json_discriminator(
1903                Some("doctor docs"),
1904                "output_kind",
1905                "doctor_docs",
1906            )],
1907        ),
1908    ),
1909    entry(
1910        &["doctor", "schemas"],
1911        json_discriminators(
1912            documented_schemas(READ_JSON, &["doctor schemas"]),
1913            &[json_discriminator(
1914                Some("doctor schemas"),
1915                "output_kind",
1916                "doctor_schemas",
1917            )],
1918        ),
1919    ),
1920    entry(
1921        &["fsck"],
1922        category(
1923            documented_core_report_schema(READ_JSON, FsckReport::CONTRACT),
1924            "recovery",
1925        ),
1926    ),
1927    entry(&["fsck", "repair"], category(GROUP, "recovery")),
1928    entry(
1929        &["fsck", "repair", "git"],
1930        category(
1931            documented_schemas(
1932                documented_core_report_schema(FSCK_GIT_REPAIR, FsckReport::CONTRACT),
1933                &["fsck repair git"],
1934            ),
1935            "recovery",
1936        ),
1937    ),
1938    entry(&["oplog"], category(GROUP, "recovery")),
1939    entry(
1940        &["oplog", "recover"],
1941        category(
1942            json_discriminators(
1943                opaque_schemas(METADATA_MUTATION_NO_OP_ID, &["oplog recover"]),
1944                &[json_discriminator(
1945                    Some("oplog recover"),
1946                    "output_kind",
1947                    "oplog_recover",
1948                )],
1949            ),
1950            "recovery",
1951        ),
1952    ),
1953    entry(
1954        &["help"],
1955        category(
1956            json_discriminators(
1957                opaque_schemas(READ_JSON, &["help"]),
1958                &[json_discriminator(Some("help"), "kind", "command_catalog")],
1959            ),
1960            "repo",
1961        ),
1962    ),
1963    entry(&["hook"], surface(GROUP, "automation")),
1964    entry(
1965        &["hook", "list"],
1966        surface(opaque_schemas(READ_JSON, &["hook list"]), "automation"),
1967    ),
1968    entry(
1969        &["hook", "install"],
1970        surface(
1971            opaque_schemas(HOOK_MUTATION, &["hook install"]),
1972            "automation",
1973        ),
1974    ),
1975    entry(
1976        &["hook", "uninstall"],
1977        surface(
1978            opaque_schemas(HOOK_MUTATION, &["hook uninstall"]),
1979            "automation",
1980        ),
1981    ),
1982    entry(
1983        &["hook", "events"],
1984        surface(opaque_schemas(READ_JSON, &["hook events"]), "automation"),
1985    ),
1986    entry(
1987        &["init"],
1988        exits(
1989            front_door(
1990                json_discriminators(
1991                    documented_schemas(INIT, &["init"]),
1992                    &[json_discriminator(Some("init"), "output_kind", "init")],
1993                ),
1994                200,
1995            ),
1996            &[
1997                (0, "ok"),
1998                (73, "cannot create state directory"),
1999                (78, "workspace config invalid"),
2000            ],
2001        ),
2002    ),
2003    entry(&["integration"], surface(GROUP, "admin")),
2004    entry(
2005        &["integration", "list"],
2006        surface(
2007            documented_schemas(READ_JSON, &["integration list"]),
2008            "admin",
2009        ),
2010    ),
2011    entry(
2012        &["integration", "install"],
2013        surface(
2014            opaque_schemas(INTEGRATION_INSTALL_MUTATION, &["integration install"]),
2015            "admin",
2016        ),
2017    ),
2018    entry(
2019        &["integration", "doctor"],
2020        surface(
2021            documented_schemas(READ_JSON, &["integration doctor"]),
2022            "admin",
2023        ),
2024    ),
2025    entry(
2026        &["integration", "uninstall"],
2027        surface(
2028            opaque_schemas(INTEGRATION_INSTALL_MUTATION, &["integration uninstall"]),
2029            "admin",
2030        ),
2031    ),
2032    entry(
2033        &["integration", "upgrade"],
2034        surface(
2035            opaque_schemas(INTEGRATION_INSTALL_MUTATION, &["integration upgrade"]),
2036            "admin",
2037        ),
2038    ),
2039    entry(
2040        &["integration", "relay"],
2041        hidden(surface(
2042            opaque_schemas(REF_METADATA_WORKTREE_MUTATION, &["integration relay"]),
2043            "admin",
2044        )),
2045    ),
2046    entry(
2047        &["log"],
2048        front_door(
2049            json_discriminators(
2050                documented_schemas(READ_JSON, &["log", "log --reflog", "log --timeline"]),
2051                &[
2052                    json_discriminator(Some("log"), "output_kind", "log"),
2053                    json_discriminator(Some("log --reflog"), "output_kind", "log_reflog"),
2054                    json_discriminator(Some("log --timeline"), "output_kind", "timeline_log"),
2055                ],
2056            ),
2057            130,
2058        ),
2059    ),
2060    entry(&["maintenance"], surface(GROUP, "admin")),
2061    entry(
2062        &["maintenance", "inspect"],
2063        surface(
2064            json_discriminators(
2065                documented_schemas(READ_JSON, &["maintenance inspect"]),
2066                &[json_discriminator(
2067                    Some("maintenance inspect"),
2068                    "output_kind",
2069                    "maintenance_inspect",
2070                )],
2071            ),
2072            "admin",
2073        ),
2074    ),
2075    entry(
2076        &["maintenance", "refresh"],
2077        surface(
2078            json_discriminators(
2079                documented_schemas(
2080                    CommandContract {
2081                        object_gc: true,
2082                        ..METADATA_MUTATION
2083                    },
2084                    &["maintenance refresh"],
2085                ),
2086                &[json_discriminator(
2087                    Some("maintenance refresh"),
2088                    "output_kind",
2089                    "maintenance_refresh",
2090                )],
2091            ),
2092            "admin",
2093        ),
2094    ),
2095    entry(
2096        &["maintenance", "gc"],
2097        surface(
2098            json_discriminators(
2099                opaque_schemas(GC_MUTATION, &["maintenance gc"]),
2100                &[json_discriminator(
2101                    Some("maintenance gc"),
2102                    "output_kind",
2103                    "gc",
2104                )],
2105            ),
2106            "admin",
2107        ),
2108    ),
2109    entry(
2110        &["pull"],
2111        exits(
2112            front_door(
2113                json_discriminators(
2114                    surface(
2115                        documented_schemas(
2116                            CommandContract {
2117                                may_import_git: true,
2118                                writes_git_refs: true,
2119                                network_io: true,
2120                                ..WORKTREE_MUTATION
2121                            },
2122                            &["pull"],
2123                        ),
2124                        "source_authority",
2125                    ),
2126                    &[json_discriminator(Some("pull"), "output_kind", "pull")],
2127                ),
2128                90,
2129            ),
2130            &[
2131                (0, "ok"),
2132                (75, "remote unreachable; safe to retry"),
2133                (76, "upstream protocol error"),
2134                (78, "no upstream configured"),
2135            ],
2136        ),
2137    ),
2138    entry(
2139        &["push"],
2140        exits(
2141            front_door(
2142                advertised_action(
2143                    json_discriminators(
2144                        surface(
2145                            documented_schemas(
2146                                CommandContract {
2147                                    writes_git_refs: true,
2148                                    writes_config: true,
2149                                    network_io: true,
2150                                    ..REF_MUTATION
2151                                },
2152                                &["push"],
2153                            ),
2154                            "source_authority",
2155                        ),
2156                        &[json_discriminator(Some("push"), "output_kind", "push")],
2157                    ),
2158                    "heddle push",
2159                    &["heddle", "push"],
2160                    &[],
2161                    true,
2162                    true,
2163                ),
2164                80,
2165            ),
2166            &[
2167                (0, "ok"),
2168                (75, "remote unreachable; safe to retry"),
2169                (
2170                    76,
2171                    "remote rejected payload; do not retry without changing inputs",
2172                ),
2173                (78, "no upstream configured"),
2174            ],
2175        ),
2176    ),
2177    entry(
2178        &["query"],
2179        category(
2180            json_discriminators(
2181                documented_schemas(
2182                    documented_core_report_schema(READ_JSON, QueryReport::CONTRACT),
2183                    QUERY_ATTRIBUTION_SCHEMA_VERBS,
2184                ),
2185                QUERY_JSON_DISCRIMINATORS,
2186            ),
2187            "states",
2188        ),
2189    ),
2190    entry(
2191        &["ready"],
2192        front_door(
2193            json_discriminators(
2194                documented_schemas(compact_json(CAPTURE), &["ready"]),
2195                &[json_discriminator(Some("ready"), "output_kind", "ready")],
2196            ),
2197            50,
2198        ),
2199    ),
2200    entry(&["redact"], category(GROUP, "recovery")),
2201    entry(
2202        &["redact", "apply"],
2203        json_discriminators(
2204            opaque_schemas(METADATA_MUTATION, &["redact apply"]),
2205            &[json_discriminator(
2206                Some("redact apply"),
2207                "output_kind",
2208                "redact_apply",
2209            )],
2210        ),
2211    ),
2212    entry(
2213        &["redact", "list"],
2214        json_discriminators(
2215            opaque_schemas(READ_JSON, &["redact list"]),
2216            &[json_discriminator(
2217                Some("redact list"),
2218                "output_kind",
2219                "redact_list",
2220            )],
2221        ),
2222    ),
2223    entry(
2224        &["redact", "show"],
2225        json_discriminators(
2226            opaque_schemas(READ_JSON, &["redact show"]),
2227            &[json_discriminator(
2228                Some("redact show"),
2229                "output_kind",
2230                "redact_show",
2231            )],
2232        ),
2233    ),
2234    entry(&["redact", "purge"], GROUP),
2235    entry(
2236        &["redact", "purge", "apply"],
2237        json_discriminators(
2238            opaque_schemas(
2239                CommandContract {
2240                    destructive_requires_force: true,
2241                    ..DESTRUCTIVE_DATA_MUTATION
2242                },
2243                &["redact purge apply"],
2244            ),
2245            &[json_discriminator(
2246                Some("redact purge apply"),
2247                "output_kind",
2248                "purge_apply",
2249            )],
2250        ),
2251    ),
2252    entry(
2253        &["redact", "purge", "list"],
2254        json_discriminators(
2255            opaque_schemas(READ_JSON, &["redact purge list"]),
2256            &[json_discriminator(
2257                Some("redact purge list"),
2258                "output_kind",
2259                "purge_list",
2260            )],
2261        ),
2262    ),
2263    entry(&["redact", "trust"], GROUP),
2264    entry(
2265        &["redact", "trust", "add"],
2266        json_discriminators(
2267            opaque_schemas(CONFIG_MUTATION, &["redact trust add"]),
2268            &[json_discriminator(
2269                Some("redact trust add"),
2270                "output_kind",
2271                "redact_trust_add",
2272            )],
2273        ),
2274    ),
2275    entry(
2276        &["redact", "trust", "list"],
2277        json_discriminators(
2278            opaque_schemas(READ_JSON, &["redact trust list"]),
2279            &[json_discriminator(
2280                Some("redact trust list"),
2281                "output_kind",
2282                "redact_trust_list",
2283            )],
2284        ),
2285    ),
2286    entry(
2287        &["redact", "trust", "remove"],
2288        json_discriminators(
2289            opaque_schemas(CONFIG_MUTATION, &["redact trust remove"]),
2290            &[json_discriminator(
2291                Some("redact trust remove"),
2292                "output_kind",
2293                "redact_trust_remove",
2294            )],
2295        ),
2296    ),
2297    entry(
2298        &["remote"],
2299        category(surface(GROUP, "source_authority"), "repo"),
2300    ),
2301    entry(
2302        &["remote", "list"],
2303        surface(
2304            json_discriminators(
2305                documented_schemas(READ_JSON, &["remote list"]),
2306                &[json_discriminator(
2307                    Some("remote list"),
2308                    "output_kind",
2309                    "remote_list",
2310                )],
2311            ),
2312            "source_authority",
2313        ),
2314    ),
2315    entry(
2316        &["remote", "add"],
2317        surface(
2318            json_discriminators(
2319                documented_schemas(CONFIG_MUTATION, &["remote add"]),
2320                &[json_discriminator(
2321                    Some("remote add"),
2322                    "output_kind",
2323                    "remote_add",
2324                )],
2325            ),
2326            "source_authority",
2327        ),
2328    ),
2329    entry(
2330        &["remote", "remove"],
2331        surface(
2332            json_discriminators(
2333                documented_schemas(CONFIG_MUTATION, &["remote remove"]),
2334                &[json_discriminator(
2335                    Some("remote remove"),
2336                    "output_kind",
2337                    "remote_remove",
2338                )],
2339            ),
2340            "source_authority",
2341        ),
2342    ),
2343    entry(
2344        &["remote", "set-default"],
2345        surface(
2346            json_discriminators(
2347                documented_schemas(CONFIG_MUTATION, &["remote set-default"]),
2348                &[json_discriminator(
2349                    Some("remote set-default"),
2350                    "output_kind",
2351                    "remote_set_default",
2352                )],
2353            ),
2354            "source_authority",
2355        ),
2356    ),
2357    entry(
2358        &["remote", "show"],
2359        surface(
2360            json_discriminators(
2361                documented_schemas(READ_JSON, &["remote show"]),
2362                &[json_discriminator(
2363                    Some("remote show"),
2364                    "output_kind",
2365                    "remote_show",
2366                )],
2367            ),
2368            "source_authority",
2369        ),
2370    ),
2371    entry(
2372        &["resolve"],
2373        front_door(
2374            json_discriminators(
2375                documented_schemas(REF_MUTATION, &["resolve"]),
2376                &[json_discriminator(
2377                    Some("resolve"),
2378                    "output_kind",
2379                    "resolve",
2380                )],
2381            ),
2382            300,
2383        ),
2384    ),
2385    entry(
2386        &["retro"],
2387        category(documented_schemas(READ_JSON, &["retro"]), "states"),
2388    ),
2389    entry(
2390        &["revert"],
2391        category(
2392            json_discriminators(
2393                documented_schemas(WORKTREE_MUTATION, &["revert"]),
2394                &[json_discriminator(Some("revert"), "output_kind", "revert")],
2395            ),
2396            "states",
2397        ),
2398    ),
2399    entry(&["review"], category(GROUP, "collab")),
2400    entry(
2401        &["review", "show"],
2402        json_discriminators(
2403            documented_schemas(READ_JSON, &["review show"]),
2404            &[json_discriminator(
2405                Some("review show"),
2406                "output_kind",
2407                "review_show",
2408            )],
2409        ),
2410    ),
2411    entry(
2412        &["review", "sign"],
2413        json_discriminators(
2414            documented_schemas(METADATA_MUTATION, &["review sign"]),
2415            &[json_discriminator(
2416                Some("review sign"),
2417                "output_kind",
2418                "review_sign",
2419            )],
2420        ),
2421    ),
2422    entry(
2423        &["review", "next"],
2424        json_discriminators(
2425            documented_schemas(READ_JSON, &["review next"]),
2426            &[json_discriminator(
2427                Some("review next"),
2428                "output_kind",
2429                "review_next",
2430            )],
2431        ),
2432    ),
2433    entry(
2434        &["review", "health"],
2435        json_discriminators(
2436            documented_schemas(READ_JSON, &["review health"]),
2437            &[json_discriminator(
2438                Some("review health"),
2439                "output_kind",
2440                "review_health",
2441            )],
2442        ),
2443    ),
2444    entry(&["run"], surface(EXTERNAL_WORKTREE_COMMAND, "automation")),
2445    entry(
2446        &["schemas"],
2447        surface(
2448            json_discriminators(
2449                documented_schemas(READ_JSON, &["schemas"]),
2450                &[json_discriminator(
2451                    Some("schemas"),
2452                    "output_kind",
2453                    "schemas",
2454                )],
2455            ),
2456            "automation",
2457        ),
2458    ),
2459    entry(&["semantic"], category(GROUP, "states")),
2460    entry(
2461        &["semantic", "hot"],
2462        opaque_schemas(READ_JSON, &["semantic hot"]),
2463    ),
2464    // `semantic index` backfills the merkle semantic index, attaching a
2465    // SemanticIndex sidecar to states that lack one. Metadata mutation, text
2466    // progress output, no op-id (idempotent + restartable).
2467    entry(
2468        &["semantic", "index"],
2469        CommandContract {
2470            supports_json: false,
2471            supports_op_id: false,
2472            json_kind: "none",
2473            writes_metadata: true,
2474            ..MUTATION_BASE
2475        },
2476    ),
2477    entry(&["shell"], category(READ_TEXT, "repo")),
2478    entry(&["shell", "init"], READ_TEXT),
2479    entry(&["shell", "completion"], READ_TEXT),
2480    entry(&["shell", "prompt"], READ_TEXT),
2481    entry(&["complete"], hidden(READ_TEXT)),
2482    entry(
2483        &["land"],
2484        front_door(
2485            json_discriminators(
2486                documented_schemas(
2487                    CommandContract {
2488                        writes_git_refs: true,
2489                        network_io: true,
2490                        ..compact_json(REF_MUTATION)
2491                    },
2492                    &["land", "land --threads"],
2493                ),
2494                &[
2495                    json_discriminator(Some("land"), "output_kind", "land"),
2496                    json_discriminator(Some("land --threads"), "output_kind", "land_batch"),
2497                ],
2498            ),
2499            70,
2500        ),
2501    ),
2502    entry(
2503        &["show"],
2504        front_door(
2505            json_discriminators(
2506                documented_schemas(READ_JSON, &["show"]),
2507                &[json_discriminator(Some("show"), "output_kind", "show")],
2508            ),
2509            140,
2510        ),
2511    ),
2512    entry(
2513        &["start"],
2514        front_door(
2515            json_discriminators(
2516                documented_schemas(WORKTREE_MUTATION, &["start"]),
2517                &[json_discriminator(
2518                    Some("start"),
2519                    "output_kind",
2520                    "thread_start",
2521                )],
2522            ),
2523            40,
2524        ),
2525    ),
2526    entry(
2527        &["status"],
2528        exits(
2529            front_door(
2530                documented_core_report_schema(
2531                    compact_json(READ_JSON_OR_JSONL),
2532                    StatusReport::CONTRACT,
2533                ),
2534                10,
2535            ),
2536            &[(0, "ok"), (74, "io reading workspace state")],
2537        ),
2538    ),
2539    entry(
2540        &["sync"],
2541        category(
2542            json_discriminators(
2543                documented_schemas(operator_envelope(compact_json(REF_MUTATION)), &["sync"]),
2544                &[json_discriminator(Some("sync"), "output_kind", "sync")],
2545            ),
2546            "threads",
2547        ),
2548    ),
2549    entry(&["thread"], category(surface(GROUP, "native"), "threads")),
2550    entry(
2551        &["thread", "create"],
2552        json_discriminators(
2553            documented_schemas(REF_MUTATION, &["thread create"]),
2554            &[json_discriminator(
2555                Some("thread create"),
2556                "output_kind",
2557                "thread_create",
2558            )],
2559        ),
2560    ),
2561    entry(
2562        &["thread", "current"],
2563        documented_schemas(READ_JSON, &["thread current"]),
2564    ),
2565    entry(
2566        &["thread", "switch"],
2567        json_discriminators(
2568            documented_schemas(WORKTREE_MUTATION, &["thread switch"]),
2569            &[json_discriminator(
2570                Some("thread switch"),
2571                "output_kind",
2572                "thread_switch",
2573            )],
2574        ),
2575    ),
2576    entry(&["thread", "cd"], READ_TEXT),
2577    entry(
2578        &["thread", "list"],
2579        json_discriminators(
2580            documented_schemas(READ_JSON, &["thread list"]),
2581            &[json_discriminator(
2582                Some("thread list"),
2583                "output_kind",
2584                "thread_list",
2585            )],
2586        ),
2587    ),
2588    entry(
2589        &["thread", "show"],
2590        json_discriminators(
2591            documented_schemas(READ_JSON_OR_JSONL, &["thread show"]),
2592            &[json_discriminator(
2593                Some("thread show"),
2594                "output_kind",
2595                "thread_show",
2596            )],
2597        ),
2598    ),
2599    entry(
2600        &["thread", "captures"],
2601        documented_schemas(READ_JSON, &["thread captures"]),
2602    ),
2603    entry(
2604        &["thread", "rename"],
2605        json_discriminators(
2606            documented_schemas(REF_MUTATION, &["thread rename"]),
2607            &[json_discriminator(
2608                Some("thread rename"),
2609                "output_kind",
2610                "thread_rename",
2611            )],
2612        ),
2613    ),
2614    entry(
2615        &["thread", "refresh"],
2616        json_discriminators(
2617            documented_schemas(WORKTREE_MUTATION, &["thread refresh"]),
2618            &[json_discriminator(
2619                Some("thread refresh"),
2620                "output_kind",
2621                "thread_refresh",
2622            )],
2623        ),
2624    ),
2625    entry(
2626        &["thread", "move"],
2627        documented_schemas(REF_MUTATION, &["thread move"]),
2628    ),
2629    entry(
2630        &["thread", "absorb"],
2631        documented_schemas(REF_MUTATION, &["thread absorb"]),
2632    ),
2633    entry(
2634        &["thread", "resolve"],
2635        json_discriminators(
2636            documented_schemas(REF_MUTATION, &["thread resolve"]),
2637            &[json_discriminator(
2638                Some("thread resolve"),
2639                "output_kind",
2640                "thread_resolve",
2641            )],
2642        ),
2643    ),
2644    entry(
2645        &["thread", "promote"],
2646        json_discriminators(
2647            documented_schemas(WORKTREE_MUTATION, &["thread promote"]),
2648            &[json_discriminator(
2649                Some("thread promote"),
2650                "output_kind",
2651                "thread_promote",
2652            )],
2653        ),
2654    ),
2655    entry(
2656        &["thread", "drop"],
2657        json_discriminators(
2658            documented_schemas(DESTRUCTIVE_WORKTREE_MUTATION, &["thread drop"]),
2659            &[json_discriminator(
2660                Some("thread drop"),
2661                "output_kind",
2662                "thread_drop",
2663            )],
2664        ),
2665    ),
2666    entry(
2667        &["thread", "approve"],
2668        documented_schemas(NETWORK_METADATA_MUTATION, &["thread approve"]),
2669    ),
2670    entry(
2671        &["thread", "approvals"],
2672        documented_schemas(READ_JSON, &["thread approvals"]),
2673    ),
2674    entry(
2675        &["thread", "revoke-approval"],
2676        json_discriminators(
2677            documented_schemas(NETWORK_METADATA_MUTATION, &["thread revoke-approval"]),
2678            &[json_discriminator(
2679                Some("thread revoke-approval"),
2680                "output_kind",
2681                "thread_revoke_approval",
2682            )],
2683        ),
2684    ),
2685    entry(
2686        &["thread", "check-merge"],
2687        documented_schemas(READ_JSON, &["thread check-merge"]),
2688    ),
2689    entry(
2690        &["thread", "cleanup"],
2691        json_discriminators(
2692            documented_schemas(DESTRUCTIVE_WORKTREE_MUTATION, &["thread cleanup"]),
2693            &[json_discriminator(
2694                Some("thread cleanup"),
2695                "output_kind",
2696                "thread_cleanup",
2697            )],
2698        ),
2699    ),
2700    entry(&["thread", "marker"], GROUP),
2701    entry(
2702        &["thread", "marker", "list"],
2703        json_discriminators(
2704            documented_schemas(READ_JSON, &["thread marker list"]),
2705            &[json_discriminator(
2706                Some("thread marker list"),
2707                "output_kind",
2708                "thread_marker_list",
2709            )],
2710        ),
2711    ),
2712    entry(
2713        &["thread", "marker", "create"],
2714        json_discriminators(
2715            documented_schemas(REF_MUTATION, &["thread marker create"]),
2716            &[json_discriminator(
2717                Some("thread marker create"),
2718                "output_kind",
2719                "thread_marker_create",
2720            )],
2721        ),
2722    ),
2723    entry(
2724        &["thread", "marker", "delete"],
2725        json_discriminators(
2726            documented_schemas(DESTRUCTIVE_REF_MUTATION, &["thread marker delete"]),
2727            &[json_discriminator(
2728                Some("thread marker delete"),
2729                "output_kind",
2730                "thread_marker_delete",
2731            )],
2732        ),
2733    ),
2734    entry(
2735        &["thread", "marker", "show"],
2736        json_discriminators(
2737            documented_schemas(READ_JSON, &["thread marker show"]),
2738            &[json_discriminator(
2739                Some("thread marker show"),
2740                "output_kind",
2741                "thread_marker_show",
2742            )],
2743        ),
2744    ),
2745    entry(&["timeline"], surface(GROUP, "automation")),
2746    entry(
2747        &["timeline", "status"],
2748        surface(
2749            json_discriminators(
2750                documented_schemas(READ_JSON, &["timeline status"]),
2751                &[json_discriminator(
2752                    Some("timeline status"),
2753                    "output_kind",
2754                    "timeline_status",
2755                )],
2756            ),
2757            "automation",
2758        ),
2759    ),
2760    entry(
2761        &["timeline", "record-start"],
2762        surface(
2763            json_discriminators(
2764                documented_schemas(METADATA_MUTATION, &["timeline record-start"]),
2765                &[json_discriminator(
2766                    Some("timeline record-start"),
2767                    "output_kind",
2768                    "timeline_record_start",
2769                )],
2770            ),
2771            "automation",
2772        ),
2773    ),
2774    entry(
2775        &["timeline", "record-finish"],
2776        surface(
2777            json_discriminators(
2778                documented_schemas(METADATA_MUTATION, &["timeline record-finish"]),
2779                &[json_discriminator(
2780                    Some("timeline record-finish"),
2781                    "output_kind",
2782                    "timeline_record_finish",
2783                )],
2784            ),
2785            "automation",
2786        ),
2787    ),
2788    entry(
2789        &["timeline", "fork"],
2790        surface(
2791            json_discriminators(
2792                documented_schemas(METADATA_MUTATION, &["timeline fork"]),
2793                &[json_discriminator(
2794                    Some("timeline fork"),
2795                    "output_kind",
2796                    "timeline_action",
2797                )],
2798            ),
2799            "automation",
2800        ),
2801    ),
2802    entry(
2803        &["timeline", "reset"],
2804        surface(
2805            json_discriminators(
2806                documented_schemas(REF_METADATA_WORKTREE_MUTATION, &["timeline reset"]),
2807                &[json_discriminator(
2808                    Some("timeline reset"),
2809                    "output_kind",
2810                    "timeline_action",
2811                )],
2812            ),
2813            "automation",
2814        ),
2815    ),
2816    entry(
2817        &["timeline", "recover"],
2818        surface(
2819            json_discriminators(
2820                documented_schemas(METADATA_MUTATION, &["timeline recover"]),
2821                &[json_discriminator(
2822                    Some("timeline recover"),
2823                    "output_kind",
2824                    "timeline_action",
2825                )],
2826            ),
2827            "automation",
2828        ),
2829    ),
2830    entry(
2831        &["verify"],
2832        exits(
2833            front_door(
2834                documented_core_report_schema(READ_JSON, VerifyReport::CONTRACT),
2835                110,
2836            ),
2837            &[
2838                (0, "verified clean"),
2839                (65, "verification reports blocked state"),
2840                (74, "io reading state"),
2841            ],
2842        ),
2843    ),
2844    entry(&["visibility"], category(GROUP, "collab")),
2845    entry(
2846        &["visibility", "set"],
2847        json_discriminators(
2848            opaque_schemas(METADATA_MUTATION, &["visibility set"]),
2849            &[json_discriminator(
2850                Some("visibility set"),
2851                "output_kind",
2852                "visibility_set",
2853            )],
2854        ),
2855    ),
2856    entry(
2857        &["visibility", "promote"],
2858        json_discriminators(
2859            opaque_schemas(METADATA_MUTATION, &["visibility promote"]),
2860            &[json_discriminator(
2861                Some("visibility promote"),
2862                "output_kind",
2863                "visibility_promote",
2864            )],
2865        ),
2866    ),
2867    entry(
2868        &["visibility", "show"],
2869        json_discriminators(
2870            opaque_schemas(READ_JSON, &["visibility show"]),
2871            &[json_discriminator(
2872                Some("visibility show"),
2873                "output_kind",
2874                "visibility_show",
2875            )],
2876        ),
2877    ),
2878    entry(
2879        &["visibility", "list"],
2880        json_discriminators(
2881            opaque_schemas(READ_JSON, &["visibility list"]),
2882            &[json_discriminator(
2883                Some("visibility list"),
2884                "output_kind",
2885                "visibility_list",
2886            )],
2887        ),
2888    ),
2889    entry(
2890        &["try"],
2891        category(
2892            documented_schemas(EXTERNAL_WORKTREE_MUTATION, &["try"]),
2893            "threads",
2894        ),
2895    ),
2896    entry(
2897        &["undo"],
2898        front_door(
2899            json_discriminators(
2900                // `undo` keeps its own history, redo, and recovery modes.
2901                // Every kind the handler can emit must be advertised or an agent
2902                // validating responses via `heddle help --output json` rejects
2903                // the off-contract record. `undo --list` has its own
2904                // `UndoListSchema`.
2905                documented_schemas(
2906                    WORKTREE_MUTATION,
2907                    &["undo", "undo --list", "undo --redo", "undo --recover"],
2908                ),
2909                &[
2910                    json_discriminator(Some("undo"), "output_kind", "undo"),
2911                    json_discriminator(Some("undo --list"), "output_kind", "undo_list"),
2912                    json_discriminator(Some("undo --redo"), "output_kind", "redo"),
2913                    json_discriminator(Some("undo --recover"), "output_kind", "undo_recover"),
2914                ],
2915            ),
2916            100,
2917        ),
2918    ),
2919    entry(
2920        &["watch"],
2921        surface(documented_schemas(READ_JSONL, &["watch"]), "automation"),
2922    ),
2923];
2924
2925static ACTIVE_COMMAND_CONTRACT_ENTRIES: OnceLock<Vec<&'static CommandContractEntry>> =
2926    OnceLock::new();
2927
2928static ADVERTISED_COMMAND_CONTRACT_ENTRIES: OnceLock<Vec<&'static CommandContractEntry>> =
2929    OnceLock::new();
2930
2931const fn entry(path: &'static [&'static str], contract: CommandContract) -> CommandContractEntry {
2932    CommandContractEntry { path, contract }
2933}
2934
2935pub fn build_command_catalog() -> CommandCatalogOutput {
2936    debug_assert!(
2937        RECOMMENDED_ACTION_PLACEHOLDERS
2938            .iter()
2939            .all(|action| validate_recommended_action(action).is_ok())
2940    );
2941
2942    let command = Cli::command();
2943    let mut global_options: Vec<_> = command
2944        .get_arguments()
2945        .filter(|arg| arg.is_global_set())
2946        .map(catalog_option)
2947        .collect();
2948    if !command.is_disable_help_flag_set() {
2949        global_options.push(generated_help_option());
2950    }
2951
2952    let mut commands = Vec::new();
2953    let op_id_option = command
2954        .get_arguments()
2955        .find(|arg| arg.get_long() == Some("op-id"))
2956        .map(catalog_option);
2957    walk_commands(&command, &mut Vec::new(), &mut commands, &op_id_option);
2958    append_feature_gated_command_entries(&command, &mut commands, &op_id_option);
2959    CommandCatalogOutput {
2960        kind: "command_catalog".to_string(),
2961        executable_path: heddle_argv0(),
2962        commands,
2963        global_options,
2964        json_discriminators: command_json_discriminators(),
2965        recommended_action_placeholders: RECOMMENDED_ACTION_PLACEHOLDERS
2966            .iter()
2967            .map(|action| (*action).to_string())
2968            .collect(),
2969        recommended_action_templates: RECOMMENDED_ACTION_TEMPLATES
2970            .iter()
2971            .map(|(action, argv_template, required_inputs, agent_may_fill)| {
2972                action_template_from_parts(action, argv_template, required_inputs, *agent_may_fill)
2973            })
2974            .collect(),
2975    }
2976}
2977
2978pub(crate) fn heddle_argv0() -> String {
2979    match std::env::current_exe() {
2980        Ok(path) => {
2981            let file_name = path.file_name().and_then(|name| name.to_str());
2982            if matches!(file_name, Some("heddle") | Some("heddle.exe")) {
2983                path.display().to_string()
2984            } else {
2985                "heddle".to_string()
2986            }
2987        }
2988        Err(_) => "heddle".to_string(),
2989    }
2990}
2991
2992pub(crate) fn normalize_heddle_argv(mut argv: Vec<String>) -> Vec<String> {
2993    if argv.first().is_some_and(|first| first == "heddle") {
2994        argv[0] = heddle_argv0();
2995    }
2996    argv
2997}
2998
2999fn walk_commands(
3000    command: &clap::Command,
3001    prefix: &mut Vec<String>,
3002    out: &mut Vec<CommandCatalogEntry>,
3003    op_id_option: &Option<CommandCatalogOption>,
3004) {
3005    for subcommand in command.get_subcommands() {
3006        prefix.push(subcommand.get_name().to_string());
3007        out.push(catalog_entry(subcommand, prefix, op_id_option));
3008        walk_commands(subcommand, prefix, out, op_id_option);
3009        prefix.pop();
3010    }
3011}
3012
3013/// Append catalog entries for `feature_gated` contracts whose clap
3014/// subcommand is compiled out of THIS build (e.g. the `client`-gated
3015/// `auth`/`support`/`presence` surfaces in a default `cargo install`).
3016///
3017/// `walk_commands` only sees the live clap tree, so without this the
3018/// hosted verbs would be missing from the catalog `commands` list even
3019/// though their contract (schema verbs, `output_kind` discriminators)
3020/// is advertised — breaking the wire-format-stable promise agents read.
3021/// Entries already produced by the walk (the feature IS on) are skipped.
3022fn append_feature_gated_command_entries(
3023    root: &clap::Command,
3024    out: &mut Vec<CommandCatalogEntry>,
3025    op_id_option: &Option<CommandCatalogOption>,
3026) {
3027    let present: std::collections::BTreeSet<Vec<String>> =
3028        out.iter().map(|entry| entry.path.clone()).collect();
3029    for entry in CONTRACTS.iter() {
3030        if entry.contract.feature_gate.is_none() {
3031            continue;
3032        }
3033        let owned_path: Vec<String> = entry.path.iter().map(|s| (*s).to_string()).collect();
3034        if present.contains(&owned_path) {
3035            continue;
3036        }
3037        // Defensive: never synthesize an entry that the clap tree
3038        // actually carries (the feature is enabled in this build).
3039        if clap_command_path_exists(root, entry.path) {
3040            continue;
3041        }
3042        out.push(feature_gated_catalog_entry(
3043            &owned_path,
3044            entry.contract,
3045            op_id_option,
3046        ));
3047    }
3048}
3049
3050/// Build a [`CommandCatalogEntry`] for a contract with no live clap node.
3051/// Clap-derived fields (aliases, summary, options, arguments,
3052/// subcommand-ness) are empty/false; every behavioral field comes from
3053/// the contract, identically to [`catalog_entry`].
3054fn feature_gated_catalog_entry(
3055    path: &[String],
3056    contract: CommandContract,
3057    op_id_option: &Option<CommandCatalogOption>,
3058) -> CommandCatalogEntry {
3059    let mut options = Vec::new();
3060    if contract.supports_op_id
3061        && let Some(op_id_option) = op_id_option
3062    {
3063        options.push(op_id_option.clone());
3064    }
3065    CommandCatalogEntry {
3066        path: path.to_vec(),
3067        display: path.join(" "),
3068        aliases: Vec::new(),
3069        tier: help_visibility_to_tier(contract.help_visibility).to_string(),
3070        surface: contract.surface.to_string(),
3071        help_visibility: contract.help_visibility.to_string(),
3072        help_rank: contract.help_rank,
3073        canonical_command: contract
3074            .canonical_command
3075            .map(std::string::ToString::to_string),
3076        canonical_action: canonical_action(contract),
3077        command_action: contract
3078            .advertised_action
3079            .map(command_action_from_advertised),
3080        summary: String::new(),
3081        has_subcommands: false,
3082        supports_json: contract.supports_json,
3083        output_modes: supported_output_modes(contract),
3084        mutates: contract.mutates,
3085        supports_op_id: contract.supports_op_id,
3086        persists_op_id: contract.persists_op_id,
3087        op_id_behavior: op_id_behavior(contract).to_string(),
3088        op_id_store_scope: op_id_store_scope(contract).to_string(),
3089        observe_only: contract.observe_only,
3090        may_initialize: contract.may_initialize,
3091        may_import_git: contract.may_import_git,
3092        may_write_worktree: contract.may_write_worktree,
3093        may_move_ref: contract.may_move_ref,
3094        destructive_requires_force: contract.destructive_requires_force,
3095        writes_heddle_refs: contract.writes_heddle_refs,
3096        writes_git_refs: contract.writes_git_refs,
3097        writes_worktree: contract.writes_worktree,
3098        writes_config: contract.writes_config,
3099        writes_hooks: contract.writes_hooks,
3100        network_io: contract.network_io,
3101        daemon_process: contract.daemon_process,
3102        object_gc: contract.object_gc,
3103        external_command: contract.external_command,
3104        requires_git_executable: contract.requires_git_executable,
3105        destructive_data: contract.destructive_data,
3106        side_effects: side_effects(contract),
3107        side_effect_class: side_effect_class(contract).to_string(),
3108        first_run_behavior: first_run_behavior(contract).to_string(),
3109        json_kind: contract.json_kind.to_string(),
3110        json_discriminators: json_discriminators_for_path(path.iter().map(String::as_str)),
3111        schema_verbs: contract_schema_verbs(contract)
3112            .map(str::to_string)
3113            .collect(),
3114        documented_schema_verbs: contract_documented_schema_verbs(contract)
3115            .map(str::to_string)
3116            .collect(),
3117        options,
3118        arguments: Vec::new(),
3119        exit_codes: contract
3120            .exit_codes
3121            .iter()
3122            .map(|(code, reason)| CommandCatalogExitCode {
3123                code: *code,
3124                reason: (*reason).to_string(),
3125            })
3126            .collect(),
3127    }
3128}
3129
3130fn catalog_entry(
3131    command: &clap::Command,
3132    path: &[String],
3133    op_id_option: &Option<CommandCatalogOption>,
3134) -> CommandCatalogEntry {
3135    let mut options = Vec::new();
3136    let mut arguments = Vec::new();
3137    for arg in command.get_arguments() {
3138        if arg.get_long().is_some() || arg.get_short().is_some() {
3139            options.push(catalog_option(arg));
3140        } else {
3141            arguments.push(catalog_argument(arg));
3142        }
3143    }
3144
3145    let contract = command_contract(path);
3146    if contract.supports_op_id
3147        && let Some(op_id_option) = op_id_option
3148    {
3149        options.push(op_id_option.clone());
3150    }
3151    CommandCatalogEntry {
3152        path: path.to_vec(),
3153        display: path.join(" "),
3154        aliases: command
3155            .get_all_aliases()
3156            .map(std::string::ToString::to_string)
3157            .collect(),
3158        tier: help_visibility_to_tier(contract.help_visibility).to_string(),
3159        surface: contract.surface.to_string(),
3160        help_visibility: contract.help_visibility.to_string(),
3161        help_rank: contract.help_rank,
3162        canonical_command: contract
3163            .canonical_command
3164            .map(std::string::ToString::to_string),
3165        canonical_action: canonical_action(contract),
3166        command_action: command_action(command, path, contract),
3167        summary: clean_catalog_summary(
3168            command
3169                .get_about()
3170                .or_else(|| command.get_long_about())
3171                .map(|about| about.to_string().lines().next().unwrap_or("").to_string())
3172                .unwrap_or_default(),
3173        ),
3174        has_subcommands: command.get_subcommands().next().is_some(),
3175        supports_json: contract.supports_json,
3176        output_modes: supported_output_modes(contract),
3177        mutates: contract.mutates,
3178        supports_op_id: contract.supports_op_id,
3179        persists_op_id: contract.persists_op_id,
3180        op_id_behavior: op_id_behavior(contract).to_string(),
3181        op_id_store_scope: op_id_store_scope(contract).to_string(),
3182        observe_only: contract.observe_only,
3183        may_initialize: contract.may_initialize,
3184        may_import_git: contract.may_import_git,
3185        may_write_worktree: contract.may_write_worktree,
3186        may_move_ref: contract.may_move_ref,
3187        destructive_requires_force: contract.destructive_requires_force,
3188        writes_heddle_refs: contract.writes_heddle_refs,
3189        writes_git_refs: contract.writes_git_refs,
3190        writes_worktree: contract.writes_worktree,
3191        writes_config: contract.writes_config,
3192        writes_hooks: contract.writes_hooks,
3193        network_io: contract.network_io,
3194        daemon_process: contract.daemon_process,
3195        object_gc: contract.object_gc,
3196        external_command: contract.external_command,
3197        requires_git_executable: contract.requires_git_executable,
3198        destructive_data: contract.destructive_data,
3199        side_effects: side_effects(contract),
3200        side_effect_class: side_effect_class(contract).to_string(),
3201        first_run_behavior: first_run_behavior(contract).to_string(),
3202        json_kind: contract.json_kind.to_string(),
3203        json_discriminators: json_discriminators_for_path(path.iter().map(String::as_str)),
3204        schema_verbs: contract_schema_verbs(contract)
3205            .map(str::to_string)
3206            .collect(),
3207        documented_schema_verbs: contract_documented_schema_verbs(contract)
3208            .map(str::to_string)
3209            .collect(),
3210        options,
3211        arguments,
3212        exit_codes: contract
3213            .exit_codes
3214            .iter()
3215            .map(|(code, reason)| CommandCatalogExitCode {
3216                code: *code,
3217                reason: (*reason).to_string(),
3218            })
3219            .collect(),
3220    }
3221}
3222
3223fn supported_output_modes(contract: CommandContract) -> Vec<String> {
3224    let mut modes = vec!["text".to_string()];
3225    if contract.supports_json {
3226        modes.push("json".to_string());
3227    }
3228    if contract.supports_json_compact {
3229        modes.push("json-compact".to_string());
3230    }
3231    modes
3232}
3233
3234fn canonical_action(contract: CommandContract) -> Option<CanonicalAction> {
3235    let command = contract.canonical_command?;
3236    let kind = contract.canonical_kind.unwrap_or("direct_command");
3237    let (argv, template) = canonical_action_metadata(command, kind);
3238    Some(CanonicalAction {
3239        command: command.to_string(),
3240        kind: kind.to_string(),
3241        executable: argv.is_some(),
3242        note: contract.canonical_note.unwrap_or_default().to_string(),
3243        argv,
3244        template,
3245    })
3246}
3247
3248fn canonical_action_metadata(
3249    command: &str,
3250    kind: &str,
3251) -> (Option<Vec<String>>, Option<ActionTemplate>) {
3252    match (command, kind) {
3253        ("adopt", "workflow") => (
3254            None,
3255            Some(action_template_from_parts(
3256                "heddle adopt --ref <branch>",
3257                &["heddle", "adopt", "--ref", "<branch>"],
3258                &["branch"],
3259                true,
3260            )),
3261        ),
3262        ("capture", "workflow") => (
3263            None,
3264            Some(action_template_from_parts(
3265                "heddle capture -m <message>",
3266                &["heddle", "capture", "-m", "<message>"],
3267                &["message"],
3268                true,
3269            )),
3270        ),
3271        ("thread switch", "direct_command") => (
3272            None,
3273            Some(action_template_from_parts(
3274                "heddle thread switch <thread>",
3275                &["heddle", "thread", "switch", "<thread>"],
3276                &["thread"],
3277                true,
3278            )),
3279        ),
3280        (_, "direct_command") => {
3281            let argv = std::iter::once("heddle".to_string())
3282                .chain(command.split_whitespace().map(str::to_string))
3283                .collect::<Vec<_>>();
3284            (Some(normalize_heddle_argv(argv)), None)
3285        }
3286        _ => (None, None),
3287    }
3288}
3289
3290fn command_action(
3291    command: &clap::Command,
3292    path: &[String],
3293    contract: CommandContract,
3294) -> Option<CommandAction> {
3295    if let Some(action) = contract.advertised_action {
3296        return Some(command_action_from_advertised(action));
3297    }
3298    if command.get_subcommands().next().is_some() {
3299        return None;
3300    }
3301
3302    let mut argv_template = std::iter::once("heddle".to_string())
3303        .chain(path.iter().cloned())
3304        .collect::<Vec<_>>();
3305    let mut required_inputs = Vec::new();
3306    for arg in command.get_arguments().filter(|arg| !arg.is_hide_set()) {
3307        if !arg.is_required_set() {
3308            continue;
3309        }
3310        if let Some(long) = arg.get_long() {
3311            argv_template.push(format!("--{long}"));
3312        } else if let Some(short) = arg.get_short() {
3313            argv_template.push(format!("-{short}"));
3314        }
3315        let names = value_names(arg);
3316        if names.is_empty() {
3317            required_inputs.push(arg.get_id().as_str().to_string());
3318            if arg.get_long().is_none() && arg.get_short().is_none() {
3319                argv_template.push(format!("<{}>", arg.get_id().as_str()));
3320            }
3321        } else {
3322            for name in names {
3323                let input = name.to_ascii_lowercase();
3324                required_inputs.push(input.clone());
3325                argv_template.push(format!("<{input}>"));
3326            }
3327        }
3328    }
3329
3330    let action = argv_template.join(" ");
3331    if required_inputs.is_empty() {
3332        Some(CommandAction {
3333            action,
3334            executable: true,
3335            argv: Some(normalize_heddle_argv(argv_template)),
3336            template: None,
3337        })
3338    } else {
3339        Some(CommandAction {
3340            action: action.clone(),
3341            executable: false,
3342            argv: None,
3343            template: Some(action_template_from_owned(
3344                action,
3345                argv_template,
3346                required_inputs,
3347                true,
3348            )),
3349        })
3350    }
3351}
3352
3353fn command_action_from_advertised(action: AdvertisedAction) -> CommandAction {
3354    let argv_template = action
3355        .argv_template
3356        .iter()
3357        .map(|part| (*part).to_string())
3358        .collect::<Vec<_>>();
3359    if action.executable {
3360        CommandAction {
3361            action: action.action.to_string(),
3362            executable: true,
3363            argv: Some(normalize_heddle_argv(argv_template)),
3364            template: None,
3365        }
3366    } else {
3367        CommandAction {
3368            action: action.action.to_string(),
3369            executable: false,
3370            argv: None,
3371            template: Some(action_template_from_parts(
3372                action.action,
3373                action.argv_template,
3374                action.required_inputs,
3375                action.agent_may_fill,
3376            )),
3377        }
3378    }
3379}
3380
3381fn action_template_from_parts(
3382    action: &str,
3383    argv_template: &[&str],
3384    required_inputs: &[&str],
3385    agent_may_fill: bool,
3386) -> ActionTemplate {
3387    action_template_from_owned(
3388        action.to_string(),
3389        argv_template
3390            .iter()
3391            .map(|part| (*part).to_string())
3392            .collect(),
3393        required_inputs
3394            .iter()
3395            .map(|input| (*input).to_string())
3396            .collect(),
3397        agent_may_fill,
3398    )
3399}
3400
3401fn action_template_from_owned(
3402    action: String,
3403    argv_template: Vec<String>,
3404    required_inputs: Vec<String>,
3405    agent_may_fill: bool,
3406) -> ActionTemplate {
3407    ActionTemplate {
3408        action,
3409        argv_template: normalize_heddle_argv(argv_template),
3410        required_inputs,
3411        agent_may_fill,
3412    }
3413}
3414
3415fn clean_catalog_summary(summary: String) -> String {
3416    let stripped = summary
3417        .trim_start_matches("Automation/workflow command:")
3418        .trim_start();
3419    let mut chars = stripped.chars();
3420    match chars.next() {
3421        Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
3422        None => String::new(),
3423    }
3424}
3425
3426fn side_effect_class(contract: CommandContract) -> &'static str {
3427    if contract.observe_only {
3428        "observe_only"
3429    } else if contract.destructive_requires_force && contract.writes_worktree {
3430        "destructive_worktree_mutation"
3431    } else if contract.destructive_data {
3432        "destructive_data"
3433    } else if contract.object_gc {
3434        "object_gc"
3435    } else if contract.writes_worktree {
3436        "worktree_mutation"
3437    } else if contract.may_import_git {
3438        "git_import"
3439    } else if contract.may_initialize {
3440        "initialize"
3441    } else if contract.writes_hooks {
3442        "hook_mutation"
3443    } else if contract.network_io {
3444        "network_mutation"
3445    } else if contract.writes_config {
3446        "config_mutation"
3447    } else if contract.daemon_process {
3448        "daemon_process"
3449    } else if contract.external_command {
3450        "external_command"
3451    } else if contract.writes_heddle_refs || contract.writes_git_refs || contract.may_move_ref {
3452        "ref_mutation"
3453    } else if contract.writes_metadata {
3454        "metadata_mutation"
3455    } else {
3456        "none"
3457    }
3458}
3459
3460fn side_effects(contract: CommandContract) -> Vec<CommandSideEffect> {
3461    if contract.observe_only {
3462        return vec![CommandSideEffect::ObserveOnly];
3463    }
3464
3465    let mut effects = Vec::new();
3466    if contract.may_initialize {
3467        effects.push(CommandSideEffect::Initialize);
3468    }
3469    if contract.may_import_git {
3470        effects.push(CommandSideEffect::ImportGit);
3471    }
3472    if contract.writes_heddle_refs {
3473        effects.push(CommandSideEffect::WritesHeddleRefs);
3474    }
3475    if contract.writes_git_refs {
3476        effects.push(CommandSideEffect::WritesGitRefs);
3477    }
3478    if contract.writes_worktree {
3479        effects.push(CommandSideEffect::WritesWorktree);
3480    } else if contract.may_write_worktree {
3481        effects.push(CommandSideEffect::MayWriteWorktree);
3482    }
3483    if contract.writes_metadata {
3484        effects.push(CommandSideEffect::WritesMetadata);
3485    }
3486    if contract.writes_config {
3487        effects.push(CommandSideEffect::WritesConfig);
3488    }
3489    if contract.writes_hooks {
3490        effects.push(CommandSideEffect::WritesHooks);
3491    }
3492    if contract.network_io {
3493        effects.push(CommandSideEffect::NetworkIo);
3494    }
3495    if contract.daemon_process {
3496        effects.push(CommandSideEffect::DaemonProcess);
3497    }
3498    if contract.object_gc {
3499        effects.push(CommandSideEffect::ObjectGc);
3500    }
3501    if contract.external_command {
3502        effects.push(CommandSideEffect::ExternalCommand);
3503    }
3504    if contract.destructive_requires_force {
3505        effects.push(CommandSideEffect::DestructiveRequiresForce);
3506    }
3507    if contract.destructive_data {
3508        effects.push(CommandSideEffect::DestructiveData);
3509    }
3510    effects
3511}
3512
3513fn op_id_behavior(contract: CommandContract) -> &'static str {
3514    if contract.persists_op_id {
3515        "generated_resume"
3516    } else if contract.supports_op_id {
3517        "explicit_replay"
3518    } else {
3519        "none"
3520    }
3521}
3522
3523fn uses_bootstrap_op_id_store(contract: CommandContract) -> bool {
3524    contract.supports_op_id && contract.may_initialize
3525}
3526
3527fn op_id_store_scope(contract: CommandContract) -> &'static str {
3528    if !contract.supports_op_id {
3529        "none"
3530    } else if uses_bootstrap_op_id_store(contract) {
3531        "bootstrap"
3532    } else {
3533        "repository"
3534    }
3535}
3536
3537fn first_run_behavior(contract: CommandContract) -> &'static str {
3538    if contract.observe_only {
3539        "observe_only_no_init"
3540    } else if contract.may_initialize && contract.may_import_git {
3541        "may_initialize_and_import_git"
3542    } else if contract.may_initialize {
3543        "may_initialize"
3544    } else if contract.may_import_git {
3545        "may_import_git"
3546    } else if contract.mutates {
3547        "requires_initialized_repo"
3548    } else {
3549        "no_repo_required"
3550    }
3551}
3552
3553fn catalog_option(arg: &clap::Arg) -> CommandCatalogOption {
3554    CommandCatalogOption {
3555        id: arg.get_id().as_str().to_string(),
3556        long: arg.get_long().map(str::to_string),
3557        aliases: option_aliases(arg),
3558        short: arg.get_short().map(|short| short.to_string()),
3559        value_names: value_names(arg),
3560        value_kind: value_kind(arg).to_string(),
3561        default_values: arg
3562            .get_default_values()
3563            .iter()
3564            .map(|value| value.to_string_lossy().into_owned())
3565            .collect(),
3566        possible_values: arg
3567            .get_possible_values()
3568            .iter()
3569            .map(|value| value.get_name().to_string())
3570            .collect(),
3571        help: arg.get_help().map(|help| help.to_string()),
3572        required: arg.is_required_set(),
3573        global: arg.is_global_set(),
3574        hidden: arg.is_hide_set(),
3575    }
3576}
3577
3578fn generated_help_option() -> CommandCatalogOption {
3579    CommandCatalogOption {
3580        id: "help".to_string(),
3581        long: Some("help".to_string()),
3582        aliases: Vec::new(),
3583        short: Some("h".to_string()),
3584        value_names: Vec::new(),
3585        value_kind: "boolean".to_string(),
3586        default_values: Vec::new(),
3587        possible_values: Vec::new(),
3588        help: Some("Print help".to_string()),
3589        required: false,
3590        global: true,
3591        hidden: false,
3592    }
3593}
3594
3595fn option_aliases(arg: &clap::Arg) -> Vec<String> {
3596    let mut aliases = std::collections::BTreeSet::new();
3597    for alias in arg.get_all_aliases().unwrap_or_default() {
3598        aliases.insert(alias.to_string());
3599    }
3600    for alias in arg.get_visible_aliases().unwrap_or_default() {
3601        aliases.insert(alias.to_string());
3602    }
3603    aliases.into_iter().collect()
3604}
3605
3606fn catalog_argument(arg: &clap::Arg) -> CommandCatalogArgument {
3607    CommandCatalogArgument {
3608        id: arg.get_id().as_str().to_string(),
3609        value_names: value_names(arg),
3610        help: arg.get_help().map(|help| help.to_string()),
3611        required: arg.is_required_set(),
3612    }
3613}
3614
3615fn value_kind(arg: &clap::Arg) -> &'static str {
3616    match arg.get_action() {
3617        ArgAction::SetTrue | ArgAction::SetFalse => "boolean",
3618        ArgAction::Count => "count",
3619        ArgAction::Append => "list",
3620        ArgAction::Set if !arg.get_possible_values().is_empty() => "enum",
3621        ArgAction::Set => "string",
3622        _ => "unknown",
3623    }
3624}
3625
3626fn value_names(arg: &clap::Arg) -> Vec<String> {
3627    arg.get_value_names()
3628        .map(|names| names.iter().map(|name| name.to_string()).collect())
3629        .unwrap_or_default()
3630}
3631
3632fn command_contract(path: &[String]) -> CommandContract {
3633    command_contract_for_path(path.iter().map(String::as_str))
3634        .unwrap_or_else(|| panic!("missing command contract for `{}`", path.join(" ")))
3635}
3636
3637fn command_contract_for_path<'a>(
3638    path: impl IntoIterator<Item = &'a str>,
3639) -> Option<CommandContract> {
3640    let path = path.into_iter().collect::<Vec<_>>();
3641    active_command_contract_entries()
3642        .iter()
3643        .copied()
3644        .find(|entry| entry.path == path.as_slice())
3645        .map(|entry| entry.contract)
3646}
3647
3648#[cfg(test)]
3649fn raw_command_contract_for_path<'a>(
3650    path: impl IntoIterator<Item = &'a str>,
3651) -> Option<CommandContract> {
3652    let path = path.into_iter().collect::<Vec<_>>();
3653    CONTRACTS
3654        .iter()
3655        .find(|entry| entry.path == path.as_slice())
3656        .map(|entry| entry.contract)
3657}
3658
3659#[cfg(test)]
3660pub(crate) fn sibling_documented_schema_verbs(schema_verb: &str) -> Vec<&'static str> {
3661    active_command_contract_entries()
3662        .iter()
3663        .filter(|entry| {
3664            contract_documented_schema_verbs(entry.contract)
3665                .any(|documented| documented == schema_verb)
3666        })
3667        .flat_map(|entry| contract_documented_schema_verbs(entry.contract))
3668        .filter(|documented| *documented != schema_verb)
3669        .collect()
3670}
3671
3672fn active_command_contract_entries() -> &'static [&'static CommandContractEntry] {
3673    ACTIVE_COMMAND_CONTRACT_ENTRIES
3674        .get_or_init(|| {
3675            let command = Cli::command();
3676            CONTRACTS
3677                .iter()
3678                .filter(|entry| clap_command_path_exists(&command, entry.path))
3679                .collect()
3680        })
3681        .as_slice()
3682}
3683
3684/// The contracts advertised to agents: every [`active_command_contract_entries`]
3685/// entry (present in the compiled clap tree) PLUS any `feature_gated` contract
3686/// whose clap subcommand is compiled out of this build. The hosted surfaces
3687/// (`auth`, `support`, `presence`) are `client`-gated, so a default
3688/// `cargo install heddle-cli` omits their clap nodes — but their schema verbs
3689/// and `output_kind` discriminators are a wire-format-stable promise that must
3690/// stay advertised in the catalog regardless of build features. Distinct from
3691/// the active set, which the clap-tree-equivalence invariants pin exactly.
3692fn advertised_command_contract_entries() -> &'static [&'static CommandContractEntry] {
3693    ADVERTISED_COMMAND_CONTRACT_ENTRIES
3694        .get_or_init(|| {
3695            let command = Cli::command();
3696            let mut entries = active_command_contract_entries().to_vec();
3697            for entry in CONTRACTS.iter() {
3698                if entry.contract.feature_gate.is_some()
3699                    && !clap_command_path_exists(&command, entry.path)
3700                {
3701                    entries.push(entry);
3702                }
3703            }
3704            entries
3705        })
3706        .as_slice()
3707}
3708
3709fn clap_command_path_exists(command: &clap::Command, path: &[&str]) -> bool {
3710    let mut current = command;
3711    for part in path {
3712        let Some(next) = current
3713            .get_subcommands()
3714            .find(|subcommand| subcommand.get_name() == *part)
3715        else {
3716            return false;
3717        };
3718        current = next;
3719    }
3720    true
3721}
3722
3723pub fn command_json_discriminators() -> Vec<CommandJsonDiscriminator> {
3724    advertised_command_contract_entries()
3725        .iter()
3726        .copied()
3727        .flat_map(|entry| {
3728            contract_json_discriminators(entry.contract)
3729                .map(move |discriminator| json_discriminator_metadata(entry.path, &discriminator))
3730        })
3731        .collect()
3732}
3733
3734pub fn operator_envelope_verbs() -> Vec<String> {
3735    active_command_contract_entries()
3736        .iter()
3737        .copied()
3738        .filter(|entry| entry.contract.operator_envelope)
3739        .map(|entry| entry.path.join(" "))
3740        .collect()
3741}
3742
3743#[cfg(test)]
3744pub fn command_json_discriminator_for_schema_verb(
3745    schema_verb: &str,
3746) -> Option<CommandJsonDiscriminator> {
3747    command_json_discriminators_for_schema_verb(schema_verb)
3748        .into_iter()
3749        .next()
3750}
3751
3752pub fn command_json_discriminators_for_schema_verb(
3753    schema_verb: &str,
3754) -> Vec<CommandJsonDiscriminator> {
3755    advertised_command_contract_entries()
3756        .iter()
3757        .copied()
3758        .filter(|entry| {
3759            contract_json_discriminators(entry.contract)
3760                .any(|discriminator| discriminator.schema_verb == Some(schema_verb))
3761        })
3762        .flat_map(|entry| {
3763            let schema_verbs = contract_schema_verbs(entry.contract).collect::<Vec<_>>();
3764            let include_same_command_siblings =
3765                schema_verbs.len() == 1 && schema_verbs[0] == schema_verb;
3766            contract_json_discriminators(entry.contract).filter_map(move |discriminator| {
3767                if discriminator.schema_verb == Some(schema_verb)
3768                    || (include_same_command_siblings && discriminator.schema_verb.is_none())
3769                {
3770                    Some(json_discriminator_metadata(entry.path, &discriminator))
3771                } else {
3772                    None
3773                }
3774            })
3775        })
3776        .collect()
3777}
3778
3779fn json_discriminators_for_path<'a>(
3780    path: impl IntoIterator<Item = &'a str>,
3781) -> Vec<CommandJsonDiscriminator> {
3782    let path = path.into_iter().collect::<Vec<_>>();
3783    advertised_command_contract_entries()
3784        .iter()
3785        .copied()
3786        .filter(|entry| entry.path == path.as_slice())
3787        .flat_map(|entry| {
3788            contract_json_discriminators(entry.contract)
3789                .map(move |discriminator| json_discriminator_metadata(entry.path, &discriminator))
3790        })
3791        .collect()
3792}
3793
3794fn json_discriminator_metadata(
3795    path: &'static [&'static str],
3796    discriminator: &CommandJsonDiscriminatorSpec,
3797) -> CommandJsonDiscriminator {
3798    CommandJsonDiscriminator {
3799        path: path.iter().map(|part| (*part).to_string()).collect(),
3800        display: path.join(" "),
3801        schema_verb: discriminator.schema_verb.map(str::to_string),
3802        field: discriminator.field.to_string(),
3803        value: discriminator.value.to_string(),
3804        no_schema_reason: discriminator.no_schema_reason.map(str::to_string),
3805    }
3806}
3807
3808pub fn command_runtime_contract_for_command(command: &Commands) -> CommandRuntimeContract {
3809    let path = command_path(command);
3810    runtime_contract_for_path(path.iter().copied())
3811        .unwrap_or_else(|| panic!("missing command contract for `{}`", path.join(" ")))
3812}
3813
3814pub fn command_runtime_contract(command_name: &str) -> Option<CommandRuntimeContract> {
3815    runtime_contract_for_path(command_name.split_whitespace())
3816}
3817
3818/// The catalog's declared mutation surface for a command, surfaced to
3819/// callers such as `--dry-run` so the plan report cites the single
3820/// source of truth (`heddle help --output json`) rather than
3821/// re-deriving side effects at each call site.
3822#[derive(Debug, Clone, Copy, Serialize)]
3823pub struct CommandSideEffectFlags {
3824    pub may_move_ref: bool,
3825    pub destructive_requires_force: bool,
3826    pub network_io: bool,
3827    pub writes_heddle_refs: bool,
3828    pub writes_git_refs: bool,
3829}
3830
3831/// Look up the declared side-effect flags for a command path (e.g.
3832/// `"push"`, `"land"`, `"ready"`). Returns `None` for an unknown command.
3833pub fn command_side_effect_flags(command_name: &str) -> Option<CommandSideEffectFlags> {
3834    let path = command_name.split_whitespace().collect::<Vec<_>>();
3835    active_command_contract_entries()
3836        .iter()
3837        .copied()
3838        .find(|entry| entry.path == path.as_slice())
3839        .map(|entry| CommandSideEffectFlags {
3840            may_move_ref: entry.contract.may_move_ref,
3841            destructive_requires_force: entry.contract.destructive_requires_force,
3842            network_io: entry.contract.network_io,
3843            writes_heddle_refs: entry.contract.writes_heddle_refs,
3844            writes_git_refs: entry.contract.writes_git_refs,
3845        })
3846}
3847
3848pub(crate) fn command_runtime_contract_for_schema_verb(
3849    schema_verb: &str,
3850) -> Option<CommandRuntimeContract> {
3851    command_runtime_contract(schema_verb)
3852        .or_else(|| command_runtime_contract(&schema_verb_without_flags(schema_verb)))
3853}
3854
3855pub(crate) fn schema_verb_without_flags(schema_verb: &str) -> String {
3856    schema_verb
3857        .split_whitespace()
3858        .filter(|part| !part.starts_with('-'))
3859        .collect::<Vec<_>>()
3860        .join(" ")
3861}
3862
3863fn runtime_contract_for_path<'a>(
3864    path: impl IntoIterator<Item = &'a str>,
3865) -> Option<CommandRuntimeContract> {
3866    let path = path.into_iter().collect::<Vec<_>>();
3867    active_command_contract_entries()
3868        .iter()
3869        .copied()
3870        .find(|entry| entry.path == path.as_slice())
3871        .map(|entry| runtime_contract(entry.path, entry.contract))
3872}
3873
3874fn runtime_contract(
3875    path: &'static [&'static str],
3876    contract: CommandContract,
3877) -> CommandRuntimeContract {
3878    CommandRuntimeContract {
3879        path: path.to_vec(),
3880        display: path.join(" "),
3881        supports_json: contract.supports_json,
3882        supports_json_compact: contract.supports_json_compact,
3883        mutates: contract.mutates,
3884        targets_current_repository: contract.targets_current_repository,
3885        supports_op_id: contract.supports_op_id,
3886        persists_op_id: contract.persists_op_id,
3887        uses_bootstrap_op_id_store: uses_bootstrap_op_id_store(contract),
3888        help_visibility: contract.help_visibility,
3889        help_rank: contract.help_rank,
3890        surface: contract.surface,
3891        canonical_command: contract.canonical_command,
3892        json_kind: contract.json_kind,
3893    }
3894}
3895
3896pub fn command_supports_op_id(command_name: &str) -> bool {
3897    command_runtime_contract(command_name)
3898        .map(|contract| contract.supports_op_id)
3899        .unwrap_or(false)
3900}
3901
3902pub fn command_persists_op_id(command_name: &str) -> bool {
3903    command_runtime_contract(command_name)
3904        .map(|contract| contract.persists_op_id)
3905        .unwrap_or(false)
3906}
3907
3908pub fn command_uses_bootstrap_op_id_store(command_name: &str) -> bool {
3909    command_runtime_contract(command_name)
3910        .map(|contract| contract.uses_bootstrap_op_id_store)
3911        .unwrap_or(false)
3912}
3913
3914pub(crate) fn feature_gated_command_roots() -> Vec<&'static str> {
3915    let mut roots = CONTRACTS
3916        .iter()
3917        .filter(|entry| entry.path.len() == 1 && entry.contract.feature_gate.is_some())
3918        .map(|entry| entry.path[0])
3919        .collect::<Vec<_>>();
3920    roots.sort_unstable();
3921    roots.dedup();
3922    roots
3923}
3924
3925pub fn command_supports_op_id_for_command(command: &Commands) -> bool {
3926    command_runtime_contract_for_command(command).supports_op_id
3927}
3928
3929pub fn command_supports_json_for_command(command: &Commands) -> bool {
3930    command_runtime_contract_for_command(command).supports_json
3931}
3932
3933pub fn command_help_tier(command_name: &str) -> &'static str {
3934    command_runtime_contract(command_name)
3935        .map(|contract| help_visibility_to_tier(contract.help_visibility))
3936        .unwrap_or("advanced")
3937}
3938
3939pub fn command_surface(command_name: &str) -> &'static str {
3940    command_runtime_contract(command_name)
3941        .map(|contract| contract.surface)
3942        .unwrap_or("native")
3943}
3944
3945pub fn command_help_visibility(command_name: &str) -> &'static str {
3946    command_runtime_contract(command_name)
3947        .map(|contract| contract.help_visibility)
3948        .unwrap_or("advanced")
3949}
3950
3951pub fn command_canonical_command(command_name: &str) -> Option<&'static str> {
3952    command_runtime_contract(command_name).and_then(|contract| contract.canonical_command)
3953}
3954
3955pub fn root_commands_for_help_visibility(visibility: &str) -> Vec<&'static str> {
3956    let mut entries = active_command_contract_entries()
3957        .iter()
3958        .copied()
3959        .filter(|entry| entry.path.len() == 1 && entry.contract.help_visibility == visibility)
3960        .collect::<Vec<_>>();
3961    entries.sort_by_key(|entry| (entry.contract.help_rank, entry.path[0]));
3962    entries.into_iter().map(|entry| entry.path[0]).collect()
3963}
3964
3965pub fn root_commands_for_advanced_help() -> Vec<&'static str> {
3966    let mut entries = advanced_help_root_entries();
3967    entries.sort_by_key(|entry| (entry.contract.help_rank, entry.path[0]));
3968    entries.into_iter().map(|entry| entry.path[0]).collect()
3969}
3970
3971/// Root commands on the advanced help surface, unsorted.
3972fn advanced_help_root_entries() -> Vec<&'static CommandContractEntry> {
3973    active_command_contract_entries()
3974        .iter()
3975        .copied()
3976        .filter(|entry| {
3977            entry.path.len() == 1
3978                && !matches!(entry.contract.help_visibility, "everyday" | "hidden")
3979        })
3980        .collect()
3981}
3982
3983/// Area groups for the `heddle help advanced` listing, in render order,
3984/// as `(display title, verbs)` pairs (heddle#652). The grouping is
3985/// contract-table data, not a hand-maintained help string: native
3986/// advanced commands carry an explicit `help_category` on their
3987/// registration, while `automation` / `admin` / `git_projection` commands
3988/// derive their group from the surface they already declare. Verbs keep
3989/// contract order (help_rank, then name) within each group — the same
3990/// ordering the flat list used. Feature-gated verbs absent from the
3991/// current build simply don't appear; a group may come back empty.
3992pub fn advanced_help_groups() -> Vec<(&'static str, Vec<&'static str>)> {
3993    const GROUPS: &[(&str, &str)] = &[
3994        ("threads", "Threads and integration"),
3995        ("states", "States and history"),
3996        ("collab", "Collaboration and review"),
3997        ("recovery", "Recovery and integrity"),
3998        ("repo", "Repo and environment"),
3999        ("automation", "Agents and automation"),
4000        ("git-interop", "Git interop"),
4001        ("admin", "Admin and maintenance"),
4002    ];
4003    let mut entries = advanced_help_root_entries();
4004    entries.sort_by_key(|entry| (entry.contract.help_rank, entry.path[0]));
4005    GROUPS
4006        .iter()
4007        .map(|(id, title)| {
4008            (
4009                *title,
4010                entries
4011                    .iter()
4012                    .filter(|entry| advanced_help_group_id(&entry.contract) == *id)
4013                    .map(|entry| entry.path[0])
4014                    .collect(),
4015            )
4016        })
4017        .collect()
4018}
4019
4020/// Resolve which advanced-help group a root command belongs to. The
4021/// non-native surfaces are themselves the grouping; native commands use
4022/// the `help_category` set at registration. An unset category on a
4023/// native advanced command maps to "" (member of no group) — the
4024/// `advanced_help_groups_cover_every_advanced_verb` test turns that into
4025/// a build failure rather than a silently missing help line.
4026fn advanced_help_group_id(contract: &CommandContract) -> &'static str {
4027    match contract.surface {
4028        "automation" => "automation",
4029        "admin" => "admin",
4030        "git_projection" => "git-interop",
4031        _ => contract.help_category.unwrap_or(""),
4032    }
4033}
4034
4035pub fn recommended_action_template(action: &str) -> Option<ActionTemplate> {
4036    let trimmed = action.trim();
4037    if trimmed.is_empty() {
4038        return None;
4039    }
4040    RECOMMENDED_ACTION_TEMPLATES
4041        .iter()
4042        .find(|(template_action, _, _, _)| *template_action == trimmed)
4043        .map(
4044            |(template_action, argv_template, required_inputs, agent_may_fill)| {
4045                action_template_from_parts(
4046                    template_action,
4047                    argv_template,
4048                    required_inputs,
4049                    *agent_may_fill,
4050                )
4051            },
4052        )
4053        .or_else(|| dynamic_recommended_action_template(trimmed))
4054        .or_else(|| concrete_recommended_action_template(trimmed))
4055}
4056
4057/// Fallback template for a concrete, placeholder-free recommended action
4058/// (e.g. `heddle status`). The template *is* the parsed argv with no inputs
4059/// left to fill — agents run `argv_template` verbatim. This makes
4060/// `recommended_action_template` total over every valid action so the
4061/// fillable `_template` is the single canonical machine shape and the
4062/// always-null `_argv` sibling could be dropped (HeddleCo/heddle#254).
4063///
4064/// Returns `None` for placeholder/display-only actions that lack a
4065/// registered structured template (they are invalid and surface upstream),
4066/// matching the previous parsed-argv contract.
4067fn concrete_recommended_action_template(action: &str) -> Option<ActionTemplate> {
4068    if RECOMMENDED_ACTION_PLACEHOLDERS.contains(&action) || is_display_only_template(action) {
4069        return None;
4070    }
4071    if validate_recommended_action(action).is_err() {
4072        return None;
4073    }
4074    let argv = split_recommended_action(action).ok()?;
4075    Some(action_template_from_owned(
4076        action.to_string(),
4077        argv,
4078        Vec::new(),
4079        false,
4080    ))
4081}
4082
4083fn dynamic_recommended_action_template(action: &str) -> Option<ActionTemplate> {
4084    let argv = split_recommended_action(action).ok()?;
4085    if let Some(template) = dynamic_message_recommended_action_template(action, &argv) {
4086        return Some(template);
4087    }
4088    match argv.as_slice() {
4089        [heddle, clone, remote, path]
4090            if heddle == "heddle" && clone == "clone" && is_placeholder_arg(path) =>
4091        {
4092            Some(action_template_from_owned(
4093                action.to_string(),
4094                vec![
4095                    "heddle".to_string(),
4096                    "clone".to_string(),
4097                    remote.clone(),
4098                    path.clone(),
4099                ],
4100                vec![placeholder_input_name(path)],
4101                false,
4102            ))
4103        }
4104        [heddle, clone, remote, path, flag, thread]
4105            if heddle == "heddle"
4106                && clone == "clone"
4107                && flag == "--thread"
4108                && is_placeholder_arg(path) =>
4109        {
4110            Some(action_template_from_owned(
4111                action.to_string(),
4112                vec![
4113                    "heddle".to_string(),
4114                    "clone".to_string(),
4115                    remote.clone(),
4116                    path.clone(),
4117                    "--thread".to_string(),
4118                    thread.clone(),
4119                ],
4120                vec![placeholder_input_name(path)],
4121                false,
4122            ))
4123        }
4124        [heddle, start, thread_name, path_flag, path]
4125            if heddle == "heddle"
4126                && start == "start"
4127                && path_flag == "--path"
4128                && is_placeholder_arg(path) =>
4129        {
4130            let required_inputs = [thread_name, path]
4131                .iter()
4132                .filter(|arg| is_placeholder_arg(arg))
4133                .map(|arg| placeholder_input_name(arg))
4134                .collect();
4135            Some(action_template_from_owned(
4136                action.to_string(),
4137                vec![
4138                    "heddle".to_string(),
4139                    "start".to_string(),
4140                    thread_name.clone(),
4141                    "--path".to_string(),
4142                    path.clone(),
4143                ],
4144                required_inputs,
4145                true,
4146            ))
4147        }
4148        [heddle, thread_cmd, absorb, thread_name, into_flag, parent]
4149            if heddle == "heddle"
4150                && thread_cmd == "thread"
4151                && absorb == "absorb"
4152                && into_flag == "--into"
4153                && is_placeholder_arg(parent) =>
4154        {
4155            Some(action_template_from_owned(
4156                action.to_string(),
4157                vec![
4158                    "heddle".to_string(),
4159                    "thread".to_string(),
4160                    "absorb".to_string(),
4161                    thread_name.clone(),
4162                    "--into".to_string(),
4163                    parent.clone(),
4164                ],
4165                vec![placeholder_input_name(parent)],
4166                true,
4167            ))
4168        }
4169        _ => None,
4170    }
4171}
4172
4173fn dynamic_message_recommended_action_template(
4174    action: &str,
4175    argv: &[String],
4176) -> Option<ActionTemplate> {
4177    match argv {
4178        [heddle, command, message_flag, message]
4179            if heddle == "heddle"
4180                && matches!(command.as_str(), "capture" | "ready")
4181                && is_message_flag(message_flag)
4182                && is_message_placeholder_arg(message) =>
4183        {
4184            Some(action_template_from_owned(
4185                action.to_string(),
4186                vec![
4187                    "heddle".to_string(),
4188                    command.clone(),
4189                    "-m".to_string(),
4190                    "<message>".to_string(),
4191                ],
4192                vec!["message".to_string()],
4193                true,
4194            ))
4195        }
4196        [
4197            heddle,
4198            capture,
4199            message_flag,
4200            message,
4201            confidence_flag,
4202            confidence,
4203        ] if heddle == "heddle"
4204            && capture == "capture"
4205            && is_message_flag(message_flag)
4206            && is_message_placeholder_arg(message)
4207            && confidence_flag == "--confidence"
4208            && is_placeholder_arg(confidence) =>
4209        {
4210            Some(action_template_from_owned(
4211                action.to_string(),
4212                vec![
4213                    "heddle".to_string(),
4214                    "capture".to_string(),
4215                    "-m".to_string(),
4216                    "<message>".to_string(),
4217                    "--confidence".to_string(),
4218                    confidence.clone(),
4219                ],
4220                vec!["message".to_string(), placeholder_input_name(confidence)],
4221                true,
4222            ))
4223        }
4224        // The confidence/verification policy-blocker recovery scopes itself to
4225        // the thread's checkout via the global `--repo <path>` flag (heddle#464).
4226        // `<path>` is a concrete worktree path, not a placeholder, so the only
4227        // fillable inputs remain the message and confidence.
4228        [
4229            heddle,
4230            repo_flag,
4231            repo_path,
4232            command,
4233            message_flag,
4234            message,
4235            confidence_flag,
4236            confidence,
4237        ] if heddle == "heddle"
4238            && repo_flag == "--repo"
4239            && command == "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                    "--repo".to_string(),
4250                    repo_path.clone(),
4251                    command.clone(),
4252                    "-m".to_string(),
4253                    "<message>".to_string(),
4254                    "--confidence".to_string(),
4255                    confidence.clone(),
4256                ],
4257                vec!["message".to_string(), placeholder_input_name(confidence)],
4258                true,
4259            ))
4260        }
4261        _ => None,
4262    }
4263}
4264
4265fn is_message_flag(value: &str) -> bool {
4266    value == "-m" || value == "--message"
4267}
4268
4269fn is_message_placeholder_arg(value: &str) -> bool {
4270    matches!(value, "..." | "…") || value == "<message>"
4271}
4272
4273fn is_placeholder_arg(value: &str) -> bool {
4274    value.starts_with('<') && value.ends_with('>') && value.len() > 2
4275}
4276
4277fn placeholder_input_name(value: &str) -> String {
4278    value
4279        .trim_start_matches('<')
4280        .trim_end_matches('>')
4281        .replace('-', "_")
4282}
4283
4284fn help_visibility_to_tier(help_visibility: &str) -> &'static str {
4285    match help_visibility {
4286        "everyday" => "everyday",
4287        "hidden" => "hidden",
4288        _ => "advanced",
4289    }
4290}
4291
4292pub fn observe_only_root_commands() -> Vec<&'static str> {
4293    active_command_contract_entries()
4294        .iter()
4295        .copied()
4296        .filter(|entry| {
4297            entry.path.len() == 1 && entry.contract.observe_only && !entry.contract.mutates
4298        })
4299        .map(|entry| entry.path[0])
4300        .collect()
4301}
4302
4303pub fn command_contract_root_commands() -> Vec<&'static str> {
4304    active_command_contract_entries()
4305        .iter()
4306        .copied()
4307        .filter(|entry| entry.path.len() == 1)
4308        .map(|entry| entry.path[0])
4309        .collect()
4310}
4311
4312pub fn validate_recommended_action(action: &str) -> std::result::Result<(), String> {
4313    let trimmed = action.trim();
4314    if trimmed.is_empty() {
4315        return Ok(());
4316    }
4317    if RECOMMENDED_ACTION_PLACEHOLDERS.contains(&trimmed) {
4318        return recommended_action_template(trimmed)
4319            .map(|_| ())
4320            .ok_or_else(|| {
4321                format!(
4322                    "recommended action placeholder `{trimmed}` must have a structured template"
4323                )
4324            });
4325    }
4326    if is_display_only_template(trimmed) {
4327        return recommended_action_template(trimmed).map(|_| ()).ok_or_else(|| {
4328            format!(
4329                "display-only recommended action `{trimmed}` must be registered as a structured template"
4330            )
4331        });
4332    }
4333
4334    let argv = split_recommended_action(trimmed)?;
4335    match argv.first().map(String::as_str) {
4336        Some("heddle") => Cli::command()
4337            .try_get_matches_from(argv)
4338            .map(|_| ())
4339            .map_err(|err| err.to_string()),
4340        Some(other) => Err(format!(
4341            "recommended action must start with `heddle`, found `{other}`"
4342        )),
4343        None => Ok(()),
4344    }
4345}
4346
4347fn is_display_only_template(action: &str) -> bool {
4348    action.contains("...") || action.contains('…') || (action.contains('<') && action.contains('>'))
4349}
4350
4351pub fn split_recommended_action(action: &str) -> std::result::Result<Vec<String>, String> {
4352    let mut args = Vec::new();
4353    let mut current = String::new();
4354    let mut chars = action.chars().peekable();
4355    let mut in_single_quote = false;
4356    let mut in_double_quote = false;
4357
4358    while let Some(ch) = chars.next() {
4359        match (ch, in_single_quote, in_double_quote) {
4360            ('\'', false, false) => in_single_quote = true,
4361            ('\'', true, false) => in_single_quote = false,
4362            ('"', false, false) => in_double_quote = true,
4363            ('"', false, true) => in_double_quote = false,
4364            ('\\', false, _) => match chars.next() {
4365                Some(next) => current.push(next),
4366                None => current.push('\\'),
4367            },
4368            (ch, false, false) if ch.is_whitespace() => {
4369                if !current.is_empty() {
4370                    args.push(std::mem::take(&mut current));
4371                }
4372            }
4373            (ch, _, _) => current.push(ch),
4374        }
4375    }
4376
4377    if in_single_quote {
4378        return Err("unterminated single quote in recommended action".to_string());
4379    }
4380    if in_double_quote {
4381        return Err("unterminated double quote in recommended action".to_string());
4382    }
4383    if !current.is_empty() {
4384        args.push(current);
4385    }
4386    Ok(args)
4387}
4388
4389pub(crate) fn schema_verbs() -> Vec<&'static str> {
4390    let mut verbs = collect_schema_verbs(contract_schema_verbs);
4391    verbs.push("error");
4392    verbs
4393}
4394
4395pub(crate) fn documented_schema_verbs() -> Vec<&'static str> {
4396    let mut verbs = collect_schema_verbs(contract_documented_schema_verbs);
4397    verbs.push("error");
4398    verbs
4399}
4400
4401pub(crate) fn opaque_schema_verbs() -> Vec<&'static str> {
4402    collect_schema_verbs(|contract| contract.opaque_schema_verbs.iter().copied())
4403}
4404
4405fn collect_schema_verbs<I>(select: impl Fn(CommandContract) -> I) -> Vec<&'static str>
4406where
4407    I: IntoIterator<Item = &'static str>,
4408{
4409    let mut verbs = Vec::new();
4410    for entry in advertised_command_contract_entries().iter().copied() {
4411        for verb in select(entry.contract) {
4412            if !verbs.contains(&verb) {
4413                verbs.push(verb);
4414            }
4415        }
4416    }
4417    verbs
4418}
4419
4420pub fn command_path(command: &Commands) -> Vec<&'static str> {
4421    match command {
4422        Commands::Init(_) => vec!["init"],
4423        Commands::Adopt(_) => vec!["adopt"],
4424        Commands::Help { .. } => vec!["help"],
4425        Commands::Status { .. } => vec!["status"],
4426        Commands::Watch(_) => vec!["watch"],
4427        Commands::Verify => vec!["verify"],
4428        Commands::Doctor(args) => match &args.command {
4429            None => vec!["doctor"],
4430            Some(DoctorCommands::Docs(_)) => vec!["doctor", "docs"],
4431            Some(DoctorCommands::Schemas(_)) => vec!["doctor", "schemas"],
4432        },
4433        Commands::Schemas { .. } => vec!["schemas"],
4434        Commands::Start(_) => vec!["start"],
4435        Commands::Try(_) => vec!["try"],
4436        Commands::Run(_) => vec!["run"],
4437        Commands::Sync(args) => {
4438            #[cfg(feature = "git-overlay")]
4439            {
4440                if matches!(args.command, Some(SyncCommands::Git { .. })) {
4441                    return vec!["sync", "git"];
4442                }
4443            }
4444            #[cfg(not(feature = "git-overlay"))]
4445            let _ = args;
4446            vec!["sync"]
4447        }
4448        Commands::Continue => vec!["continue"],
4449        Commands::Abort => vec!["abort"],
4450        Commands::Land(_) => vec!["land"],
4451        Commands::Ready(_) => vec!["ready"],
4452        Commands::Capture(_) => vec!["capture"],
4453        Commands::Commit(_) => vec!["commit"],
4454        Commands::Log(_) => vec!["log"],
4455        Commands::Show { .. } => vec!["show"],
4456        Commands::Retro(_) => vec!["retro"],
4457        Commands::Diff(_) => vec!["diff"],
4458        Commands::Discuss { command } => match command {
4459            DiscussCommands::Open(_) => vec!["discuss", "open"],
4460            DiscussCommands::Append(_) => vec!["discuss", "append"],
4461            DiscussCommands::Resolve(_) => vec!["discuss", "resolve"],
4462            DiscussCommands::Reopen(_) => vec!["discuss", "reopen"],
4463            DiscussCommands::List(_) => vec!["discuss", "list"],
4464            DiscussCommands::Show(_) => vec!["discuss", "show"],
4465        },
4466        Commands::Query(_) => vec!["query"],
4467        Commands::Review { command } => match command {
4468            ReviewCommands::Show(_) => vec!["review", "show"],
4469            ReviewCommands::Sign(_) => vec!["review", "sign"],
4470            ReviewCommands::Next(_) => vec!["review", "next"],
4471            ReviewCommands::Health(_) => vec!["review", "health"],
4472        },
4473        Commands::Redact { command } => match command {
4474            RedactCommands::Apply(_) => vec!["redact", "apply"],
4475            RedactCommands::List(_) => vec!["redact", "list"],
4476            RedactCommands::Show(_) => vec!["redact", "show"],
4477            RedactCommands::Trust(command) => match command {
4478                RedactTrustCommands::Add(_) => vec!["redact", "trust", "add"],
4479                RedactTrustCommands::List(_) => vec!["redact", "trust", "list"],
4480                RedactTrustCommands::Remove(_) => vec!["redact", "trust", "remove"],
4481            },
4482            RedactCommands::Purge(command) => match command {
4483                PurgeCommands::Apply(_) => vec!["redact", "purge", "apply"],
4484                PurgeCommands::List(_) => vec!["redact", "purge", "list"],
4485            },
4486        },
4487        Commands::Visibility { command } => match command {
4488            VisibilityCommands::Set(_) => vec!["visibility", "set"],
4489            VisibilityCommands::Promote(_) => vec!["visibility", "promote"],
4490            VisibilityCommands::Show(_) => vec!["visibility", "show"],
4491            VisibilityCommands::List(_) => vec!["visibility", "list"],
4492        },
4493        Commands::Revert(_) => vec!["revert"],
4494        Commands::Undo(_) => vec!["undo"],
4495        Commands::Collapse(_) => vec!["collapse"],
4496        Commands::Expand(_) => vec!["expand"],
4497        Commands::Thread { command } => match command {
4498            ThreadCommands::Create { .. } => vec!["thread", "create"],
4499            ThreadCommands::Current => vec!["thread", "current"],
4500            ThreadCommands::Switch { .. } => vec!["thread", "switch"],
4501            ThreadCommands::Cd { .. } => vec!["thread", "cd"],
4502            ThreadCommands::List(_) => vec!["thread", "list"],
4503            ThreadCommands::Show(_) => vec!["thread", "show"],
4504            ThreadCommands::Captures(_) => vec!["thread", "captures"],
4505            ThreadCommands::Rename(_) => vec!["thread", "rename"],
4506            ThreadCommands::Refresh(_) => vec!["thread", "refresh"],
4507            ThreadCommands::Move(_) => vec!["thread", "move"],
4508            ThreadCommands::Absorb(_) => vec!["thread", "absorb"],
4509            ThreadCommands::Resolve(_) => vec!["thread", "resolve"],
4510            ThreadCommands::Promote(_) => vec!["thread", "promote"],
4511            ThreadCommands::Drop(_) => vec!["thread", "drop"],
4512            ThreadCommands::Approve(_) => vec!["thread", "approve"],
4513            ThreadCommands::Approvals(_) => vec!["thread", "approvals"],
4514            ThreadCommands::RevokeApproval(_) => vec!["thread", "revoke-approval"],
4515            ThreadCommands::CheckMerge(_) => vec!["thread", "check-merge"],
4516            ThreadCommands::Cleanup(_) => vec!["thread", "cleanup"],
4517            ThreadCommands::Marker { command } => match command {
4518                ThreadMarkerCommands::List { .. } => vec!["thread", "marker", "list"],
4519                ThreadMarkerCommands::Create { .. } => {
4520                    vec!["thread", "marker", "create"]
4521                }
4522                ThreadMarkerCommands::Delete { .. } => {
4523                    vec!["thread", "marker", "delete"]
4524                }
4525                ThreadMarkerCommands::Show { .. } => vec!["thread", "marker", "show"],
4526            },
4527        },
4528        Commands::Timeline(args) => match &args.command {
4529            TimelineCommands::Status(_) => vec!["timeline", "status"],
4530            TimelineCommands::RecordStart(_) => vec!["timeline", "record-start"],
4531            TimelineCommands::RecordFinish(_) => vec!["timeline", "record-finish"],
4532            TimelineCommands::Fork(_) => vec!["timeline", "fork"],
4533            TimelineCommands::Reset(_) => vec!["timeline", "reset"],
4534            TimelineCommands::Recover(_) => vec!["timeline", "recover"],
4535        },
4536        Commands::Shell { command } => match command {
4537            ShellCommands::Init { .. } => vec!["shell", "init"],
4538            ShellCommands::Completion { .. } => vec!["shell", "completion"],
4539            ShellCommands::Prompt => vec!["shell", "prompt"],
4540        },
4541        Commands::Complete { .. } => vec!["complete"],
4542        Commands::Resolve(_) => vec!["resolve"],
4543        Commands::Fsck(args) => match &args.command {
4544            None => vec!["fsck"],
4545            Some(crate::cli::FsckCommands::Repair { target }) => match target {
4546                crate::cli::FsckRepairCommands::Git(_) => vec!["fsck", "repair", "git"],
4547            },
4548        },
4549        #[cfg(feature = "git-overlay")]
4550        Commands::Import { command } => match command {
4551            ImportCommands::Git { .. } => vec!["import", "git"],
4552        },
4553        #[cfg(feature = "git-overlay")]
4554        Commands::Export { command } => match command {
4555            ExportCommands::Git { .. } => vec!["export", "git"],
4556        },
4557        Commands::Oplog { command } => match command {
4558            OplogCommands::Recover => vec!["oplog", "recover"],
4559        },
4560        Commands::Push(_) => vec!["push"],
4561        Commands::Pull(_) => vec!["pull"],
4562        Commands::Remote { command } => match command {
4563            RemoteCommands::List => vec!["remote", "list"],
4564            RemoteCommands::Add { .. } => vec!["remote", "add"],
4565            RemoteCommands::Remove { .. } => vec!["remote", "remove"],
4566            RemoteCommands::SetDefault { .. } => vec!["remote", "set-default"],
4567            RemoteCommands::Show { .. } => vec!["remote", "show"],
4568        },
4569        #[cfg(feature = "client")]
4570        Commands::Auth { command } => match command {
4571            AuthCommands::Login { .. } => vec!["auth", "login"],
4572            AuthCommands::Logout { .. } => vec!["auth", "logout"],
4573            AuthCommands::Status { .. } => vec!["auth", "status"],
4574            AuthCommands::DeriveAgent { .. } => vec!["auth", "derive-agent"],
4575            AuthCommands::CreateServiceToken { .. } => vec!["auth", "create-service-token"],
4576        },
4577        #[cfg(feature = "client")]
4578        Commands::Whoami { .. } => vec!["whoami"],
4579        Commands::Context { command } => match command {
4580            ContextCommands::Set(_) => vec!["context", "set"],
4581            ContextCommands::Get(_) => vec!["context", "get"],
4582            ContextCommands::List(_) => vec!["context", "list"],
4583            ContextCommands::History(_) => vec!["context", "history"],
4584            ContextCommands::Edit(_) => vec!["context", "edit"],
4585            ContextCommands::Supersede(_) => vec!["context", "supersede"],
4586            ContextCommands::Rm(_) => vec!["context", "rm"],
4587            ContextCommands::Check(_) => vec!["context", "check"],
4588            ContextCommands::Suggest(_) => vec!["context", "suggest"],
4589            ContextCommands::Audit(_) => vec!["context", "audit"],
4590            #[cfg(all(feature = "git-overlay", feature = "ingest"))]
4591            ContextCommands::Reason { command } => match command {
4592                crate::cli::cli_args::ContextReasonCommands::Git(_) => {
4593                    vec!["context", "reason", "git"]
4594                }
4595            },
4596        },
4597        Commands::Integration { command } => match command {
4598            IntegrationCommands::List => vec!["integration", "list"],
4599            IntegrationCommands::Install(_) => vec!["integration", "install"],
4600            IntegrationCommands::Doctor => vec!["integration", "doctor"],
4601            IntegrationCommands::Uninstall(_) => vec!["integration", "uninstall"],
4602            IntegrationCommands::Upgrade(_) => vec!["integration", "upgrade"],
4603            IntegrationCommands::Relay(_) => vec!["integration", "relay"],
4604        },
4605        #[cfg(feature = "semantic")]
4606        Commands::Semantic { command } => match command {
4607            SemanticCommands::Hot { .. } => vec!["semantic", "hot"],
4608            SemanticCommands::Index { .. } => vec!["semantic", "index"],
4609        },
4610        Commands::Daemon { command } => match command {
4611            DaemonCommands::Serve => vec!["daemon", "serve"],
4612            DaemonCommands::Status => vec!["daemon", "status"],
4613            DaemonCommands::Stop => vec!["daemon", "stop"],
4614        },
4615        Commands::Agent { command } => match command {
4616            AgentCommands::Reserve(_) => vec!["agent", "reserve"],
4617            AgentCommands::Heartbeat(_) => vec!["agent", "heartbeat"],
4618            AgentCommands::Capture(_) => vec!["agent", "capture"],
4619            AgentCommands::Ready(_) => vec!["agent", "ready"],
4620            AgentCommands::Release(_) => vec!["agent", "release"],
4621            AgentCommands::List(_) => vec!["agent", "list"],
4622            AgentCommands::Task(command) => match command {
4623                AgentTaskCommands::Create(_) => vec!["agent", "task", "create"],
4624                AgentTaskCommands::List(_) => vec!["agent", "task", "list"],
4625                AgentTaskCommands::Show(_) => vec!["agent", "task", "show"],
4626                AgentTaskCommands::Update(_) => vec!["agent", "task", "update"],
4627            },
4628            AgentCommands::Fanout(command) => match command {
4629                AgentFanoutCommands::Plan(_) => vec!["agent", "fanout", "plan"],
4630                AgentFanoutCommands::Start(_) => vec!["agent", "fanout", "start"],
4631            },
4632            AgentCommands::Presence(command) => match command {
4633                AgentPresenceCommands::List(_) => vec!["agent", "presence", "list"],
4634                AgentPresenceCommands::Show(_) => vec!["agent", "presence", "show"],
4635                AgentPresenceCommands::Explain(_) => vec!["agent", "presence", "explain"],
4636                AgentPresenceCommands::Complete(_) => vec!["agent", "presence", "complete"],
4637            },
4638            AgentCommands::Provenance(command) => match command {
4639                AgentProvenanceCommands::Begin(_) => vec!["agent", "provenance", "begin"],
4640                AgentProvenanceCommands::Segment(_) => vec!["agent", "provenance", "segment"],
4641                AgentProvenanceCommands::End(_) => vec!["agent", "provenance", "end"],
4642                AgentProvenanceCommands::Show(_) => vec!["agent", "provenance", "show"],
4643                AgentProvenanceCommands::List(_) => vec!["agent", "provenance", "list"],
4644            },
4645        },
4646        Commands::Maintenance { command } => match command {
4647            MaintenanceCommands::Inspect => vec!["maintenance", "inspect"],
4648            MaintenanceCommands::Refresh => vec!["maintenance", "refresh"],
4649            MaintenanceCommands::Gc { .. } => vec!["maintenance", "gc"],
4650        },
4651        Commands::Clone(_) => vec!["clone"],
4652        Commands::Hook { command } => match command {
4653            HookCommands::List => vec!["hook", "list"],
4654            HookCommands::Install { .. } => vec!["hook", "install"],
4655            HookCommands::Uninstall { .. } => vec!["hook", "uninstall"],
4656            HookCommands::Events { .. } => vec!["hook", "events"],
4657        },
4658    }
4659}
4660
4661#[cfg(test)]
4662mod tests;