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