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