1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum Tier {
21 Everyday,
25 Advanced,
28 Hidden,
30}
31
32pub fn tier_of(verb: &str) -> Tier {
36 match crate::cli::commands::command_help_tier(verb) {
37 "everyday" => Tier::Everyday,
38 "hidden" => Tier::Hidden,
39 _ => Tier::Advanced,
40 }
41}
42
43pub fn everyday_verbs() -> Vec<&'static str> {
46 crate::cli::commands::root_commands_for_help_visibility("everyday")
47}
48
49fn primary_loop_verbs(catalog: &crate::cli::commands::CommandCatalogOutput) -> Vec<&'static str> {
53 everyday_verbs()
54 .into_iter()
55 .filter(|verb| {
56 catalog
57 .command_by_display(verb)
58 .is_some_and(|entry| entry.help_rank <= 70)
59 })
60 .collect()
61}
62
63pub fn advanced_verbs() -> Vec<&'static str> {
67 crate::cli::commands::root_commands_for_advanced_help()
68}
69
70fn catalog_summary(catalog: &crate::cli::commands::CommandCatalogOutput, verb: &str) -> String {
73 catalog
74 .command_by_display(verb)
75 .map(|entry| entry.summary.clone())
76 .unwrap_or_default()
77}
78
79pub fn print_help(cmd: &clap::Command, topic: &[String]) -> std::io::Result<()> {
88 crate::cli::render::write_stdout(&render_help(cmd, topic))
89 .map_err(|err| std::io::Error::other(err.to_string()))
90}
91
92pub fn render_help(cmd: &clap::Command, topic: &[String]) -> String {
98 use std::fmt::Write;
99 let mut out = String::new();
100 match topic {
101 [] => {
102 let catalog = crate::cli::commands::build_command_catalog();
103 let _ = writeln!(out, "Heddle — agent-native version control");
104 let _ = writeln!(out);
105 let _ = writeln!(out, "Common loop:");
106 for name in primary_loop_verbs(&catalog) {
107 let blurb = catalog_summary(&catalog, name);
108 if blurb.is_empty() {
109 continue;
110 }
111 let _ = writeln!(out, " {:<10} {}", name, blurb);
112 }
113 let _ = writeln!(out);
114 let _ = writeln!(
115 out,
116 "Existing Git: heddle status -> heddle init -> heddle capture -m \"...\" -> heddle commit -> heddle push"
117 );
118 let _ = writeln!(
119 out,
120 "Isolated work: heddle start <name> --path ../<name> -> heddle capture -m \"...\" -> heddle ready -> heddle land"
121 );
122 let _ = writeln!(out);
123 let _ = writeln!(
124 out,
125 "Nearby: `heddle pull`, `heddle undo`, and `heddle verify`."
126 );
127 let _ = writeln!(
128 out,
129 "Start here: `heddle init`, `heddle clone`, or `heddle capture`."
130 );
131 let _ = writeln!(
132 out,
133 "In Git Overlay, Heddle uses its embedded Sley engine to operate directly on the checkout's `.git`."
134 );
135 let _ = writeln!(
136 out,
137 "Coming from Git? Run `heddle help git-concepts` for the concept map."
138 );
139 let _ = writeln!(out);
140 let _ = writeln!(
145 out,
146 "Output: text is the default; pass `--output json` for the \
147 full machine contract (stable `output_kind`, exit codes, recovery \
148 templates), or `--output json-compact` for the decision surface \
149 only (fewer tokens, same `output_kind`). No TTY/pipe auto-detection. \
150 Details: `heddle help output-formats`."
151 );
152 let _ = writeln!(out);
153 let _ = writeln!(
154 out,
155 "Run `heddle help model` for the short mental model, \
156 `heddle help advanced` for power surfaces, automation, and Git interop, \
157 or `heddle help <topic>` for a topic page (e.g. `git-concepts`, \
158 `git-overlay`, \
159 `threads`, `daemon`, `signals`, `git-projection`, `operation-ids`, \
160 `remotes`, `output-formats`, `git-dependencies`)."
161 );
162 }
163 [name] if name == "advanced" => {
164 let catalog = crate::cli::commands::build_command_catalog();
165 let _ = writeln!(out, "{}", ADVANCED_HELP);
166 for (title, verbs) in crate::cli::commands::advanced_help_groups() {
172 let mut lines = Vec::new();
173 for name in verbs {
174 let blurb = catalog_summary(&catalog, name);
175 if blurb.is_empty() {
176 continue;
177 }
178 let canonical = crate::cli::commands::command_canonical_command(name)
179 .map(|canonical| format!(" [use `{canonical}`]"))
180 .unwrap_or_default();
181 lines.push(format!(" {name:<14} {blurb}{canonical}"));
182 }
183 if lines.is_empty() {
184 continue;
185 }
186 let _ = writeln!(out, "{title}:");
187 for line in lines {
188 let _ = writeln!(out, "{line}");
189 }
190 let _ = writeln!(out);
191 }
192 }
193 [name] if topic_text(name).is_some() => {
194 let _ = writeln!(out, "{}", topic_text(name).expect("checked above"));
195 }
196 path => {
197 if let Some(mut subcommand) = help_command_for_path(cmd, path) {
198 let _ = write!(out, "{}", subcommand.render_help());
202 } else {
203 let name = path.join(" ");
204 let _ = writeln!(
205 out,
206 "no topic or command '{name}'. Run `heddle help advanced` for \
207 the full advanced list, or `heddle help` for the \
208 curated everyday surface."
209 );
210 }
211 }
212 }
213 out
214}
215
216pub fn print_direct_help_for_raw(
217 cmd: &clap::Command,
218 raw: &[String],
219) -> Option<std::io::Result<()>> {
220 let rendered = render_direct_help_for_raw(cmd, raw)?;
221 Some(
222 crate::cli::render::write_stdout(&rendered)
223 .map_err(|err| std::io::Error::other(err.to_string())),
224 )
225}
226
227pub fn render_direct_help_for_raw(cmd: &clap::Command, raw: &[String]) -> Option<String> {
232 let path = command_path_from_raw_help_request(cmd, raw)?;
233 Some(match help_command_for_path(cmd, &path) {
234 Some(mut subcommand) => {
235 if command_has_long_help_content(&subcommand) {
243 subcommand.render_long_help().to_string()
244 } else {
245 subcommand.render_help().to_string()
246 }
247 }
248 None => render_help(cmd, &path),
249 })
250}
251
252fn command_has_long_help_content(command: &clap::Command) -> bool {
258 command.get_long_about().is_some()
259 || command.get_after_long_help().is_some()
260 || command.get_arguments().any(|arg| {
261 !arg.is_hide_set()
262 && (arg.get_long_help().is_some()
263 || arg
264 .get_possible_values()
265 .iter()
266 .any(|value| value.get_help().is_some()))
267 })
268}
269
270const CAPTURE_AGENT_FLAG_IDS: &[&str] = &[
276 "agent_provider",
277 "agent_model",
278 "agent_session",
279 "agent_segment",
280 "policy",
281 "no_policy",
282 "no_agent",
283 "split",
284 "into",
285 "paths",
286];
287
288fn reveal_capture_agent_flags(command: clap::Command) -> clap::Command {
291 command.mut_args(|arg| {
292 if CAPTURE_AGENT_FLAG_IDS.contains(&arg.get_id().as_str()) {
293 arg.hide(false)
294 } else {
295 arg
296 }
297 })
298}
299
300pub fn print_capture_agent_help(cmd: &clap::Command) -> std::io::Result<()> {
309 crate::cli::render::write_stdout(&render_capture_agent_help(cmd))
310 .map_err(|err| std::io::Error::other(err.to_string()))
311}
312
313pub fn render_capture_agent_help(cmd: &clap::Command) -> String {
318 let capture = find_subcommand_or_alias(cmd, "capture")
319 .expect("capture subcommand exists in the clap command tree");
320 let bin_name = format!("{} {}", cmd.get_name(), capture.get_name());
321 let mut help = reveal_capture_agent_flags(capture.clone()).bin_name(bin_name);
322 help.render_long_help().to_string()
323}
324
325fn help_command_for_path(cmd: &clap::Command, path: &[String]) -> Option<clap::Command> {
326 if path.is_empty() {
327 return None;
328 }
329
330 let mut current = cmd;
331 let mut bin_name = cmd.get_name().to_string();
332 let mut canonical_path = Vec::new();
333 for part in path {
334 let subcommand = find_subcommand_or_alias(current, part)?;
335 bin_name.push(' ');
336 bin_name.push_str(part);
337 canonical_path.push(subcommand.get_name().to_string());
338 current = subcommand;
339 }
340
341 let mut help = current.clone().bin_name(bin_name);
342 for arg in cmd
343 .get_arguments()
344 .filter(|arg| arg.is_global_set() && !arg.is_hide_set())
345 {
346 help = help.arg(arg.clone());
347 }
348 if crate::cli::commands::command_runtime_contract(&canonical_path.join(" "))
349 .is_some_and(|contract| contract.supports_op_id)
350 && let Some(arg) = cmd
351 .get_arguments()
352 .find(|arg| arg.get_long() == Some("op-id"))
353 {
354 help = help.arg(arg.clone().hide(false).value_name("UUID"));
355 }
356 Some(help)
357}
358
359fn global_option_takes_value(command: &clap::Command, token: &str) -> Option<bool> {
373 command
374 .get_arguments()
375 .find(|arg| {
376 arg.get_long()
377 .is_some_and(|long| token == format!("--{long}"))
378 || arg
379 .get_short()
380 .is_some_and(|short| token == format!("-{short}"))
381 })
382 .map(|arg| arg.get_action().takes_values())
383}
384
385fn find_subcommand_or_alias<'a>(
386 command: &'a clap::Command,
387 name: &str,
388) -> Option<&'a clap::Command> {
389 command.find_subcommand(name).or_else(|| {
390 command
391 .get_subcommands()
392 .find(|subcommand| subcommand.get_all_aliases().any(|alias| alias == name))
393 })
394}
395
396fn command_path_from_raw_help_request(cmd: &clap::Command, raw: &[String]) -> Option<Vec<String>> {
397 if !raw.iter().any(|arg| arg == "--help" || arg == "-h") {
398 return None;
399 }
400 if raw
401 .iter()
402 .all(|arg| arg == "--help" || arg == "-h" || arg.starts_with('-'))
403 {
404 return None;
405 }
406
407 let mut current = cmd;
408 let mut path = Vec::new();
409 let mut skip_next = false;
410 for token in raw {
411 if skip_next {
412 skip_next = false;
413 continue;
414 }
415 if token == "--help" || token == "-h" {
416 continue;
417 }
418 if let Some(takes_value) = global_option_takes_value(current, token) {
419 skip_next = takes_value;
420 continue;
421 }
422 if token.starts_with('-') {
423 continue;
424 }
425 if let Some(subcommand) = find_subcommand_or_alias(current, token) {
426 path.push(subcommand.get_name().to_string());
427 current = subcommand;
428 }
429 }
430
431 (!path.is_empty()).then_some(path)
432}
433
434pub fn render_for_args(args: &[&str]) -> Option<String> {
451 use clap::{CommandFactory, Parser};
452
453 use crate::cli::cli_args::{Cli, Commands};
454
455 let command = Cli::command();
456 let raw: Vec<String> = args.iter().map(|arg| (*arg).to_string()).collect();
457
458 if raw.is_empty() || raw == ["--help"] || raw == ["-h"] || raw == ["help"] {
462 return Some(render_help(&command, &[]));
463 }
464
465 if let Some(rendered) = render_direct_help_for_raw(&command, &raw) {
468 return Some(rendered);
469 }
470
471 if let Ok(cli) = Cli::try_parse_from(std::iter::once("heddle".to_string()).chain(raw.clone()))
474 && let Commands::Help { topics } = &cli.command
475 {
476 return Some(render_help(&command, topics));
478 }
479 if let Ok(cli) = Cli::try_parse_from(std::iter::once("heddle".to_string()).chain(raw.clone()))
480 && let Commands::Capture(args) = &cli.command
481 && args.help_agent
482 {
483 return Some(render_capture_agent_help(&command));
484 }
485
486 None
487}
488
489pub fn topic_text(topic: &str) -> Option<&'static str> {
491 Some(match topic {
492 "advanced" => ADVANCED_HELP,
493 "agent-flags" => AGENT_FLAGS_TOPIC,
494 "agent" | "daemon" => DAEMON_TOPIC,
495 "output-formats" | "output-format" | "output" => OUTPUT_FORMATS_TOPIC,
496 "clone" => CLONE_TOPIC,
497 "git-overlay" => GIT_OVERLAY_TOPIC,
498 "git-concepts" | "git-concept-map" | "git-veteran" => GIT_CONCEPTS_TOPIC,
499 "model" | "mental-model" | "concepts" => MODEL_TOPIC,
500 "threads" => THREADS_TOPIC,
501 "operation-ids" | "idempotency" => OPERATION_IDS_TOPIC,
502 "remotes" => REMOTES_TOPIC,
503 "git-dependencies" | "git-deps" | "git-dependency" => GIT_DEPENDENCIES_TOPIC,
504 "review" => REVIEW_TOPIC,
505 "discuss" | "discussions" => DISCUSS_TOPIC,
506 "git-projection" | "git-projections" | "footer" | "notes" => GIT_PROJECTION_TOPIC,
507 "signals" | "risk-signals" => SIGNALS_TOPIC,
508 _ => return None,
509 })
510}
511
512const ADVANCED_HELP: &str = "Advanced commands for power users, agents, automation, Git interop, and recovery.\n\
513\n\
514The default `heddle help` curates the authority-aware loop: init/adopt/clone,\n\
515status/diff/commit/start, ready/land/push/pull, resolve/continue/abort,\n\
516doctor/verify. Power nouns such as thread/remote/Git projection/agent and\n\
517Git projection commands live behind this topic. Use `heddle help\n\
518<verb>` for curated topics or `heddle <verb> --help` for the full clap-derived\n\
519docs.\n\
520\n\
521This is intentional. The everyday surface stays minimal so first-time users aren't\n\
522overwhelmed; agents and power users reach for the advanced affordances when they\n\
523need them.\n\
524\n\
525Configuration environment:\n\
526 HEDDLE_HOME Relocates global Heddle state (credentials, device identity,\n\
527 session state, and the default user config).\n\
528 HEDDLE_CONFIG Overrides the user config file. Takes precedence over\n\
529 HEDDLE_HOME; otherwise HEDDLE_HOME uses <path>/config.toml.\n\
530\n\
531Principal resolution (highest first): HEDDLE_PRINCIPAL_NAME and\n\
532HEDDLE_PRINCIPAL_EMAIL, repository config, Git config, user config, then\n\
533Unknown <unknown@example.com>. Init, status, and capture use this same order.\n";
534
535const OUTPUT_FORMATS_TOPIC: &str = r#"Output formats — `--output text | json | json-compact`.
540
541`text` is the default, always. There is no TTY/pipe auto-detection — the
542default never switches under you, so scripts and humans see the same thing
543until a flag says otherwise.
544
545`--output json` emits the full machine contract: a stable `output_kind`
546discriminator, exit codes, and recovery templates. Schemas per verb:
547`heddle schemas <verb>`; the catalog of which commands emit what:
548`heddle help --output json`.
549
550`--output json-compact` emits only the decision-surface fields —
551`output_kind`, `status`/`coordination_status`, `blockers`, `next_action`,
552`changed_paths`, `conflicts` — fewer tokens, same `output_kind`, so callers
553can still dispatch on it. Commands advertise `supports_json_compact` in the
554command catalog.
555
556Related: `heddle help operation-ids` for idempotent retries, `heddle help
557agent-flags` for capture attribution overrides.
558"#;
559
560const CLONE_TOPIC: &str = r#"Cloning — Git Overlay and native Heddle repositories.
564
565 heddle clone <remote> <dir> [--thread <name>] [--depth <n>]
566
567Run `heddle clone --help` for the flag list.
568
569# Repository authority
570
571- A Git source is streamed by Sley directly into the destination `.git`, then
572 initialized as Git Overlay. No Git executable or `.heddle/git` mirror is used.
573- A native source is cloned into Heddle-owned storage.
574- Native clones target `main` directly; if the remote has no `main` thread,
575 pass `--thread <name>` to select one.
576- Clone never prompts.
577
578# Shallow clones (--depth)
579
580--depth 0 (the default) clones full history. --depth N fetches only the
581tip plus N generations of ancestry (--depth 1: the tip plus its immediate parents),
582so `heddle log` stops at the depth boundary; history older than that is
583not present locally — re-clone at a greater --depth (or --depth 0) to
584obtain it.
585
586Depth controls native Heddle history extent only — how many states the clone fetches —
587and says nothing about object contents. Whether a state's blobs are
588present locally or fetched lazily is a separate concern that `--depth`
589never governs. Git Overlay clones ingest full history and reject partial-history
590options. Advanced/planned flags `--lazy` and `--filter blob:none`
591skip blob content and hydrate it on demand for hosted/network Heddle
592remotes; local clone paths reject them today.
593
594See `heddle help threads` for the thread model and `heddle help remotes`
595for remote management.
596"#;
597
598const AGENT_FLAGS_TOPIC: &str = r#"Agent automation flags for `heddle capture`.
599
600These flags are hidden from the everyday `heddle capture --help` so it stays
601terse for human use. They let an automated caller override agent attribution
602and split captures across threads. Run `heddle capture --help-agent` to see
603them inline in capture's own help.
604
605Attribution overrides:
606
607 --agent-provider <NAME> Override HEDDLE_AGENT_PROVIDER.
608 --agent-model <NAME> Override HEDDLE_AGENT_MODEL.
609 --agent-session <ID> Set the session id for this capture.
610 --agent-segment <ID> Set the session segment for this capture.
611 --policy <ID> Override HEDDLE_AGENT_POLICY.
612 --no-policy Omit policy attribution.
613 --no-agent Omit agent attribution.
614
615Path splitting (no env equivalent):
616
617 --split Split selected paths into another thread instead of
618 capturing the whole worktree.
619 --into <THREAD> Target thread when using --split.
620 --path <PATH> Repository-relative path prefix to include with
621 --split (repeatable).
622
623Attribution precedence (highest first): explicit flag, active thread actor,
624supported agent env var, harness probe, active session, user config, repo config. See
625`crates/cli/src/cli/commands/snapshot.rs` for the full cascade.
626"#;
627
628const DAEMON_TOPIC: &str = "The mount daemon owns virtualized workspace sessions.\n\
629\n\
630`heddle daemon` — FUSE mount-daemon control plane. Owns FUSE sessions for\n\
631 `--workspace virtualized --daemon` threads. Linux only.\n\
632 Subcommands: serve | status | stop.\n";
633
634const MODEL_TOPIC: &str = r#"Heddle mental model — the everyday loop in one screen.
635
636Heddle is built around saved states and isolated threads. Git compatibility is
637an output and interop layer, not the thing you have to think about first.
638
639Core nouns:
640
641- State: a captured tree with a stable change id, attribution, intent, and
642 provenance. States are what `log`, `show`, `diff`, `undo`, and agents can
643 reason about.
644- Thread: a named line of work with its own checkout and captured history.
645 Use it for risky edits, agent work, or parallel experiments without
646 serializing everything through one checkout.
647- Capture: the Heddle save boundary for provenance, undo, and review.
648- Commit: publish captured source history to `.git` in Git Overlay.
649- Verify: the proof surface. It says whether Heddle, Git mapping, worktree,
650 remotes, active operations, clone state, and machine contracts agree.
651
652Everyday loop:
653
654 heddle status
655 heddle diff
656 heddle capture -m "..."
657 heddle commit
658 heddle start <name> --path ../<name>
659 heddle ready
660 heddle land --thread <name>
661 heddle undo
662 heddle verify
663
664Existing Git checkout:
665
666 heddle status
667 heddle init # initialize Heddle metadata; Git commits stay in .git
668 heddle capture -m "..."
669 heddle commit # Sley writes directly to .git
670 heddle verify
671
672If a command refuses, read the first `Next:` line. Heddle fails closed when it
673cannot prove the move is safe.
674"#;
675
676const GIT_CONCEPTS_TOPIC: &str = r#"Git and Heddle own different layers.
677
678In a Git Overlay repository, the checkout's real `.git` owns commits, refs,
679packs, the index, and worktree state. Heddle's thin Git surface — `clone`,
680`commit`, `pull`, `push`, and `remote` — uses the embedded Sley engine directly
681against that store. Heddle owns coordination and durable metadata in `.heddle`:
682captures, provenance, threads, readiness, review, and safe landing. Normal
683overlay operation neither creates nor uses `.heddle/git`. Legacy explicit Git
684Projection maintenance can still use an existing Bridge Mirror while its
685retirement is completed.
686
687Use `heddle init` to add that sidecar to an existing Git checkout. Use
688`heddle adopt` when you want one atomic transition that imports source history,
689makes Heddle the repository authority, and enables the full native feature set.
690
691Common mappings:
692
693| Intent | Git Overlay | Native Heddle |
694|--------|-------------|---------------|
695| Save source history | `heddle capture`, then `heddle commit` | `heddle capture` |
696| Isolate coordinated work | `heddle start` | `heddle start` |
697| Record a granular Heddle savepoint | `heddle capture` | `heddle capture` |
698| Check integration readiness | `heddle ready` | `heddle ready` |
699| Integrate a managed thread | `heddle land` | `heddle land` |
700| Synchronize source | `heddle pull` / `heddle push` | `heddle pull` / `heddle push` |
701| Configure remotes | `heddle remote` | `heddle remote` |
702| Inspect source history | another Git-compatible client | `heddle log` |
703
704Heddle intentionally does not reproduce the full Git command surface. An
705optional Git-compatible client can perform unsupported Git operations against
706the same `.git`; it is not a Heddle dependency. Explicit `import git`, `export
707git`, and `sync git` translate data between authorities. After `heddle adopt`,
708the retained `.git` is an explicit Git Projection adapter; it no longer selects
709repository source authority.
710"#;
711
712const THREADS_TOPIC: &str = "Threads — Heddle's unit of in-progress work.\n\
713\n\
714A thread is a named line of work with its own checkout, its own captured\n\
715history, and a target it eventually merges into. It is not a Git branch. In\n\
716Git Overlay, the branch remains part of Git source storage; the Heddle thread\n\
717owns coordination and captured history. You start isolated work with\n\
718`heddle start <name> --path <dir>`, switch\n\
719between threads with `heddle thread switch <name>`, and integrate with\n\
720`heddle land` (or check readiness without merging via `heddle ready`).\n\
721\n\
722# Threads vs. git branches\n\
723\n\
724- A thread carries an isolated checkout (its own directory), captured\n\
725 state history, agent/task metadata, a freshness verdict against its\n\
726 target, and a workflow state (Ready/Blocked/Merged/...). A git branch\n\
727 is just a ref.\n\
728- Multiple threads coexist on disk simultaneously. Each thread's working\n\
729 tree is its own.\n\
730- `heddle capture` records Heddle metadata; `heddle commit` asks Sley to\n\
731 write source history directly to `.git` in Git Overlay.\n\
732\n\
733# Workspace modes (`--workspace`)\n\
734\n\
735The `--workspace` flag on `heddle start` selects how the thread's\n\
736checkout is realized on disk. These are storage strategies, not\n\
737workflow states:\n\
738\n\
739- `materialized` — clonefile/reflink the captured tree into the thread's\n\
740 directory (APFS / btrfs / XFS-with-reflinks / bcachefs / ReFS). Real\n\
741 `read(2)`-able bytes; ~zero disk cost until the agent diverges blocks.\n\
742 Day-one default on reflink-capable hosts.\n\
743- `virtualized` — project the captured tree through a content-addressed\n\
744 FUSE/FSKit/ProjFS mount. Nothing on disk until the kernel asks.\n\
745 Requires the `mount` feature.\n\
746- `solid` — full file copies, no shared extents. Strong isolation;\n\
747 the right choice on ext4/NTFS hosts that have neither reflinks nor a\n\
748 usable mount API.\n\
749- `auto` (default) — pick `materialized` when reflinks are available,\n\
750 `virtualized` when a mount is available, otherwise `solid`.\n\
751\n\
752A `solid` thread and a `materialized` thread are interchangeable from\n\
753the workflow's point of view — `capture`, `ready`, and `land` behave\n\
754identically. The mode only controls bytes-on-disk semantics.\n\
755\n\
756# Isolated checkout path\n\
757\n\
758- Use `heddle start <name> --path <dir>` when you want an isolated\n\
759 checkout. It creates the thread ref and materializes the checkout in\n\
760 one step.\n\
761- Advanced split form: `heddle thread create <name>` creates only the\n\
762 ref, and `heddle thread promote <name> --path <dir>` materializes it\n\
763 later. Use this only when you intentionally need to create the ref\n\
764 now and materialize the checkout later.\n\
765- `--workspace` on `heddle start` selects byte storage for that checkout;\n\
766 it is not a separate workflow path.\n\
767\n\
768# Sync: stale\n\
769\n\
770A thread is `current` when its base is the tip of its target, and\n\
771`stale` once the target has advanced past it. `heddle status` and\n\
772`heddle thread show` print this as `Sync: stale`.\n\
773\n\
774Resolution paths:\n\
775\n\
776- `heddle sync` — refresh the current thread onto its target when\n\
777 the replay is clean. The fast path for a stale thread with no\n\
778 conflicts.\n\
779- If `sync` reports conflicts or other blockers, use\n\
780 `heddle resolve` or `heddle continue` to handle the conflicts.\n\
781- `heddle land` will refresh-then-merge for you when the replay is\n\
782 clean; it fails closed when manual resolution is required.\n\
783\n\
784# Changing active work\n\
785\n\
786- `heddle thread switch <name>` changes which *thread* is active.\n\
787 Each thread has its own checkout; switching may auto-capture\n\
788 outstanding work on the thread you're leaving. Pair with the shell\n\
789 hook (`heddle shell init`) to auto-cd into the target thread's\n\
790 directory.\n\
791- In Git Overlay, branch switching remains outside Heddle's deliberately\n\
792 narrow Git surface. An optional Git-compatible client can update `.git`\n\
793 without changing Heddle's active thread or coordination metadata.\n\
794\n\
795# Capture and Git commits\n\
796\n\
797- `heddle capture` records a recoverable Heddle step on the current\n\
798 thread — for undo, provenance, and review. Captures are\n\
799 fine-grained and accumulate freely as work progresses.\n\
800- In Git Overlay, run `heddle commit` when captured source history is ready.\n\
801- Agents and tools can take many small captures without producing noisy Git history.\n\
802\n\
803See also: `heddle help advanced` for the full operational surface,\n\
804`heddle thread --help` for the thread subcommand list.\n";
805
806const OPERATION_IDS_TOPIC: &str = "Idempotency — machine retries for supported mutating commands.\n\
807\n\
808Commands that advertise `supports_op_id: true` in `heddle help --output json`\n\
809accept `--op-id <UUID>` or `HEDDLE_OPERATION_ID`. Replaying the same id\n\
810with the same body returns the recorded outcome; with a different body it\n\
811returns a typed conflict.\n\
812\n\
813`op_id_behavior: explicit_replay` means the caller must provide the id.\n\
814`op_id_behavior: generated_resume` is reserved for commands that also\n\
815advertise `persists_op_id: true` and can save a generated id across an\n\
816interrupted retry loop. Commands with `op_id_behavior: none` reject --op-id.\n\
817\n\
818The dedup store is file-backed locally (`.heddle/state/operation_dedup.bin`,\n\
819rmp-serde, 7-day default retention) and Postgres-backed in hosted deployments.\n\
820\n\
821Without an id, dedup is bypassed and the call executes normally. For the\n\
822authoritative per-command contract, use `heddle help --output json`.\n";
823
824const REMOTES_TOPIC: &str = r#"Remotes — dispatched by repository source authority.
825
826Common loop:
827
828 heddle remote add origin <url-or-path>
829 heddle remote set-default origin
830 heddle pull
831 heddle push
832 heddle verify
833
834Remote values may be Git URLs, hosted endpoints, or local paths. `push` and
835`pull` use the default remote unless a positional remote is supplied. In Git
836Overlay, Sley reads and edits the repository's Git configuration and streams
837objects directly between the remote and `.git`. In Native Heddle, the same
838verbs use Heddle transport and storage. The Git executable is not involved.
839
840When a remote action is unsafe, Heddle reports the blocker and one primary next
841command for the active source authority. `heddle verify` reports repository
842verification state before and after remote operations.
843"#;
844
845const GIT_DEPENDENCIES_TOPIC: &str = r#"Git executable dependencies — Heddle does not have one.
846
847Heddle never requires the `git` executable. Sley is its embedded Git engine. In
848Git Overlay, `heddle clone`, `commit`, `pull`, `push`, and `remote`
849operate on the checkout's real `.git` directly; `.heddle` contains Heddle
850metadata, not a second Git object store.
851
852If Heddle detects an externally-started Git sequencer operation, it leaves Git
853metadata, refs, index, and worktree files unchanged and reports a Heddle
854preservation command. Finish or abort that operation with the optional
855Git-compatible client that started it, then run `heddle verify`.
856
857Operations outside Heddle's narrow Git surface can be performed by another
858Git-compatible client, but that client is optional and is never invoked by
859Heddle. Unsupported capabilities fail closed.
860
861Run `heddle help --output json` to inspect the public command surface, and
862`heddle doctor` / `heddle fsck --full` when a repository reports integrity
863or Git Projection state problems.
864"#;
865
866const REVIEW_TOPIC: &str = "Review surface — `heddle review show | sign | next | health`.\n\
867\n\
868`show <state>` — render the review payload (summary, agent narrative,\n\
869 in-budget signals, anchored discussions).\n\
870 `--all-signals` also surfaces hidden ones.\n\
871`sign <state>` — submit a `read | agent_preview | agent_co_review`\n\
872 signature. `--symbols file:symbol` scopes to\n\
873 specific symbols; default is the whole change.\n\
874`next` — show the next locally discoverable review item, or explain\n\
875 why none is available.\n\
876`health [--window N]`\n\
877 — per-module signal fire-rate over the last N states.\n\
878\n\
879Tick budget: at most 3 signals per state by default. Priority:\n\
880invariant_adjacency > self_flagged_uncertainty > pattern_deviation >\n\
881novelty > test_reachability.\n";
882
883const DISCUSS_TOPIC: &str = "`heddle discuss open | append | resolve | reopen | list | show`\n\
884\n\
885Discussions are stable records in the repository collaboration log. Turns,\n\
886resolutions, and reopenings append immutable operations; concurrent turns\n\
887converge without rewriting source history. Symbol anchors record the state,\n\
888file, and symbol where the discussion began:\n\
889\n\
890- `resolve <id> --mode by-edit` with `--state` (defaults to HEAD).\n\
891 Records that a subsequent edit addressed the discussion.\n\
892- `resolve <id> --mode dismiss` requires non-empty `--reason`.\n\
893- `reopen <id> --reason <text>` compensates a prior resolution.\n\
894\n\
895Visibility: `--visibility public|internal|team:NAME|restricted:LABEL|private:LABEL`.\n\
896Empty visibility uses the configured discussion visibility policy.\n";
897
898const GIT_OVERLAY_TOPIC: &str = r#"Git Overlay workflow
899
900Use this when `.git` should remain the source authority while Heddle adds
901captures, isolated threads, merge previews, undo, provenance, and machine-safe
902JSON in `.heddle`. Sley is the embedded Git engine; no Git executable is
903required. Normal Git Overlay operation neither creates nor reads `.heddle/git`.
904
905Start in an existing Git checkout:
906
907 heddle status
908 heddle init # add Heddle metadata
909 heddle verify
910
911Save and synchronize ordinary work:
912
913 heddle diff
914 heddle capture -m "..." # save Heddle metadata and provenance
915 heddle commit # Sley writes source history to .git
916 heddle pull
917 heddle push
918
919Isolate risky work:
920
921 heddle start <name> --path ../<name>
922 cd ../<name>
923 heddle capture -m "..."
924 heddle ready
925 cd -
926 heddle land --thread <name>
927 heddle push
928
929Recover or prove state:
930
931 heddle undo
932 heddle verify
933
934State-specific recovery:
935
936 Worktree has unsaved edits: heddle capture -m "...", then heddle commit
937 Move atomically to the full Native Heddle feature set: heddle adopt --ref <branch>
938"#;
939
940const GIT_PROJECTION_TOPIC: &str = r#"Git Projection — translate between Native Heddle and Git.
941
942Git Projection is the explicit interoperability adapter for Native Heddle
943repositories. It is not Git Overlay: when `.git` remains authoritative, Sley
944operates on that repository directly and normal operation never reads or creates
945`.heddle/git`. Heddle does not require the Git executable in either mode.
946
947Move an existing Git repository to Native Heddle atomically:
948
949 heddle adopt --ref <branch>
950
951Translate explicitly without changing source authority:
952
953 heddle import git --path <git-repository> --ref <branch>
954 heddle export git --destination <bare-git-repository>
955 heddle sync git --path <git-repository>
956
957`import git` and `sync git` accept a local path or Git URL. Omit `--ref` to
958import all local branches and tags. `export git` writes a bare Git repository.
959Legacy migration and repair may still read an existing Bridge Mirror at
960`.heddle/git` while ADR 0042's retirement work remains incomplete.
961
962Export metadata for Git readers:
963
964Every exported commit carries a footer at the tail of the commit message:
965
966 Heddle-State: <state_id>
967 Heddle-URL: <hosted_url>/state/<state_id> (omitted if no hosted URL)
968 Heddle-Annotations-Omitted: <count>
969
970This is the durable record — every reader on every host sees it regardless
971of remote configuration.
972
973Per-scope annotation drop counts and signal counts ride on the opt-in
974Git note at `refs/notes/heddle`. Heddle reads and writes that ref natively;
975people who still inspect the repository through another Git client can opt
976that client into showing notes, but Heddle itself does not require a Git
977executable on the system.
978"#;
979
980const SIGNALS_TOPIC: &str = "Risk signals — five modules behind a pure trait.\n\
981\n\
982- `invariant_adjacency` — fires when a changed symbol carries an\n\
983 Invariant or `enforces`-tagged annotation.\n\
984- `self_flagged_uncertainty` — passthrough of agent-emitted self-flags\n\
985 from the captured state's intent.\n\
986- `pattern_deviation` — fires when a symbol's body diverges\n\
987 from siblings or the prior version\n\
988 (tree-sitter token similarity).\n\
989- `novelty` — fires when a function shape is unique\n\
990 in the repo corpus.\n\
991- `test_reachability` — fires when no test statically reaches\n\
992 the changed symbol via tree-sitter\n\
993 call-graph traversal. The reason text\n\
994 is honest: this is *not* runtime\n\
995 coverage.\n\
996\n\
997Configure under `[review.signals]` in `.heddle/config.toml`. Each module\n\
998ships fires-correctly + stays-quiet tests; defaults are conservative\n\
999so a fresh repo isn't noisy.\n";
1000
1001#[cfg(test)]
1002mod tests {
1003 use super::*;
1004
1005 #[test]
1006 fn everyday_verbs_in_curated_list_have_everyday_tier() {
1007 for verb in everyday_verbs() {
1008 assert_eq!(tier_of(verb), Tier::Everyday, "{verb}");
1009 }
1010 }
1011
1012 #[test]
1013 fn topic_text_returns_none_for_unknown() {
1014 assert!(topic_text("definitely-not-a-topic").is_none());
1015 }
1016
1017 #[test]
1018 fn topic_text_returns_some_for_advertised_topics() {
1019 for topic in [
1020 "advanced",
1021 "agent-flags",
1022 "git-concepts",
1023 "git-concept-map",
1024 "git-veteran",
1025 "git-overlay",
1026 "agent",
1027 "daemon",
1028 "threads",
1029 "model",
1030 "mental-model",
1031 "concepts",
1032 "operation-ids",
1033 "idempotency",
1034 "remotes",
1035 "git-dependencies",
1036 "review",
1037 "discuss",
1038 "discussions",
1039 "git-projection",
1040 "git-projections",
1041 "footer",
1042 "notes",
1043 "signals",
1044 "risk-signals",
1045 "output-formats",
1046 "output-format",
1047 "output",
1048 "clone",
1049 ] {
1050 assert!(topic_text(topic).is_some(), "{topic}");
1051 }
1052 }
1053
1054 #[test]
1055 fn tier_of_advanced_verbs_classifies_correctly() {
1056 for verb in advanced_verbs() {
1057 let t = tier_of(verb);
1058 assert!(
1059 matches!(t, Tier::Advanced),
1060 "expected Advanced for {verb}, got {t:?}"
1061 );
1062 }
1063 }
1064
1065 #[test]
1068 fn advanced_verbs_lists_tip_referenced_commands() {
1069 let advanced: std::collections::HashSet<&str> = advanced_verbs().into_iter().collect();
1070 for verb in ["query", "continue", "abort", "shell"] {
1071 assert!(
1072 advanced.contains(verb),
1073 "`{verb}` is referenced in user-facing tips but is not \
1074 advertised by `heddle help advanced`"
1075 );
1076 }
1077 }
1078
1079 #[test]
1084 fn everyday_verbs_surface_the_core_loop() {
1085 let everyday: std::collections::HashSet<&str> = everyday_verbs().into_iter().collect();
1086 for verb in [
1087 "init", "clone", "status", "start", "capture", "commit", "ready", "diff", "land",
1088 "resolve", "undo", "log", "show", "pull", "push", "doctor", "verify",
1089 ] {
1090 assert!(
1091 everyday.contains(verb),
1092 "`{verb}` is part of the core loop but is not advertised on \
1093 the everyday surface"
1094 );
1095 }
1096 for verb in ["review", "discuss", "context", "thread", "git-projection"] {
1097 assert!(
1098 !everyday.contains(verb),
1099 "`{verb}` belongs behind advanced/topic help, not the core-loop surface"
1100 );
1101 }
1102 }
1103
1104 #[test]
1108 fn agent_flags_topic_lists_hidden_capture_flags() {
1109 let text = topic_text("agent-flags").expect("agent-flags topic should exist");
1110 for flag in [
1111 "--agent-provider",
1112 "--agent-model",
1113 "--agent-session",
1114 "--agent-segment",
1115 "--policy",
1116 "--no-policy",
1117 "--no-agent",
1118 "--split",
1119 "--into",
1120 "--path",
1121 ] {
1122 assert!(text.contains(flag), "agent-flags topic missing `{flag}`");
1123 }
1124 for env in [
1125 "HEDDLE_AGENT_PROVIDER",
1126 "HEDDLE_AGENT_MODEL",
1127 "HEDDLE_AGENT_POLICY",
1128 ] {
1129 assert!(text.contains(env), "agent-flags topic missing env `{env}`");
1130 }
1131 for internal_env in ["HEDDLE_SESSION_ID", "HEDDLE_SESSION_SEGMENT"] {
1132 assert!(
1133 !text.contains(internal_env),
1134 "agent-flags topic must not advertise internal env `{internal_env}`"
1135 );
1136 }
1137 }
1138
1139 #[test]
1140 fn git_help_distinguishes_overlay_from_projection_storage() {
1141 let overlay = topic_text("git-overlay").expect("git-overlay topic should exist");
1142 assert!(overlay.contains("neither creates nor reads `.heddle/git`"));
1143
1144 let projection = topic_text("git-projection").expect("git-projection topic should exist");
1145 assert!(projection.contains("It is not Git Overlay"));
1146 assert!(projection.contains("heddle adopt --ref <branch>"));
1147 assert!(projection.contains("heddle export git --destination"));
1148 assert!(projection.contains("Bridge Mirror"));
1149 }
1150
1151 #[test]
1154 fn capture_agent_flags_hidden_by_default_revealed_on_demand() {
1155 use clap::CommandFactory;
1156 let cmd = crate::cli::cli_args::Cli::command();
1157 let capture = cmd
1158 .find_subcommand("capture")
1159 .expect("capture subcommand exists");
1160 for id in CAPTURE_AGENT_FLAG_IDS {
1161 let arg = capture
1162 .get_arguments()
1163 .find(|arg| arg.get_id().as_str() == *id)
1164 .unwrap_or_else(|| panic!("capture has no `{id}` arg"));
1165 assert!(arg.is_hide_set(), "`{id}` should be hidden by default");
1166 }
1167 let revealed = reveal_capture_agent_flags(capture.clone());
1168 for id in CAPTURE_AGENT_FLAG_IDS {
1169 let arg = revealed
1170 .get_arguments()
1171 .find(|arg| arg.get_id().as_str() == *id)
1172 .expect("revealed arg present");
1173 assert!(
1174 !arg.is_hide_set(),
1175 "`{id}` should be revealed by --help-agent"
1176 );
1177 }
1178 }
1179
1180 fn wants_reveal(args: &[&str]) -> bool {
1191 use clap::Parser;
1192
1193 use crate::cli::cli_args::{Cli, Commands};
1194 let argv = std::iter::once("heddle").chain(args.iter().copied());
1195 match Cli::try_parse_from(argv) {
1196 Ok(cli) => matches!(&cli.command, Commands::Capture(a) if a.help_agent),
1197 Err(_) => false,
1201 }
1202 }
1203
1204 #[test]
1208 fn capture_help_agent_is_capture_scoped() {
1209 assert!(
1210 wants_reveal(&["capture", "--help-agent"]),
1211 "capture --help-agent should request the reveal help"
1212 );
1213 assert!(
1214 !wants_reveal(&["status", "--help-agent"]),
1215 "--help-agent on a non-capture verb is not a capture reveal request"
1216 );
1217 assert!(
1220 !wants_reveal(&["capture", "--help"]),
1221 "plain --help is clap's help, not the agent reveal"
1222 );
1223 }
1224
1225 #[test]
1232 fn capture_help_agent_handles_every_global_form_clap_accepts() {
1233 assert!(
1235 wants_reveal(&["-C", "/tmp/repo", "capture", "--help-agent"]),
1236 "`-C <path> capture --help-agent` should reveal — clap parses the path"
1237 );
1238 assert!(
1239 wants_reveal(&["--output", "text", "capture", "--help-agent"]),
1240 "`--output text capture --help-agent` should reveal"
1241 );
1242 assert!(
1245 wants_reveal(&["-C", "capture", "capture", "--help-agent"]),
1246 "`-C capture capture --help-agent` (repo dir named `capture`) should reveal"
1247 );
1248 assert!(
1250 wants_reveal(&["-vC", "/tmp/repo", "capture", "--help-agent"]),
1251 "`-vC <path> capture --help-agent` (clustered short globals) should reveal"
1252 );
1253 assert!(
1254 wants_reveal(&["-vC", "capture", "capture", "--help-agent"]),
1255 "`-vC capture capture --help-agent` (clustered, repo dir named `capture`) should reveal"
1256 );
1257 assert!(
1259 wants_reveal(&["-C/tmp/repo", "capture", "--help-agent"]),
1260 "`-C<path> capture --help-agent` (attached) should reveal"
1261 );
1262 assert!(
1264 wants_reveal(&["capture", "--help-agent"]),
1265 "plain `capture --help-agent` should reveal"
1266 );
1267
1268 assert!(
1273 !wants_reveal(&["-C", "capture", "status", "--help-agent"]),
1274 "`-C capture status --help-agent` — verb is `status`, no reveal"
1275 );
1276 assert!(
1277 !wants_reveal(&["-vC", "capture", "status", "--help-agent"]),
1278 "`-vC capture status --help-agent` (clustered) — verb is `status`, no reveal"
1279 );
1280 assert!(
1281 !wants_reveal(&["--output", "text", "status", "--help-agent"]),
1282 "`--output text status --help-agent` — verb is `status`, no reveal"
1283 );
1284 }
1285
1286 #[test]
1293 fn output_blurb_stated_once_not_per_command() {
1294 let marker = "full machine contract";
1295 let top = render_for_args(&["--help"]).expect("top-level help renders");
1296 assert_eq!(
1297 top.matches(marker).count(),
1298 1,
1299 "top-level help should state the --output contract exactly once: {top}"
1300 );
1301 assert!(
1302 topic_text("output-formats")
1303 .expect("output-formats topic exists")
1304 .contains(marker),
1305 "the output-formats topic carries the full contract"
1306 );
1307 for argv in [
1308 &["clone", "--help"][..],
1309 &["status", "--help"][..],
1310 &["capture", "--help"][..],
1311 &["thread", "--help"][..],
1312 &["push", "--help"][..],
1313 ] {
1314 let help = render_for_args(argv).expect("command help renders");
1315 assert!(
1316 !help.contains(marker),
1317 "`{argv:?}` should not restate the --output machine contract: {help}"
1318 );
1319 assert!(
1320 help.contains("heddle help output-formats"),
1321 "`{argv:?}` should breadcrumb to the output-formats topic: {help}"
1322 );
1323 }
1324 }
1325
1326 #[test]
1331 fn clone_help_fits_one_screen() {
1332 let help = render_for_args(&["clone", "--help"]).expect("clone help renders");
1333 let lines = help.lines().count();
1334 assert!(
1335 lines <= 40,
1336 "clone --help should fit one screen (<= 40 lines), got {lines}:\n{help}"
1337 );
1338 assert!(
1341 help.contains("Advanced/planned flags: see `heddle help clone`."),
1342 "clone --help keeps the hidden-flags affordance: {help}"
1343 );
1344 assert!(
1345 help.contains("heddle help clone"),
1346 "clone --help points at the clone topic for the full behavior: {help}"
1347 );
1348 }
1349
1350 #[test]
1353 fn advanced_help_renders_area_groups() {
1354 use clap::CommandFactory;
1355 let cmd = crate::cli::cli_args::Cli::command();
1356 let advanced = render_help(&cmd, &["advanced".to_string()]);
1357 for header in [
1358 "Threads and integration:",
1359 "States and history:",
1360 "Recovery and integrity:",
1361 "Repo and environment:",
1362 "Agents and automation:",
1363 "Git interop:",
1364 "Admin and maintenance:",
1365 ] {
1366 assert!(
1367 advanced.contains(&format!("\n{header}\n")),
1368 "advanced help should render the `{header}` group: {advanced}"
1369 );
1370 }
1371 assert!(
1372 !advanced.contains("Advanced commands:"),
1373 "the flat list header is replaced by area groups: {advanced}"
1374 );
1375 }
1376
1377 #[test]
1378 fn advanced_help_documents_home_and_config_overrides() {
1379 use clap::CommandFactory;
1380 let cmd = crate::cli::cli_args::Cli::command();
1381 let advanced = render_help(&cmd, &["advanced".to_string()]);
1382 assert!(advanced.contains("HEDDLE_HOME"));
1383 assert!(advanced.contains("HEDDLE_CONFIG"));
1384 assert!(advanced.contains("<path>/config.toml"));
1385 assert!(advanced.contains("Principal resolution (highest first)"));
1386 }
1387
1388 #[test]
1395 fn advanced_help_groups_cover_every_advanced_verb() {
1396 let grouped: Vec<&str> = crate::cli::commands::advanced_help_groups()
1397 .into_iter()
1398 .flat_map(|(_, verbs)| verbs)
1399 .collect();
1400 let mut deduped = grouped.clone();
1401 deduped.sort_unstable();
1402 deduped.dedup();
1403 assert_eq!(
1404 deduped.len(),
1405 grouped.len(),
1406 "no verb may appear in two advanced-help groups: {grouped:?}"
1407 );
1408 let grouped: std::collections::HashSet<&str> = grouped.into_iter().collect();
1409 let flat: std::collections::HashSet<&str> = advanced_verbs().into_iter().collect();
1410 let missing: Vec<&&str> = flat.difference(&grouped).collect();
1411 assert!(
1412 missing.is_empty(),
1413 "advanced verbs missing from every group — native advanced root \
1414 commands must register a help_category in the command contract \
1415 table: {missing:?}"
1416 );
1417 let extra: Vec<&&str> = grouped.difference(&flat).collect();
1418 assert!(
1419 extra.is_empty(),
1420 "grouped verbs not on the advanced surface: {extra:?}"
1421 );
1422 }
1423
1424 #[test]
1433 fn verb_blurbs_resolve_from_command_catalog() {
1434 let catalog = crate::cli::commands::build_command_catalog();
1435 for verb in everyday_verbs().into_iter().chain(advanced_verbs()) {
1436 if catalog.command_by_display(verb).is_none() {
1439 continue;
1440 }
1441 let blurb = catalog_summary(&catalog, verb);
1442 assert!(
1443 !blurb.is_empty(),
1444 "verb `{verb}` is cataloged but its summary is empty. \
1445 The curated help printer needs a non-empty catalog summary."
1446 );
1447 }
1448 }
1449
1450 #[test]
1470 fn hidden_flags_carry_discovery_affordances() {
1471 use clap::CommandFactory;
1472
1473 fn walk(cmd: &clap::Command, path: &str, violations: &mut Vec<String>) {
1474 let after_help = [
1475 cmd.get_after_help().map(ToString::to_string),
1476 cmd.get_after_long_help().map(ToString::to_string),
1477 ]
1478 .into_iter()
1479 .flatten()
1480 .collect::<Vec<_>>()
1481 .join("\n");
1482 let after_lower = after_help.to_lowercase();
1483 let breadcrumb_marker = after_lower.contains("hidden")
1484 || after_lower.contains("advanced flag")
1485 || after_lower.contains("advanced/planned flags");
1486 let points_at_reveal =
1487 after_help.contains("heddle help ") || after_help.contains("--help-agent");
1488
1489 for arg in cmd.get_arguments() {
1490 if !arg.is_hide_set() || arg.is_global_set() {
1491 continue;
1492 }
1493 let flag_help = arg
1494 .get_long_help()
1495 .or_else(|| arg.get_help())
1496 .map(ToString::to_string)
1497 .unwrap_or_default();
1498 if flag_help.starts_with("Internal") {
1499 continue;
1500 }
1501 let named_inline = arg
1502 .get_long()
1503 .is_some_and(|long| after_help.contains(&format!("--{long}")));
1504 if breadcrumb_marker && (points_at_reveal || named_inline) {
1505 continue;
1506 }
1507 let name = arg
1508 .get_long()
1509 .map(|long| format!("--{long}"))
1510 .unwrap_or_else(|| arg.get_id().to_string());
1511 violations.push(format!("`{path}` hides `{name}`"));
1512 }
1513
1514 for sub in cmd.get_subcommands() {
1515 if sub.is_hide_set() {
1516 continue;
1517 }
1518 walk(sub, &format!("{path} {}", sub.get_name()), violations);
1519 }
1520 }
1521
1522 let cmd = crate::cli::cli_args::Cli::command();
1523 let mut violations = Vec::new();
1524 walk(&cmd, cmd.get_name(), &mut violations);
1525 assert!(
1526 violations.is_empty(),
1527 "hidden flags without a discovery affordance (heddle#646): either \
1528 prefix the flag's help with `Internal` (plumbing, not for users) \
1529 or add an after-help breadcrumb that mentions the hidden/advanced \
1530 flags and names the flag or a reveal surface:\n {}",
1531 violations.join("\n ")
1532 );
1533 }
1534}