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
525const OUTPUT_FORMATS_TOPIC: &str = r#"Output formats — `--output text | json | json-compact`.
530
531`text` is the default, always. There is no TTY/pipe auto-detection — the
532default never switches under you, so scripts and humans see the same thing
533until a flag says otherwise.
534
535`--output json` emits the full machine contract: a stable `output_kind`
536discriminator, exit codes, and recovery templates. Schemas per verb:
537`heddle schemas <verb>`; the catalog of which commands emit what:
538`heddle help --output json`.
539
540`--output json-compact` emits only the decision-surface fields —
541`output_kind`, `status`/`coordination_status`, `blockers`, `next_action`,
542`changed_paths`, `conflicts` — fewer tokens, same `output_kind`, so callers
543can still dispatch on it. Commands advertise `supports_json_compact` in the
544command catalog.
545
546Related: `heddle help operation-ids` for idempotent retries, `heddle help
547agent-flags` for capture attribution overrides.
548"#;
549
550const CLONE_TOPIC: &str = r#"Cloning — Git Overlay and native Heddle repositories.
554
555 heddle clone <remote> <dir> [--thread <name>] [--depth <n>]
556
557Run `heddle clone --help` for the flag list.
558
559# Repository authority
560
561- A Git source is streamed by Sley directly into the destination `.git`, then
562 initialized as Git Overlay. No Git executable or `.heddle/git` mirror is used.
563- A native source is cloned into Heddle-owned storage.
564- Native clones target `main` directly; if the remote has no `main` thread,
565 pass `--thread <name>` to select one.
566- Clone never prompts.
567
568# Shallow clones (--depth)
569
570--depth 0 (the default) clones full history. --depth N fetches only the
571tip plus N generations of ancestry (--depth 1: the tip plus its immediate parents),
572so `heddle log` stops at the depth boundary; history older than that is
573not present locally — re-clone at a greater --depth (or --depth 0) to
574obtain it.
575
576Depth controls native Heddle history extent only — how many states the clone fetches —
577and says nothing about object contents. Whether a state's blobs are
578present locally or fetched lazily is a separate concern that `--depth`
579never governs. Git Overlay clones ingest full history and reject partial-history
580options. Advanced/planned flags `--lazy` and `--filter blob:none`
581skip blob content and hydrate it on demand for hosted/network Heddle
582remotes; local clone paths reject them today.
583
584See `heddle help threads` for the thread model and `heddle help remotes`
585for remote management.
586"#;
587
588const AGENT_FLAGS_TOPIC: &str = r#"Agent automation flags for `heddle capture`.
589
590These flags are hidden from the everyday `heddle capture --help` so it stays
591terse for human use. They let an automated caller override agent attribution
592and split captures across threads. Run `heddle capture --help-agent` to see
593them inline in capture's own help.
594
595Attribution overrides:
596
597 --agent-provider <NAME> Override HEDDLE_AGENT_PROVIDER.
598 --agent-model <NAME> Override HEDDLE_AGENT_MODEL.
599 --agent-session <ID> Set the session id for this capture.
600 --agent-segment <ID> Set the session segment for this capture.
601 --policy <ID> Override HEDDLE_AGENT_POLICY.
602 --no-policy Omit policy attribution.
603 --no-agent Omit agent attribution.
604
605Path splitting (no env equivalent):
606
607 --split Split selected paths into another thread instead of
608 capturing the whole worktree.
609 --into <THREAD> Target thread when using --split.
610 --path <PATH> Repository-relative path prefix to include with
611 --split (repeatable).
612
613Attribution precedence (highest first): explicit flag, active thread actor,
614supported agent env var, harness probe, active session, user config, repo config. See
615`crates/cli/src/cli/commands/snapshot.rs` for the full cascade.
616"#;
617
618const DAEMON_TOPIC: &str = "The mount daemon owns virtualized workspace sessions.\n\
619\n\
620`heddle daemon` — FUSE mount-daemon control plane. Owns FUSE sessions for\n\
621 `--workspace virtualized --daemon` threads. Linux only.\n\
622 Subcommands: serve | status | stop.\n";
623
624const MODEL_TOPIC: &str = r#"Heddle mental model — the everyday loop in one screen.
625
626Heddle is built around saved states and isolated threads. Git compatibility is
627an output and interop layer, not the thing you have to think about first.
628
629Core nouns:
630
631- State: a captured tree with a stable change id, attribution, intent, and
632 provenance. States are what `log`, `show`, `diff`, `undo`, and agents can
633 reason about.
634- Thread: a named line of work with its own checkout and captured history.
635 Use it for risky edits, agent work, or parallel experiments without
636 serializing everything through one checkout.
637- Capture: the Heddle save boundary for provenance, undo, and review.
638- Commit: publish captured source history to `.git` in Git Overlay.
639- Verify: the proof surface. It says whether Heddle, Git mapping, worktree,
640 remotes, active operations, clone state, and machine contracts agree.
641
642Everyday loop:
643
644 heddle status
645 heddle diff
646 heddle capture -m "..."
647 heddle commit
648 heddle start <name> --path ../<name>
649 heddle ready
650 heddle land --thread <name>
651 heddle undo
652 heddle verify
653
654Existing Git checkout:
655
656 heddle status
657 heddle init # initialize Heddle metadata; Git commits stay in .git
658 heddle capture -m "..."
659 heddle commit # Sley writes directly to .git
660 heddle verify
661
662If a command refuses, read the first `Next:` line. Heddle fails closed when it
663cannot prove the move is safe.
664"#;
665
666const GIT_CONCEPTS_TOPIC: &str = r#"Git and Heddle own different layers.
667
668In a Git Overlay repository, the checkout's real `.git` owns commits, refs,
669packs, the index, and worktree state. Heddle's thin Git surface — `clone`,
670`commit`, `pull`, `push`, and `remote` — uses the embedded Sley engine directly
671against that store. Heddle owns coordination and durable metadata in `.heddle`:
672captures, provenance, threads, readiness, review, and safe landing. Normal
673overlay operation neither creates nor uses `.heddle/git`. Legacy explicit Git
674Projection maintenance can still use an existing Bridge Mirror while its
675retirement is completed.
676
677Use `heddle init` to add that sidecar to an existing Git checkout. Use
678`heddle adopt` when you want one atomic transition that imports source history,
679makes Heddle the repository authority, and enables the full native feature set.
680
681Common mappings:
682
683| Intent | Git Overlay | Native Heddle |
684|--------|-------------|---------------|
685| Save source history | `heddle capture`, then `heddle commit` | `heddle capture` |
686| Isolate coordinated work | `heddle start` | `heddle start` |
687| Record a granular Heddle savepoint | `heddle capture` | `heddle capture` |
688| Check integration readiness | `heddle ready` | `heddle ready` |
689| Integrate a managed thread | `heddle land` | `heddle land` |
690| Synchronize source | `heddle pull` / `heddle push` | `heddle pull` / `heddle push` |
691| Configure remotes | `heddle remote` | `heddle remote` |
692| Inspect source history | another Git-compatible client | `heddle log` |
693
694Heddle intentionally does not reproduce the full Git command surface. An
695optional Git-compatible client can perform unsupported Git operations against
696the same `.git`; it is not a Heddle dependency. Explicit `import git`, `export
697git`, and `sync git` translate data between authorities. After `heddle adopt`,
698the retained `.git` is an explicit Git Projection adapter; it no longer selects
699repository source authority.
700"#;
701
702const THREADS_TOPIC: &str = "Threads — Heddle's unit of in-progress work.\n\
703\n\
704A thread is a named line of work with its own checkout, its own captured\n\
705history, and a target it eventually merges into. It is not a Git branch. In\n\
706Git Overlay, the branch remains part of Git source storage; the Heddle thread\n\
707owns coordination and captured history. You start isolated work with\n\
708`heddle start <name> --path <dir>`, switch\n\
709between threads with `heddle thread switch <name>`, and integrate with\n\
710`heddle land` (or check readiness without merging via `heddle ready`).\n\
711\n\
712# Threads vs. git branches\n\
713\n\
714- A thread carries an isolated checkout (its own directory), captured\n\
715 state history, agent/task metadata, a freshness verdict against its\n\
716 target, and a workflow state (Ready/Blocked/Merged/...). A git branch\n\
717 is just a ref.\n\
718- Multiple threads coexist on disk simultaneously. Each thread's working\n\
719 tree is its own.\n\
720- `heddle capture` records Heddle metadata; `heddle commit` asks Sley to\n\
721 write source history directly to `.git` in Git Overlay.\n\
722\n\
723# Workspace modes (`--workspace`)\n\
724\n\
725The `--workspace` flag on `heddle start` selects how the thread's\n\
726checkout is realized on disk. These are storage strategies, not\n\
727workflow states:\n\
728\n\
729- `materialized` — clonefile/reflink the captured tree into the thread's\n\
730 directory (APFS / btrfs / XFS-with-reflinks / bcachefs / ReFS). Real\n\
731 `read(2)`-able bytes; ~zero disk cost until the agent diverges blocks.\n\
732 Day-one default on reflink-capable hosts.\n\
733- `virtualized` — project the captured tree through a content-addressed\n\
734 FUSE/FSKit/ProjFS mount. Nothing on disk until the kernel asks.\n\
735 Requires the `mount` feature.\n\
736- `solid` — full file copies, no shared extents. Strong isolation;\n\
737 the right choice on ext4/NTFS hosts that have neither reflinks nor a\n\
738 usable mount API.\n\
739- `auto` (default) — pick `materialized` when reflinks are available,\n\
740 `virtualized` when a mount is available, otherwise `solid`.\n\
741\n\
742A `solid` thread and a `materialized` thread are interchangeable from\n\
743the workflow's point of view — `capture`, `ready`, and `land` behave\n\
744identically. The mode only controls bytes-on-disk semantics.\n\
745\n\
746# Isolated checkout path\n\
747\n\
748- Use `heddle start <name> --path <dir>` when you want an isolated\n\
749 checkout. It creates the thread ref and materializes the checkout in\n\
750 one step.\n\
751- Advanced split form: `heddle thread create <name>` creates only the\n\
752 ref, and `heddle thread promote <name> --path <dir>` materializes it\n\
753 later. Use this only when you intentionally need to create the ref\n\
754 now and materialize the checkout later.\n\
755- `--workspace` on `heddle start` selects byte storage for that checkout;\n\
756 it is not a separate workflow path.\n\
757\n\
758# Sync: stale\n\
759\n\
760A thread is `current` when its base is the tip of its target, and\n\
761`stale` once the target has advanced past it. `heddle status` and\n\
762`heddle thread show` print this as `Sync: stale`.\n\
763\n\
764Resolution paths:\n\
765\n\
766- `heddle sync` — refresh the current thread onto its target when\n\
767 the replay is clean. The fast path for a stale thread with no\n\
768 conflicts.\n\
769- If `sync` reports conflicts or other blockers, use\n\
770 `heddle resolve` or `heddle continue` to handle the conflicts.\n\
771- `heddle land` will refresh-then-merge for you when the replay is\n\
772 clean; it fails closed when manual resolution is required.\n\
773\n\
774# Changing active work\n\
775\n\
776- `heddle thread switch <name>` changes which *thread* is active.\n\
777 Each thread has its own checkout; switching may auto-capture\n\
778 outstanding work on the thread you're leaving. Pair with the shell\n\
779 hook (`heddle shell init`) to auto-cd into the target thread's\n\
780 directory.\n\
781- In Git Overlay, branch switching remains outside Heddle's deliberately\n\
782 narrow Git surface. An optional Git-compatible client can update `.git`\n\
783 without changing Heddle's active thread or coordination metadata.\n\
784\n\
785# Capture and Git commits\n\
786\n\
787- `heddle capture` records a recoverable Heddle step on the current\n\
788 thread — for undo, provenance, and review. Captures are\n\
789 fine-grained and accumulate freely as work progresses.\n\
790- In Git Overlay, run `heddle commit` when captured source history is ready.\n\
791- Agents and tools can take many small captures without producing noisy Git history.\n\
792\n\
793See also: `heddle help advanced` for the full operational surface,\n\
794`heddle thread --help` for the thread subcommand list.\n";
795
796const OPERATION_IDS_TOPIC: &str = "Idempotency — machine retries for supported mutating commands.\n\
797\n\
798Commands that advertise `supports_op_id: true` in `heddle help --output json`\n\
799accept `--op-id <UUID>` or `HEDDLE_OPERATION_ID`. Replaying the same id\n\
800with the same body returns the recorded outcome; with a different body it\n\
801returns a typed conflict.\n\
802\n\
803`op_id_behavior: explicit_replay` means the caller must provide the id.\n\
804`op_id_behavior: generated_resume` is reserved for commands that also\n\
805advertise `persists_op_id: true` and can save a generated id across an\n\
806interrupted retry loop. Commands with `op_id_behavior: none` reject --op-id.\n\
807\n\
808The dedup store is file-backed locally (`.heddle/state/operation_dedup.bin`,\n\
809rmp-serde, 7-day default retention) and Postgres-backed in hosted deployments.\n\
810\n\
811Without an id, dedup is bypassed and the call executes normally. For the\n\
812authoritative per-command contract, use `heddle help --output json`.\n";
813
814const REMOTES_TOPIC: &str = r#"Remotes — dispatched by repository source authority.
815
816Common loop:
817
818 heddle remote add origin <url-or-path>
819 heddle remote set-default origin
820 heddle pull
821 heddle push
822 heddle verify
823
824Remote values may be Git URLs, hosted endpoints, or local paths. `push` and
825`pull` use the default remote unless a positional remote is supplied. In Git
826Overlay, Sley reads and edits the repository's Git configuration and streams
827objects directly between the remote and `.git`. In Native Heddle, the same
828verbs use Heddle transport and storage. The Git executable is not involved.
829
830When a remote action is unsafe, Heddle reports the blocker and one primary next
831command for the active source authority. `heddle verify` reports repository
832verification state before and after remote operations.
833"#;
834
835const GIT_DEPENDENCIES_TOPIC: &str = r#"Git executable dependencies — Heddle does not have one.
836
837Heddle never requires the `git` executable. Sley is its embedded Git engine. In
838Git Overlay, `heddle clone`, `commit`, `pull`, `push`, and `remote`
839operate on the checkout's real `.git` directly; `.heddle` contains Heddle
840metadata, not a second Git object store.
841
842If Heddle detects an externally-started Git sequencer operation, it leaves Git
843metadata, refs, index, and worktree files unchanged and reports a Heddle
844preservation command. Finish or abort that operation with the optional
845Git-compatible client that started it, then run `heddle verify`.
846
847Operations outside Heddle's narrow Git surface can be performed by another
848Git-compatible client, but that client is optional and is never invoked by
849Heddle. Unsupported capabilities fail closed.
850
851Run `heddle help --output json` to inspect the public command surface, and
852`heddle doctor` / `heddle fsck --full` when a repository reports integrity
853or Git Projection state problems.
854"#;
855
856const REVIEW_TOPIC: &str = "Review surface — `heddle review show | sign | next | health`.\n\
857\n\
858`show <state>` — render the review payload (summary, agent narrative,\n\
859 in-budget signals, anchored discussions).\n\
860 `--all-signals` also surfaces hidden ones.\n\
861`sign <state>` — submit a `read | agent_preview | agent_co_review`\n\
862 signature. `--symbols file:symbol` scopes to\n\
863 specific symbols; default is the whole change.\n\
864`next` — show the next locally discoverable review item, or explain\n\
865 why none is available.\n\
866`health [--window N]`\n\
867 — per-module signal fire-rate over the last N states.\n\
868\n\
869Tick budget: at most 3 signals per state by default. Priority:\n\
870invariant_adjacency > self_flagged_uncertainty > pattern_deviation >\n\
871novelty > test_reachability.\n";
872
873const DISCUSS_TOPIC: &str = "`heddle discuss open | append | resolve | reopen | list | show`\n\
874\n\
875Discussions are stable records in the repository collaboration log. Turns,\n\
876resolutions, and reopenings append immutable operations; concurrent turns\n\
877converge without rewriting source history. Symbol anchors record the state,\n\
878file, and symbol where the discussion began:\n\
879\n\
880- `resolve <id> --mode by-edit` with `--state` (defaults to HEAD).\n\
881 Records that a subsequent edit addressed the discussion.\n\
882- `resolve <id> --mode dismiss` requires non-empty `--reason`.\n\
883- `reopen <id> --reason <text>` compensates a prior resolution.\n\
884\n\
885Visibility: `--visibility public|internal|team:NAME|restricted:LABEL|private:LABEL`.\n\
886Empty visibility uses the configured discussion visibility policy.\n";
887
888const GIT_OVERLAY_TOPIC: &str = r#"Git Overlay workflow
889
890Use this when `.git` should remain the source authority while Heddle adds
891captures, isolated threads, merge previews, undo, provenance, and machine-safe
892JSON in `.heddle`. Sley is the embedded Git engine; no Git executable is
893required. Normal Git Overlay operation neither creates nor reads `.heddle/git`.
894
895Start in an existing Git checkout:
896
897 heddle status
898 heddle init # add Heddle metadata
899 heddle verify
900
901Save and synchronize ordinary work:
902
903 heddle diff
904 heddle capture -m "..." # save Heddle metadata and provenance
905 heddle commit # Sley writes source history to .git
906 heddle pull
907 heddle push
908
909Isolate risky work:
910
911 heddle start <name> --path ../<name>
912 cd ../<name>
913 heddle capture -m "..."
914 heddle ready
915 cd -
916 heddle land --thread <name>
917 heddle push
918
919Recover or prove state:
920
921 heddle undo
922 heddle verify
923
924State-specific recovery:
925
926 Worktree has unsaved edits: heddle capture -m "...", then heddle commit
927 Move atomically to the full Native Heddle feature set: heddle adopt --ref <branch>
928"#;
929
930const GIT_PROJECTION_TOPIC: &str = r#"Git Projection — translate between Native Heddle and Git.
931
932Git Projection is the explicit interoperability adapter for Native Heddle
933repositories. It is not Git Overlay: when `.git` remains authoritative, Sley
934operates on that repository directly and normal operation never reads or creates
935`.heddle/git`. Heddle does not require the Git executable in either mode.
936
937Move an existing Git repository to Native Heddle atomically:
938
939 heddle adopt --ref <branch>
940
941Translate explicitly without changing source authority:
942
943 heddle import git --path <git-repository> --ref <branch>
944 heddle export git --destination <bare-git-repository>
945 heddle sync git --path <git-repository>
946
947`import git` and `sync git` accept a local path or Git URL. Omit `--ref` to
948import all local branches and tags. `export git` writes a bare Git repository.
949Legacy migration and repair may still read an existing Bridge Mirror at
950`.heddle/git` while ADR 0042's retirement work remains incomplete.
951
952Export metadata for Git readers:
953
954Every exported commit carries a footer at the tail of the commit message:
955
956 Heddle-State: <state_id>
957 Heddle-URL: <hosted_url>/state/<state_id> (omitted if no hosted URL)
958 Heddle-Annotations-Omitted: <count>
959
960This is the durable record — every reader on every host sees it regardless
961of remote configuration.
962
963Per-scope annotation drop counts and signal counts ride on the opt-in
964Git note at `refs/notes/heddle`. Heddle reads and writes that ref natively;
965people who still inspect the repository through another Git client can opt
966that client into showing notes, but Heddle itself does not require a Git
967executable on the system.
968"#;
969
970const SIGNALS_TOPIC: &str = "Risk signals — five modules behind a pure trait.\n\
971\n\
972- `invariant_adjacency` — fires when a changed symbol carries an\n\
973 Invariant or `enforces`-tagged annotation.\n\
974- `self_flagged_uncertainty` — passthrough of agent-emitted self-flags\n\
975 from the captured state's intent.\n\
976- `pattern_deviation` — fires when a symbol's body diverges\n\
977 from siblings or the prior version\n\
978 (tree-sitter token similarity).\n\
979- `novelty` — fires when a function shape is unique\n\
980 in the repo corpus.\n\
981- `test_reachability` — fires when no test statically reaches\n\
982 the changed symbol via tree-sitter\n\
983 call-graph traversal. The reason text\n\
984 is honest: this is *not* runtime\n\
985 coverage.\n\
986\n\
987Configure under `[review.signals]` in `.heddle/config.toml`. Each module\n\
988ships fires-correctly + stays-quiet tests; defaults are conservative\n\
989so a fresh repo isn't noisy.\n";
990
991#[cfg(test)]
992mod tests {
993 use super::*;
994
995 #[test]
996 fn everyday_verbs_in_curated_list_have_everyday_tier() {
997 for verb in everyday_verbs() {
998 assert_eq!(tier_of(verb), Tier::Everyday, "{verb}");
999 }
1000 }
1001
1002 #[test]
1003 fn topic_text_returns_none_for_unknown() {
1004 assert!(topic_text("definitely-not-a-topic").is_none());
1005 }
1006
1007 #[test]
1008 fn topic_text_returns_some_for_advertised_topics() {
1009 for topic in [
1010 "advanced",
1011 "agent-flags",
1012 "git-concepts",
1013 "git-concept-map",
1014 "git-veteran",
1015 "git-overlay",
1016 "agent",
1017 "daemon",
1018 "threads",
1019 "model",
1020 "mental-model",
1021 "concepts",
1022 "operation-ids",
1023 "idempotency",
1024 "remotes",
1025 "git-dependencies",
1026 "review",
1027 "discuss",
1028 "discussions",
1029 "git-projection",
1030 "git-projections",
1031 "footer",
1032 "notes",
1033 "signals",
1034 "risk-signals",
1035 "output-formats",
1036 "output-format",
1037 "output",
1038 "clone",
1039 ] {
1040 assert!(topic_text(topic).is_some(), "{topic}");
1041 }
1042 }
1043
1044 #[test]
1045 fn tier_of_advanced_verbs_classifies_correctly() {
1046 for verb in advanced_verbs() {
1047 let t = tier_of(verb);
1048 assert!(
1049 matches!(t, Tier::Advanced),
1050 "expected Advanced for {verb}, got {t:?}"
1051 );
1052 }
1053 }
1054
1055 #[test]
1058 fn advanced_verbs_lists_tip_referenced_commands() {
1059 let advanced: std::collections::HashSet<&str> = advanced_verbs().into_iter().collect();
1060 for verb in ["query", "continue", "abort", "shell"] {
1061 assert!(
1062 advanced.contains(verb),
1063 "`{verb}` is referenced in user-facing tips but is not \
1064 advertised by `heddle help advanced`"
1065 );
1066 }
1067 }
1068
1069 #[test]
1074 fn everyday_verbs_surface_the_core_loop() {
1075 let everyday: std::collections::HashSet<&str> = everyday_verbs().into_iter().collect();
1076 for verb in [
1077 "init", "clone", "status", "start", "capture", "commit", "ready", "diff", "land",
1078 "resolve", "undo", "log", "show", "pull", "push", "doctor", "verify",
1079 ] {
1080 assert!(
1081 everyday.contains(verb),
1082 "`{verb}` is part of the core loop but is not advertised on \
1083 the everyday surface"
1084 );
1085 }
1086 for verb in ["review", "discuss", "context", "thread", "git-projection"] {
1087 assert!(
1088 !everyday.contains(verb),
1089 "`{verb}` belongs behind advanced/topic help, not the core-loop surface"
1090 );
1091 }
1092 }
1093
1094 #[test]
1098 fn agent_flags_topic_lists_hidden_capture_flags() {
1099 let text = topic_text("agent-flags").expect("agent-flags topic should exist");
1100 for flag in [
1101 "--agent-provider",
1102 "--agent-model",
1103 "--agent-session",
1104 "--agent-segment",
1105 "--policy",
1106 "--no-policy",
1107 "--no-agent",
1108 "--split",
1109 "--into",
1110 "--path",
1111 ] {
1112 assert!(text.contains(flag), "agent-flags topic missing `{flag}`");
1113 }
1114 for env in [
1115 "HEDDLE_AGENT_PROVIDER",
1116 "HEDDLE_AGENT_MODEL",
1117 "HEDDLE_AGENT_POLICY",
1118 ] {
1119 assert!(text.contains(env), "agent-flags topic missing env `{env}`");
1120 }
1121 for internal_env in ["HEDDLE_SESSION_ID", "HEDDLE_SESSION_SEGMENT"] {
1122 assert!(
1123 !text.contains(internal_env),
1124 "agent-flags topic must not advertise internal env `{internal_env}`"
1125 );
1126 }
1127 }
1128
1129 #[test]
1130 fn git_help_distinguishes_overlay_from_projection_storage() {
1131 let overlay = topic_text("git-overlay").expect("git-overlay topic should exist");
1132 assert!(overlay.contains("neither creates nor reads `.heddle/git`"));
1133
1134 let projection = topic_text("git-projection").expect("git-projection topic should exist");
1135 assert!(projection.contains("It is not Git Overlay"));
1136 assert!(projection.contains("heddle adopt --ref <branch>"));
1137 assert!(projection.contains("heddle export git --destination"));
1138 assert!(projection.contains("Bridge Mirror"));
1139 }
1140
1141 #[test]
1144 fn capture_agent_flags_hidden_by_default_revealed_on_demand() {
1145 use clap::CommandFactory;
1146 let cmd = crate::cli::cli_args::Cli::command();
1147 let capture = cmd
1148 .find_subcommand("capture")
1149 .expect("capture subcommand exists");
1150 for id in CAPTURE_AGENT_FLAG_IDS {
1151 let arg = capture
1152 .get_arguments()
1153 .find(|arg| arg.get_id().as_str() == *id)
1154 .unwrap_or_else(|| panic!("capture has no `{id}` arg"));
1155 assert!(arg.is_hide_set(), "`{id}` should be hidden by default");
1156 }
1157 let revealed = reveal_capture_agent_flags(capture.clone());
1158 for id in CAPTURE_AGENT_FLAG_IDS {
1159 let arg = revealed
1160 .get_arguments()
1161 .find(|arg| arg.get_id().as_str() == *id)
1162 .expect("revealed arg present");
1163 assert!(
1164 !arg.is_hide_set(),
1165 "`{id}` should be revealed by --help-agent"
1166 );
1167 }
1168 }
1169
1170 fn wants_reveal(args: &[&str]) -> bool {
1181 use clap::Parser;
1182
1183 use crate::cli::cli_args::{Cli, Commands};
1184 let argv = std::iter::once("heddle").chain(args.iter().copied());
1185 match Cli::try_parse_from(argv) {
1186 Ok(cli) => matches!(&cli.command, Commands::Capture(a) if a.help_agent),
1187 Err(_) => false,
1191 }
1192 }
1193
1194 #[test]
1198 fn capture_help_agent_is_capture_scoped() {
1199 assert!(
1200 wants_reveal(&["capture", "--help-agent"]),
1201 "capture --help-agent should request the reveal help"
1202 );
1203 assert!(
1204 !wants_reveal(&["status", "--help-agent"]),
1205 "--help-agent on a non-capture verb is not a capture reveal request"
1206 );
1207 assert!(
1210 !wants_reveal(&["capture", "--help"]),
1211 "plain --help is clap's help, not the agent reveal"
1212 );
1213 }
1214
1215 #[test]
1222 fn capture_help_agent_handles_every_global_form_clap_accepts() {
1223 assert!(
1225 wants_reveal(&["-C", "/tmp/repo", "capture", "--help-agent"]),
1226 "`-C <path> capture --help-agent` should reveal — clap parses the path"
1227 );
1228 assert!(
1229 wants_reveal(&["--output", "text", "capture", "--help-agent"]),
1230 "`--output text capture --help-agent` should reveal"
1231 );
1232 assert!(
1235 wants_reveal(&["-C", "capture", "capture", "--help-agent"]),
1236 "`-C capture capture --help-agent` (repo dir named `capture`) should reveal"
1237 );
1238 assert!(
1240 wants_reveal(&["-vC", "/tmp/repo", "capture", "--help-agent"]),
1241 "`-vC <path> capture --help-agent` (clustered short globals) should reveal"
1242 );
1243 assert!(
1244 wants_reveal(&["-vC", "capture", "capture", "--help-agent"]),
1245 "`-vC capture capture --help-agent` (clustered, repo dir named `capture`) should reveal"
1246 );
1247 assert!(
1249 wants_reveal(&["-C/tmp/repo", "capture", "--help-agent"]),
1250 "`-C<path> capture --help-agent` (attached) should reveal"
1251 );
1252 assert!(
1254 wants_reveal(&["capture", "--help-agent"]),
1255 "plain `capture --help-agent` should reveal"
1256 );
1257
1258 assert!(
1263 !wants_reveal(&["-C", "capture", "status", "--help-agent"]),
1264 "`-C capture status --help-agent` — verb is `status`, no reveal"
1265 );
1266 assert!(
1267 !wants_reveal(&["-vC", "capture", "status", "--help-agent"]),
1268 "`-vC capture status --help-agent` (clustered) — verb is `status`, no reveal"
1269 );
1270 assert!(
1271 !wants_reveal(&["--output", "text", "status", "--help-agent"]),
1272 "`--output text status --help-agent` — verb is `status`, no reveal"
1273 );
1274 }
1275
1276 #[test]
1283 fn output_blurb_stated_once_not_per_command() {
1284 let marker = "full machine contract";
1285 let top = render_for_args(&["--help"]).expect("top-level help renders");
1286 assert_eq!(
1287 top.matches(marker).count(),
1288 1,
1289 "top-level help should state the --output contract exactly once: {top}"
1290 );
1291 assert!(
1292 topic_text("output-formats")
1293 .expect("output-formats topic exists")
1294 .contains(marker),
1295 "the output-formats topic carries the full contract"
1296 );
1297 for argv in [
1298 &["clone", "--help"][..],
1299 &["status", "--help"][..],
1300 &["capture", "--help"][..],
1301 &["thread", "--help"][..],
1302 &["push", "--help"][..],
1303 ] {
1304 let help = render_for_args(argv).expect("command help renders");
1305 assert!(
1306 !help.contains(marker),
1307 "`{argv:?}` should not restate the --output machine contract: {help}"
1308 );
1309 assert!(
1310 help.contains("heddle help output-formats"),
1311 "`{argv:?}` should breadcrumb to the output-formats topic: {help}"
1312 );
1313 }
1314 }
1315
1316 #[test]
1321 fn clone_help_fits_one_screen() {
1322 let help = render_for_args(&["clone", "--help"]).expect("clone help renders");
1323 let lines = help.lines().count();
1324 assert!(
1325 lines <= 40,
1326 "clone --help should fit one screen (<= 40 lines), got {lines}:\n{help}"
1327 );
1328 assert!(
1331 help.contains("Advanced/planned flags: see `heddle help clone`."),
1332 "clone --help keeps the hidden-flags affordance: {help}"
1333 );
1334 assert!(
1335 help.contains("heddle help clone"),
1336 "clone --help points at the clone topic for the full behavior: {help}"
1337 );
1338 }
1339
1340 #[test]
1343 fn advanced_help_renders_area_groups() {
1344 use clap::CommandFactory;
1345 let cmd = crate::cli::cli_args::Cli::command();
1346 let advanced = render_help(&cmd, &["advanced".to_string()]);
1347 for header in [
1348 "Threads and integration:",
1349 "States and history:",
1350 "Recovery and integrity:",
1351 "Repo and environment:",
1352 "Agents and automation:",
1353 "Git interop:",
1354 "Admin and maintenance:",
1355 ] {
1356 assert!(
1357 advanced.contains(&format!("\n{header}\n")),
1358 "advanced help should render the `{header}` group: {advanced}"
1359 );
1360 }
1361 assert!(
1362 !advanced.contains("Advanced commands:"),
1363 "the flat list header is replaced by area groups: {advanced}"
1364 );
1365 }
1366
1367 #[test]
1374 fn advanced_help_groups_cover_every_advanced_verb() {
1375 let grouped: Vec<&str> = crate::cli::commands::advanced_help_groups()
1376 .into_iter()
1377 .flat_map(|(_, verbs)| verbs)
1378 .collect();
1379 let mut deduped = grouped.clone();
1380 deduped.sort_unstable();
1381 deduped.dedup();
1382 assert_eq!(
1383 deduped.len(),
1384 grouped.len(),
1385 "no verb may appear in two advanced-help groups: {grouped:?}"
1386 );
1387 let grouped: std::collections::HashSet<&str> = grouped.into_iter().collect();
1388 let flat: std::collections::HashSet<&str> = advanced_verbs().into_iter().collect();
1389 let missing: Vec<&&str> = flat.difference(&grouped).collect();
1390 assert!(
1391 missing.is_empty(),
1392 "advanced verbs missing from every group — native advanced root \
1393 commands must register a help_category in the command contract \
1394 table: {missing:?}"
1395 );
1396 let extra: Vec<&&str> = grouped.difference(&flat).collect();
1397 assert!(
1398 extra.is_empty(),
1399 "grouped verbs not on the advanced surface: {extra:?}"
1400 );
1401 }
1402
1403 #[test]
1412 fn verb_blurbs_resolve_from_command_catalog() {
1413 let catalog = crate::cli::commands::build_command_catalog();
1414 for verb in everyday_verbs().into_iter().chain(advanced_verbs()) {
1415 if catalog.command_by_display(verb).is_none() {
1418 continue;
1419 }
1420 let blurb = catalog_summary(&catalog, verb);
1421 assert!(
1422 !blurb.is_empty(),
1423 "verb `{verb}` is cataloged but its summary is empty. \
1424 The curated help printer needs a non-empty catalog summary."
1425 );
1426 }
1427 }
1428
1429 #[test]
1449 fn hidden_flags_carry_discovery_affordances() {
1450 use clap::CommandFactory;
1451
1452 fn walk(cmd: &clap::Command, path: &str, violations: &mut Vec<String>) {
1453 let after_help = [
1454 cmd.get_after_help().map(ToString::to_string),
1455 cmd.get_after_long_help().map(ToString::to_string),
1456 ]
1457 .into_iter()
1458 .flatten()
1459 .collect::<Vec<_>>()
1460 .join("\n");
1461 let after_lower = after_help.to_lowercase();
1462 let breadcrumb_marker = after_lower.contains("hidden")
1463 || after_lower.contains("advanced flag")
1464 || after_lower.contains("advanced/planned flags");
1465 let points_at_reveal =
1466 after_help.contains("heddle help ") || after_help.contains("--help-agent");
1467
1468 for arg in cmd.get_arguments() {
1469 if !arg.is_hide_set() || arg.is_global_set() {
1470 continue;
1471 }
1472 let flag_help = arg
1473 .get_long_help()
1474 .or_else(|| arg.get_help())
1475 .map(ToString::to_string)
1476 .unwrap_or_default();
1477 if flag_help.starts_with("Internal") {
1478 continue;
1479 }
1480 let named_inline = arg
1481 .get_long()
1482 .is_some_and(|long| after_help.contains(&format!("--{long}")));
1483 if breadcrumb_marker && (points_at_reveal || named_inline) {
1484 continue;
1485 }
1486 let name = arg
1487 .get_long()
1488 .map(|long| format!("--{long}"))
1489 .unwrap_or_else(|| arg.get_id().to_string());
1490 violations.push(format!("`{path}` hides `{name}`"));
1491 }
1492
1493 for sub in cmd.get_subcommands() {
1494 if sub.is_hide_set() {
1495 continue;
1496 }
1497 walk(sub, &format!("{path} {}", sub.get_name()), violations);
1498 }
1499 }
1500
1501 let cmd = crate::cli::cli_args::Cli::command();
1502 let mut violations = Vec::new();
1503 walk(&cmd, cmd.get_name(), &mut violations);
1504 assert!(
1505 violations.is_empty(),
1506 "hidden flags without a discovery affordance (heddle#646): either \
1507 prefix the flag's help with `Internal` (plumbing, not for users) \
1508 or add an after-help breadcrumb that mentions the hidden/advanced \
1509 flags and names the flag or a reveal surface:\n {}",
1510 violations.join("\n ")
1511 );
1512 }
1513}