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)]
1027pub struct ResolveArgs {
1028 pub path: Option<String>,
1030
1031 #[arg(long)]
1033 pub all: bool,
1034
1035 #[arg(long)]
1037 pub list: bool,
1038
1039 #[arg(long, conflicts_with = "theirs")]
1041 pub ours: bool,
1042
1043 #[arg(long, conflicts_with = "ours")]
1045 pub theirs: bool,
1046
1047 #[arg(long)]
1049 pub force: bool,
1050}
1051
1052#[derive(Clone, Debug, clap::Args)]
1055pub struct RemoteOperationArgs {
1056 pub remote: Option<String>,
1058
1059 #[arg(short, long)]
1061 pub thread: Option<String>,
1062
1063 #[arg(long)]
1066 pub insecure: bool,
1067}
1068
1069#[derive(Clone, Debug, clap::Args)]
1071#[command(after_help = "\
1072Git Overlay refs:
1073 A normal push writes refs/heads/<thread> and refs/notes/heddle.
1074 --all-threads writes every refs/heads/<thread> and refs/tags/<tag>, plus refs/notes/heddle.
1075 JSON output lists changed refs in refs_written; verify with git ls-remote <remote>.
1076")]
1077pub struct PushArgs {
1078 pub remote: Option<String>,
1080
1081 #[arg(short, long, conflicts_with = "thread_arg")]
1083 pub thread: Option<String>,
1084
1085 #[arg(value_name = "THREAD")]
1087 pub thread_arg: Option<String>,
1088
1089 #[arg(short, long)]
1091 pub state: Option<String>,
1092
1093 #[arg(short, long)]
1095 pub force: bool,
1096
1097 #[arg(long)]
1099 pub all_threads: bool,
1100
1101 #[arg(long)]
1104 pub insecure: bool,
1105
1106 #[command(flatten)]
1107 pub dry_run: super::DryRunArgs,
1108}
1109
1110impl PushArgs {
1111 pub fn thread_name(&self) -> Option<String> {
1112 self.thread.clone().or_else(|| self.thread_arg.clone())
1113 }
1114}
1115
1116#[derive(Clone, Debug, clap::Args)]
1118#[command(after_help = "\
1119Advanced (hidden) flags:
1120 --lazy is reserved for hosted lazy hydration and is rejected until end-to-end support lands.
1121")]
1122pub struct PullArgs {
1123 #[command(flatten)]
1124 pub remote_op: RemoteOperationArgs,
1125
1126 #[arg(short, long)]
1128 pub local_thread: Option<String>,
1129
1130 #[arg(long, hide = true)]
1132 pub lazy: bool,
1133}
1134
1135#[derive(Clone, Debug, clap::Args)]
1143#[command(after_help = "\
1144Behavior:
1145 `--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`.
1146
1147Advanced/planned flags: see `heddle help clone`.
1148")]
1149pub struct CloneArgs {
1150 pub remote: String,
1152
1153 pub local: Option<String>,
1155
1156 #[arg(long, value_enum, hide_possible_values = true)]
1158 pub source: Option<super::CloneSourceArg>,
1159
1160 #[arg(long)]
1162 pub thread: Option<String>,
1163
1164 #[arg(long)]
1166 pub depth: Option<u32>,
1167
1168 #[arg(long, hide = true)]
1172 pub lazy: bool,
1173
1174 #[arg(long)]
1176 pub insecure: bool,
1177
1178 #[arg(long, hide = true, value_name = "SPEC", value_parser = parse_clone_filter_spec)]
1184 pub filter: Option<String>,
1185
1186 #[arg(long, visible_alias = "monorepo")]
1190 pub recursive: bool,
1191}
1192
1193impl CloneArgs {
1194 pub fn destination_dir(&self) -> Result<String, String> {
1197 if let Some(local) = self
1198 .local
1199 .as_deref()
1200 .map(str::trim)
1201 .filter(|value| !value.is_empty())
1202 {
1203 return Ok(local.to_string());
1204 }
1205 super::safe_clone_destination_basename(&self.remote).ok_or_else(|| {
1206 "clone destination is required when the source has no unambiguous basename; pass a directory".to_string()
1207 })
1208 }
1209}
1210
1211fn parse_clone_filter_spec(s: &str) -> Result<String, String> {
1212 match s {
1213 "blob:none" => Ok(s.to_string()),
1214 other => Err(format!(
1215 "unsupported --filter spec `{other}`; only `blob:none` is supported today"
1216 )),
1217 }
1218}
1219
1220#[derive(Clone, Debug, clap::Args)]
1222pub struct AgentProvenanceBeginArgs {
1223 #[arg(long)]
1225 pub provider: String,
1226
1227 #[arg(long)]
1229 pub model: String,
1230
1231 #[arg(long)]
1233 pub policy: Option<String>,
1234}
1235
1236#[derive(Clone, Debug, clap::Args)]
1238pub struct AgentProvenanceSegmentArgs {
1239 #[arg(long)]
1241 pub provider: String,
1242
1243 #[arg(long)]
1245 pub model: String,
1246
1247 #[arg(long)]
1249 pub policy: Option<String>,
1250}
1251
1252#[derive(Clone, Debug, clap::Args)]
1254pub struct AgentProvenanceEndArgs {
1255 pub session_id: Option<String>,
1257}
1258
1259#[derive(Clone, Debug, clap::Args)]
1261pub struct AgentProvenanceShowArgs {
1262 pub session_id: Option<String>,
1264}
1265
1266#[derive(Clone, Debug, clap::Args)]
1268pub struct AgentProvenanceListArgs {
1269 #[arg(long)]
1271 pub active: bool,
1272}
1273
1274#[derive(Clone, Debug, clap::Args)]
1276pub struct WorktreeAddArgs {
1277 pub path: std::path::PathBuf,
1279
1280 #[arg(long)]
1282 pub thread: Option<String>,
1283
1284 #[arg(long)]
1286 pub from: Option<String>,
1287}
1288
1289#[derive(Clone, Debug, clap::Args)]
1291pub struct WorktreeRemoveArgs {
1292 pub path: std::path::PathBuf,
1294
1295 #[arg(long)]
1297 pub delete_thread: bool,
1298}
1299
1300#[derive(Clone, Debug, clap::Args)]
1302pub struct AgentPresenceListArgs {
1303 #[arg(long)]
1305 pub active: bool,
1306}
1307
1308#[derive(Clone, Debug, clap::Args)]
1310pub struct AgentPresenceShowArgs {
1311 pub session: Option<String>,
1313}
1314
1315#[derive(Clone, Debug, clap::Args)]
1317pub struct AgentPresenceExplainArgs {
1318 pub session: Option<String>,
1320}
1321
1322#[derive(Clone, Debug, clap::Args)]
1324pub struct AgentPresenceCompleteArgs {
1325 #[arg(long)]
1327 pub session: Option<String>,
1328}
1329
1330#[derive(Clone, Debug, clap::Args)]
1332pub struct AgentReserveArgs {
1333 #[arg(long)]
1335 pub thread: String,
1336
1337 #[arg(long)]
1339 pub anchor: Option<String>,
1340
1341 #[arg(long)]
1343 pub task: Option<String>,
1344
1345 #[arg(long)]
1347 pub task_id: Option<String>,
1348
1349 #[arg(long, value_name = "PID")]
1351 pub hold_for_pid: Option<u32>,
1352}
1353
1354#[derive(Clone, Debug, clap::Args)]
1356pub struct AgentHeartbeatArgs {
1357 #[arg(long)]
1359 pub lease: String,
1360
1361 #[arg(long, env = "HEDDLE_RESERVATION_TOKEN", hide_env_values = true)]
1363 pub token: String,
1364}
1365
1366#[derive(Clone, Debug, clap::Args)]
1368pub struct AgentReleaseArgs {
1369 #[arg(long)]
1371 pub lease: String,
1372
1373 #[arg(long, env = "HEDDLE_RESERVATION_TOKEN", hide_env_values = true)]
1375 pub token: String,
1376
1377 #[arg(long, default_value = "complete")]
1379 pub status: AgentReleaseStatusArg,
1380}
1381
1382#[derive(Clone, Debug, clap::ValueEnum)]
1383pub enum AgentReleaseStatusArg {
1384 Complete,
1385 Abandoned,
1386}
1387
1388#[derive(Clone, Debug, clap::Args)]
1390pub struct AgentApiListArgs {
1391 #[arg(long)]
1393 pub thread: Option<String>,
1394
1395 #[arg(long)]
1397 pub alive_only: bool,
1398}
1399
1400#[derive(Clone, Debug, clap::ValueEnum)]
1401pub enum AgentTaskStatusArg {
1402 Open,
1403 InProgress,
1404 Blocked,
1405 Complete,
1406 Abandoned,
1407}
1408
1409#[derive(Clone, Debug, clap::Args)]
1411pub struct AgentTaskCreateArgs {
1412 #[arg(long)]
1414 pub task_id: Option<String>,
1415
1416 #[arg(long)]
1418 pub title: String,
1419
1420 #[arg(long)]
1422 pub body: Option<String>,
1423
1424 #[arg(long)]
1426 pub thread: String,
1427
1428 #[arg(long)]
1430 pub base_state: Option<String>,
1431
1432 #[arg(long)]
1434 pub base_root: Option<String>,
1435
1436 #[arg(long)]
1438 pub parent_task_id: Option<String>,
1439
1440 #[arg(long)]
1442 pub coordination_discussion_id: Option<String>,
1443
1444 #[arg(long)]
1446 pub allow_offline: bool,
1447
1448 #[arg(long)]
1450 pub delegated_by: Option<String>,
1451}
1452
1453#[derive(Clone, Debug, clap::Args)]
1455pub struct AgentTaskListArgs {
1456 #[arg(long)]
1458 pub thread: Option<String>,
1459
1460 #[arg(long)]
1462 pub status: Option<AgentTaskStatusArg>,
1463}
1464
1465#[derive(Clone, Debug, clap::Args)]
1467pub struct AgentTaskShowArgs {
1468 pub task_id: String,
1470}
1471
1472#[derive(Clone, Debug, clap::Args)]
1474pub struct AgentTaskUpdateArgs {
1475 pub task_id: String,
1477
1478 #[arg(long)]
1480 pub title: Option<String>,
1481
1482 #[arg(long)]
1484 pub body: Option<String>,
1485
1486 #[arg(long)]
1488 pub status: Option<AgentTaskStatusArg>,
1489
1490 #[arg(long)]
1492 pub thread: Option<String>,
1493
1494 #[arg(long)]
1496 pub base_state: Option<String>,
1497
1498 #[arg(long)]
1500 pub base_root: Option<String>,
1501
1502 #[arg(long)]
1504 pub parent_task_id: Option<String>,
1505
1506 #[arg(long)]
1508 pub coordination_discussion_id: Option<String>,
1509
1510 #[arg(long, conflicts_with = "no_allow_offline")]
1512 pub allow_offline: bool,
1513
1514 #[arg(long, conflicts_with = "allow_offline")]
1516 pub no_allow_offline: bool,
1517
1518 #[arg(long)]
1520 pub delegated_by: Option<String>,
1521}
1522
1523#[derive(Clone, Debug, clap::Args)]
1525pub struct AgentFanoutPlanArgs {
1526 #[arg(long)]
1528 pub title: String,
1529
1530 #[arg(long, value_name = "THREAD=TITLE")]
1532 pub lane: Vec<String>,
1533
1534 #[arg(long)]
1536 pub coordination_discussion_id: Option<String>,
1537}
1538
1539#[derive(Clone, Debug, clap::Args)]
1541pub struct AgentFanoutStartArgs {
1542 #[arg(long)]
1544 pub title: String,
1545
1546 #[arg(long, value_name = "THREAD=TITLE")]
1548 pub lane: Vec<String>,
1549
1550 #[arg(long)]
1552 pub coordination_discussion_id: Option<String>,
1553}
1554
1555#[derive(Clone, Debug, clap::Args)]
1557pub struct AgentCaptureArgs {
1558 #[arg(long)]
1560 pub lease: String,
1561
1562 #[arg(long, env = "HEDDLE_RESERVATION_TOKEN", hide_env_values = true)]
1564 pub token: String,
1565
1566 #[arg(long, short = 'm', alias = "intent")]
1568 pub message: Option<String>,
1569
1570 #[arg(long, value_parser = parse_confidence)]
1572 pub confidence: Option<f32>,
1573}
1574
1575#[derive(Clone, Debug, clap::Args)]
1577pub struct AgentReadyArgs {
1578 #[arg(long)]
1580 pub lease: String,
1581
1582 #[arg(long, env = "HEDDLE_RESERVATION_TOKEN", hide_env_values = true)]
1584 pub token: String,
1585
1586 #[arg(long, short = 'm')]
1588 pub message: Option<String>,
1589
1590 #[arg(long, value_parser = parse_confidence)]
1592 pub confidence: Option<f32>,
1593}
1594
1595#[derive(Clone, Debug, clap::Args)]
1603pub struct WatchArgs {
1604 #[arg(long, value_name = "DURATION")]
1608 pub since: Option<String>,
1609
1610 #[arg(long, value_name = "KINDS")]
1614 pub filter: Option<String>,
1615
1616 #[arg(long, hide = true)]
1619 pub max_iterations: Option<usize>,
1620
1621 #[arg(long, hide = true)]
1624 pub poll_interval_ms: Option<u64>,
1625}
1626
1627#[cfg(test)]
1633mod capture_message_alias_tests {
1634 use clap::Parser;
1635
1636 use crate::cli::{Cli, Commands, SnapshotArgs};
1637
1638 fn parse_capture(extra: &[&str]) -> Result<SnapshotArgs, clap::Error> {
1639 let mut argv: Vec<&str> = vec!["heddle", "capture"];
1640 argv.extend_from_slice(extra);
1641 let cli = Cli::try_parse_from(argv)?;
1642 match cli.command {
1643 Commands::Capture(args) => Ok(args),
1644 _ => panic!("expected Commands::Capture"),
1645 }
1646 }
1647
1648 #[test]
1649 fn capture_accepts_message_alias() {
1650 let args = parse_capture(&["--message", "my change"]).expect("--message should parse");
1651 assert_eq!(args.intent.as_deref(), Some("my change"));
1652 }
1653
1654 #[test]
1655 fn capture_accepts_intent_long_form() {
1656 let args = parse_capture(&["--intent", "my change"]).expect("--intent should parse");
1657 assert_eq!(args.intent.as_deref(), Some("my change"));
1658 }
1659
1660 #[test]
1661 fn capture_accepts_short_m() {
1662 let args = parse_capture(&["-m", "my change"]).expect("-m should parse");
1663 assert_eq!(args.intent.as_deref(), Some("my change"));
1664 }
1665
1666 #[test]
1667 fn capture_parses_without_intent_so_the_refuse_can_fire() {
1668 let args =
1669 parse_capture(&[]).expect("omitted -m is a semantic refuse, not a clap usage error");
1670 assert!(args.intent.is_none());
1671 }
1672
1673 #[test]
1674 fn capture_rejects_non_finite_or_out_of_range_confidence() {
1675 for value in ["NaN", "inf", "-0.1", "1.7"] {
1676 let confidence_arg = format!("--confidence={value}");
1677 let err = parse_capture(&["-m", "bad confidence", &confidence_arg])
1678 .expect_err("invalid confidence should fail to parse");
1679 assert!(
1680 err.to_string()
1681 .contains("confidence must be a finite number from 0.0 to 1.0"),
1682 "unexpected parse error for {value}: {err}"
1683 );
1684 }
1685 }
1686}
1687
1688#[cfg(test)]
1689mod clone_filter_tests {
1690 use clap::Parser;
1691
1692 use crate::cli::{Cli, CloneArgs, Commands};
1693
1694 fn parse_clone(extra: &[&str]) -> Result<CloneArgs, clap::Error> {
1695 let mut argv: Vec<&str> = vec!["heddle", "clone", "remote", "local"];
1696 argv.extend_from_slice(extra);
1697 let cli = Cli::try_parse_from(argv)?;
1698 match cli.command {
1699 Commands::Clone(args) => Ok(args),
1700 _ => panic!("expected Commands::Clone"),
1701 }
1702 }
1703
1704 #[test]
1705 fn parses_clone_filter_blob_none() {
1706 let args = parse_clone(&["--filter", "blob:none"]).expect("parse --filter blob:none");
1707 assert_eq!(args.filter.as_deref(), Some("blob:none"));
1708 assert!(!args.lazy);
1709 }
1710
1711 #[test]
1712 fn rejects_unknown_filter_spec() {
1713 let err = parse_clone(&["--filter", "tree:0"])
1714 .expect_err("unknown --filter spec should fail to parse");
1715 let msg = err.to_string();
1716 assert!(
1717 msg.contains("tree:0") && msg.contains("blob:none"),
1718 "error should name the bad spec and the supported one: {msg}"
1719 );
1720 }
1721}