1#[cfg(feature = "git-overlay")]
5use super::commands_git_projection::SyncCommands;
6
7pub const INIT_VERB: &str = "init";
13pub const IMPORT_VERB: &str = "import";
14
15#[derive(Clone, Debug, clap::Args)]
17#[command(after_help = "\
18Examples:
19 heddle init # initialize here; existing Git becomes Git Overlay
20 heddle init my-project # initialize a native Heddle subdirectory
21 heddle init --principal-name 'Ada Lovelace' --principal-email ada@example.com
22")]
23pub struct InitArgs {
24 pub path: Option<std::path::PathBuf>,
26
27 #[arg(long)]
29 pub principal_name: Option<String>,
30
31 #[arg(long)]
33 pub principal_email: Option<String>,
34
35 #[arg(long)]
37 pub install_harnesses: Option<String>,
38
39 #[arg(long)]
41 pub no_harness_install: bool,
42
43 #[arg(long, visible_alias = "scope", default_value = "repo")]
45 pub harness_install_scope: String,
46
47 #[arg(long)]
49 pub harness_install_force: bool,
50}
51
52impl InitArgs {
53 pub const VERB: &'static str = INIT_VERB;
55}
56
57#[derive(Clone, Debug, clap::Args)]
59#[command(after_help = "\
60Examples:
61 heddle import local # import all local Git refs into native Heddle storage
62 heddle import local --ref main # import one branch or tag
63 heddle import local ../repo --ref main --ref v1.0 # import selected refs in another repo
64
65Importing locally makes Heddle the source authority and retains `.git` for explicit Git Projection. Normal Git Overlay setup uses `heddle init` instead.
66")]
67pub struct ImportLocalArgs {
68 pub path: Option<std::path::PathBuf>,
70
71 #[arg(long = "ref", value_name = "REF")]
73 pub refs: Vec<String>,
74}
75
76#[derive(Clone, Debug, clap::Args)]
83pub struct DoctorArgs {
84 #[arg(long, global = false)]
89 pub profile: bool,
90
91 #[command(subcommand)]
92 pub command: Option<DoctorCommands>,
93}
94
95#[derive(Clone, Debug, clap::Subcommand)]
97pub enum DoctorCommands {
98 Docs(DoctorDocsArgs),
111}
112
113#[derive(Clone, Debug, clap::Args)]
115pub struct DoctorDocsArgs {
116 #[arg(long, value_name = "PATH")]
121 pub path: Vec<std::path::PathBuf>,
122
123 #[arg(long)]
125 pub all: bool,
126}
127
128fn parse_confidence(s: &str) -> Result<f32, String> {
129 let value = s
130 .parse::<f32>()
131 .map_err(|_| format!("confidence must be a finite number from 0.0 to 1.0, got `{s}`"))?;
132 if !value.is_finite() || !(0.0..=1.0).contains(&value) {
133 return Err(format!(
134 "confidence must be a finite number from 0.0 to 1.0, got `{s}`"
135 ));
136 }
137 Ok(value)
138}
139
140#[derive(Clone, Debug, clap::Args)]
142#[command(override_usage = "heddle capture -m <INTENT> [OPTIONS]")]
143#[command(after_help = "\
144Examples:
145 heddle capture -m 'add login route' # save state (and its Git Overlay checkpoint)
146 heddle capture -m 'wip' --confidence 0.6 # honest confidence on a draft step
147
148Agent automation flags (provider/model/session/policy/split) are hidden here.
149Run `heddle help agent-flags`, or `heddle capture --help-agent` to list them inline.
150")]
151pub struct SnapshotArgs {
152 #[arg(long, hide = true)]
162 pub help_agent: bool,
163
164 #[arg(short = 'm', long, visible_alias = "message", value_name = "INTENT")]
166 pub intent: Option<String>,
167
168 #[arg(long, value_parser = parse_confidence)]
170 pub confidence: Option<f32>,
171
172 #[arg(short, long)]
174 pub force: bool,
175
176 #[arg(long, hide = true)]
178 pub agent_provider: Option<String>,
179
180 #[arg(long, hide = true)]
182 pub agent_model: Option<String>,
183
184 #[arg(long, hide = true)]
186 pub agent_session: Option<String>,
187
188 #[arg(long, hide = true)]
190 pub agent_segment: Option<String>,
191
192 #[arg(long, hide = true)]
194 pub policy: Option<String>,
195
196 #[arg(long, hide = true)]
198 pub no_policy: bool,
199
200 #[arg(long, hide = true)]
202 pub no_agent: bool,
203
204 #[arg(long, hide = true)]
206 pub split: bool,
207
208 #[arg(long, hide = true, requires = "split")]
210 pub into: Option<String>,
211
212 #[arg(long = "path", hide = true, requires = "split", value_name = "PATH")]
214 pub paths: Vec<String>,
215}
216
217#[derive(Clone, Debug, clap::Args)]
219#[command(after_help = "\
220Examples:
221 heddle log # walk the current thread
222 heddle log --oneline -n 20 # 20 most recent states in compact form
223 heddle log --timeline # show agent timeline tool-call cursor
224 heddle log --reflog # include re-attributed history
225 heddle log --path src/auth.rs # restrict to states touching a path
226")]
227pub struct LogArgs {
228 pub state: Option<String>,
230
231 #[arg(short = 'n', long, default_value = "20")]
233 pub limit: usize,
234
235 #[arg(long)]
237 pub all: bool,
238
239 #[arg(long)]
241 pub graph: bool,
242
243 #[arg(long)]
245 pub oneline: bool,
246
247 #[arg(long)]
249 pub reflog: bool,
250
251 #[arg(long)]
253 pub timeline: bool,
254
255 #[arg(long, default_value = "main")]
257 pub thread: String,
258
259 #[arg(long)]
261 pub agent: Option<String>,
262
263 #[arg(long = "path", value_name = "PATH")]
265 pub paths: Vec<String>,
266
267 #[arg(long, value_name = "STATE")]
273 pub since: Option<String>,
274}
275
276#[derive(Clone, Debug, clap::Subcommand)]
278pub enum TimelineCommands {
279 Status(TimelineStatusArgs),
281
282 #[command(name = "record-start")]
284 RecordStart(TimelineRecordStartArgs),
285
286 #[command(name = "record-finish")]
288 RecordFinish(TimelineRecordFinishArgs),
289
290 #[command(after_help = "\
292Examples:
293 heddle agent timeline fork --step tls-abc --branch tlb-experiment
294 heddle agent timeline fork --tool-call call_123 --session ses_456 --branch tlb-alt
295")]
296 Fork(TimelineForkArgs),
297
298 #[command(after_help = "\
300Examples:
301 heddle agent timeline reset --step tls-abc
302 heddle agent timeline reset --tool-call call_123 --materialize
303")]
304 Reset(TimelineResetArgs),
305
306 Recover(TimelineRecoverArgs),
308}
309
310#[derive(Clone, Debug, clap::Args)]
312pub struct TimelineTargetArgs {
313 #[arg(long, default_value = "main")]
315 pub thread: String,
316
317 #[arg(long = "from-branch", value_name = "BRANCH")]
319 pub from_branch: Option<String>,
320
321 #[arg(long, conflicts_with_all = ["tool_call", "undo", "redo", "current"])]
323 pub step: Option<String>,
324
325 #[arg(long = "tool-call", conflicts_with_all = ["step", "undo", "redo", "current"])]
327 pub tool_call: Option<String>,
328
329 #[arg(long, default_value = "opencode")]
331 pub harness: String,
332
333 #[arg(long)]
335 pub session: Option<String>,
336
337 #[arg(long)]
339 pub message: Option<String>,
340
341 #[arg(long, conflicts_with_all = ["step", "tool_call", "redo", "current"])]
343 pub undo: bool,
344
345 #[arg(long, conflicts_with_all = ["step", "tool_call", "undo", "current"])]
347 pub redo: bool,
348
349 #[arg(long, conflicts_with_all = ["step", "tool_call", "undo", "redo"])]
351 pub current: bool,
352}
353
354#[derive(Clone, Debug, clap::Args)]
356pub struct TimelineForkArgs {
357 #[command(flatten)]
358 pub target: TimelineTargetArgs,
359
360 #[arg(long, value_name = "BRANCH")]
362 pub branch: Option<String>,
363
364 #[arg(long, default_value = "explicit-fork")]
366 pub reason: String,
367}
368
369#[derive(Clone, Debug, clap::Args)]
371pub struct TimelineResetArgs {
372 #[command(flatten)]
373 pub target: TimelineTargetArgs,
374
375 #[arg(long)]
377 pub materialize: bool,
378
379 #[arg(long, default_value = "fail-if-dirty")]
381 pub mode: String,
382}
383
384#[derive(Clone, Debug, clap::Args)]
386pub struct TimelineRecoverArgs {
387 #[arg(long, default_value = "main")]
389 pub thread: String,
390}
391
392#[derive(Clone, Debug, clap::Args)]
394pub struct TimelineStatusArgs {
395 #[arg(long, default_value = "main")]
397 pub thread: String,
398}
399
400#[derive(Clone, Debug, clap::Args)]
402pub struct TimelineRecordToolArgs {
403 #[arg(long, default_value = "main")]
405 pub thread: String,
406
407 #[arg(long, default_value = "opencode")]
409 pub harness: String,
410
411 #[arg(long)]
413 pub session: Option<String>,
414
415 #[arg(long)]
417 pub message: Option<String>,
418
419 #[arg(long = "tool-call")]
421 pub tool_call: String,
422
423 #[arg(long = "step-id")]
425 pub step_id: Option<String>,
426
427 #[arg(long = "branch")]
429 pub branch: Option<String>,
430
431 #[arg(long = "summary")]
433 pub summary: Option<String>,
434
435 #[arg(long = "payload-hash")]
437 pub payload_hash: Option<String>,
438}
439
440#[derive(Clone, Debug, clap::Args)]
442pub struct TimelineRecordStartArgs {
443 #[command(flatten)]
444 pub tool: TimelineRecordToolArgs,
445
446 #[arg(long = "tool-name", default_value = "tool")]
448 pub tool_name: String,
449}
450
451#[derive(Clone, Debug, clap::Args)]
453pub struct TimelineRecordFinishArgs {
454 #[command(flatten)]
455 pub tool: TimelineRecordToolArgs,
456
457 #[arg(long, default_value = "succeeded")]
459 pub status: String,
460}
461
462#[derive(Clone, Debug, clap::Args)]
464#[command(after_help = "\
465Examples:
466 heddle diff # worktree vs HEAD
467 heddle diff --base last-turn # worktree vs this agent peer's turn start
468 heddle diff NOTES.md # only that path
469 heddle diff -- NOTES.md # same, after the path separator
470 heddle diff --path NOTES.md # same, explicit path filter
471 heddle diff HEAD~1 HEAD -- src # two states, filtered to src/
472
473Path-shaped positionals and arguments after `--` are worktree/path filters,
474not missing states. `log --path` uses the same filter spelling.
475
476Restore:
477 Heddle does not restore one file from a saved state (no restore/checkout/reset).
478 Materialize one state in a new checkout: heddle start <name> --from <state> --path <dir>
479 Apply the inverse of one state to this worktree: heddle revert <state> [--no-commit]
480 Restore the tree preserved by the last undo: heddle undo --recover
481
482Patch compatibility:
483 --patch output uses Git-compatible unified diff, including extended headers for type and mode changes.
484")]
485pub struct DiffArgs {
486 pub from: Option<String>,
488
489 pub to: Option<String>,
491
492 #[arg(long, value_enum)]
496 pub base: Option<DiffBaseArg>,
497
498 #[arg(long = "path", value_name = "PATH")]
500 pub path_filters: Vec<String>,
501
502 #[arg(last = true, value_name = "PATH")]
504 pub paths: Vec<String>,
505
506 #[arg(long)]
508 pub semantic: bool,
509
510 #[arg(long)]
512 pub stat: bool,
513
514 #[arg(long)]
516 pub name_only: bool,
517
518 #[arg(short = 'U', long = "unified", default_value_t = 3)]
520 pub unified: usize,
521
522 #[arg(long)]
524 pub context: bool,
525
526 #[arg(short = 'p', long = "patch")]
528 pub patch: bool,
529}
530
531#[derive(Clone, Copy, Debug, PartialEq, Eq, clap::ValueEnum)]
533pub enum DiffBaseArg {
534 LastTurn,
536}
537
538impl DiffBaseArg {
539 pub fn as_str(self) -> &'static str {
540 match self {
541 Self::LastTurn => "last-turn",
542 }
543 }
544}
545
546#[derive(Clone, Debug, clap::Args)]
548#[command(after_help = "\
549Restore:
550 `revert` applies the inverse of one state's changes. It is not a single-file
551 restore, and Heddle has no restore/checkout/reset verb.
552 Materialize one state in a new checkout: heddle start <name> --from <state> --path <dir>
553 Restore the tree preserved by the last undo: heddle undo --recover
554 Heddle cannot put one file back from a saved state.
555")]
556pub struct RevertArgs {
557 pub state: String,
559
560 #[arg(short = 'm', long)]
562 pub message: Option<String>,
563
564 #[arg(long)]
566 pub no_commit: bool,
567}
568
569#[derive(Clone, Debug, clap::Args)]
571#[command(after_help = "\
572Examples:
573 heddle undo --dry-run # inspect the most recent operation
574 heddle undo --hard --dry-run # preview the worktree rewind --hard would apply
575 heddle undo --hard # roll it back and rewind the worktree
576 heddle undo -n 3 --hard # roll back the last three operations
577 heddle undo --recover # restore the state preserved by the last undo
578 heddle undo --list # list undoable operations on this thread
579
580Restore:
581 `--recover` restores only the last undo's preserved tree as worktree changes.
582 Heddle does not restore one arbitrary file or an arbitrary saved state
583 (no restore/checkout/reset). Materialize a state with
584 `heddle start <name> --from <state> --path <dir>`, or invert one with
585 `heddle revert <state>`.
586
587Undoable operations:
588 - heddle capture (restores HEAD to the pre-capture parent)
589 - heddle land (non-FF) (restores HEAD + both thread refs)
590 - heddle land (FF) (restores HEAD + the landed-into thread ref to
591 the pre-merge tip; the merged-in thread is
592 untouched.)
593 - heddle thread switch (restores HEAD to the previous thread state)
594 - heddle thread create/drop/rename
595 - heddle thread marker create/drop
596 - heddle redact apply (with --allow-redact-undo; removes the
597 redaction record so future materializes
598 restore the original blob bytes. Refused
599 when a Purge has destroyed the bytes.)
600 - heddle undo --redo re-apply the most recently undone operation
601
602Not undoable (file a follow-up if you need one):
603 - heddle push / pull (remote-affecting; out of scope)
604 - heddle redact purge apply (destructive by design; irreversible)
605 - heddle start <name> --path <dir> (refused while the materialized worktree
606 still exists — run `heddle thread drop
607 <name> --delete-thread` first, then
608 re-run `heddle undo`)
609 - cross-worktree shared-backend undo (no worktree registry yet; single-
610 worktree usage is the supported
611 configuration for 0.3)
612")]
613pub struct UndoArgs {
614 #[arg(short = 'n', long, default_value = "1")]
616 pub steps: usize,
617
618 #[arg(long)]
620 pub list: bool,
621
622 #[arg(long, default_value = "20")]
624 pub depth: usize,
625
626 #[command(flatten)]
627 pub dry_run: super::DryRunArgs,
628
629 #[arg(long, conflicts_with_all = ["list", "redo", "recover"])]
633 pub hard: bool,
634
635 #[arg(long, conflicts_with = "list")]
637 pub redo: bool,
638
639 #[arg(
642 long,
643 conflicts_with_all = ["steps", "list", "dry_run", "hard", "redo", "allow_redact_undo"]
644 )]
645 pub recover: bool,
646
647 #[arg(long)]
655 pub allow_redact_undo: bool,
656}
657
658#[derive(Clone, Copy, Debug, clap::ValueEnum, PartialEq, Eq)]
664pub enum WorkspaceModeArg {
665 Auto,
667 Materialized,
669 Virtualized,
671 Solid,
673}
674
675#[derive(Clone, Debug, clap::Args)]
677#[command(after_help = "\
678Examples:
679 heddle start feature/auth # checkout under .heddle/threads/
680 heddle start feature/auth --path ../feature-auth # place the checkout explicitly
681 heddle start fix-flake --path ../fix-flake --task 'fix CI flake'
682
683When `--path` is omitted, start always uses `.heddle/threads/<name>/…` (not
684TTY-gated; the managed layout under the repo). Pass `--path` to choose a
685different directory.
686To stay on this checkout without an isolated tree, use
687`heddle thread create <name>` then `heddle thread switch <name>`.
688
689Isolated checkouts are Heddle-managed working directories. They do not contain a .git directory; use Heddle commands inside them, and run Git-authority operations through Heddle from the parent Git-overlay repository.
690
691`heddle start <name> --path <dir>` is the one-step form of the advanced split flow: `heddle thread create <name>` creates the ref now, and `heddle thread checkout <name> --path <dir>` materializes it later. Use the split form only when you intentionally need ref-first, checkout-later staging.
692
693Advanced (hidden) flags:
694 --agent-provider/--agent-model (agent attribution for the registered thread), --parent-thread (delegated child work), --print-cd-path (print only the checkout path for shell wrappers), --daemon/--no-daemon (virtualized-mount ownership), --shared-target/--no-shared-target (workspace-shared cargo target dir; default on for Rust solid/materialized). All are accepted here; they stay out of the flag list to keep everyday help terse.
695")]
696pub struct ThreadStartArgs {
697 pub name: String,
699
700 #[arg(long)]
702 pub from: Option<String>,
703
704 #[arg(long)]
706 pub path: Option<std::path::PathBuf>,
707
708 #[arg(long, value_enum)]
711 pub workspace: Option<WorkspaceModeArg>,
712
713 #[arg(long, hide = true)]
715 pub agent_provider: Option<String>,
716
717 #[arg(long, hide = true)]
719 pub agent_model: Option<String>,
720
721 #[arg(long)]
723 pub task: Option<String>,
724
725 #[arg(long, hide = true)]
727 pub parent_thread: Option<String>,
728
729 #[arg(long, hide = true)]
731 pub automated: bool,
732
733 #[arg(long, hide = true, conflicts_with_all = ["agent_provider", "agent_model"])]
740 pub print_cd_path: bool,
741
742 #[arg(
749 long,
750 overrides_with = "no_daemon",
751 action = clap::ArgAction::SetTrue,
752 default_value_t = true,
753 hide = true,
754 )]
755 pub daemon: bool,
756
757 #[arg(
763 long,
764 overrides_with = "daemon",
765 action = clap::ArgAction::SetTrue,
766 hide = true,
767 )]
768 pub no_daemon: bool,
769
770 #[arg(long)]
774 pub interactive_setup: bool,
775
776 #[arg(
790 long,
791 overrides_with = "no_shared_target",
792 action = clap::ArgAction::SetTrue,
793 hide = true,
794 )]
795 pub shared_target: bool,
796
797 #[arg(
801 long,
802 overrides_with = "shared_target",
803 action = clap::ArgAction::SetTrue,
804 hide = true,
805 )]
806 pub no_shared_target: bool,
807
808 #[arg(long)]
819 pub hydrate: bool,
820}
821
822#[derive(Clone, Debug, clap::Args)]
824pub struct ReadyArgs {
825 #[arg(long = "thread")]
827 pub thread: Option<String>,
828
829 #[arg(short = 'm', long)]
831 pub message: Option<String>,
832
833 #[arg(long, value_parser = parse_confidence)]
835 pub confidence: Option<f32>,
836
837 #[command(flatten)]
838 pub dry_run: super::DryRunArgs,
839}
840
841#[derive(Clone, Debug, clap::Args)]
843pub struct SyncArgs {
844 #[cfg(feature = "git-overlay")]
846 #[command(subcommand)]
847 pub command: Option<SyncCommands>,
848
849 #[arg(long = "thread")]
851 pub thread: Option<String>,
852}
853
854#[derive(Clone, Debug, clap::Args)]
856pub struct LandArgs {
857 #[arg(long = "thread")]
859 pub thread: Option<String>,
860
861 #[arg(long = "threads", value_delimiter = ',')]
866 pub threads: Vec<String>,
867
868 #[arg(short = 'm', long)]
870 pub message: Option<String>,
871
872 #[arg(long)]
874 pub no_squash: bool,
875
876 #[command(flatten)]
877 pub dry_run: super::DryRunArgs,
878}
879
880#[derive(Clone, Debug, clap::Args)]
882pub struct ThreadShowArgs {
883 pub thread: Option<String>,
885
886 #[arg(long)]
888 pub watch: bool,
889
890 #[arg(long, hide = true)]
892 pub watch_iterations: Option<usize>,
893
894 #[arg(long, hide = true)]
896 pub watch_interval_ms: Option<u64>,
897}
898
899#[derive(Clone, Debug, clap::Args)]
901pub struct ThreadCapturesArgs {
902 pub thread: Option<String>,
904
905 #[arg(long, default_value_t = 20)]
907 pub limit: usize,
908}
909
910#[derive(Clone, Debug, clap::Args)]
914pub struct ThreadNameArgs {
915 pub thread: Option<String>,
917}
918
919#[derive(Clone, Debug, clap::Args)]
921pub struct ThreadRenameArgs {
922 pub old: String,
924
925 pub new: String,
927}
928
929#[derive(Clone, Debug, clap::Args)]
931pub struct ThreadCheckoutArgs {
932 pub thread: String,
934
935 #[arg(long, required = true)]
937 pub path: std::path::PathBuf,
938
939 #[arg(long)]
941 pub force: bool,
942}
943
944#[derive(Clone, Debug, clap::Args)]
946pub struct ThreadMoveArgs {
947 pub from: String,
949
950 pub to: String,
952
953 #[arg(long = "path", required = true, value_name = "PATH")]
955 pub paths: Vec<String>,
956
957 #[arg(short = 'm', long)]
959 pub message: Option<String>,
960}
961
962#[derive(Clone, Debug, clap::Args)]
964pub struct ThreadAbsorbArgs {
965 pub thread: String,
967
968 #[arg(long)]
970 pub into: Option<String>,
971
972 #[arg(short = 'm', long)]
974 pub message: Option<String>,
975
976 #[command(flatten)]
977 pub dry_run: super::DryRunArgs,
978}
979
980#[derive(Clone, Debug, clap::Args)]
982pub struct ThreadResolveArgs {
983 pub thread: String,
985}
986
987#[derive(Clone, Debug, clap::Args)]
989pub struct ThreadDropArgs {
990 pub thread: String,
992
993 #[arg(long)]
995 pub delete_thread: bool,
996
997 #[arg(short, long)]
999 pub force: bool,
1000}
1001
1002#[derive(Clone, Debug, clap::Args)]
1004pub struct CollapseArgs {
1005 #[arg(required = true)]
1007 pub states: Vec<String>,
1008
1009 #[arg(long)]
1011 pub into: String,
1012
1013 #[arg(long)]
1015 pub confidence: Option<f32>,
1016}
1017
1018#[derive(Clone, Debug, clap::Args)]
1020pub struct ExpandArgs {
1021 pub reference: String,
1023}
1024
1025#[derive(Clone, Debug, clap::Args)]
1027#[command(after_help = "\
1028Concurrent source heads:
1029 When two writers publish divergent heads on one Thread, clone and pull check
1030 out one of them and keep the rest. `status` lists them until you choose:
1031 heddle resolve --heads list every head and who produced it
1032 heddle resolve --pick <STATE> take exactly that head's tree
1033 heddle resolve --merge <STATE> three-way merge that head into your tip
1034 <STATE> is a head's State ID or a unique prefix of it.
1035")]
1036pub struct ResolveArgs {
1037 #[arg(conflicts_with_all = ["heads", "pick", "merge"])]
1039 pub path: Option<String>,
1040
1041 #[arg(long)]
1043 pub all: bool,
1044
1045 #[arg(long)]
1047 pub list: bool,
1048
1049 #[arg(long, conflicts_with = "theirs")]
1051 pub ours: bool,
1052
1053 #[arg(long, conflicts_with = "ours")]
1055 pub theirs: bool,
1056
1057 #[arg(long)]
1059 pub force: bool,
1060
1061 #[arg(long, conflicts_with_all = ["all", "list", "ours", "theirs", "force", "pick", "merge"])]
1063 pub heads: bool,
1064
1065 #[arg(
1067 long,
1068 value_name = "STATE",
1069 conflicts_with_all = ["all", "list", "ours", "theirs", "force", "merge"]
1070 )]
1071 pub pick: Option<String>,
1072
1073 #[arg(
1075 long,
1076 value_name = "STATE",
1077 conflicts_with_all = ["all", "list", "ours", "theirs", "force"]
1078 )]
1079 pub merge: Option<String>,
1080}
1081
1082#[derive(Clone, Debug, clap::Args)]
1085pub struct RemoteOperationArgs {
1086 pub remote: Option<String>,
1088
1089 #[arg(short, long)]
1091 pub thread: Option<String>,
1092
1093 #[arg(long)]
1096 pub insecure: bool,
1097}
1098
1099#[derive(Clone, Debug, clap::Args)]
1101#[command(after_help = "\
1102Git Overlay refs:
1103 A normal push writes refs/heads/<thread> and refs/notes/heddle.
1104 --all-threads writes every refs/heads/<thread> and refs/tags/<tag>, plus refs/notes/heddle.
1105 JSON output lists changed refs in refs_written; verify with git ls-remote <remote>.
1106")]
1107pub struct PushArgs {
1108 pub remote: Option<String>,
1110
1111 #[arg(short, long, conflicts_with = "thread_arg")]
1113 pub thread: Option<String>,
1114
1115 #[arg(value_name = "THREAD")]
1117 pub thread_arg: Option<String>,
1118
1119 #[arg(short, long)]
1121 pub state: Option<String>,
1122
1123 #[arg(short, long)]
1125 pub force: bool,
1126
1127 #[arg(long)]
1129 pub all_threads: bool,
1130
1131 #[arg(long)]
1134 pub insecure: bool,
1135
1136 #[command(flatten)]
1137 pub dry_run: super::DryRunArgs,
1138}
1139
1140impl PushArgs {
1141 pub fn thread_name(&self) -> Option<String> {
1142 self.thread.clone().or_else(|| self.thread_arg.clone())
1143 }
1144}
1145
1146#[derive(Clone, Debug, clap::Args)]
1148#[command(after_help = "\
1149Advanced (hidden) flags:
1150 --lazy is reserved for hosted lazy hydration and is rejected until end-to-end support lands.
1151")]
1152pub struct PullArgs {
1153 #[command(flatten)]
1154 pub remote_op: RemoteOperationArgs,
1155
1156 #[arg(short, long)]
1158 pub local_thread: Option<String>,
1159
1160 #[arg(long, hide = true)]
1162 pub lazy: bool,
1163}
1164
1165#[derive(Clone, Debug, clap::Args)]
1173#[command(after_help = "\
1174Behavior:
1175 `--source git|heddle` selects the protocol; omitted, `.git` URLs are Git and other HTTPS URLs are hosted Heddle. No protocol retry on failure. Convert a local Git checkout with `heddle import local`. Full details: `heddle help clone`.
1176
1177Advanced/planned flags: see `heddle help clone`.
1178")]
1179pub struct CloneArgs {
1180 pub remote: String,
1182
1183 pub local: Option<String>,
1185
1186 #[arg(long, value_enum, hide_possible_values = true)]
1188 pub source: Option<super::CloneSourceArg>,
1189
1190 #[arg(long)]
1192 pub thread: Option<String>,
1193
1194 #[arg(long)]
1196 pub depth: Option<u32>,
1197
1198 #[arg(long, hide = true)]
1202 pub lazy: bool,
1203
1204 #[arg(long)]
1206 pub insecure: bool,
1207
1208 #[arg(long, hide = true, value_name = "SPEC", value_parser = parse_clone_filter_spec)]
1214 pub filter: Option<String>,
1215
1216 #[arg(long, visible_alias = "monorepo")]
1220 pub recursive: bool,
1221}
1222
1223impl CloneArgs {
1224 pub fn destination_dir(&self) -> Result<String, String> {
1227 if let Some(local) = self
1228 .local
1229 .as_deref()
1230 .map(str::trim)
1231 .filter(|value| !value.is_empty())
1232 {
1233 return Ok(local.to_string());
1234 }
1235 super::safe_clone_destination_basename(&self.remote).ok_or_else(|| {
1236 "clone destination is required when the source has no unambiguous basename; pass a directory".to_string()
1237 })
1238 }
1239}
1240
1241fn parse_clone_filter_spec(s: &str) -> Result<String, String> {
1242 match s {
1243 "blob:none" => Ok(s.to_string()),
1244 other => Err(format!(
1245 "unsupported --filter spec `{other}`; only `blob:none` is supported today"
1246 )),
1247 }
1248}
1249
1250#[derive(Clone, Debug, clap::Args)]
1252pub struct AgentProvenanceBeginArgs {
1253 #[arg(long)]
1255 pub provider: String,
1256
1257 #[arg(long)]
1259 pub model: String,
1260
1261 #[arg(long)]
1263 pub policy: Option<String>,
1264}
1265
1266#[derive(Clone, Debug, clap::Args)]
1268pub struct AgentProvenanceSegmentArgs {
1269 #[arg(long)]
1271 pub provider: String,
1272
1273 #[arg(long)]
1275 pub model: String,
1276
1277 #[arg(long)]
1279 pub policy: Option<String>,
1280}
1281
1282#[derive(Clone, Debug, clap::Args)]
1284pub struct AgentProvenanceEndArgs {
1285 pub session_id: Option<String>,
1287}
1288
1289#[derive(Clone, Debug, clap::Args)]
1291pub struct AgentProvenanceShowArgs {
1292 pub session_id: Option<String>,
1294}
1295
1296#[derive(Clone, Debug, clap::Args)]
1298pub struct AgentProvenanceListArgs {
1299 #[arg(long)]
1301 pub active: bool,
1302}
1303
1304#[derive(Clone, Debug, clap::Args)]
1306pub struct WorktreeAddArgs {
1307 pub path: std::path::PathBuf,
1309
1310 #[arg(long)]
1312 pub thread: Option<String>,
1313
1314 #[arg(long)]
1316 pub from: Option<String>,
1317}
1318
1319#[derive(Clone, Debug, clap::Args)]
1321pub struct WorktreeRemoveArgs {
1322 pub path: std::path::PathBuf,
1324
1325 #[arg(long)]
1327 pub delete_thread: bool,
1328}
1329
1330#[derive(Clone, Debug, clap::Args)]
1332pub struct AgentPresenceListArgs {
1333 #[arg(long)]
1335 pub active: bool,
1336}
1337
1338#[derive(Clone, Debug, clap::Args)]
1340pub struct AgentPresenceShowArgs {
1341 pub session: Option<String>,
1343}
1344
1345#[derive(Clone, Debug, clap::Args)]
1347pub struct AgentPresenceExplainArgs {
1348 pub session: Option<String>,
1350}
1351
1352#[derive(Clone, Debug, clap::Args)]
1354pub struct AgentPresenceCompleteArgs {
1355 #[arg(long)]
1357 pub session: Option<String>,
1358}
1359
1360#[derive(Clone, Debug, clap::Args)]
1362pub struct AgentReserveArgs {
1363 #[arg(long)]
1365 pub thread: String,
1366
1367 #[arg(long)]
1369 pub anchor: Option<String>,
1370
1371 #[arg(long)]
1373 pub task: Option<String>,
1374
1375 #[arg(long)]
1377 pub task_id: Option<String>,
1378
1379 #[arg(long, value_name = "PID")]
1381 pub hold_for_pid: Option<u32>,
1382}
1383
1384#[derive(Clone, Debug, clap::Args)]
1386pub struct AgentHeartbeatArgs {
1387 #[arg(long)]
1389 pub lease: String,
1390
1391 #[arg(long, env = "HEDDLE_RESERVATION_TOKEN", hide_env_values = true)]
1393 pub token: String,
1394}
1395
1396#[derive(Clone, Debug, clap::Args)]
1398pub struct AgentReleaseArgs {
1399 #[arg(long)]
1401 pub lease: String,
1402
1403 #[arg(long, env = "HEDDLE_RESERVATION_TOKEN", hide_env_values = true)]
1405 pub token: String,
1406
1407 #[arg(long, default_value = "complete")]
1409 pub status: AgentReleaseStatusArg,
1410}
1411
1412#[derive(Clone, Debug, clap::ValueEnum)]
1413pub enum AgentReleaseStatusArg {
1414 Complete,
1415 Abandoned,
1416}
1417
1418#[derive(Clone, Debug, clap::Args)]
1420pub struct AgentApiListArgs {
1421 #[arg(long)]
1423 pub thread: Option<String>,
1424
1425 #[arg(long)]
1427 pub alive_only: bool,
1428}
1429
1430#[derive(Clone, Debug, clap::ValueEnum)]
1431pub enum AgentTaskStatusArg {
1432 Open,
1433 InProgress,
1434 Blocked,
1435 Complete,
1436 Abandoned,
1437}
1438
1439#[derive(Clone, Debug, clap::Args)]
1441pub struct AgentTaskCreateArgs {
1442 #[arg(long)]
1444 pub task_id: Option<String>,
1445
1446 #[arg(long)]
1448 pub title: String,
1449
1450 #[arg(long)]
1452 pub body: Option<String>,
1453
1454 #[arg(long)]
1456 pub thread: String,
1457
1458 #[arg(long)]
1460 pub base_state: Option<String>,
1461
1462 #[arg(long)]
1464 pub base_root: Option<String>,
1465
1466 #[arg(long)]
1468 pub parent_task_id: Option<String>,
1469
1470 #[arg(long)]
1472 pub coordination_discussion_id: Option<String>,
1473
1474 #[arg(long)]
1476 pub allow_offline: bool,
1477
1478 #[arg(long)]
1480 pub delegated_by: Option<String>,
1481}
1482
1483#[derive(Clone, Debug, clap::Args)]
1485pub struct AgentTaskListArgs {
1486 #[arg(long)]
1488 pub thread: Option<String>,
1489
1490 #[arg(long)]
1492 pub status: Option<AgentTaskStatusArg>,
1493}
1494
1495#[derive(Clone, Debug, clap::Args)]
1497pub struct AgentTaskShowArgs {
1498 pub task_id: String,
1500}
1501
1502#[derive(Clone, Debug, clap::Args)]
1504pub struct AgentTaskUpdateArgs {
1505 pub task_id: String,
1507
1508 #[arg(long)]
1510 pub title: Option<String>,
1511
1512 #[arg(long)]
1514 pub body: Option<String>,
1515
1516 #[arg(long)]
1518 pub status: Option<AgentTaskStatusArg>,
1519
1520 #[arg(long)]
1522 pub thread: Option<String>,
1523
1524 #[arg(long)]
1526 pub base_state: Option<String>,
1527
1528 #[arg(long)]
1530 pub base_root: Option<String>,
1531
1532 #[arg(long)]
1534 pub parent_task_id: Option<String>,
1535
1536 #[arg(long)]
1538 pub coordination_discussion_id: Option<String>,
1539
1540 #[arg(long, conflicts_with = "no_allow_offline")]
1542 pub allow_offline: bool,
1543
1544 #[arg(long, conflicts_with = "allow_offline")]
1546 pub no_allow_offline: bool,
1547
1548 #[arg(long)]
1550 pub delegated_by: Option<String>,
1551}
1552
1553#[derive(Clone, Debug, clap::Args)]
1555pub struct AgentFanoutPlanArgs {
1556 #[arg(long)]
1558 pub title: String,
1559
1560 #[arg(long, value_name = "THREAD=TITLE")]
1562 pub lane: Vec<String>,
1563
1564 #[arg(long)]
1566 pub coordination_discussion_id: Option<String>,
1567}
1568
1569#[derive(Clone, Debug, clap::Args)]
1571pub struct AgentFanoutStartArgs {
1572 #[arg(long)]
1574 pub title: String,
1575
1576 #[arg(long, value_name = "THREAD=TITLE")]
1578 pub lane: Vec<String>,
1579
1580 #[arg(long, value_enum, value_name = "claude-code|codex|opencode")]
1582 pub harness: Vec<FanoutHarnessArg>,
1583
1584 #[arg(long)]
1586 pub run: bool,
1587
1588 #[arg(long)]
1590 pub coordination_discussion_id: Option<String>,
1591}
1592
1593#[derive(Clone, Copy, Debug, clap::ValueEnum)]
1594pub enum FanoutHarnessArg {
1595 ClaudeCode,
1596 Codex,
1597 Opencode,
1598}
1599
1600impl FanoutHarnessArg {
1601 pub fn executable(self) -> &'static str {
1602 match self {
1603 Self::ClaudeCode => "claude",
1604 Self::Codex => "codex",
1605 Self::Opencode => "opencode",
1606 }
1607 }
1608
1609 pub fn label(self) -> &'static str {
1610 match self {
1611 Self::ClaudeCode => "claude-code",
1612 Self::Codex => "codex",
1613 Self::Opencode => "opencode",
1614 }
1615 }
1616}
1617
1618#[derive(Clone, Debug, clap::Args)]
1620pub struct AgentCaptureArgs {
1621 #[arg(long)]
1623 pub lease: String,
1624
1625 #[arg(long, env = "HEDDLE_RESERVATION_TOKEN", hide_env_values = true)]
1627 pub token: String,
1628
1629 #[arg(long, short = 'm', alias = "intent")]
1631 pub message: Option<String>,
1632
1633 #[arg(long, value_parser = parse_confidence)]
1635 pub confidence: Option<f32>,
1636}
1637
1638#[derive(Clone, Debug, clap::Args)]
1640pub struct AgentReadyArgs {
1641 #[arg(long)]
1643 pub lease: String,
1644
1645 #[arg(long, env = "HEDDLE_RESERVATION_TOKEN", hide_env_values = true)]
1647 pub token: String,
1648
1649 #[arg(long, short = 'm')]
1651 pub message: Option<String>,
1652
1653 #[arg(long, value_parser = parse_confidence)]
1655 pub confidence: Option<f32>,
1656}
1657
1658#[derive(Clone, Debug, clap::Args)]
1666pub struct WatchArgs {
1667 #[arg(long, value_name = "DURATION")]
1671 pub since: Option<String>,
1672
1673 #[arg(long, value_name = "KINDS")]
1677 pub filter: Option<String>,
1678
1679 #[arg(long, hide = true)]
1682 pub max_iterations: Option<usize>,
1683
1684 #[arg(long, hide = true)]
1687 pub poll_interval_ms: Option<u64>,
1688}
1689
1690#[cfg(test)]
1696mod capture_message_alias_tests {
1697 use clap::Parser;
1698
1699 use crate::cli::{Cli, Commands, SnapshotArgs};
1700
1701 fn parse_capture(extra: &[&str]) -> Result<SnapshotArgs, clap::Error> {
1702 let mut argv: Vec<&str> = vec!["heddle", "capture"];
1703 argv.extend_from_slice(extra);
1704 let cli = Cli::try_parse_from(argv)?;
1705 match cli.command {
1706 Commands::Capture(args) => Ok(args),
1707 _ => panic!("expected Commands::Capture"),
1708 }
1709 }
1710
1711 #[test]
1712 fn capture_accepts_message_alias() {
1713 let args = parse_capture(&["--message", "my change"]).expect("--message should parse");
1714 assert_eq!(args.intent.as_deref(), Some("my change"));
1715 }
1716
1717 #[test]
1718 fn capture_accepts_intent_long_form() {
1719 let args = parse_capture(&["--intent", "my change"]).expect("--intent should parse");
1720 assert_eq!(args.intent.as_deref(), Some("my change"));
1721 }
1722
1723 #[test]
1724 fn capture_accepts_short_m() {
1725 let args = parse_capture(&["-m", "my change"]).expect("-m should parse");
1726 assert_eq!(args.intent.as_deref(), Some("my change"));
1727 }
1728
1729 #[test]
1730 fn capture_parses_without_intent_so_the_refuse_can_fire() {
1731 let args =
1732 parse_capture(&[]).expect("omitted -m is a semantic refuse, not a clap usage error");
1733 assert!(args.intent.is_none());
1734 }
1735
1736 #[test]
1737 fn capture_rejects_non_finite_or_out_of_range_confidence() {
1738 for value in ["NaN", "inf", "-0.1", "1.7"] {
1739 let confidence_arg = format!("--confidence={value}");
1740 let err = parse_capture(&["-m", "bad confidence", &confidence_arg])
1741 .expect_err("invalid confidence should fail to parse");
1742 assert!(
1743 err.to_string()
1744 .contains("confidence must be a finite number from 0.0 to 1.0"),
1745 "unexpected parse error for {value}: {err}"
1746 );
1747 }
1748 }
1749}
1750
1751#[cfg(test)]
1752mod clone_filter_tests {
1753 use clap::Parser;
1754
1755 use crate::cli::{Cli, CloneArgs, Commands};
1756
1757 fn parse_clone(extra: &[&str]) -> Result<CloneArgs, clap::Error> {
1758 let mut argv: Vec<&str> = vec!["heddle", "clone", "remote", "local"];
1759 argv.extend_from_slice(extra);
1760 let cli = Cli::try_parse_from(argv)?;
1761 match cli.command {
1762 Commands::Clone(args) => Ok(args),
1763 _ => panic!("expected Commands::Clone"),
1764 }
1765 }
1766
1767 #[test]
1768 fn parses_clone_filter_blob_none() {
1769 let args = parse_clone(&["--filter", "blob:none"]).expect("parse --filter blob:none");
1770 assert_eq!(args.filter.as_deref(), Some("blob:none"));
1771 assert!(!args.lazy);
1772 }
1773
1774 #[test]
1775 fn rejects_unknown_filter_spec() {
1776 let err = parse_clone(&["--filter", "tree:0"])
1777 .expect_err("unknown --filter spec should fail to parse");
1778 let msg = err.to_string();
1779 assert!(
1780 msg.contains("tree:0") && msg.contains("blob:none"),
1781 "error should name the bad spec and the supported one: {msg}"
1782 );
1783 }
1784}