use std::sync::OnceLock;
use clap::{ArgAction, CommandFactory};
pub use heddle_core::ActionTemplate;
use heddle_core::{
DiffReport, FsckReport, MachineOutputKind, QueryReport, ReportContract as CoreReportContract,
StatusReport, VerifyReport,
};
use schemars::JsonSchema;
use serde::Serialize;
#[cfg(feature = "semantic")]
use crate::cli::SemanticCommands;
use crate::cli::{
ActorCommands, AgentCommands, Cli, Commands, ContextCommands, DaemonCommands, DoctorCommands,
HookCommands, IntegrationCommands, MaintenanceCommands, OplogCommands, PurgeCommands,
RedactCommands, RedactTrustCommands, RemoteCommands, SessionCommands, ShellCommands,
StashCommands, ThreadCommands, ThreadMarkerCommands, TimelineCommands, VisibilityCommands,
cli_args::{
AgentFanoutCommands, AgentTaskCommands, DiscussCommands, ReviewCommands,
TransactionCommands,
},
render::shell_quote,
};
#[cfg(feature = "client")]
use crate::cli::{AuthCommands, PresenceCommands, SupportCommands};
#[cfg(feature = "git-overlay")]
use crate::cli::{BridgeCommands, GitCommands};
#[derive(Debug, Clone, Serialize, JsonSchema)]
pub struct CommandCatalogOutput {
pub kind: String,
pub executable_path: String,
pub commands: Vec<CommandCatalogEntry>,
pub global_options: Vec<CommandCatalogOption>,
pub json_discriminators: Vec<CommandJsonDiscriminator>,
pub recommended_action_placeholders: Vec<String>,
pub recommended_action_templates: Vec<ActionTemplate>,
}
#[derive(Debug, Clone, Serialize, JsonSchema)]
pub struct CommandCatalogEntry {
pub path: Vec<String>,
pub display: String,
pub aliases: Vec<String>,
pub tier: String,
pub surface: String,
pub help_visibility: String,
pub help_rank: u16,
pub canonical_command: Option<String>,
pub canonical_action: Option<CanonicalAction>,
pub command_action: Option<CommandAction>,
pub summary: String,
pub has_subcommands: bool,
pub supports_json: bool,
pub mutates: bool,
pub supports_op_id: bool,
pub persists_op_id: bool,
pub op_id_behavior: String,
pub op_id_store_scope: String,
pub observe_only: bool,
pub may_initialize: bool,
pub may_import_git: bool,
pub may_write_worktree: bool,
pub may_move_ref: bool,
pub destructive_requires_force: bool,
pub writes_heddle_refs: bool,
pub writes_git_refs: bool,
pub writes_worktree: bool,
pub writes_config: bool,
pub writes_hooks: bool,
pub network_io: bool,
pub daemon_process: bool,
pub object_gc: bool,
pub external_command: bool,
pub requires_git_executable: bool,
pub destructive_data: bool,
pub side_effects: Vec<String>,
pub side_effect_class: String,
pub first_run_behavior: String,
pub json_kind: String,
pub json_discriminators: Vec<CommandJsonDiscriminator>,
pub schema_verbs: Vec<String>,
pub documented_schema_verbs: Vec<String>,
pub options: Vec<CommandCatalogOption>,
pub arguments: Vec<CommandCatalogArgument>,
pub exit_codes: Vec<CommandCatalogExitCode>,
}
#[derive(Debug, Clone, Serialize, JsonSchema)]
pub struct CommandCatalogExitCode {
pub code: u8,
pub reason: String,
}
#[derive(Debug, Clone, Serialize, JsonSchema)]
pub struct CanonicalAction {
pub command: String,
pub kind: String,
pub executable: bool,
pub note: String,
pub argv: Option<Vec<String>>,
pub template: Option<ActionTemplate>,
}
#[derive(Debug, Clone, Serialize, JsonSchema)]
pub struct CommandAction {
pub action: String,
pub executable: bool,
pub argv: Option<Vec<String>>,
pub template: Option<ActionTemplate>,
}
#[derive(Debug, Clone, Serialize, JsonSchema)]
pub struct CommandCatalogOption {
pub id: String,
pub long: Option<String>,
pub aliases: Vec<String>,
pub short: Option<String>,
pub value_names: Vec<String>,
pub value_kind: String,
pub default_values: Vec<String>,
pub possible_values: Vec<String>,
pub help: Option<String>,
pub required: bool,
pub global: bool,
pub hidden: bool,
}
#[derive(Debug, Clone, Serialize, JsonSchema)]
pub struct CommandCatalogArgument {
pub id: String,
pub value_names: Vec<String>,
pub help: Option<String>,
pub required: bool,
}
#[derive(Debug, Clone)]
pub(crate) struct ActionFields {
pub action: Option<String>,
pub template: Option<ActionTemplate>,
}
impl ActionFields {
pub(crate) fn from_optional_action(action: Option<String>) -> Self {
let Some(action) = action.filter(|action| !action.trim().is_empty()) else {
return Self::none();
};
validate_recommended_action(&action)
.unwrap_or_else(|err| panic!("invalid recommended action `{action}`: {err}"));
Self {
template: recommended_action_template(&action),
action: Some(action),
}
}
pub(crate) fn from_optional_action_ref(action: Option<&str>) -> Self {
Self::from_optional_action(action.map(str::to_string))
}
pub(crate) fn from_action(action: &str) -> Self {
Self::from_optional_action_ref(Some(action))
}
pub(crate) fn none() -> Self {
Self {
action: None,
template: None,
}
}
}
pub(crate) fn checked_action_from_argv<I, S>(argv: I) -> String
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
let action = argv
.into_iter()
.map(|arg| shell_quote(arg.as_ref()))
.collect::<Vec<_>>()
.join(" ");
validate_recommended_action(&action)
.unwrap_or_else(|err| panic!("invalid recommended action `{action}`: {err}"));
action
}
pub(crate) fn heddle_action<I, S>(args: I) -> String
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
let argv = std::iter::once("heddle".to_string())
.chain(args.into_iter().map(|arg| arg.as_ref().to_string()))
.collect::<Vec<_>>();
checked_action_from_argv(argv)
}
pub(crate) fn thread_flag_args(thread_id: &str) -> Vec<String> {
if thread_id.starts_with('-') {
vec![format!("--thread={thread_id}")]
} else {
vec!["--thread".to_string(), thread_id.to_string()]
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, JsonSchema)]
pub struct CommandJsonDiscriminator {
pub path: Vec<String>,
pub display: String,
pub schema_verb: Option<String>,
pub field: String,
pub value: String,
pub no_schema_reason: Option<String>,
}
impl CommandCatalogOutput {
pub fn command_by_display(&self, display: &str) -> Option<&CommandCatalogEntry> {
self.commands.iter().find(|entry| entry.display == display)
}
pub fn command_by_path(&self, path: &[String]) -> Option<&CommandCatalogEntry> {
self.commands.iter().find(|entry| entry.path == path)
}
pub fn options_for_display(&self, display: &str) -> Option<Vec<&CommandCatalogOption>> {
let entry = self.command_by_display(display)?;
Some(self.options_for_entry(entry))
}
pub fn options_for_path(&self, path: &[String]) -> Option<Vec<&CommandCatalogOption>> {
let entry = self.command_by_path(path)?;
Some(self.options_for_entry(entry))
}
pub fn options_for_entry<'a>(
&'a self,
entry: &'a CommandCatalogEntry,
) -> Vec<&'a CommandCatalogOption> {
self.global_options
.iter()
.chain(entry.options.iter())
.collect()
}
}
#[derive(Debug, Clone, Copy)]
struct CommandContract {
supports_json: bool,
supports_json_compact: bool,
mutates: bool,
supports_op_id: bool,
persists_op_id: bool,
observe_only: bool,
may_initialize: bool,
may_import_git: bool,
may_write_worktree: bool,
may_move_ref: bool,
destructive_requires_force: bool,
writes_heddle_refs: bool,
writes_git_refs: bool,
writes_worktree: bool,
writes_config: bool,
writes_hooks: bool,
network_io: bool,
daemon_process: bool,
object_gc: bool,
external_command: bool,
requires_git_executable: bool,
destructive_data: bool,
operator_envelope: bool,
json_kind: &'static str,
report_contract: Option<CoreReportContract>,
json_discriminators: &'static [CommandJsonDiscriminatorSpec],
schema_verbs: &'static [&'static str],
documented_schema_verbs: &'static [&'static str],
opaque_schema_verbs: &'static [&'static str],
surface: &'static str,
help_visibility: &'static str,
help_rank: u16,
help_category: Option<&'static str>,
canonical_command: Option<&'static str>,
canonical_kind: Option<&'static str>,
canonical_note: Option<&'static str>,
advertised_action: Option<AdvertisedAction>,
feature_gate: Option<&'static str>,
exit_codes: &'static [(u8, &'static str)],
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct CommandJsonDiscriminatorSpec {
schema_verb: Option<&'static str>,
field: &'static str,
value: &'static str,
no_schema_reason: Option<&'static str>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AdvertisedAction {
action: &'static str,
argv_template: &'static [&'static str],
required_inputs: &'static [&'static str],
agent_may_fill: bool,
executable: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CommandRuntimeContract {
pub path: Vec<&'static str>,
pub display: String,
pub supports_json: bool,
pub supports_json_compact: bool,
pub supports_op_id: bool,
pub persists_op_id: bool,
pub uses_bootstrap_op_id_store: bool,
pub help_visibility: &'static str,
pub help_rank: u16,
pub surface: &'static str,
pub canonical_command: Option<&'static str>,
pub json_kind: &'static str,
}
#[derive(Debug, Clone, Copy)]
struct CommandContractEntry {
path: &'static [&'static str],
contract: CommandContract,
}
const RECOMMENDED_ACTION_PLACEHOLDERS: &[&str] = &[
"heddle capture -m \"...\"",
"heddle capture -m \"...\" --confidence <confidence>",
"heddle checkpoint -m \"...\"",
"heddle commit -m \"...\"",
"heddle commit -m \"...\" --confidence <confidence>",
"heddle init --principal-name <name> --principal-email <email>",
"heddle ready -m \"...\"",
"heddle context get --path <path>",
"heddle context set --path <path> --scope file -m \"...\"",
"heddle session start",
"heddle start <name> --path <empty-path>",
"heddle start <name> --path ../<name>",
"heddle actor show <session>",
"heddle stash push -m \"...\"",
"heddle thread show <THREAD>",
"heddle remote add <name> <url>",
"heddle remote set-default <name>",
"heddle clone <local-path> <path>",
"heddle clone <remote> <path>",
"heddle clone <remote> <new-path>",
"heddle clone <remote> <fresh-path>",
"heddle clone <remote> <path> --thread <thread>",
"heddle bridge git import --path <full-git-repo>",
"heddle bridge git import --path <full-git-repo> --ref <ref>",
"heddle switch <branch>",
"heddle merge <thread> --git-commit",
];
const RECOMMENDED_ACTION_TEMPLATES: &[(&str, &[&str], &[&str], bool)] = &[
(
"heddle capture -m \"...\"",
&["heddle", "capture", "-m", "<message>"],
&["message"],
true,
),
(
"heddle capture -m \"...\" --confidence <confidence>",
&[
"heddle",
"capture",
"-m",
"<message>",
"--confidence",
"<confidence>",
],
&["message", "confidence"],
true,
),
(
"heddle checkpoint -m \"...\"",
&["heddle", "checkpoint", "-m", "<message>"],
&["message"],
true,
),
(
"heddle commit -m \"...\"",
&["heddle", "commit", "-m", "<message>"],
&["message"],
true,
),
(
"heddle commit -m \"...\" --confidence <confidence>",
&[
"heddle",
"commit",
"-m",
"<message>",
"--confidence",
"<confidence>",
],
&["message", "confidence"],
true,
),
(
"heddle commit --all -m \"...\"",
&["heddle", "commit", "--all", "-m", "<message>"],
&["message"],
true,
),
(
"heddle init --principal-name <name> --principal-email <email>",
&[
"heddle",
"init",
"--principal-name",
"<name>",
"--principal-email",
"<email>",
],
&["name", "email"],
true,
),
(
"heddle ready -m \"...\"",
&["heddle", "ready", "-m", "<message>"],
&["message"],
true,
),
(
"heddle context get --path <path>",
&["heddle", "context", "get", "--path", "<path>"],
&["path"],
true,
),
(
"heddle context set --path <path> --scope file -m \"...\"",
&[
"heddle",
"context",
"set",
"--path",
"<path>",
"--scope",
"file",
"-m",
"<message>",
],
&["path", "message"],
true,
),
(
"heddle session start",
&[
"heddle",
"session",
"start",
"--provider",
"<provider>",
"--model",
"<model>",
],
&["provider", "model"],
true,
),
(
"heddle start <name> --path <empty-path>",
&["heddle", "start", "<name>", "--path", "<empty-path>"],
&["name", "path"],
true,
),
(
"heddle start <name> --path ../<name>",
&["heddle", "start", "<name>", "--path", "../<name>"],
&["name", "path"],
true,
),
(
"heddle actor show <session>",
&["heddle", "actor", "show", "<session>"],
&["session"],
true,
),
(
"heddle ready --thread <name>",
&["heddle", "ready", "--thread", "<thread>"],
&["thread"],
true,
),
(
"heddle land --thread <name>",
&["heddle", "land", "--thread", "<thread>"],
&["thread"],
true,
),
(
"heddle sync --thread <name>",
&["heddle", "sync", "--thread", "<thread>"],
&["thread"],
true,
),
(
"heddle run --thread <name> -- <cmd...>",
&["heddle", "run", "--thread", "<thread>", "--", "<cmd...>"],
&["thread", "command"],
false,
),
(
"heddle thread switch <name>",
&["heddle", "thread", "switch", "<thread>"],
&["thread"],
true,
),
(
"heddle thread switch <other>",
&["heddle", "thread", "switch", "<other>"],
&["other"],
true,
),
(
"heddle thread create <other>",
&["heddle", "thread", "create", "<other>"],
&["other"],
true,
),
(
"heddle thread show <THREAD>",
&["heddle", "thread", "show", "<thread>"],
&["thread"],
true,
),
(
"heddle start <task> --parent-thread <THREAD>",
&["heddle", "start", "<task>", "--parent-thread", "<thread>"],
&["thread", "task"],
false,
),
(
"heddle stash push -m \"...\"",
&["heddle", "stash", "push", "-m", "<message>"],
&["message"],
true,
),
(
"heddle remote add <name> <url>",
&["heddle", "remote", "add", "<name>", "<url>"],
&["name", "url"],
false,
),
(
"heddle remote set-default <name>",
&["heddle", "remote", "set-default", "<name>"],
&["name"],
false,
),
(
"heddle merge <thread> --git-commit",
&["heddle", "merge", "<thread>", "--git-commit"],
&["thread"],
false,
),
(
"heddle clone <local-path> <path>",
&["heddle", "clone", "<local-path>", "<path>"],
&["local_path", "path"],
false,
),
(
"heddle clone <remote> <path>",
&["heddle", "clone", "<remote>", "<path>"],
&["remote", "path"],
false,
),
(
"heddle clone <remote> <new-path>",
&["heddle", "clone", "<remote>", "<new-path>"],
&["remote", "path"],
false,
),
(
"heddle clone <remote> <path> --thread <thread>",
&[
"heddle", "clone", "<remote>", "<path>", "--thread", "<thread>",
],
&["remote", "path", "thread"],
false,
),
(
"heddle clone <remote> <fresh-path>",
&["heddle", "clone", "<remote>", "<fresh-path>"],
&["remote", "path"],
false,
),
(
"heddle bridge git import --path <full-git-repo>",
&[
"heddle",
"bridge",
"git",
"import",
"--path",
"<full-git-repo>",
],
&["path"],
false,
),
(
"heddle bridge git import --path <full-git-repo> --ref <ref>",
&[
"heddle",
"bridge",
"git",
"import",
"--path",
"<full-git-repo>",
"--ref",
"<ref>",
],
&["path", "ref"],
false,
),
(
"heddle switch <branch>",
&["heddle", "switch", "<branch>"],
&["branch"],
false,
),
];
const READ_JSON: CommandContract = CommandContract {
supports_json: true,
supports_json_compact: false,
mutates: false,
supports_op_id: false,
persists_op_id: false,
observe_only: true,
may_initialize: false,
may_import_git: false,
may_write_worktree: false,
may_move_ref: false,
destructive_requires_force: false,
writes_heddle_refs: false,
writes_git_refs: false,
writes_worktree: false,
writes_config: false,
writes_hooks: false,
network_io: false,
daemon_process: false,
object_gc: false,
external_command: false,
requires_git_executable: false,
destructive_data: false,
operator_envelope: false,
json_kind: "json",
report_contract: None,
json_discriminators: &[],
schema_verbs: &[],
documented_schema_verbs: &[],
opaque_schema_verbs: &[],
surface: "native",
help_visibility: "advanced",
help_rank: 1000,
help_category: None,
canonical_command: None,
canonical_kind: None,
canonical_note: None,
advertised_action: None,
feature_gate: None,
exit_codes: &[],
};
const READ_TEXT: CommandContract = CommandContract {
supports_json: false,
json_kind: "none",
..READ_JSON
};
const GROUP: CommandContract = CommandContract {
supports_json: false,
json_kind: "none",
..READ_JSON
};
const READ_JSONL: CommandContract = CommandContract {
json_kind: "jsonl",
..READ_JSON
};
const READ_JSON_OR_JSONL: CommandContract = CommandContract {
json_kind: "json_or_jsonl",
..READ_JSON
};
const MUTATING: CommandContract = CommandContract {
mutates: true,
supports_op_id: true,
observe_only: false,
may_move_ref: true,
writes_heddle_refs: true,
..READ_JSON
};
const MUTATING_NO_OP_ID: CommandContract = CommandContract {
supports_op_id: false,
..MUTATING
};
const MUTATING_TEXT: CommandContract = CommandContract {
supports_json: false,
supports_op_id: false,
json_kind: "none",
..MUTATING
};
const INIT: CommandContract = CommandContract {
may_initialize: true,
may_move_ref: false,
writes_heddle_refs: false,
writes_config: true,
..MUTATING
};
const CAPTURE: CommandContract = CommandContract { ..MUTATING };
const fn compact_json(contract: CommandContract) -> CommandContract {
CommandContract {
supports_json_compact: true,
..contract
}
}
const fn operator_envelope(contract: CommandContract) -> CommandContract {
CommandContract {
operator_envelope: true,
..contract
}
}
const WORKTREE_MUTATION: CommandContract = CommandContract {
may_write_worktree: true,
writes_worktree: true,
..MUTATING
};
const WORKTREE_MUTATION_JSONL: CommandContract = CommandContract {
json_kind: "jsonl",
..WORKTREE_MUTATION
};
const WORKTREE_ONLY_MUTATION: CommandContract = CommandContract {
may_move_ref: false,
writes_heddle_refs: false,
..WORKTREE_MUTATION
};
const DESTRUCTIVE_WORKTREE_MUTATION: CommandContract = CommandContract {
destructive_requires_force: true,
destructive_data: true,
..WORKTREE_MUTATION
};
const DESTRUCTIVE_WORKTREE_ONLY_MUTATION: CommandContract = CommandContract {
destructive_requires_force: true,
destructive_data: true,
..WORKTREE_ONLY_MUTATION
};
const DATA_MUTATION: CommandContract = CommandContract {
may_move_ref: false,
writes_heddle_refs: false,
..MUTATING
};
const DESTRUCTIVE_DATA_MUTATION: CommandContract = CommandContract {
destructive_data: true,
..DATA_MUTATION
};
const IMPORTING_MUTATION: CommandContract = CommandContract {
may_import_git: true,
..MUTATING
};
const ADOPT: CommandContract = CommandContract {
may_initialize: true,
may_import_git: true,
writes_config: true,
..MUTATING
};
const CONFIG_MUTATION: CommandContract = CommandContract {
may_move_ref: false,
writes_heddle_refs: false,
writes_config: true,
..MUTATING
};
const HOOK_MUTATION: CommandContract = CommandContract {
writes_hooks: true,
..CONFIG_MUTATION
};
const DAEMON_MUTATION: CommandContract = CommandContract {
may_move_ref: false,
writes_heddle_refs: false,
daemon_process: true,
..MUTATING_NO_OP_ID
};
const GC_MUTATION: CommandContract = CommandContract {
may_move_ref: false,
writes_heddle_refs: false,
object_gc: true,
..MUTATING
};
const EXTERNAL_COMMAND_MUTATION: CommandContract = CommandContract {
may_move_ref: false,
writes_heddle_refs: false,
external_command: true,
..MUTATING_TEXT
};
const EXTERNAL_WORKTREE_COMMAND: CommandContract = CommandContract {
may_write_worktree: true,
external_command: true,
..EXTERNAL_COMMAND_MUTATION
};
const EXTERNAL_WORKTREE_MUTATION: CommandContract = CommandContract {
external_command: true,
..WORKTREE_MUTATION
};
const fn documented_schemas(
contract: CommandContract,
schema_verbs: &'static [&'static str],
) -> CommandContract {
CommandContract {
schema_verbs,
documented_schema_verbs: schema_verbs,
..contract
}
}
const fn documented_core_report_schema(
contract: CommandContract,
report_contract: CoreReportContract,
) -> CommandContract {
CommandContract {
supports_json: true,
json_kind: machine_output_kind_json_kind(report_contract.machine_output_kind),
report_contract: Some(report_contract),
..contract
}
}
const fn opaque_schemas(
contract: CommandContract,
schema_verbs: &'static [&'static str],
) -> CommandContract {
CommandContract {
schema_verbs,
documented_schema_verbs: schema_verbs,
opaque_schema_verbs: schema_verbs,
..contract
}
}
const fn json_discriminators(
contract: CommandContract,
discriminators: &'static [CommandJsonDiscriminatorSpec],
) -> CommandContract {
CommandContract {
json_discriminators: discriminators,
..contract
}
}
const fn json_discriminator(
schema_verb: Option<&'static str>,
field: &'static str,
value: &'static str,
) -> CommandJsonDiscriminatorSpec {
CommandJsonDiscriminatorSpec {
schema_verb,
field,
value,
no_schema_reason: None,
}
}
fn contract_schema_verbs(contract: CommandContract) -> impl Iterator<Item = &'static str> {
contract
.report_contract
.into_iter()
.map(|report_contract| report_contract.schema_name)
.chain(contract.schema_verbs.iter().copied())
}
fn contract_documented_schema_verbs(
contract: CommandContract,
) -> impl Iterator<Item = &'static str> {
contract
.report_contract
.into_iter()
.map(|report_contract| report_contract.schema_name)
.chain(contract.documented_schema_verbs.iter().copied())
}
fn contract_json_discriminators(
contract: CommandContract,
) -> impl Iterator<Item = CommandJsonDiscriminatorSpec> {
contract
.report_contract
.into_iter()
.filter_map(report_json_discriminator_from_contract)
.chain(contract.json_discriminators.iter().copied())
}
fn report_json_discriminator_from_contract(
report_contract: CoreReportContract,
) -> Option<CommandJsonDiscriminatorSpec> {
report_contract
.output_discriminator
.map(|discriminator| CommandJsonDiscriminatorSpec {
schema_verb: Some(report_contract.schema_name),
field: discriminator.field,
value: discriminator.value,
no_schema_reason: None,
})
}
const fn machine_output_kind_json_kind(kind: MachineOutputKind) -> &'static str {
match kind {
MachineOutputKind::Json => "json",
MachineOutputKind::JsonLines => "jsonl",
MachineOutputKind::JsonOrJsonLines => "json_or_jsonl",
}
}
const fn json_discriminator_no_schema(
reason: &'static str,
field: &'static str,
value: &'static str,
) -> CommandJsonDiscriminatorSpec {
CommandJsonDiscriminatorSpec {
schema_verb: None,
field,
value,
no_schema_reason: Some(reason),
}
}
const fn front_door(contract: CommandContract, help_rank: u16) -> CommandContract {
CommandContract {
help_visibility: "everyday",
help_rank,
..contract
}
}
const fn hidden(contract: CommandContract) -> CommandContract {
CommandContract {
surface: "internal",
help_visibility: "hidden",
..contract
}
}
const fn surface(contract: CommandContract, surface: &'static str) -> CommandContract {
CommandContract {
surface,
..contract
}
}
const fn category(contract: CommandContract, help_category: &'static str) -> CommandContract {
CommandContract {
help_category: Some(help_category),
..contract
}
}
const fn feature_gated(contract: CommandContract, feature_gate: &'static str) -> CommandContract {
CommandContract {
feature_gate: Some(feature_gate),
..contract
}
}
const QUERY_ATTRIBUTION_SCHEMA_VERBS: &[&str] = &["query --attribution"];
const QUERY_JSON_DISCRIMINATORS: &[CommandJsonDiscriminatorSpec] = &[json_discriminator(
Some("query --attribution"),
"output_kind",
"query_attribution",
)];
const fn exits(
contract: CommandContract,
exit_codes: &'static [(u8, &'static str)],
) -> CommandContract {
CommandContract {
exit_codes,
..contract
}
}
const fn git_adapter_alias(
contract: CommandContract,
canonical_command: &'static str,
) -> CommandContract {
git_adapter_action(
contract,
canonical_command,
"direct_command",
"Use this native Heddle command for the same operation.",
)
}
const fn git_adapter_action(
contract: CommandContract,
canonical_command: &'static str,
canonical_kind: &'static str,
canonical_note: &'static str,
) -> CommandContract {
CommandContract {
surface: "git_adapter",
help_visibility: "git_adapter",
canonical_command: Some(canonical_command),
canonical_kind: Some(canonical_kind),
canonical_note: Some(canonical_note),
..contract
}
}
const fn advertised_action(
contract: CommandContract,
action: &'static str,
argv_template: &'static [&'static str],
required_inputs: &'static [&'static str],
agent_may_fill: bool,
executable: bool,
) -> CommandContract {
CommandContract {
advertised_action: Some(AdvertisedAction {
action,
argv_template,
required_inputs,
agent_may_fill,
executable,
}),
..contract
}
}
const CONTRACTS: &[CommandContractEntry] = &[
entry(
&["abort"],
category(
json_discriminators(
documented_schemas(operator_envelope(compact_json(MUTATING)), &["abort"]),
&[json_discriminator(Some("abort"), "output_kind", "abort")],
),
"recovery",
),
),
entry(
&["adopt"],
front_door(
advertised_action(
json_discriminators(
documented_schemas(ADOPT, &["adopt"]),
&[json_discriminator(Some("adopt"), "output_kind", "adopt")],
),
"heddle adopt --ref <branch>",
&["heddle", "adopt", "--ref", "<branch>"],
&["branch"],
true,
false,
),
210,
),
),
entry(&["actor"], surface(GROUP, "automation")),
entry(
&["actor", "spawn"],
surface(
json_discriminators(
documented_schemas(MUTATING, &["actor spawn"]),
&[json_discriminator(
Some("actor spawn"),
"output_kind",
"actor_spawn",
)],
),
"automation",
),
),
entry(
&["actor", "list"],
surface(
json_discriminators(
documented_schemas(READ_JSON, &["actor list"]),
&[json_discriminator(
Some("actor list"),
"output_kind",
"actor_list",
)],
),
"automation",
),
),
entry(
&["actor", "show"],
surface(
json_discriminators(
documented_schemas(READ_JSON, &["actor show"]),
&[json_discriminator(
Some("actor show"),
"output_kind",
"actor_show",
)],
),
"automation",
),
),
entry(
&["actor", "explain"],
surface(
json_discriminators(
documented_schemas(READ_JSON, &["actor explain"]),
&[json_discriminator(
Some("actor explain"),
"output_kind",
"actor_explain",
)],
),
"automation",
),
),
entry(
&["actor", "done"],
surface(
json_discriminators(
documented_schemas(MUTATING, &["actor done"]),
&[json_discriminator(
Some("actor done"),
"output_kind",
"actor_done",
)],
),
"automation",
),
),
entry(&["agent"], surface(GROUP, "automation")),
entry(
&["agent", "serve"],
surface(
json_discriminators(
documented_schemas(DAEMON_MUTATION, &["agent serve"]),
&[json_discriminator(
Some("agent serve"),
"output_kind",
"agent_serve",
)],
),
"automation",
),
),
entry(
&["agent", "status"],
surface(
json_discriminators(
documented_schemas(READ_JSON, &["agent status"]),
&[json_discriminator(
Some("agent status"),
"output_kind",
"agent_status",
)],
),
"automation",
),
),
entry(
&["agent", "stop"],
surface(
json_discriminators(
documented_schemas(DAEMON_MUTATION, &["agent stop"]),
&[json_discriminator(
Some("agent stop"),
"output_kind",
"agent_stop",
)],
),
"automation",
),
),
entry(
&["agent", "reserve"],
surface(
documented_schemas(MUTATING, &["agent reserve"]),
"automation",
),
),
entry(
&["agent", "heartbeat"],
surface(
documented_schemas(MUTATING, &["agent heartbeat"]),
"automation",
),
),
entry(
&["agent", "capture"],
surface(
json_discriminators(
documented_schemas(CAPTURE, &["agent capture"]),
&[json_discriminator(
Some("agent capture"),
"output_kind",
"capture",
)],
),
"automation",
),
),
entry(
&["agent", "ready"],
surface(
json_discriminators(
documented_schemas(CAPTURE, &["agent ready"]),
&[json_discriminator(
Some("agent ready"),
"output_kind",
"ready",
)],
),
"automation",
),
),
entry(
&["agent", "release"],
surface(
documented_schemas(MUTATING, &["agent release"]),
"automation",
),
),
entry(
&["agent", "list"],
surface(documented_schemas(READ_JSON, &["agent list"]), "automation"),
),
entry(&["agent", "task"], surface(GROUP, "automation")),
entry(
&["agent", "task", "create"],
surface(
json_discriminators(
documented_schemas(MUTATING, &["agent task create"]),
&[json_discriminator(
Some("agent task create"),
"output_kind",
"agent_task_create",
)],
),
"automation",
),
),
entry(
&["agent", "task", "list"],
surface(
json_discriminators(
documented_schemas(READ_JSON, &["agent task list"]),
&[json_discriminator(
Some("agent task list"),
"output_kind",
"agent_task_list",
)],
),
"automation",
),
),
entry(
&["agent", "task", "show"],
surface(
json_discriminators(
documented_schemas(READ_JSON, &["agent task show"]),
&[json_discriminator(
Some("agent task show"),
"output_kind",
"agent_task_show",
)],
),
"automation",
),
),
entry(
&["agent", "task", "update"],
surface(
json_discriminators(
documented_schemas(MUTATING, &["agent task update"]),
&[json_discriminator(
Some("agent task update"),
"output_kind",
"agent_task_update",
)],
),
"automation",
),
),
entry(&["agent", "fanout"], surface(GROUP, "automation")),
entry(
&["agent", "fanout", "plan"],
surface(
json_discriminators(
documented_schemas(READ_JSON, &["agent fanout plan"]),
&[json_discriminator(
Some("agent fanout plan"),
"output_kind",
"agent_fanout_plan",
)],
),
"automation",
),
),
entry(
&["agent", "fanout", "start"],
surface(
json_discriminators(
documented_schemas(WORKTREE_MUTATION, &["agent fanout start"]),
&[json_discriminator(
Some("agent fanout start"),
"output_kind",
"agent_fanout_start",
)],
),
"automation",
),
),
entry(&["auth"], category(feature_gated(GROUP, "client"), "repo")),
entry(&["auth", "login"], feature_gated(MUTATING_TEXT, "client")),
entry(
&["auth", "logout"],
feature_gated(
json_discriminators(
documented_schemas(MUTATING_NO_OP_ID, &["auth logout"]),
&[json_discriminator(
Some("auth logout"),
"output_kind",
"auth_logout",
)],
),
"client",
),
),
entry(
&["auth", "status"],
feature_gated(
json_discriminators(
documented_schemas(READ_JSON, &["auth status"]),
&[json_discriminator(
Some("auth status"),
"output_kind",
"auth_status",
)],
),
"client",
),
),
entry(
&["auth", "create-service-token"],
feature_gated(
json_discriminators(
documented_schemas(MUTATING_NO_OP_ID, &["auth create-service-token"]),
&[json_discriminator(
Some("auth create-service-token"),
"output_kind",
"auth_create_service_token",
)],
),
"client",
),
),
entry(&["bridge"], surface(GROUP, "git_adapter")),
entry(&["bridge", "git"], surface(GROUP, "git_adapter")),
entry(
&["bridge", "git", "status"],
git_adapter_alias(
json_discriminators(
documented_schemas(READ_JSON, &["bridge git status"]),
&[json_discriminator(
Some("bridge git status"),
"output_kind",
"bridge_git_status",
)],
),
"status",
),
),
entry(
&["bridge", "git", "init"],
git_adapter_alias(documented_schemas(INIT, &["bridge git init"]), "init"),
),
entry(
&["bridge", "git", "export"],
git_adapter_alias(
documented_schemas(
CommandContract {
writes_git_refs: true,
..MUTATING
},
&["bridge git export"],
),
"push",
),
),
entry(
&["bridge", "git", "import"],
exits(
git_adapter_action(
json_discriminators(
documented_schemas(IMPORTING_MUTATION, &["bridge git import"]),
&[json_discriminator(
Some("bridge git import"),
"output_kind",
"bridge_git_import",
)],
),
"adopt",
"workflow",
"Use adopt for the guided Git-to-Heddle conversion workflow.",
),
&[
(0, "ok"),
(65, "malformed git repo or unimportable refs"),
(74, "io reading git refs"),
],
),
),
entry(
&["bridge", "git", "sync"],
exits(
git_adapter_action(
json_discriminators(
documented_schemas(IMPORTING_MUTATION, &["bridge git sync"]),
&[json_discriminator(
Some("bridge git sync"),
"output_kind",
"bridge_git_sync",
)],
),
"adopt",
"workflow",
"Use adopt for the guided Git-to-Heddle conversion workflow.",
),
&[
(0, "ok"),
(75, "remote unreachable; safe to retry"),
(76, "remote rejected payload"),
],
),
),
entry(
&["bridge", "git", "reconcile"],
exits(
git_adapter_action(
json_discriminators(
documented_schemas(IMPORTING_MUTATION, &["bridge git reconcile"]),
&[json_discriminator(
Some("bridge git reconcile"),
"output_kind",
"bridge_git_reconcile",
)],
),
"adopt",
"workflow",
"Use adopt for the guided Git-to-Heddle conversion workflow.",
),
&[
(0, "ok"),
(65, "unmergeable divergence; manual resolution required"),
],
),
),
entry(
&["bridge", "git", "push"],
git_adapter_alias(
json_discriminators(
documented_schemas(
CommandContract {
writes_heddle_refs: false,
writes_git_refs: true,
network_io: true,
..MUTATING
},
&["bridge git push"],
),
&[json_discriminator(
Some("bridge git push"),
"output_kind",
"bridge_git_push",
)],
),
"push",
),
),
entry(
&["bridge", "git", "pull"],
git_adapter_alias(
json_discriminators(
documented_schemas(
CommandContract {
writes_git_refs: true,
network_io: true,
..WORKTREE_MUTATION
},
&["bridge git pull"],
),
&[json_discriminator(
Some("bridge git pull"),
"output_kind",
"bridge_git_pull",
)],
),
"pull",
),
),
entry(
&["bridge", "git", "reason"],
surface(
opaque_schemas(DATA_MUTATION, &["bridge git reason"]),
"git_adapter",
),
),
entry(
&["capture"],
category(
json_discriminators(
documented_schemas(compact_json(CAPTURE), &["capture"]),
&[json_discriminator(
Some("capture"),
"output_kind",
"capture",
)],
),
"states",
),
),
entry(
&["checkpoint"],
category(
json_discriminators(
documented_schemas(
CommandContract {
writes_git_refs: true,
..CAPTURE
},
&["checkpoint"],
),
&[json_discriminator(
Some("checkpoint"),
"output_kind",
"checkpoint",
)],
),
"states",
),
),
entry(
&["cherry-pick"],
category(
json_discriminators(
opaque_schemas(WORKTREE_MUTATION, &["cherry-pick"]),
&[json_discriminator(
Some("cherry-pick"),
"output_kind",
"cherry_pick",
)],
),
"states",
),
),
entry(
&["clean"],
category(
json_discriminators(
documented_schemas(DESTRUCTIVE_WORKTREE_ONLY_MUTATION, &["clean"]),
&[json_discriminator(Some("clean"), "output_kind", "clean")],
),
"recovery",
),
),
entry(
&["clone"],
front_door(
json_discriminators(
documented_schemas(
CommandContract {
may_initialize: true,
may_write_worktree: true,
may_move_ref: true,
writes_worktree: true,
network_io: true,
..MUTATING
},
&["clone"],
),
&[
json_discriminator(Some("clone"), "output_kind", "clone"),
json_discriminator_no_schema(
"preliminary connection envelope emitted by hosted clones \
before the final clone payload (no separate schema)",
"output_kind",
"clone_connection",
),
],
),
220,
),
),
entry(
&["collapse"],
category(opaque_schemas(MUTATING, &["collapse"]), "states"),
),
entry(
&["expand"],
category(
json_discriminators(
documented_schemas(READ_JSON, &["expand"]),
&[json_discriminator(Some("expand"), "output_kind", "expand")],
),
"states",
),
),
entry(
&["commit"],
exits(
front_door(
advertised_action(
json_discriminators(
documented_schemas(
CommandContract {
writes_git_refs: true,
..CAPTURE
},
&["commit"],
),
&[json_discriminator(Some("commit"), "output_kind", "commit")],
),
"heddle commit -m <message>",
&["heddle", "commit", "-m", "<message>"],
&["message"],
true,
false,
),
30,
),
&[
(0, "ok"),
(65, "dirty worktree refused or unmergeable input"),
(74, "io while writing state"),
],
),
),
entry(
&["continue"],
category(
json_discriminators(
documented_schemas(operator_envelope(compact_json(MUTATING)), &["continue"]),
&[json_discriminator(
Some("continue"),
"output_kind",
"continue",
)],
),
"recovery",
),
),
entry(&["context"], category(GROUP, "collab")),
entry(
&["context", "set"],
json_discriminators(
opaque_schemas(MUTATING, &["context set"]),
&[json_discriminator(
Some("context set"),
"output_kind",
"context_set",
)],
),
),
entry(
&["context", "get"],
json_discriminators(
opaque_schemas(READ_JSON, &["context get"]),
&[json_discriminator(
Some("context get"),
"output_kind",
"context_get",
)],
),
),
entry(
&["context", "list"],
json_discriminators(
opaque_schemas(READ_JSON, &["context list"]),
&[json_discriminator(
Some("context list"),
"output_kind",
"context_list",
)],
),
),
entry(
&["context", "history"],
json_discriminators(
opaque_schemas(READ_JSON, &["context history"]),
&[json_discriminator(
Some("context history"),
"output_kind",
"context_history",
)],
),
),
entry(
&["context", "edit"],
json_discriminators(
opaque_schemas(MUTATING, &["context edit"]),
&[json_discriminator(
Some("context edit"),
"output_kind",
"context_edit",
)],
),
),
entry(
&["context", "supersede"],
json_discriminators(
opaque_schemas(MUTATING, &["context supersede"]),
&[json_discriminator(
Some("context supersede"),
"output_kind",
"context_supersede",
)],
),
),
entry(
&["context", "rm"],
json_discriminators(
opaque_schemas(MUTATING, &["context rm"]),
&[json_discriminator(
Some("context rm"),
"output_kind",
"context_rm",
)],
),
),
entry(
&["context", "check"],
json_discriminators(
opaque_schemas(READ_JSON, &["context check"]),
&[json_discriminator(
Some("context check"),
"output_kind",
"context_check",
)],
),
),
entry(
&["context", "suggest"],
json_discriminators(
opaque_schemas(READ_JSON, &["context suggest"]),
&[json_discriminator(
Some("context suggest"),
"output_kind",
"context_suggest",
)],
),
),
entry(
&["context", "audit"],
json_discriminators(
opaque_schemas(READ_JSON, &["context audit"]),
&[json_discriminator(
Some("context audit"),
"output_kind",
"context_audit",
)],
),
),
entry(&["daemon"], surface(GROUP, "admin")),
entry(
&["daemon", "serve"],
surface(opaque_schemas(DAEMON_MUTATION, &["daemon serve"]), "admin"),
),
entry(
&["daemon", "status"],
surface(opaque_schemas(READ_JSON, &["daemon status"]), "admin"),
),
entry(
&["daemon", "stop"],
surface(
json_discriminators(
opaque_schemas(DAEMON_MUTATION, &["daemon stop"]),
&[json_discriminator(
Some("daemon stop"),
"output_kind",
"daemon_stop",
)],
),
"admin",
),
),
entry(
&["diff"],
front_door(
documented_core_report_schema(READ_JSON, DiffReport::CONTRACT),
20,
),
),
entry(&["discuss"], category(GROUP, "collab")),
entry(
&["discuss", "open"],
json_discriminators(
documented_schemas(MUTATING, &["discuss open"]),
&[json_discriminator(
Some("discuss open"),
"output_kind",
"discuss_open",
)],
),
),
entry(
&["discuss", "append"],
json_discriminators(
documented_schemas(MUTATING, &["discuss append"]),
&[json_discriminator(
Some("discuss append"),
"output_kind",
"discuss_append",
)],
),
),
entry(
&["discuss", "resolve"],
json_discriminators(
documented_schemas(MUTATING, &["discuss resolve"]),
&[json_discriminator(
Some("discuss resolve"),
"output_kind",
"discuss_resolve",
)],
),
),
entry(
&["discuss", "list"],
json_discriminators(
documented_schemas(READ_JSON, &["discuss list"]),
&[json_discriminator(
Some("discuss list"),
"output_kind",
"discuss_list",
)],
),
),
entry(
&["discuss", "show"],
json_discriminators(
documented_schemas(READ_JSON, &["discuss show"]),
&[json_discriminator(
Some("discuss show"),
"output_kind",
"discuss_show",
)],
),
),
entry(
&["doctor"],
front_door(
json_discriminators(
documented_schemas(READ_JSON, &["doctor"]),
&[json_discriminator(
Some("doctor"),
"output_kind",
"diagnose",
)],
),
120,
),
),
entry(
&["doctor", "docs"],
json_discriminators(
documented_schemas(READ_JSON, &["doctor docs"]),
&[json_discriminator(
Some("doctor docs"),
"output_kind",
"doctor_docs",
)],
),
),
entry(
&["doctor", "schemas"],
json_discriminators(
documented_schemas(READ_JSON, &["doctor schemas"]),
&[json_discriminator(
Some("doctor schemas"),
"output_kind",
"doctor_schemas",
)],
),
),
entry(
&["fetch"],
git_adapter_action(
json_discriminators(
documented_schemas(
CommandContract {
writes_git_refs: true,
network_io: true,
..MUTATING
},
&["fetch"],
),
&[json_discriminator(Some("fetch"), "output_kind", "fetch")],
),
"pull",
"workflow",
"Use pull for the normal remote update workflow; inspect verification output before materializing changes.",
),
),
entry(
&["fsck"],
category(
documented_core_report_schema(READ_JSON, FsckReport::CONTRACT),
"recovery",
),
),
entry(&["oplog"], category(GROUP, "recovery")),
entry(
&["oplog", "recover"],
category(
json_discriminators(
opaque_schemas(MUTATING_NO_OP_ID, &["oplog recover"]),
&[json_discriminator(
Some("oplog recover"),
"output_kind",
"oplog_recover",
)],
),
"recovery",
),
),
entry(
&["git-overlay"],
category(documented_schemas(READ_JSON, &["git-overlay"]), "repo"),
),
entry(
&["help"],
category(
json_discriminators(
opaque_schemas(READ_JSON, &["help"]),
&[json_discriminator(Some("help"), "kind", "command_catalog")],
),
"repo",
),
),
entry(&["hook"], surface(GROUP, "automation")),
entry(
&["hook", "list"],
surface(opaque_schemas(READ_JSON, &["hook list"]), "automation"),
),
entry(
&["hook", "install"],
surface(
opaque_schemas(HOOK_MUTATION, &["hook install"]),
"automation",
),
),
entry(
&["hook", "uninstall"],
surface(
opaque_schemas(HOOK_MUTATION, &["hook uninstall"]),
"automation",
),
),
entry(
&["hook", "events"],
surface(opaque_schemas(READ_JSON, &["hook events"]), "automation"),
),
entry(
&["init"],
exits(
front_door(
json_discriminators(
documented_schemas(INIT, &["init"]),
&[json_discriminator(Some("init"), "output_kind", "init")],
),
200,
),
&[
(0, "ok"),
(73, "cannot create state directory"),
(78, "workspace config invalid"),
],
),
),
entry(&["integration"], surface(GROUP, "admin")),
entry(
&["integration", "list"],
surface(
documented_schemas(READ_JSON, &["integration list"]),
"admin",
),
),
entry(
&["integration", "install"],
surface(opaque_schemas(MUTATING, &["integration install"]), "admin"),
),
entry(
&["integration", "doctor"],
surface(
documented_schemas(READ_JSON, &["integration doctor"]),
"admin",
),
),
entry(
&["integration", "uninstall"],
surface(
opaque_schemas(MUTATING, &["integration uninstall"]),
"admin",
),
),
entry(
&["integration", "upgrade"],
surface(opaque_schemas(MUTATING, &["integration upgrade"]), "admin"),
),
entry(
&["integration", "relay"],
hidden(surface(
opaque_schemas(MUTATING, &["integration relay"]),
"admin",
)),
),
entry(
&["log"],
front_door(
json_discriminators(
documented_schemas(READ_JSON, &["log", "log --reflog", "log --timeline"]),
&[
json_discriminator(Some("log"), "output_kind", "log"),
json_discriminator(Some("log --reflog"), "output_kind", "log_reflog"),
json_discriminator(Some("log --timeline"), "output_kind", "timeline_log"),
],
),
130,
),
),
entry(&["maintenance"], surface(GROUP, "admin")),
entry(
&["maintenance", "inspect"],
surface(opaque_schemas(READ_JSON, &["maintenance inspect"]), "admin"),
),
entry(
&["maintenance", "run"],
surface(opaque_schemas(MUTATING, &["maintenance run"]), "admin"),
),
entry(
&["maintenance", "gc"],
surface(
json_discriminators(
opaque_schemas(GC_MUTATION, &["maintenance gc"]),
&[json_discriminator(
Some("maintenance gc"),
"output_kind",
"gc",
)],
),
"admin",
),
),
entry(
&["maintenance", "index"],
surface(
json_discriminators(
documented_schemas(READ_JSON, &["maintenance index"]),
&[json_discriminator(
Some("maintenance index"),
"output_kind",
"index",
)],
),
"admin",
),
),
entry(
&["maintenance", "monitor"],
surface(opaque_schemas(READ_JSON, &["maintenance monitor"]), "admin"),
),
entry(
&["merge"],
category(
exits(
surface(
advertised_action(
json_discriminators(
documented_schemas(
compact_json(WORKTREE_MUTATION),
&["merge --preview"],
),
&[json_discriminator(
Some("merge --preview"),
"output_kind",
"merge",
)],
),
"heddle merge <thread> --preview",
&["heddle", "merge", "<thread>", "--preview"],
&["thread"],
true,
false,
),
"native",
),
&[
(0, "ok"),
(65, "conflict requires manual resolution"),
(74, "io while writing state"),
],
),
"threads",
),
),
entry(
&["presence"],
category(feature_gated(GROUP, "client"), "collab"),
),
entry(
&["presence", "publish"],
feature_gated(MUTATING_TEXT, "client"),
),
entry(
&["pull"],
exits(
front_door(
json_discriminators(
documented_schemas(
CommandContract {
writes_git_refs: true,
network_io: true,
..WORKTREE_MUTATION
},
&["pull"],
),
&[json_discriminator(Some("pull"), "output_kind", "pull")],
),
90,
),
&[
(0, "ok"),
(75, "remote unreachable; safe to retry"),
(76, "upstream protocol error"),
(78, "no upstream configured"),
],
),
),
entry(
&["push"],
exits(
front_door(
advertised_action(
json_discriminators(
documented_schemas(
CommandContract {
writes_git_refs: true,
network_io: true,
..MUTATING
},
&["push"],
),
&[json_discriminator(Some("push"), "output_kind", "push")],
),
"heddle push",
&["heddle", "push"],
&[],
true,
true,
),
80,
),
&[
(0, "ok"),
(75, "remote unreachable; safe to retry"),
(
76,
"remote rejected payload; do not retry without changing inputs",
),
(78, "no upstream configured"),
],
),
),
entry(
&["query"],
category(
json_discriminators(
documented_schemas(
documented_core_report_schema(READ_JSON, QueryReport::CONTRACT),
QUERY_ATTRIBUTION_SCHEMA_VERBS,
),
QUERY_JSON_DISCRIMINATORS,
),
"states",
),
),
entry(
&["ready"],
front_door(
json_discriminators(
documented_schemas(compact_json(CAPTURE), &["ready"]),
&[json_discriminator(Some("ready"), "output_kind", "ready")],
),
50,
),
),
entry(
&["rebase"],
category(
json_discriminators(
opaque_schemas(WORKTREE_MUTATION_JSONL, &["rebase"]),
&[json_discriminator(
Some("rebase"),
"output_kind",
"rebase_progress",
)],
),
"threads",
),
),
entry(&["redact"], category(GROUP, "recovery")),
entry(
&["redact", "apply"],
json_discriminators(
opaque_schemas(DATA_MUTATION, &["redact apply"]),
&[json_discriminator(
Some("redact apply"),
"output_kind",
"redact_apply",
)],
),
),
entry(
&["redact", "list"],
json_discriminators(
opaque_schemas(READ_JSON, &["redact list"]),
&[json_discriminator(
Some("redact list"),
"output_kind",
"redact_list",
)],
),
),
entry(
&["redact", "show"],
json_discriminators(
opaque_schemas(READ_JSON, &["redact show"]),
&[json_discriminator(
Some("redact show"),
"output_kind",
"redact_show",
)],
),
),
entry(&["redact", "purge"], GROUP),
entry(
&["redact", "purge", "apply"],
json_discriminators(
opaque_schemas(
CommandContract {
destructive_requires_force: true,
..DESTRUCTIVE_DATA_MUTATION
},
&["redact purge apply"],
),
&[json_discriminator(
Some("redact purge apply"),
"output_kind",
"purge_apply",
)],
),
),
entry(
&["redact", "purge", "list"],
json_discriminators(
opaque_schemas(READ_JSON, &["redact purge list"]),
&[json_discriminator(
Some("redact purge list"),
"output_kind",
"purge_list",
)],
),
),
entry(&["redact", "trust"], GROUP),
entry(
&["redact", "trust", "add"],
json_discriminators(
opaque_schemas(DATA_MUTATION, &["redact trust add"]),
&[json_discriminator(
Some("redact trust add"),
"output_kind",
"redact_trust_add",
)],
),
),
entry(
&["redact", "trust", "list"],
json_discriminators(
opaque_schemas(READ_JSON, &["redact trust list"]),
&[json_discriminator(
Some("redact trust list"),
"output_kind",
"redact_trust_list",
)],
),
),
entry(
&["redact", "trust", "remove"],
json_discriminators(
opaque_schemas(DATA_MUTATION, &["redact trust remove"]),
&[json_discriminator(
Some("redact trust remove"),
"output_kind",
"redact_trust_remove",
)],
),
),
entry(&["remote"], category(surface(GROUP, "native"), "repo")),
entry(
&["remote", "list"],
json_discriminators(
documented_schemas(READ_JSON, &["remote list"]),
&[json_discriminator(
Some("remote list"),
"output_kind",
"remote_list",
)],
),
),
entry(
&["remote", "add"],
json_discriminators(
documented_schemas(CONFIG_MUTATION, &["remote add"]),
&[json_discriminator(
Some("remote add"),
"output_kind",
"remote_add",
)],
),
),
entry(
&["remote", "remove"],
json_discriminators(
documented_schemas(CONFIG_MUTATION, &["remote remove"]),
&[json_discriminator(
Some("remote remove"),
"output_kind",
"remote_remove",
)],
),
),
entry(
&["remote", "set-default"],
json_discriminators(
documented_schemas(CONFIG_MUTATION, &["remote set-default"]),
&[json_discriminator(
Some("remote set-default"),
"output_kind",
"remote_set_default",
)],
),
),
entry(
&["remote", "show"],
json_discriminators(
documented_schemas(READ_JSON, &["remote show"]),
&[json_discriminator(
Some("remote show"),
"output_kind",
"remote_show",
)],
),
),
entry(
&["resolve"],
front_door(
json_discriminators(
documented_schemas(MUTATING, &["resolve"]),
&[json_discriminator(
Some("resolve"),
"output_kind",
"resolve",
)],
),
300,
),
),
entry(
&["retro"],
category(documented_schemas(READ_JSON, &["retro"]), "states"),
),
entry(
&["revert"],
category(
json_discriminators(
documented_schemas(WORKTREE_MUTATION, &["revert"]),
&[json_discriminator(Some("revert"), "output_kind", "revert")],
),
"states",
),
),
entry(&["review"], category(GROUP, "collab")),
entry(
&["review", "show"],
json_discriminators(
documented_schemas(READ_JSON, &["review show"]),
&[json_discriminator(
Some("review show"),
"output_kind",
"review_show",
)],
),
),
entry(
&["review", "sign"],
json_discriminators(
documented_schemas(MUTATING, &["review sign"]),
&[json_discriminator(
Some("review sign"),
"output_kind",
"review_sign",
)],
),
),
entry(
&["review", "next"],
json_discriminators(
documented_schemas(READ_JSON, &["review next"]),
&[json_discriminator(
Some("review next"),
"output_kind",
"review_next",
)],
),
),
entry(
&["review", "health"],
json_discriminators(
documented_schemas(READ_JSON, &["review health"]),
&[json_discriminator(
Some("review health"),
"output_kind",
"review_health",
)],
),
),
entry(&["run"], surface(EXTERNAL_WORKTREE_COMMAND, "automation")),
entry(
&["schemas"],
surface(
json_discriminators(
documented_schemas(READ_JSON, &["schemas"]),
&[json_discriminator(
Some("schemas"),
"output_kind",
"schemas",
)],
),
"automation",
),
),
entry(&["semantic"], category(GROUP, "states")),
entry(
&["semantic", "hot"],
opaque_schemas(READ_JSON, &["semantic hot"]),
),
entry(&["session"], surface(GROUP, "automation")),
entry(
&["session", "start"],
surface(
documented_schemas(MUTATING, &["session start"]),
"automation",
),
),
entry(
&["session", "segment"],
surface(
documented_schemas(MUTATING, &["session segment"]),
"automation",
),
),
entry(
&["session", "end"],
surface(documented_schemas(MUTATING, &["session end"]), "automation"),
),
entry(
&["session", "show"],
surface(
documented_schemas(READ_JSON, &["session show"]),
"automation",
),
),
entry(
&["session", "list"],
surface(
documented_schemas(READ_JSON, &["session list"]),
"automation",
),
),
entry(&["shell"], category(READ_TEXT, "repo")),
entry(&["shell", "init"], READ_TEXT),
entry(&["shell", "completion"], READ_TEXT),
entry(&["shell", "prompt"], READ_TEXT),
entry(&["complete"], hidden(READ_TEXT)),
entry(
&["land"],
front_door(
json_discriminators(
documented_schemas(
CommandContract {
writes_git_refs: true,
network_io: true,
..compact_json(MUTATING)
},
&["land"],
),
&[json_discriminator(Some("land"), "output_kind", "land")],
),
70,
),
),
entry(
&["show"],
front_door(
json_discriminators(
documented_schemas(READ_JSON, &["show"]),
&[json_discriminator(Some("show"), "output_kind", "show")],
),
140,
),
),
entry(
&["start"],
front_door(
json_discriminators(
documented_schemas(WORKTREE_MUTATION, &["start"]),
&[json_discriminator(
Some("start"),
"output_kind",
"thread_start",
)],
),
40,
),
),
entry(
&["stash"],
git_adapter_action(
READ_TEXT,
"capture",
"conceptual_home",
"Use capture, commit, and thread captures for durable Heddle saves.",
),
),
entry(
&["stash", "push"],
git_adapter_action(
documented_schemas(WORKTREE_ONLY_MUTATION, &["stash push"]),
"capture",
"workflow",
"Use capture for a durable named save point before changing the worktree.",
),
),
entry(
&["stash", "list"],
git_adapter_action(
json_discriminators(
documented_schemas(READ_JSON, &["stash list"]),
&[json_discriminator(
Some("stash list"),
"output_kind",
"stash_list",
)],
),
"thread captures",
"conceptual_home",
"Use thread captures to inspect durable Heddle save points.",
),
),
entry(
&["stash", "pop"],
git_adapter_action(
documented_schemas(
CommandContract {
destructive_data: true,
..WORKTREE_ONLY_MUTATION
},
&["stash pop"],
),
"undo",
"conceptual_home",
"Use undo to reverse the last Heddle operation; stash pop is not a direct semantic match.",
),
),
entry(
&["stash", "apply"],
git_adapter_action(
documented_schemas(WORKTREE_ONLY_MUTATION, &["stash apply"]),
"undo",
"conceptual_home",
"Use undo to reverse the last Heddle operation; stash apply is not a direct semantic match.",
),
),
entry(
&["stash", "drop"],
git_adapter_action(
documented_schemas(DESTRUCTIVE_DATA_MUTATION, &["stash drop"]),
"thread captures",
"conceptual_home",
"Use thread captures to inspect and manage durable Heddle save points.",
),
),
entry(
&["stash", "clear"],
git_adapter_action(
documented_schemas(DESTRUCTIVE_DATA_MUTATION, &["stash clear"]),
"thread captures",
"conceptual_home",
"Use thread captures to inspect and manage durable Heddle save points.",
),
),
entry(
&["stash", "show"],
git_adapter_alias(
json_discriminators(
documented_schemas(READ_JSON, &["stash show"]),
&[json_discriminator(
Some("stash show"),
"output_kind",
"stash_show",
)],
),
"show",
),
),
entry(
&["status"],
exits(
front_door(
documented_core_report_schema(
compact_json(READ_JSON_OR_JSONL),
StatusReport::CONTRACT,
),
10,
),
&[(0, "ok"), (74, "io reading workspace state")],
),
),
entry(
&["support"],
category(feature_gated(GROUP, "client"), "repo"),
),
entry(
&["support", "grant"],
feature_gated(
json_discriminators(
documented_schemas(MUTATING_NO_OP_ID, &["support grant"]),
&[json_discriminator(
Some("support grant"),
"output_kind",
"support_grant",
)],
),
"client",
),
),
entry(
&["support", "list"],
feature_gated(
json_discriminators(
documented_schemas(READ_JSON, &["support list"]),
&[json_discriminator(
Some("support list"),
"output_kind",
"support_list",
)],
),
"client",
),
),
entry(
&["support", "revoke"],
feature_gated(
json_discriminators(
documented_schemas(MUTATING_NO_OP_ID, &["support revoke"]),
&[json_discriminator(
Some("support revoke"),
"output_kind",
"support_revoke",
)],
),
"client",
),
),
entry(
&["switch"],
git_adapter_alias(
json_discriminators(
documented_schemas(WORKTREE_MUTATION, &["switch"]),
&[json_discriminator(
Some("switch"),
"output_kind",
"thread_switch",
)],
),
"thread switch",
),
),
entry(
&["sync"],
category(
json_discriminators(
documented_schemas(operator_envelope(compact_json(MUTATING)), &["sync"]),
&[json_discriminator(Some("sync"), "output_kind", "sync")],
),
"threads",
),
),
entry(&["thread"], category(surface(GROUP, "native"), "threads")),
entry(
&["thread", "create"],
json_discriminators(
documented_schemas(MUTATING, &["thread create"]),
&[json_discriminator(
Some("thread create"),
"output_kind",
"thread_create",
)],
),
),
entry(
&["thread", "current"],
documented_schemas(READ_JSON, &["thread current"]),
),
entry(
&["thread", "switch"],
json_discriminators(
documented_schemas(WORKTREE_MUTATION, &["thread switch"]),
&[json_discriminator(
Some("thread switch"),
"output_kind",
"thread_switch",
)],
),
),
entry(&["thread", "cd"], READ_TEXT),
entry(
&["thread", "list"],
json_discriminators(
documented_schemas(READ_JSON, &["thread list"]),
&[json_discriminator(
Some("thread list"),
"output_kind",
"thread_list",
)],
),
),
entry(
&["thread", "show"],
json_discriminators(
documented_schemas(READ_JSON_OR_JSONL, &["thread show"]),
&[json_discriminator(
Some("thread show"),
"output_kind",
"thread_show",
)],
),
),
entry(
&["thread", "captures"],
documented_schemas(READ_JSON, &["thread captures"]),
),
entry(
&["thread", "rename"],
json_discriminators(
documented_schemas(MUTATING, &["thread rename"]),
&[json_discriminator(
Some("thread rename"),
"output_kind",
"thread_rename",
)],
),
),
entry(
&["thread", "refresh"],
json_discriminators(
documented_schemas(WORKTREE_MUTATION, &["thread refresh"]),
&[json_discriminator(
Some("thread refresh"),
"output_kind",
"thread_refresh",
)],
),
),
entry(
&["thread", "move"],
documented_schemas(MUTATING, &["thread move"]),
),
entry(
&["thread", "absorb"],
documented_schemas(MUTATING, &["thread absorb"]),
),
entry(
&["thread", "resolve"],
json_discriminators(
documented_schemas(MUTATING, &["thread resolve"]),
&[json_discriminator(
Some("thread resolve"),
"output_kind",
"thread_resolve",
)],
),
),
entry(
&["thread", "promote"],
json_discriminators(
documented_schemas(WORKTREE_MUTATION, &["thread promote"]),
&[json_discriminator(
Some("thread promote"),
"output_kind",
"thread_promote",
)],
),
),
entry(
&["thread", "drop"],
json_discriminators(
documented_schemas(DESTRUCTIVE_WORKTREE_MUTATION, &["thread drop"]),
&[json_discriminator(
Some("thread drop"),
"output_kind",
"thread_drop",
)],
),
),
entry(
&["thread", "approve"],
documented_schemas(MUTATING, &["thread approve"]),
),
entry(
&["thread", "approvals"],
documented_schemas(READ_JSON, &["thread approvals"]),
),
entry(
&["thread", "revoke-approval"],
json_discriminators(
documented_schemas(MUTATING, &["thread revoke-approval"]),
&[json_discriminator(
Some("thread revoke-approval"),
"output_kind",
"thread_revoke_approval",
)],
),
),
entry(
&["thread", "check-merge"],
documented_schemas(READ_JSON, &["thread check-merge"]),
),
entry(
&["thread", "cleanup"],
json_discriminators(
documented_schemas(DESTRUCTIVE_WORKTREE_MUTATION, &["thread cleanup"]),
&[json_discriminator(
Some("thread cleanup"),
"output_kind",
"thread_cleanup",
)],
),
),
entry(&["thread", "marker"], GROUP),
entry(
&["thread", "marker", "list"],
json_discriminators(
documented_schemas(READ_JSON, &["thread marker list"]),
&[json_discriminator(
Some("thread marker list"),
"output_kind",
"thread_marker_list",
)],
),
),
entry(
&["thread", "marker", "create"],
json_discriminators(
documented_schemas(MUTATING, &["thread marker create"]),
&[json_discriminator(
Some("thread marker create"),
"output_kind",
"thread_marker_create",
)],
),
),
entry(
&["thread", "marker", "delete"],
json_discriminators(
documented_schemas(DESTRUCTIVE_DATA_MUTATION, &["thread marker delete"]),
&[json_discriminator(
Some("thread marker delete"),
"output_kind",
"thread_marker_delete",
)],
),
),
entry(
&["thread", "marker", "show"],
json_discriminators(
documented_schemas(READ_JSON, &["thread marker show"]),
&[json_discriminator(
Some("thread marker show"),
"output_kind",
"thread_marker_show",
)],
),
),
entry(&["timeline"], surface(GROUP, "automation")),
entry(
&["timeline", "status"],
surface(
json_discriminators(
documented_schemas(READ_JSON, &["timeline status"]),
&[json_discriminator(
Some("timeline status"),
"output_kind",
"timeline_status",
)],
),
"automation",
),
),
entry(
&["timeline", "record-start"],
surface(
json_discriminators(
documented_schemas(MUTATING, &["timeline record-start"]),
&[json_discriminator(
Some("timeline record-start"),
"output_kind",
"timeline_record_start",
)],
),
"automation",
),
),
entry(
&["timeline", "record-finish"],
surface(
json_discriminators(
documented_schemas(MUTATING, &["timeline record-finish"]),
&[json_discriminator(
Some("timeline record-finish"),
"output_kind",
"timeline_record_finish",
)],
),
"automation",
),
),
entry(
&["timeline", "fork"],
surface(
json_discriminators(
documented_schemas(MUTATING, &["timeline fork"]),
&[json_discriminator(
Some("timeline fork"),
"output_kind",
"timeline_action",
)],
),
"automation",
),
),
entry(
&["timeline", "reset"],
surface(
json_discriminators(
documented_schemas(WORKTREE_MUTATION, &["timeline reset"]),
&[json_discriminator(
Some("timeline reset"),
"output_kind",
"timeline_action",
)],
),
"automation",
),
),
entry(
&["timeline", "recover"],
surface(
json_discriminators(
documented_schemas(MUTATING, &["timeline recover"]),
&[json_discriminator(
Some("timeline recover"),
"output_kind",
"timeline_action",
)],
),
"automation",
),
),
entry(&["transaction"], hidden(GROUP)),
entry(
&["transaction", "begin"],
hidden(opaque_schemas(MUTATING, &["transaction begin"])),
),
entry(
&["transaction", "commit"],
documented_schemas(MUTATING, &["transaction commit"]),
),
entry(
&["transaction", "abort"],
hidden(opaque_schemas(MUTATING, &["transaction abort"])),
),
entry(
&["transaction", "status"],
hidden(opaque_schemas(READ_JSON, &["transaction status"])),
),
entry(
&["verify"],
exits(
front_door(
documented_core_report_schema(READ_JSON, VerifyReport::CONTRACT),
110,
),
&[
(0, "verified clean"),
(65, "verification reports blocked state"),
(74, "io reading state"),
],
),
),
entry(&["visibility"], category(GROUP, "collab")),
entry(
&["visibility", "set"],
json_discriminators(
opaque_schemas(DATA_MUTATION, &["visibility set"]),
&[json_discriminator(
Some("visibility set"),
"output_kind",
"visibility_set",
)],
),
),
entry(
&["visibility", "promote"],
json_discriminators(
opaque_schemas(DATA_MUTATION, &["visibility promote"]),
&[json_discriminator(
Some("visibility promote"),
"output_kind",
"visibility_promote",
)],
),
),
entry(
&["visibility", "show"],
json_discriminators(
opaque_schemas(READ_JSON, &["visibility show"]),
&[json_discriminator(
Some("visibility show"),
"output_kind",
"visibility_show",
)],
),
),
entry(
&["visibility", "list"],
json_discriminators(
opaque_schemas(READ_JSON, &["visibility list"]),
&[json_discriminator(
Some("visibility list"),
"output_kind",
"visibility_list",
)],
),
),
entry(
&["try"],
category(
documented_schemas(EXTERNAL_WORKTREE_MUTATION, &["try"]),
"threads",
),
),
entry(
&["undo"],
front_door(
json_discriminators(
documented_schemas(WORKTREE_MUTATION, &["undo", "undo --list", "undo --redo"]),
&[
json_discriminator(Some("undo"), "output_kind", "undo"),
json_discriminator(Some("undo --list"), "output_kind", "undo_list"),
json_discriminator(Some("undo --redo"), "output_kind", "redo"),
],
),
100,
),
),
entry(
&["watch"],
surface(documented_schemas(READ_JSONL, &["watch"]), "automation"),
),
];
static ACTIVE_COMMAND_CONTRACT_ENTRIES: OnceLock<Vec<&'static CommandContractEntry>> =
OnceLock::new();
static ADVERTISED_COMMAND_CONTRACT_ENTRIES: OnceLock<Vec<&'static CommandContractEntry>> =
OnceLock::new();
const fn entry(path: &'static [&'static str], contract: CommandContract) -> CommandContractEntry {
CommandContractEntry { path, contract }
}
pub fn build_command_catalog() -> CommandCatalogOutput {
debug_assert!(
RECOMMENDED_ACTION_PLACEHOLDERS
.iter()
.all(|action| validate_recommended_action(action).is_ok())
);
let command = Cli::command();
let mut global_options: Vec<_> = command
.get_arguments()
.filter(|arg| arg.is_global_set())
.map(catalog_option)
.collect();
if !command.is_disable_help_flag_set() {
global_options.push(generated_help_option());
}
let mut commands = Vec::new();
let op_id_option = command
.get_arguments()
.find(|arg| arg.get_long() == Some("op-id"))
.map(catalog_option);
walk_commands(&command, &mut Vec::new(), &mut commands, &op_id_option);
append_feature_gated_command_entries(&command, &mut commands, &op_id_option);
CommandCatalogOutput {
kind: "command_catalog".to_string(),
executable_path: heddle_argv0(),
commands,
global_options,
json_discriminators: command_json_discriminators(),
recommended_action_placeholders: RECOMMENDED_ACTION_PLACEHOLDERS
.iter()
.map(|action| (*action).to_string())
.collect(),
recommended_action_templates: RECOMMENDED_ACTION_TEMPLATES
.iter()
.map(|(action, argv_template, required_inputs, agent_may_fill)| {
action_template_from_parts(action, argv_template, required_inputs, *agent_may_fill)
})
.collect(),
}
}
pub(crate) fn heddle_argv0() -> String {
match std::env::current_exe() {
Ok(path) => {
let file_name = path.file_name().and_then(|name| name.to_str());
if matches!(file_name, Some("heddle") | Some("heddle.exe")) {
path.display().to_string()
} else {
"heddle".to_string()
}
}
Err(_) => "heddle".to_string(),
}
}
pub(crate) fn normalize_heddle_argv(mut argv: Vec<String>) -> Vec<String> {
if argv.first().is_some_and(|first| first == "heddle") {
argv[0] = heddle_argv0();
}
argv
}
fn walk_commands(
command: &clap::Command,
prefix: &mut Vec<String>,
out: &mut Vec<CommandCatalogEntry>,
op_id_option: &Option<CommandCatalogOption>,
) {
for subcommand in command.get_subcommands() {
prefix.push(subcommand.get_name().to_string());
out.push(catalog_entry(subcommand, prefix, op_id_option));
walk_commands(subcommand, prefix, out, op_id_option);
prefix.pop();
}
}
fn append_feature_gated_command_entries(
root: &clap::Command,
out: &mut Vec<CommandCatalogEntry>,
op_id_option: &Option<CommandCatalogOption>,
) {
let present: std::collections::BTreeSet<Vec<String>> =
out.iter().map(|entry| entry.path.clone()).collect();
for entry in CONTRACTS.iter() {
if entry.contract.feature_gate.is_none() {
continue;
}
let owned_path: Vec<String> = entry.path.iter().map(|s| (*s).to_string()).collect();
if present.contains(&owned_path) {
continue;
}
if clap_command_path_exists(root, entry.path) {
continue;
}
out.push(feature_gated_catalog_entry(
&owned_path,
entry.contract,
op_id_option,
));
}
}
fn feature_gated_catalog_entry(
path: &[String],
contract: CommandContract,
op_id_option: &Option<CommandCatalogOption>,
) -> CommandCatalogEntry {
let mut options = Vec::new();
if contract.supports_op_id
&& let Some(op_id_option) = op_id_option
{
options.push(op_id_option.clone());
}
CommandCatalogEntry {
path: path.to_vec(),
display: path.join(" "),
aliases: Vec::new(),
tier: help_visibility_to_tier(contract.help_visibility).to_string(),
surface: contract.surface.to_string(),
help_visibility: contract.help_visibility.to_string(),
help_rank: contract.help_rank,
canonical_command: contract
.canonical_command
.map(std::string::ToString::to_string),
canonical_action: canonical_action(contract),
command_action: contract
.advertised_action
.map(command_action_from_advertised),
summary: String::new(),
has_subcommands: false,
supports_json: contract.supports_json,
mutates: contract.mutates,
supports_op_id: contract.supports_op_id,
persists_op_id: contract.persists_op_id,
op_id_behavior: op_id_behavior(contract).to_string(),
op_id_store_scope: op_id_store_scope(contract).to_string(),
observe_only: contract.observe_only,
may_initialize: contract.may_initialize,
may_import_git: contract.may_import_git,
may_write_worktree: contract.may_write_worktree,
may_move_ref: contract.may_move_ref,
destructive_requires_force: contract.destructive_requires_force,
writes_heddle_refs: contract.writes_heddle_refs,
writes_git_refs: contract.writes_git_refs,
writes_worktree: contract.writes_worktree,
writes_config: contract.writes_config,
writes_hooks: contract.writes_hooks,
network_io: contract.network_io,
daemon_process: contract.daemon_process,
object_gc: contract.object_gc,
external_command: contract.external_command,
requires_git_executable: contract.requires_git_executable,
destructive_data: contract.destructive_data,
side_effects: side_effects(contract)
.iter()
.map(|effect| (*effect).to_string())
.collect(),
side_effect_class: side_effect_class(contract).to_string(),
first_run_behavior: first_run_behavior(contract).to_string(),
json_kind: contract.json_kind.to_string(),
json_discriminators: json_discriminators_for_path(path.iter().map(String::as_str)),
schema_verbs: contract_schema_verbs(contract)
.map(str::to_string)
.collect(),
documented_schema_verbs: contract_documented_schema_verbs(contract)
.map(str::to_string)
.collect(),
options,
arguments: Vec::new(),
exit_codes: contract
.exit_codes
.iter()
.map(|(code, reason)| CommandCatalogExitCode {
code: *code,
reason: (*reason).to_string(),
})
.collect(),
}
}
fn catalog_entry(
command: &clap::Command,
path: &[String],
op_id_option: &Option<CommandCatalogOption>,
) -> CommandCatalogEntry {
let mut options = Vec::new();
let mut arguments = Vec::new();
for arg in command.get_arguments() {
if arg.get_long().is_some() || arg.get_short().is_some() {
options.push(catalog_option(arg));
} else {
arguments.push(catalog_argument(arg));
}
}
let contract = command_contract(path);
if contract.supports_op_id
&& let Some(op_id_option) = op_id_option
{
options.push(op_id_option.clone());
}
CommandCatalogEntry {
path: path.to_vec(),
display: path.join(" "),
aliases: command
.get_all_aliases()
.map(std::string::ToString::to_string)
.collect(),
tier: help_visibility_to_tier(contract.help_visibility).to_string(),
surface: contract.surface.to_string(),
help_visibility: contract.help_visibility.to_string(),
help_rank: contract.help_rank,
canonical_command: contract
.canonical_command
.map(std::string::ToString::to_string),
canonical_action: canonical_action(contract),
command_action: command_action(command, path, contract),
summary: clean_catalog_summary(
command
.get_about()
.or_else(|| command.get_long_about())
.map(|about| about.to_string().lines().next().unwrap_or("").to_string())
.unwrap_or_default(),
),
has_subcommands: command.get_subcommands().next().is_some(),
supports_json: contract.supports_json,
mutates: contract.mutates,
supports_op_id: contract.supports_op_id,
persists_op_id: contract.persists_op_id,
op_id_behavior: op_id_behavior(contract).to_string(),
op_id_store_scope: op_id_store_scope(contract).to_string(),
observe_only: contract.observe_only,
may_initialize: contract.may_initialize,
may_import_git: contract.may_import_git,
may_write_worktree: contract.may_write_worktree,
may_move_ref: contract.may_move_ref,
destructive_requires_force: contract.destructive_requires_force,
writes_heddle_refs: contract.writes_heddle_refs,
writes_git_refs: contract.writes_git_refs,
writes_worktree: contract.writes_worktree,
writes_config: contract.writes_config,
writes_hooks: contract.writes_hooks,
network_io: contract.network_io,
daemon_process: contract.daemon_process,
object_gc: contract.object_gc,
external_command: contract.external_command,
requires_git_executable: contract.requires_git_executable,
destructive_data: contract.destructive_data,
side_effects: side_effects(contract)
.iter()
.map(|effect| (*effect).to_string())
.collect(),
side_effect_class: side_effect_class(contract).to_string(),
first_run_behavior: first_run_behavior(contract).to_string(),
json_kind: contract.json_kind.to_string(),
json_discriminators: json_discriminators_for_path(path.iter().map(String::as_str)),
schema_verbs: contract_schema_verbs(contract)
.map(str::to_string)
.collect(),
documented_schema_verbs: contract_documented_schema_verbs(contract)
.map(str::to_string)
.collect(),
options,
arguments,
exit_codes: contract
.exit_codes
.iter()
.map(|(code, reason)| CommandCatalogExitCode {
code: *code,
reason: (*reason).to_string(),
})
.collect(),
}
}
fn canonical_action(contract: CommandContract) -> Option<CanonicalAction> {
let command = contract.canonical_command?;
let kind = contract.canonical_kind.unwrap_or("direct_command");
let (argv, template) = canonical_action_metadata(command, kind);
Some(CanonicalAction {
command: command.to_string(),
kind: kind.to_string(),
executable: argv.is_some(),
note: contract.canonical_note.unwrap_or_default().to_string(),
argv,
template,
})
}
fn canonical_action_metadata(
command: &str,
kind: &str,
) -> (Option<Vec<String>>, Option<ActionTemplate>) {
match (command, kind) {
("adopt", "workflow") => (
None,
Some(action_template_from_parts(
"heddle adopt --ref <branch>",
&["heddle", "adopt", "--ref", "<branch>"],
&["branch"],
true,
)),
),
("capture", "workflow") => (
None,
Some(action_template_from_parts(
"heddle capture -m <message>",
&["heddle", "capture", "-m", "<message>"],
&["message"],
true,
)),
),
("thread switch", "direct_command") => (
None,
Some(action_template_from_parts(
"heddle thread switch <thread>",
&["heddle", "thread", "switch", "<thread>"],
&["thread"],
true,
)),
),
(_, "direct_command") => {
let argv = std::iter::once("heddle".to_string())
.chain(command.split_whitespace().map(str::to_string))
.collect::<Vec<_>>();
(Some(normalize_heddle_argv(argv)), None)
}
_ => (None, None),
}
}
fn command_action(
command: &clap::Command,
path: &[String],
contract: CommandContract,
) -> Option<CommandAction> {
if let Some(action) = contract.advertised_action {
return Some(command_action_from_advertised(action));
}
if command.get_subcommands().next().is_some() {
return None;
}
let mut argv_template = std::iter::once("heddle".to_string())
.chain(path.iter().cloned())
.collect::<Vec<_>>();
let mut required_inputs = Vec::new();
for arg in command.get_arguments().filter(|arg| !arg.is_hide_set()) {
if !arg.is_required_set() {
continue;
}
if let Some(long) = arg.get_long() {
argv_template.push(format!("--{long}"));
} else if let Some(short) = arg.get_short() {
argv_template.push(format!("-{short}"));
}
let names = value_names(arg);
if names.is_empty() {
required_inputs.push(arg.get_id().as_str().to_string());
if arg.get_long().is_none() && arg.get_short().is_none() {
argv_template.push(format!("<{}>", arg.get_id().as_str()));
}
} else {
for name in names {
let input = name.to_ascii_lowercase();
required_inputs.push(input.clone());
argv_template.push(format!("<{input}>"));
}
}
}
let action = argv_template.join(" ");
if required_inputs.is_empty() {
Some(CommandAction {
action,
executable: true,
argv: Some(normalize_heddle_argv(argv_template)),
template: None,
})
} else {
Some(CommandAction {
action: action.clone(),
executable: false,
argv: None,
template: Some(action_template_from_owned(
action,
argv_template,
required_inputs,
true,
)),
})
}
}
fn command_action_from_advertised(action: AdvertisedAction) -> CommandAction {
let argv_template = action
.argv_template
.iter()
.map(|part| (*part).to_string())
.collect::<Vec<_>>();
if action.executable {
CommandAction {
action: action.action.to_string(),
executable: true,
argv: Some(normalize_heddle_argv(argv_template)),
template: None,
}
} else {
CommandAction {
action: action.action.to_string(),
executable: false,
argv: None,
template: Some(action_template_from_parts(
action.action,
action.argv_template,
action.required_inputs,
action.agent_may_fill,
)),
}
}
}
fn action_template_from_parts(
action: &str,
argv_template: &[&str],
required_inputs: &[&str],
agent_may_fill: bool,
) -> ActionTemplate {
action_template_from_owned(
action.to_string(),
argv_template
.iter()
.map(|part| (*part).to_string())
.collect(),
required_inputs
.iter()
.map(|input| (*input).to_string())
.collect(),
agent_may_fill,
)
}
fn action_template_from_owned(
action: String,
argv_template: Vec<String>,
required_inputs: Vec<String>,
agent_may_fill: bool,
) -> ActionTemplate {
ActionTemplate {
action,
argv_template: normalize_heddle_argv(argv_template),
required_inputs,
agent_may_fill,
}
}
fn clean_catalog_summary(summary: String) -> String {
let stripped = summary
.trim_start_matches("Automation/workflow command:")
.trim_start();
let mut chars = stripped.chars();
match chars.next() {
Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
None => String::new(),
}
}
fn side_effect_class(contract: CommandContract) -> &'static str {
if contract.observe_only {
"observe_only"
} else if contract.destructive_requires_force && contract.writes_worktree {
"destructive_worktree_mutation"
} else if contract.destructive_data {
"destructive_data"
} else if contract.object_gc {
"object_gc"
} else if contract.writes_worktree {
"worktree_mutation"
} else if contract.may_import_git {
"git_import"
} else if contract.may_initialize {
"initialize"
} else if contract.writes_hooks {
"hook_mutation"
} else if contract.writes_config {
"config_mutation"
} else if contract.network_io {
"network_mutation"
} else if contract.daemon_process {
"daemon_process"
} else if contract.external_command {
"external_command"
} else if contract.writes_heddle_refs || contract.writes_git_refs || contract.may_move_ref {
"ref_mutation"
} else if contract.mutates {
"mutation"
} else {
"none"
}
}
fn side_effects(contract: CommandContract) -> Vec<&'static str> {
if contract.observe_only {
return vec!["observe_only"];
}
let mut effects = Vec::new();
if contract.may_initialize {
effects.push("initialize");
}
if contract.may_import_git {
effects.push("import_git");
}
if contract.writes_heddle_refs {
effects.push("writes_heddle_refs");
}
if contract.writes_git_refs {
effects.push("writes_git_refs");
}
if contract.writes_worktree {
effects.push("writes_worktree");
} else if contract.may_write_worktree {
effects.push("may_write_worktree");
}
if contract.writes_config {
effects.push("writes_config");
}
if contract.writes_hooks {
effects.push("writes_hooks");
}
if contract.network_io {
effects.push("network_io");
}
if contract.daemon_process {
effects.push("daemon_process");
}
if contract.object_gc {
effects.push("object_gc");
}
if contract.external_command {
effects.push("external_command");
}
if contract.destructive_requires_force {
effects.push("destructive_requires_force");
}
if contract.destructive_data {
effects.push("destructive_data");
}
if effects.is_empty() && contract.mutates {
effects.push("mutation");
}
effects
}
fn op_id_behavior(contract: CommandContract) -> &'static str {
if contract.persists_op_id {
"generated_resume"
} else if contract.supports_op_id {
"explicit_replay"
} else {
"none"
}
}
fn uses_bootstrap_op_id_store(contract: CommandContract) -> bool {
contract.supports_op_id && contract.may_initialize
}
fn op_id_store_scope(contract: CommandContract) -> &'static str {
if !contract.supports_op_id {
"none"
} else if uses_bootstrap_op_id_store(contract) {
"bootstrap"
} else {
"repository"
}
}
fn first_run_behavior(contract: CommandContract) -> &'static str {
if contract.observe_only {
"observe_only_no_init"
} else if contract.may_initialize && contract.may_import_git {
"may_initialize_and_import_git"
} else if contract.may_initialize {
"may_initialize"
} else if contract.may_import_git {
"may_import_git"
} else if contract.mutates {
"requires_initialized_repo"
} else {
"no_repo_required"
}
}
fn catalog_option(arg: &clap::Arg) -> CommandCatalogOption {
CommandCatalogOption {
id: arg.get_id().as_str().to_string(),
long: arg.get_long().map(str::to_string),
aliases: option_aliases(arg),
short: arg.get_short().map(|short| short.to_string()),
value_names: value_names(arg),
value_kind: value_kind(arg).to_string(),
default_values: arg
.get_default_values()
.iter()
.map(|value| value.to_string_lossy().into_owned())
.collect(),
possible_values: arg
.get_possible_values()
.iter()
.map(|value| value.get_name().to_string())
.collect(),
help: arg.get_help().map(|help| help.to_string()),
required: arg.is_required_set(),
global: arg.is_global_set(),
hidden: arg.is_hide_set(),
}
}
fn generated_help_option() -> CommandCatalogOption {
CommandCatalogOption {
id: "help".to_string(),
long: Some("help".to_string()),
aliases: Vec::new(),
short: Some("h".to_string()),
value_names: Vec::new(),
value_kind: "boolean".to_string(),
default_values: Vec::new(),
possible_values: Vec::new(),
help: Some("Print help".to_string()),
required: false,
global: true,
hidden: false,
}
}
fn option_aliases(arg: &clap::Arg) -> Vec<String> {
let mut aliases = std::collections::BTreeSet::new();
for alias in arg.get_all_aliases().unwrap_or_default() {
aliases.insert(alias.to_string());
}
for alias in arg.get_visible_aliases().unwrap_or_default() {
aliases.insert(alias.to_string());
}
aliases.into_iter().collect()
}
fn catalog_argument(arg: &clap::Arg) -> CommandCatalogArgument {
CommandCatalogArgument {
id: arg.get_id().as_str().to_string(),
value_names: value_names(arg),
help: arg.get_help().map(|help| help.to_string()),
required: arg.is_required_set(),
}
}
fn value_kind(arg: &clap::Arg) -> &'static str {
match arg.get_action() {
ArgAction::SetTrue | ArgAction::SetFalse => "boolean",
ArgAction::Count => "count",
ArgAction::Append => "list",
ArgAction::Set if !arg.get_possible_values().is_empty() => "enum",
ArgAction::Set => "string",
_ => "unknown",
}
}
fn value_names(arg: &clap::Arg) -> Vec<String> {
arg.get_value_names()
.map(|names| names.iter().map(|name| name.to_string()).collect())
.unwrap_or_default()
}
fn command_contract(path: &[String]) -> CommandContract {
command_contract_for_path(path.iter().map(String::as_str))
.unwrap_or_else(|| panic!("missing command contract for `{}`", path.join(" ")))
}
fn command_contract_for_path<'a>(
path: impl IntoIterator<Item = &'a str>,
) -> Option<CommandContract> {
let path = path.into_iter().collect::<Vec<_>>();
active_command_contract_entries()
.iter()
.copied()
.find(|entry| entry.path == path.as_slice())
.map(|entry| entry.contract)
}
#[cfg(test)]
fn raw_command_contract_for_path<'a>(
path: impl IntoIterator<Item = &'a str>,
) -> Option<CommandContract> {
let path = path.into_iter().collect::<Vec<_>>();
CONTRACTS
.iter()
.find(|entry| entry.path == path.as_slice())
.map(|entry| entry.contract)
}
#[cfg(test)]
pub(crate) fn sibling_documented_schema_verbs(schema_verb: &str) -> Vec<&'static str> {
active_command_contract_entries()
.iter()
.filter(|entry| {
contract_documented_schema_verbs(entry.contract)
.any(|documented| documented == schema_verb)
})
.flat_map(|entry| contract_documented_schema_verbs(entry.contract))
.filter(|documented| *documented != schema_verb)
.collect()
}
fn active_command_contract_entries() -> &'static [&'static CommandContractEntry] {
ACTIVE_COMMAND_CONTRACT_ENTRIES
.get_or_init(|| {
let command = Cli::command();
CONTRACTS
.iter()
.filter(|entry| clap_command_path_exists(&command, entry.path))
.collect()
})
.as_slice()
}
fn advertised_command_contract_entries() -> &'static [&'static CommandContractEntry] {
ADVERTISED_COMMAND_CONTRACT_ENTRIES
.get_or_init(|| {
let command = Cli::command();
let mut entries = active_command_contract_entries().to_vec();
for entry in CONTRACTS.iter() {
if entry.contract.feature_gate.is_some()
&& !clap_command_path_exists(&command, entry.path)
{
entries.push(entry);
}
}
entries
})
.as_slice()
}
fn clap_command_path_exists(command: &clap::Command, path: &[&str]) -> bool {
let mut current = command;
for part in path {
let Some(next) = current
.get_subcommands()
.find(|subcommand| subcommand.get_name() == *part)
else {
return false;
};
current = next;
}
true
}
pub fn command_json_discriminators() -> Vec<CommandJsonDiscriminator> {
advertised_command_contract_entries()
.iter()
.copied()
.flat_map(|entry| {
contract_json_discriminators(entry.contract)
.map(move |discriminator| json_discriminator_metadata(entry.path, &discriminator))
})
.collect()
}
pub fn operator_envelope_verbs() -> Vec<String> {
active_command_contract_entries()
.iter()
.copied()
.filter(|entry| entry.contract.operator_envelope)
.map(|entry| entry.path.join(" "))
.collect()
}
#[cfg(test)]
pub fn command_json_discriminator_for_schema_verb(
schema_verb: &str,
) -> Option<CommandJsonDiscriminator> {
command_json_discriminators_for_schema_verb(schema_verb)
.into_iter()
.next()
}
pub fn command_json_discriminators_for_schema_verb(
schema_verb: &str,
) -> Vec<CommandJsonDiscriminator> {
advertised_command_contract_entries()
.iter()
.copied()
.filter(|entry| {
contract_json_discriminators(entry.contract)
.any(|discriminator| discriminator.schema_verb == Some(schema_verb))
})
.flat_map(|entry| {
let schema_verbs = contract_schema_verbs(entry.contract).collect::<Vec<_>>();
let include_same_command_siblings =
schema_verbs.len() == 1 && schema_verbs[0] == schema_verb;
contract_json_discriminators(entry.contract).filter_map(move |discriminator| {
if discriminator.schema_verb == Some(schema_verb)
|| (include_same_command_siblings && discriminator.schema_verb.is_none())
{
Some(json_discriminator_metadata(entry.path, &discriminator))
} else {
None
}
})
})
.collect()
}
fn json_discriminators_for_path<'a>(
path: impl IntoIterator<Item = &'a str>,
) -> Vec<CommandJsonDiscriminator> {
let path = path.into_iter().collect::<Vec<_>>();
advertised_command_contract_entries()
.iter()
.copied()
.filter(|entry| entry.path == path.as_slice())
.flat_map(|entry| {
contract_json_discriminators(entry.contract)
.map(move |discriminator| json_discriminator_metadata(entry.path, &discriminator))
})
.collect()
}
fn json_discriminator_metadata(
path: &'static [&'static str],
discriminator: &CommandJsonDiscriminatorSpec,
) -> CommandJsonDiscriminator {
CommandJsonDiscriminator {
path: path.iter().map(|part| (*part).to_string()).collect(),
display: path.join(" "),
schema_verb: discriminator.schema_verb.map(str::to_string),
field: discriminator.field.to_string(),
value: discriminator.value.to_string(),
no_schema_reason: discriminator.no_schema_reason.map(str::to_string),
}
}
pub fn command_runtime_contract_for_command(command: &Commands) -> CommandRuntimeContract {
let path = command_path(command);
runtime_contract_for_path(path.iter().copied())
.unwrap_or_else(|| panic!("missing command contract for `{}`", path.join(" ")))
}
pub fn command_runtime_contract(command_name: &str) -> Option<CommandRuntimeContract> {
runtime_contract_for_path(command_name.split_whitespace())
}
pub(crate) fn command_runtime_contract_for_schema_verb(
schema_verb: &str,
) -> Option<CommandRuntimeContract> {
command_runtime_contract(schema_verb)
.or_else(|| command_runtime_contract(&schema_verb_without_flags(schema_verb)))
}
pub(crate) fn schema_verb_without_flags(schema_verb: &str) -> String {
schema_verb
.split_whitespace()
.filter(|part| !part.starts_with('-'))
.collect::<Vec<_>>()
.join(" ")
}
fn runtime_contract_for_path<'a>(
path: impl IntoIterator<Item = &'a str>,
) -> Option<CommandRuntimeContract> {
let path = path.into_iter().collect::<Vec<_>>();
active_command_contract_entries()
.iter()
.copied()
.find(|entry| entry.path == path.as_slice())
.map(|entry| runtime_contract(entry.path, entry.contract))
}
fn runtime_contract(
path: &'static [&'static str],
contract: CommandContract,
) -> CommandRuntimeContract {
CommandRuntimeContract {
path: path.to_vec(),
display: path.join(" "),
supports_json: contract.supports_json,
supports_json_compact: contract.supports_json_compact,
supports_op_id: contract.supports_op_id,
persists_op_id: contract.persists_op_id,
uses_bootstrap_op_id_store: uses_bootstrap_op_id_store(contract),
help_visibility: contract.help_visibility,
help_rank: contract.help_rank,
surface: contract.surface,
canonical_command: contract.canonical_command,
json_kind: contract.json_kind,
}
}
pub fn command_supports_op_id(command_name: &str) -> bool {
command_runtime_contract(command_name)
.map(|contract| contract.supports_op_id)
.unwrap_or(false)
}
pub fn command_persists_op_id(command_name: &str) -> bool {
command_runtime_contract(command_name)
.map(|contract| contract.persists_op_id)
.unwrap_or(false)
}
pub fn command_uses_bootstrap_op_id_store(command_name: &str) -> bool {
command_runtime_contract(command_name)
.map(|contract| contract.uses_bootstrap_op_id_store)
.unwrap_or(false)
}
pub(crate) fn feature_gated_command_roots() -> Vec<&'static str> {
let mut roots = CONTRACTS
.iter()
.filter(|entry| entry.path.len() == 1 && entry.contract.feature_gate.is_some())
.map(|entry| entry.path[0])
.collect::<Vec<_>>();
roots.sort_unstable();
roots.dedup();
roots
}
pub fn command_supports_op_id_for_command(command: &Commands) -> bool {
command_runtime_contract_for_command(command).supports_op_id
}
pub fn command_supports_json_for_command(command: &Commands) -> bool {
command_runtime_contract_for_command(command).supports_json
}
pub fn command_help_tier(command_name: &str) -> &'static str {
command_runtime_contract(command_name)
.map(|contract| help_visibility_to_tier(contract.help_visibility))
.unwrap_or("advanced")
}
pub fn command_surface(command_name: &str) -> &'static str {
command_runtime_contract(command_name)
.map(|contract| contract.surface)
.unwrap_or("native")
}
pub fn command_help_visibility(command_name: &str) -> &'static str {
command_runtime_contract(command_name)
.map(|contract| contract.help_visibility)
.unwrap_or("advanced")
}
pub fn command_canonical_command(command_name: &str) -> Option<&'static str> {
command_runtime_contract(command_name).and_then(|contract| contract.canonical_command)
}
pub fn root_commands_for_help_visibility(visibility: &str) -> Vec<&'static str> {
let mut entries = active_command_contract_entries()
.iter()
.copied()
.filter(|entry| entry.path.len() == 1 && entry.contract.help_visibility == visibility)
.collect::<Vec<_>>();
entries.sort_by_key(|entry| (entry.contract.help_rank, entry.path[0]));
entries.into_iter().map(|entry| entry.path[0]).collect()
}
pub fn root_commands_for_advanced_help() -> Vec<&'static str> {
let mut entries = advanced_help_root_entries();
entries.sort_by_key(|entry| (entry.contract.help_rank, entry.path[0]));
entries.into_iter().map(|entry| entry.path[0]).collect()
}
fn advanced_help_root_entries() -> Vec<&'static CommandContractEntry> {
active_command_contract_entries()
.iter()
.copied()
.filter(|entry| {
entry.path.len() == 1
&& !matches!(entry.contract.help_visibility, "everyday" | "hidden")
})
.collect()
}
pub fn advanced_help_groups() -> Vec<(&'static str, Vec<&'static str>)> {
const GROUPS: &[(&str, &str)] = &[
("threads", "Threads and integration"),
("states", "States and history"),
("collab", "Collaboration and review"),
("recovery", "Recovery and integrity"),
("repo", "Repo and environment"),
("automation", "Agents and automation"),
("git-interop", "Git interop"),
("admin", "Admin and maintenance"),
];
let mut entries = advanced_help_root_entries();
entries.sort_by_key(|entry| (entry.contract.help_rank, entry.path[0]));
GROUPS
.iter()
.map(|(id, title)| {
(
*title,
entries
.iter()
.filter(|entry| advanced_help_group_id(&entry.contract) == *id)
.map(|entry| entry.path[0])
.collect(),
)
})
.collect()
}
fn advanced_help_group_id(contract: &CommandContract) -> &'static str {
match contract.surface {
"automation" => "automation",
"admin" => "admin",
"git_adapter" => "git-interop",
_ => contract.help_category.unwrap_or(""),
}
}
pub(crate) fn recommended_action_template(action: &str) -> Option<ActionTemplate> {
let trimmed = action.trim();
if trimmed.is_empty() {
return None;
}
RECOMMENDED_ACTION_TEMPLATES
.iter()
.find(|(template_action, _, _, _)| *template_action == trimmed)
.map(
|(template_action, argv_template, required_inputs, agent_may_fill)| {
action_template_from_parts(
template_action,
argv_template,
required_inputs,
*agent_may_fill,
)
},
)
.or_else(|| dynamic_recommended_action_template(trimmed))
.or_else(|| concrete_recommended_action_template(trimmed))
}
fn concrete_recommended_action_template(action: &str) -> Option<ActionTemplate> {
if RECOMMENDED_ACTION_PLACEHOLDERS.contains(&action) || is_display_only_template(action) {
return None;
}
if validate_recommended_action(action).is_err() {
return None;
}
let argv = split_recommended_action(action).ok()?;
Some(action_template_from_owned(
action.to_string(),
argv,
Vec::new(),
false,
))
}
fn dynamic_recommended_action_template(action: &str) -> Option<ActionTemplate> {
let argv = split_recommended_action(action).ok()?;
if let Some(template) = dynamic_message_recommended_action_template(action, &argv) {
return Some(template);
}
match argv.as_slice() {
[heddle, clone, remote, path]
if heddle == "heddle" && clone == "clone" && is_placeholder_arg(path) =>
{
Some(action_template_from_owned(
action.to_string(),
vec![
"heddle".to_string(),
"clone".to_string(),
remote.clone(),
path.clone(),
],
vec![placeholder_input_name(path)],
false,
))
}
[heddle, clone, remote, path, flag, thread]
if heddle == "heddle"
&& clone == "clone"
&& flag == "--thread"
&& is_placeholder_arg(path) =>
{
Some(action_template_from_owned(
action.to_string(),
vec![
"heddle".to_string(),
"clone".to_string(),
remote.clone(),
path.clone(),
"--thread".to_string(),
thread.clone(),
],
vec![placeholder_input_name(path)],
false,
))
}
[heddle, start, thread_name, path_flag, path]
if heddle == "heddle"
&& start == "start"
&& path_flag == "--path"
&& is_placeholder_arg(path) =>
{
let required_inputs = [thread_name, path]
.iter()
.filter(|arg| is_placeholder_arg(arg))
.map(|arg| placeholder_input_name(arg))
.collect();
Some(action_template_from_owned(
action.to_string(),
vec![
"heddle".to_string(),
"start".to_string(),
thread_name.clone(),
"--path".to_string(),
path.clone(),
],
required_inputs,
true,
))
}
[heddle, thread_cmd, absorb, thread_name, into_flag, parent]
if heddle == "heddle"
&& thread_cmd == "thread"
&& absorb == "absorb"
&& into_flag == "--into"
&& is_placeholder_arg(parent) =>
{
Some(action_template_from_owned(
action.to_string(),
vec![
"heddle".to_string(),
"thread".to_string(),
"absorb".to_string(),
thread_name.clone(),
"--into".to_string(),
parent.clone(),
],
vec![placeholder_input_name(parent)],
true,
))
}
_ => None,
}
}
fn dynamic_message_recommended_action_template(
action: &str,
argv: &[String],
) -> Option<ActionTemplate> {
match argv {
[heddle, command, message_flag, message]
if heddle == "heddle"
&& matches!(
command.as_str(),
"capture" | "checkpoint" | "commit" | "ready"
)
&& is_message_flag(message_flag)
&& is_message_placeholder_arg(message) =>
{
Some(action_template_from_owned(
action.to_string(),
vec![
"heddle".to_string(),
command.clone(),
"-m".to_string(),
"<message>".to_string(),
],
vec!["message".to_string()],
true,
))
}
[
heddle,
capture,
message_flag,
message,
confidence_flag,
confidence,
] if heddle == "heddle"
&& capture == "capture"
&& is_message_flag(message_flag)
&& is_message_placeholder_arg(message)
&& confidence_flag == "--confidence"
&& is_placeholder_arg(confidence) =>
{
Some(action_template_from_owned(
action.to_string(),
vec![
"heddle".to_string(),
"capture".to_string(),
"-m".to_string(),
"<message>".to_string(),
"--confidence".to_string(),
confidence.clone(),
],
vec!["message".to_string(), placeholder_input_name(confidence)],
true,
))
}
[
heddle,
repo_flag,
repo_path,
command,
message_flag,
message,
confidence_flag,
confidence,
] if heddle == "heddle"
&& repo_flag == "--repo"
&& matches!(command.as_str(), "capture" | "commit")
&& is_message_flag(message_flag)
&& is_message_placeholder_arg(message)
&& confidence_flag == "--confidence"
&& is_placeholder_arg(confidence) =>
{
Some(action_template_from_owned(
action.to_string(),
vec![
"heddle".to_string(),
"--repo".to_string(),
repo_path.clone(),
command.clone(),
"-m".to_string(),
"<message>".to_string(),
"--confidence".to_string(),
confidence.clone(),
],
vec!["message".to_string(), placeholder_input_name(confidence)],
true,
))
}
[heddle, stash, push, message_flag, message]
if heddle == "heddle"
&& stash == "stash"
&& push == "push"
&& is_message_flag(message_flag)
&& is_message_placeholder_arg(message) =>
{
Some(action_template_from_owned(
action.to_string(),
vec![
"heddle".to_string(),
"stash".to_string(),
"push".to_string(),
"-m".to_string(),
"<message>".to_string(),
],
vec!["message".to_string()],
true,
))
}
_ => None,
}
}
fn is_message_flag(value: &str) -> bool {
value == "-m" || value == "--message"
}
fn is_message_placeholder_arg(value: &str) -> bool {
matches!(value, "..." | "…") || value == "<message>"
}
fn is_placeholder_arg(value: &str) -> bool {
value.starts_with('<') && value.ends_with('>') && value.len() > 2
}
fn placeholder_input_name(value: &str) -> String {
value
.trim_start_matches('<')
.trim_end_matches('>')
.replace('-', "_")
}
fn help_visibility_to_tier(help_visibility: &str) -> &'static str {
match help_visibility {
"everyday" => "everyday",
"hidden" => "hidden",
_ => "advanced",
}
}
pub fn observe_only_root_commands() -> Vec<&'static str> {
active_command_contract_entries()
.iter()
.copied()
.filter(|entry| {
entry.path.len() == 1 && entry.contract.observe_only && !entry.contract.mutates
})
.map(|entry| entry.path[0])
.collect()
}
pub fn command_contract_root_commands() -> Vec<&'static str> {
active_command_contract_entries()
.iter()
.copied()
.filter(|entry| entry.path.len() == 1)
.map(|entry| entry.path[0])
.collect()
}
pub(crate) fn validate_recommended_action(action: &str) -> std::result::Result<(), String> {
let trimmed = action.trim();
if trimmed.is_empty() {
return Ok(());
}
if RECOMMENDED_ACTION_PLACEHOLDERS.contains(&trimmed) {
return recommended_action_template(trimmed)
.map(|_| ())
.ok_or_else(|| {
format!(
"recommended action placeholder `{trimmed}` must have a structured template"
)
});
}
if is_display_only_template(trimmed) {
return recommended_action_template(trimmed).map(|_| ()).ok_or_else(|| {
format!(
"display-only recommended action `{trimmed}` must be registered as a structured template"
)
});
}
let argv = split_recommended_action(trimmed)?;
match argv.first().map(String::as_str) {
Some("heddle") => Cli::command()
.try_get_matches_from(argv)
.map(|_| ())
.map_err(|err| err.to_string()),
Some(other) => Err(format!(
"recommended action must start with `heddle` or be registered as a placeholder, found `{other}`"
)),
None => Ok(()),
}
}
fn is_display_only_template(action: &str) -> bool {
action.contains("...") || action.contains('…') || (action.contains('<') && action.contains('>'))
}
pub(crate) fn split_recommended_action(action: &str) -> std::result::Result<Vec<String>, String> {
let mut args = Vec::new();
let mut current = String::new();
let mut chars = action.chars().peekable();
let mut in_single_quote = false;
let mut in_double_quote = false;
while let Some(ch) = chars.next() {
match (ch, in_single_quote, in_double_quote) {
('\'', false, false) => in_single_quote = true,
('\'', true, false) => in_single_quote = false,
('"', false, false) => in_double_quote = true,
('"', false, true) => in_double_quote = false,
('\\', false, _) => match chars.next() {
Some(next) => current.push(next),
None => current.push('\\'),
},
(ch, false, false) if ch.is_whitespace() => {
if !current.is_empty() {
args.push(std::mem::take(&mut current));
}
}
(ch, _, _) => current.push(ch),
}
}
if in_single_quote {
return Err("unterminated single quote in recommended action".to_string());
}
if in_double_quote {
return Err("unterminated double quote in recommended action".to_string());
}
if !current.is_empty() {
args.push(current);
}
Ok(args)
}
pub(crate) fn schema_verbs() -> Vec<&'static str> {
let mut verbs = collect_schema_verbs(contract_schema_verbs);
verbs.push("error");
verbs
}
pub(crate) fn documented_schema_verbs() -> Vec<&'static str> {
let mut verbs = collect_schema_verbs(contract_documented_schema_verbs);
verbs.push("error");
verbs
}
pub(crate) fn opaque_schema_verbs() -> Vec<&'static str> {
collect_schema_verbs(|contract| contract.opaque_schema_verbs.iter().copied())
}
fn collect_schema_verbs<I>(select: impl Fn(CommandContract) -> I) -> Vec<&'static str>
where
I: IntoIterator<Item = &'static str>,
{
let mut verbs = Vec::new();
for entry in advertised_command_contract_entries().iter().copied() {
for verb in select(entry.contract) {
if !verbs.contains(&verb) {
verbs.push(verb);
}
}
}
verbs
}
pub fn command_path(command: &Commands) -> Vec<&'static str> {
match command {
Commands::Init(_) => vec!["init"],
Commands::Adopt(_) => vec!["adopt"],
Commands::Help { .. } => vec!["help"],
Commands::Status { .. } => vec!["status"],
Commands::Watch(_) => vec!["watch"],
Commands::Verify => vec!["verify"],
Commands::Doctor(args) => match &args.command {
None => vec!["doctor"],
Some(DoctorCommands::Docs(_)) => vec!["doctor", "docs"],
Some(DoctorCommands::Schemas(_)) => vec!["doctor", "schemas"],
},
#[cfg(feature = "git-overlay")]
Commands::GitOverlay => vec!["git-overlay"],
Commands::Schemas { .. } => vec!["schemas"],
Commands::Start(_) => vec!["start"],
Commands::Try(_) => vec!["try"],
Commands::Run(_) => vec!["run"],
Commands::Sync(_) => vec!["sync"],
Commands::Continue => vec!["continue"],
Commands::Abort => vec!["abort"],
Commands::Land(_) => vec!["land"],
Commands::Ready(_) => vec!["ready"],
Commands::Capture(_) => vec!["capture"],
Commands::Commit(_) => vec!["commit"],
Commands::Checkpoint(_) => vec!["checkpoint"],
Commands::Log(_) => vec!["log"],
Commands::Show { .. } => vec!["show"],
Commands::Retro(_) => vec!["retro"],
Commands::Clean { .. } => vec!["clean"],
Commands::Diff(_) => vec!["diff"],
Commands::Switch(_) => vec!["switch"],
Commands::Discuss { command } => match command {
DiscussCommands::Open(_) => vec!["discuss", "open"],
DiscussCommands::Append(_) => vec!["discuss", "append"],
DiscussCommands::Resolve(_) => vec!["discuss", "resolve"],
DiscussCommands::List(_) => vec!["discuss", "list"],
DiscussCommands::Show(_) => vec!["discuss", "show"],
},
Commands::Query(_) => vec!["query"],
Commands::Transaction { command } => match command {
TransactionCommands::Begin(_) => vec!["transaction", "begin"],
TransactionCommands::Commit(_) => vec!["transaction", "commit"],
TransactionCommands::Abort(_) => vec!["transaction", "abort"],
TransactionCommands::Status(_) => vec!["transaction", "status"],
},
Commands::Review { command } => match command {
ReviewCommands::Show(_) => vec!["review", "show"],
ReviewCommands::Sign(_) => vec!["review", "sign"],
ReviewCommands::Next(_) => vec!["review", "next"],
ReviewCommands::Health(_) => vec!["review", "health"],
},
Commands::Redact { command } => match command {
RedactCommands::Apply(_) => vec!["redact", "apply"],
RedactCommands::List(_) => vec!["redact", "list"],
RedactCommands::Show(_) => vec!["redact", "show"],
RedactCommands::Trust(command) => match command {
RedactTrustCommands::Add(_) => vec!["redact", "trust", "add"],
RedactTrustCommands::List(_) => vec!["redact", "trust", "list"],
RedactTrustCommands::Remove(_) => vec!["redact", "trust", "remove"],
},
RedactCommands::Purge(command) => match command {
PurgeCommands::Apply(_) => vec!["redact", "purge", "apply"],
PurgeCommands::List(_) => vec!["redact", "purge", "list"],
},
},
Commands::Visibility { command } => match command {
VisibilityCommands::Set(_) => vec!["visibility", "set"],
VisibilityCommands::Promote(_) => vec!["visibility", "promote"],
VisibilityCommands::Show(_) => vec!["visibility", "show"],
VisibilityCommands::List(_) => vec!["visibility", "list"],
},
Commands::Revert(_) => vec!["revert"],
Commands::Undo(_) => vec!["undo"],
Commands::Collapse(_) => vec!["collapse"],
Commands::Expand(_) => vec!["expand"],
Commands::Thread { command } => match command {
ThreadCommands::Create { .. } => vec!["thread", "create"],
ThreadCommands::Current => vec!["thread", "current"],
ThreadCommands::Switch { .. } => vec!["thread", "switch"],
ThreadCommands::Cd { .. } => vec!["thread", "cd"],
ThreadCommands::List(_) => vec!["thread", "list"],
ThreadCommands::Show(_) => vec!["thread", "show"],
ThreadCommands::Captures(_) => vec!["thread", "captures"],
ThreadCommands::Rename(_) => vec!["thread", "rename"],
ThreadCommands::Refresh(_) => vec!["thread", "refresh"],
ThreadCommands::Move(_) => vec!["thread", "move"],
ThreadCommands::Absorb(_) => vec!["thread", "absorb"],
ThreadCommands::Resolve(_) => vec!["thread", "resolve"],
ThreadCommands::Promote(_) => vec!["thread", "promote"],
ThreadCommands::Drop(_) => vec!["thread", "drop"],
ThreadCommands::Approve(_) => vec!["thread", "approve"],
ThreadCommands::Approvals(_) => vec!["thread", "approvals"],
ThreadCommands::RevokeApproval(_) => vec!["thread", "revoke-approval"],
ThreadCommands::CheckMerge(_) => vec!["thread", "check-merge"],
ThreadCommands::Cleanup(_) => vec!["thread", "cleanup"],
ThreadCommands::Marker { command } => match command {
ThreadMarkerCommands::List { .. } => vec!["thread", "marker", "list"],
ThreadMarkerCommands::Create { .. } => {
vec!["thread", "marker", "create"]
}
ThreadMarkerCommands::Delete { .. } => {
vec!["thread", "marker", "delete"]
}
ThreadMarkerCommands::Show { .. } => vec!["thread", "marker", "show"],
},
},
Commands::Timeline(args) => match &args.command {
TimelineCommands::Status(_) => vec!["timeline", "status"],
TimelineCommands::RecordStart(_) => vec!["timeline", "record-start"],
TimelineCommands::RecordFinish(_) => vec!["timeline", "record-finish"],
TimelineCommands::Fork(_) => vec!["timeline", "fork"],
TimelineCommands::Reset(_) => vec!["timeline", "reset"],
TimelineCommands::Recover(_) => vec!["timeline", "recover"],
},
Commands::Shell { command } => match command {
ShellCommands::Init { .. } => vec!["shell", "init"],
ShellCommands::Completion { .. } => vec!["shell", "completion"],
ShellCommands::Prompt => vec!["shell", "prompt"],
},
Commands::Complete { .. } => vec!["complete"],
Commands::Merge(_) => vec!["merge"],
Commands::Resolve(_) => vec!["resolve"],
Commands::Fsck { .. } => vec!["fsck"],
Commands::Oplog { command } => match command {
OplogCommands::Recover => vec!["oplog", "recover"],
},
Commands::Fetch { .. } => vec!["fetch"],
Commands::Push(_) => vec!["push"],
Commands::Pull(_) => vec!["pull"],
Commands::Remote { command } => match command {
RemoteCommands::List => vec!["remote", "list"],
RemoteCommands::Add { .. } => vec!["remote", "add"],
RemoteCommands::Remove { .. } => vec!["remote", "remove"],
RemoteCommands::SetDefault { .. } => vec!["remote", "set-default"],
RemoteCommands::Show { .. } => vec!["remote", "show"],
},
#[cfg(feature = "client")]
Commands::Auth { command } => match command {
AuthCommands::Login { .. } => vec!["auth", "login"],
AuthCommands::Logout { .. } => vec!["auth", "logout"],
AuthCommands::Status { .. } => vec!["auth", "status"],
AuthCommands::CreateServiceToken { .. } => vec!["auth", "create-service-token"],
},
Commands::Context { command } => match command {
ContextCommands::Set(_) => vec!["context", "set"],
ContextCommands::Get(_) => vec!["context", "get"],
ContextCommands::List(_) => vec!["context", "list"],
ContextCommands::History(_) => vec!["context", "history"],
ContextCommands::Edit(_) => vec!["context", "edit"],
ContextCommands::Supersede(_) => vec!["context", "supersede"],
ContextCommands::Rm(_) => vec!["context", "rm"],
ContextCommands::Check(_) => vec!["context", "check"],
ContextCommands::Suggest(_) => vec!["context", "suggest"],
ContextCommands::Audit(_) => vec!["context", "audit"],
},
Commands::Integration { command } => match command {
IntegrationCommands::List => vec!["integration", "list"],
IntegrationCommands::Install(_) => vec!["integration", "install"],
IntegrationCommands::Doctor => vec!["integration", "doctor"],
IntegrationCommands::Uninstall(_) => vec!["integration", "uninstall"],
IntegrationCommands::Upgrade(_) => vec!["integration", "upgrade"],
IntegrationCommands::Relay(_) => vec!["integration", "relay"],
},
Commands::Stash { command } => match command {
StashCommands::Push { .. } => vec!["stash", "push"],
StashCommands::List => vec!["stash", "list"],
StashCommands::Pop => vec!["stash", "pop"],
StashCommands::Apply => vec!["stash", "apply"],
StashCommands::Drop => vec!["stash", "drop"],
StashCommands::Clear => vec!["stash", "clear"],
StashCommands::Show => vec!["stash", "show"],
},
#[cfg(feature = "client")]
Commands::Support { command } => match command {
SupportCommands::Grant(_) => vec!["support", "grant"],
SupportCommands::List(_) => vec!["support", "list"],
SupportCommands::Revoke(_) => vec!["support", "revoke"],
},
#[cfg(feature = "git-overlay")]
Commands::Bridge { command } => match command {
BridgeCommands::Git { command } => match command {
GitCommands::Status => vec!["bridge", "git", "status"],
GitCommands::Init { .. } => vec!["bridge", "git", "init"],
GitCommands::Export { .. } => vec!["bridge", "git", "export"],
GitCommands::Import { .. } => vec!["bridge", "git", "import"],
GitCommands::Sync { .. } => vec!["bridge", "git", "sync"],
GitCommands::Reconcile { .. } => vec!["bridge", "git", "reconcile"],
GitCommands::Push { .. } => vec!["bridge", "git", "push"],
GitCommands::Pull { .. } => vec!["bridge", "git", "pull"],
#[cfg(feature = "ingest")]
GitCommands::Reason { .. } => vec!["bridge", "git", "reason"],
},
},
#[cfg(feature = "semantic")]
Commands::Semantic { command } => match command {
SemanticCommands::Hot { .. } => vec!["semantic", "hot"],
},
Commands::Daemon { command } => match command {
DaemonCommands::Serve => vec!["daemon", "serve"],
DaemonCommands::Status => vec!["daemon", "status"],
DaemonCommands::Stop => vec!["daemon", "stop"],
},
Commands::Agent { command } => match command {
AgentCommands::Serve(_) => vec!["agent", "serve"],
AgentCommands::Status => vec!["agent", "status"],
AgentCommands::Stop => vec!["agent", "stop"],
AgentCommands::Reserve(_) => vec!["agent", "reserve"],
AgentCommands::Heartbeat(_) => vec!["agent", "heartbeat"],
AgentCommands::Capture(_) => vec!["agent", "capture"],
AgentCommands::Ready(_) => vec!["agent", "ready"],
AgentCommands::Release(_) => vec!["agent", "release"],
AgentCommands::List(_) => vec!["agent", "list"],
AgentCommands::Task(command) => match command {
AgentTaskCommands::Create(_) => vec!["agent", "task", "create"],
AgentTaskCommands::List(_) => vec!["agent", "task", "list"],
AgentTaskCommands::Show(_) => vec!["agent", "task", "show"],
AgentTaskCommands::Update(_) => vec!["agent", "task", "update"],
},
AgentCommands::Fanout(command) => match command {
AgentFanoutCommands::Plan(_) => vec!["agent", "fanout", "plan"],
AgentFanoutCommands::Start(_) => vec!["agent", "fanout", "start"],
},
},
Commands::Maintenance { command } => match command {
MaintenanceCommands::Inspect => vec!["maintenance", "inspect"],
MaintenanceCommands::Run => vec!["maintenance", "run"],
MaintenanceCommands::Gc { .. } => vec!["maintenance", "gc"],
MaintenanceCommands::Index { .. } => vec!["maintenance", "index"],
MaintenanceCommands::Monitor { .. } => vec!["maintenance", "monitor"],
},
Commands::CherryPick { .. } => vec!["cherry-pick"],
Commands::Clone(_) => vec!["clone"],
Commands::Rebase { .. } => vec!["rebase"],
Commands::Hook { command } => match command {
HookCommands::List => vec!["hook", "list"],
HookCommands::Install { .. } => vec!["hook", "install"],
HookCommands::Uninstall { .. } => vec!["hook", "uninstall"],
HookCommands::Events { .. } => vec!["hook", "events"],
},
Commands::Actor { command } => match command {
ActorCommands::Spawn(_) => vec!["actor", "spawn"],
ActorCommands::List(_) => vec!["actor", "list"],
ActorCommands::Show(_) => vec!["actor", "show"],
ActorCommands::Explain(_) => vec!["actor", "explain"],
ActorCommands::Done(_) => vec!["actor", "done"],
},
Commands::Session { command } => match command {
SessionCommands::Start(_) => vec!["session", "start"],
SessionCommands::Segment(_) => vec!["session", "segment"],
SessionCommands::End(_) => vec!["session", "end"],
SessionCommands::Show(_) => vec!["session", "show"],
SessionCommands::List(_) => vec!["session", "list"],
},
#[cfg(feature = "client")]
Commands::Presence { command } => match command {
PresenceCommands::Publish { .. } => vec!["presence", "publish"],
},
}
}
#[cfg(test)]
mod tests;