Skip to main content

heddle_cli_args/cli/cli_args/
commands_args.rs

1// SPDX-License-Identifier: Apache-2.0
2//! Named argument structs for top-level CLI commands.
3
4#[cfg(feature = "git-overlay")]
5use super::commands_git_projection::SyncCommands;
6
7/// Verb key for `heddle init`.
8///
9/// Shared lookup string for clap, the command catalog, and the schema
10/// registry. A second `"init"` literal can still compile; pairing is
11/// checked in tests, not by the type system.
12pub const INIT_VERB: &str = "init";
13
14/// Arguments for the `init` command.
15#[derive(Clone, Debug, clap::Args)]
16#[command(after_help = "\
17Examples:
18  heddle init                                                    # initialize here; existing Git becomes Git Overlay
19  heddle init my-project                                         # initialize a native Heddle subdirectory
20  heddle init --principal-name 'Ada Lovelace' --principal-email ada@example.com
21")]
22pub struct InitArgs {
23    /// Directory to initialize (default: current directory).
24    pub path: Option<std::path::PathBuf>,
25
26    /// Principal name for attribution.
27    #[arg(long)]
28    pub principal_name: Option<String>,
29
30    /// Principal email for attribution.
31    #[arg(long)]
32    pub principal_email: Option<String>,
33
34    /// Install harness integrations after init.
35    #[arg(long)]
36    pub install_harnesses: Option<String>,
37
38    /// Skip harness integration installation during init.
39    #[arg(long)]
40    pub no_harness_install: bool,
41
42    /// Preferred install scope (`repo` or `user`).
43    #[arg(long, visible_alias = "scope", default_value = "repo")]
44    pub harness_install_scope: String,
45
46    /// Overwrite Heddle-managed integration entries when needed.
47    #[arg(long)]
48    pub harness_install_force: bool,
49}
50
51impl InitArgs {
52    /// Same identifier as [`INIT_VERB`].
53    pub const VERB: &'static str = INIT_VERB;
54}
55
56/// Arguments for the `adopt` command.
57#[derive(Clone, Debug, clap::Args)]
58#[command(after_help = "\
59Examples:
60  heddle adopt                                # adopt all local Git refs into native Heddle storage
61  heddle adopt --ref main                     # adopt one branch or tag
62  heddle adopt ../repo --ref main --ref v1.0  # adopt selected refs in another repo
63
64Adoption imports Git refs, makes Heddle the source authority, and retains `.git` for explicit Git Projection. Normal Git Overlay setup uses `heddle init` instead.
65")]
66pub struct AdoptArgs {
67    /// Git repository to adopt into native Heddle storage (default: current directory).
68    pub path: Option<std::path::PathBuf>,
69
70    /// Git branch or tag to adopt. Repeat for selected refs; omit to adopt all refs.
71    #[arg(long = "ref", value_name = "REF")]
72    pub refs: Vec<String>,
73}
74
75/// Arguments for the `doctor` command (and its subcommands).
76///
77/// `heddle doctor` with no subcommand reports repository, thread, actor,
78/// and workspace health. `heddle doctor docs`
79/// runs the documentation truthfulness checker — see [`DoctorDocsArgs`]
80/// for that surface.
81#[derive(Clone, Debug, clap::Args)]
82pub struct DoctorArgs {
83    /// Include local timing for the diagnosis read path.
84    ///
85    /// Only honoured when no subcommand is given. Subcommands like
86    /// `heddle doctor docs` ignore it.
87    #[arg(long, global = false)]
88    pub profile: bool,
89
90    #[command(subcommand)]
91    pub command: Option<DoctorCommands>,
92}
93
94/// `heddle doctor <subcommand>` surface.
95#[derive(Clone, Debug, clap::Subcommand)]
96pub enum DoctorCommands {
97    /// Diff-check markdown documentation against the actual CLI surface.
98    ///
99    /// Walks every `heddle <verb> [<subverb>] [flags]` invocation in
100    /// the requested markdown files and reports any drift: missing
101    /// verbs, unknown long flags, or invalid literal values for flags
102    /// like `--workspace`, `--scope`, and `--kind`. It also enforces the
103    /// accepted closed root surface and centralized `continue`/`abort`
104    /// lifecycle.
105    ///
106    /// Exits non-zero when any drift is found, so it's safe to run in
107    /// CI. Pair with `--output json` for structured output. Run on every PR
108    /// to prevent the docs from drifting from the CLI again.
109    Docs(DoctorDocsArgs),
110
111    /// Drift-check `docs/json-schemas.md` against the registered
112    /// schemas.
113    ///
114    /// Generates the canonical schema for every verb in the schemas
115    /// registry, parses every `## heddle <verb> --output json` sample in
116    /// `docs/json-schemas.md`, and verifies that every key in the
117    /// sample is declared in the schema. Exits non-zero on drift.
118    /// Pair with `--output json` for CI. Run alongside `heddle doctor docs`
119    /// on every PR.
120    Schemas(DoctorSchemasArgs),
121}
122
123/// Arguments for `heddle doctor docs`.
124#[derive(Clone, Debug, clap::Args)]
125pub struct DoctorDocsArgs {
126    /// Markdown file(s) to scan. Repeatable.
127    ///
128    /// When neither `--path` nor `--all` is given, defaults to
129    /// `--all`.
130    #[arg(long, value_name = "PATH")]
131    pub path: Vec<std::path::PathBuf>,
132
133    /// Scan every tracked `.md` file in the repository.
134    #[arg(long)]
135    pub all: bool,
136}
137
138/// Arguments for `heddle doctor schemas`.
139#[derive(Clone, Debug, clap::Args)]
140pub struct DoctorSchemasArgs {
141    /// Refresh the generated command-contract coverage sample in
142    /// `docs/json-schemas.md`, then run the normal schema drift check.
143    #[arg(long)]
144    pub update_docs: bool,
145}
146
147fn parse_confidence(s: &str) -> Result<f32, String> {
148    let value = s
149        .parse::<f32>()
150        .map_err(|_| format!("confidence must be a finite number from 0.0 to 1.0, got `{s}`"))?;
151    if !value.is_finite() || !(0.0..=1.0).contains(&value) {
152        return Err(format!(
153            "confidence must be a finite number from 0.0 to 1.0, got `{s}`"
154        ));
155    }
156    Ok(value)
157}
158
159/// Arguments for the `capture` command.
160#[derive(Clone, Debug, clap::Args)]
161#[command(override_usage = "heddle capture -m <INTENT> [OPTIONS]")]
162#[command(after_help = "\
163Examples:
164  heddle capture -m 'add login route'           # capture the worktree with intent
165  heddle capture -m 'wip' --confidence 0.6      # honest confidence on a draft step
166
167Agent automation flags (provider/model/session/policy/split) are hidden here.
168Run `heddle help agent-flags`, or `heddle capture --help-agent` to list them inline.
169")]
170pub struct SnapshotArgs {
171    /// Reveal the hidden agent-automation flags inline instead of capturing.
172    /// A first-class clap flag so the whole command line (including global
173    /// options in any spelling clap accepts) is parsed by clap; the dispatch
174    /// arm inspects the parsed result rather than scanning raw tokens.
175    /// `hide`d to keep everyday `capture --help` terse (the after-help
176    /// pointer is the discovery route). It is still a registered clap arg,
177    /// so `doctor docs` recognizes `heddle capture --help-agent` via the
178    /// registered-but-hidden flag seam — the machine contract stays in sync
179    /// without cluttering human help.
180    #[arg(long, hide = true)]
181    pub help_agent: bool,
182
183    /// Required natural-language intent for this recoverable step.
184    #[arg(short = 'm', long, visible_alias = "message", value_name = "INTENT")]
185    pub intent: Option<String>,
186
187    /// Confidence level (0.0-1.0).
188    #[arg(long, value_parser = parse_confidence)]
189    pub confidence: Option<f32>,
190
191    /// Allow a large or deletion-heavy capture without the safety preflight.
192    #[arg(short, long)]
193    pub force: bool,
194
195    /// Override HEDDLE_AGENT_PROVIDER.
196    #[arg(long, hide = true)]
197    pub agent_provider: Option<String>,
198
199    /// Override HEDDLE_AGENT_MODEL.
200    #[arg(long, hide = true)]
201    pub agent_model: Option<String>,
202
203    /// Override active agent session id.
204    #[arg(long, hide = true)]
205    pub agent_session: Option<String>,
206
207    /// Override active agent session segment.
208    #[arg(long, hide = true)]
209    pub agent_segment: Option<String>,
210
211    /// Override HEDDLE_AGENT_POLICY.
212    #[arg(long, hide = true)]
213    pub policy: Option<String>,
214
215    /// Omit policy attribution.
216    #[arg(long, hide = true)]
217    pub no_policy: bool,
218
219    /// Omit agent attribution.
220    #[arg(long, hide = true)]
221    pub no_agent: bool,
222
223    /// Split selected paths into another thread instead of capturing the whole worktree.
224    #[arg(long, hide = true)]
225    pub split: bool,
226
227    /// Target thread when using `--split`.
228    #[arg(long, hide = true, requires = "split")]
229    pub into: Option<String>,
230
231    /// Repository-relative path prefix to include when using `--split`.
232    #[arg(long = "path", hide = true, requires = "split", value_name = "PATH")]
233    pub paths: Vec<String>,
234}
235
236/// Arguments for the Git-overlay `commit` command.
237#[derive(Clone, Debug, clap::Args)]
238#[command(after_help = "\
239Examples:
240  heddle capture -m 'add login route'
241  heddle commit
242  heddle commit -m 'add login route'
243
244Behavior:
245  Commits the complete captured tree and replaces the Git index with that tree.
246  Git pre-commit and commit-msg hooks are not run.
247")]
248pub struct CommitArgs {
249    /// Git commit message. Defaults to the current capture intent.
250    #[arg(short = 'm', long = "message")]
251    pub message: Option<String>,
252}
253
254/// Arguments for the `log` command.
255#[derive(Clone, Debug, clap::Args)]
256#[command(after_help = "\
257Examples:
258  heddle log                          # walk the current thread
259  heddle log --oneline -n 20          # 20 most recent states in compact form
260  heddle log --timeline               # show agent timeline tool-call cursor
261  heddle log --reflog                 # include re-attributed history
262  heddle log --path src/auth.rs       # restrict to states touching a path
263")]
264pub struct LogArgs {
265    /// Starting state (default: HEAD).
266    pub state: Option<String>,
267
268    /// Maximum states to show.
269    #[arg(short = 'n', long, default_value = "20")]
270    pub limit: usize,
271
272    /// Show all states, not just ancestors.
273    #[arg(long)]
274    pub all: bool,
275
276    /// Show ASCII DAG graph.
277    #[arg(long)]
278    pub graph: bool,
279
280    /// One state per line.
281    #[arg(long)]
282    pub oneline: bool,
283
284    /// Show Git-overlay reflog entries instead of Heddle capture history.
285    #[arg(long)]
286    pub reflog: bool,
287
288    /// Show agent timeline tool-call navigation instead of capture history.
289    #[arg(long)]
290    pub timeline: bool,
291
292    /// Timeline thread to render with `--timeline`.
293    #[arg(long, default_value = "main")]
294    pub thread: String,
295
296    /// Filter by agent model.
297    #[arg(long)]
298    pub agent: Option<String>,
299
300    /// Show only states that changed the given repository-relative path.
301    #[arg(long = "path", value_name = "PATH")]
302    pub paths: Vec<String>,
303
304    /// Lower bound: walk back until reaching this state or marker
305    /// (exclusive of the bound itself). Accepts a marker name, a
306    /// state ID (short or full), or any spec the state resolver
307    /// understands. When combined with `--limit`, the bound is
308    /// applied first, then the result is trimmed to `--limit`.
309    #[arg(long, value_name = "STATE")]
310    pub since: Option<String>,
311}
312
313/// Arguments for `heddle timeline`.
314#[derive(Clone, Debug, clap::Args)]
315pub struct TimelineArgs {
316    #[command(subcommand)]
317    pub command: TimelineCommands,
318}
319
320/// Timeline navigation action commands.
321#[derive(Clone, Debug, clap::Subcommand)]
322pub enum TimelineCommands {
323    /// Show the current timeline cursor, counts, and recovery status.
324    Status(TimelineStatusArgs),
325
326    /// Record the start of a native tool timeline step.
327    #[command(name = "record-start")]
328    RecordStart(TimelineRecordStartArgs),
329
330    /// Record the finish of a native tool timeline step.
331    #[command(name = "record-finish")]
332    RecordFinish(TimelineRecordFinishArgs),
333
334    /// Fork a timeline branch from a step or native harness tool call.
335    #[command(after_help = "\
336Examples:
337  heddle timeline fork --step tls-abc --branch tlb-experiment
338  heddle timeline fork --tool-call call_123 --session ses_456 --branch tlb-alt
339")]
340    Fork(TimelineForkArgs),
341
342    /// Reset the logical timeline cursor, optionally materializing checkout files.
343    #[command(after_help = "\
344Examples:
345  heddle timeline reset --step tls-abc
346  heddle timeline reset --tool-call call_123 --materialize
347")]
348    Reset(TimelineResetArgs),
349
350    /// Recover a pending timeline materialization after an interrupted reset/seek.
351    Recover(TimelineRecoverArgs),
352}
353
354/// Shared selector arguments for timeline action commands.
355#[derive(Clone, Debug, clap::Args)]
356pub struct TimelineTargetArgs {
357    /// Timeline thread to target.
358    #[arg(long, default_value = "main")]
359    pub thread: String,
360
361    /// Constrain the target to this branch when selecting by step/current cursor.
362    #[arg(long = "from-branch", value_name = "BRANCH")]
363    pub from_branch: Option<String>,
364
365    /// Target a timeline step id.
366    #[arg(long, conflicts_with_all = ["tool_call", "undo", "redo", "current"])]
367    pub step: Option<String>,
368
369    /// Target a native harness tool call id, such as an OpenCode tool call id.
370    #[arg(long = "tool-call", conflicts_with_all = ["step", "undo", "redo", "current"])]
371    pub tool_call: Option<String>,
372
373    /// Native harness name for `--tool-call`.
374    #[arg(long, default_value = "opencode")]
375    pub harness: String,
376
377    /// Native harness session id for `--tool-call`.
378    #[arg(long)]
379    pub session: Option<String>,
380
381    /// Native harness message id for `--tool-call`.
382    #[arg(long)]
383    pub message: Option<String>,
384
385    /// Target the previous step from the current cursor.
386    #[arg(long, conflicts_with_all = ["step", "tool_call", "redo", "current"])]
387    pub undo: bool,
388
389    /// Target the next step from the current cursor.
390    #[arg(long, conflicts_with_all = ["step", "tool_call", "undo", "current"])]
391    pub redo: bool,
392
393    /// Target the current logical cursor.
394    #[arg(long, conflicts_with_all = ["step", "tool_call", "undo", "redo"])]
395    pub current: bool,
396}
397
398/// Arguments for `heddle timeline fork`.
399#[derive(Clone, Debug, clap::Args)]
400pub struct TimelineForkArgs {
401    #[command(flatten)]
402    pub target: TimelineTargetArgs,
403
404    /// New timeline branch id. Generated when omitted.
405    #[arg(long, value_name = "BRANCH")]
406    pub branch: Option<String>,
407
408    /// Branch reason: explicit-fork, edit-from-rewound-cursor, retry, fan-out.
409    #[arg(long, default_value = "explicit-fork")]
410    pub reason: String,
411}
412
413/// Arguments for `heddle timeline reset`.
414#[derive(Clone, Debug, clap::Args)]
415pub struct TimelineResetArgs {
416    #[command(flatten)]
417    pub target: TimelineTargetArgs,
418
419    /// Materialize checkout files to the target state after moving the cursor.
420    #[arg(long)]
421    pub materialize: bool,
422
423    /// Materialization mode: fail-if-dirty or capture-current-then-seek.
424    #[arg(long, default_value = "fail-if-dirty")]
425    pub mode: String,
426}
427
428/// Arguments for `heddle timeline recover`.
429#[derive(Clone, Debug, clap::Args)]
430pub struct TimelineRecoverArgs {
431    /// Timeline thread to recover.
432    #[arg(long, default_value = "main")]
433    pub thread: String,
434}
435
436/// Arguments for `heddle timeline status`.
437#[derive(Clone, Debug, clap::Args)]
438pub struct TimelineStatusArgs {
439    /// Timeline thread to inspect.
440    #[arg(long, default_value = "main")]
441    pub thread: String,
442}
443
444/// Shared scrubbed native tool-call identity for timeline recording commands.
445#[derive(Clone, Debug, clap::Args)]
446pub struct TimelineRecordToolArgs {
447    /// Timeline thread to record into.
448    #[arg(long, default_value = "main")]
449    pub thread: String,
450
451    /// Native harness name.
452    #[arg(long, default_value = "opencode")]
453    pub harness: String,
454
455    /// Native harness session id.
456    #[arg(long)]
457    pub session: Option<String>,
458
459    /// Native harness message id.
460    #[arg(long)]
461    pub message: Option<String>,
462
463    /// Native harness tool-call id.
464    #[arg(long = "tool-call")]
465    pub tool_call: String,
466
467    /// Explicit timeline step id. When omitted, Heddle derives one from the native identity.
468    #[arg(long = "step-id")]
469    pub step_id: Option<String>,
470
471    /// Explicit timeline branch id. Defaults to the current timeline branch or `tlb-main`.
472    #[arg(long = "branch")]
473    pub branch: Option<String>,
474
475    /// Scrubbed human summary for the native payload.
476    #[arg(long = "summary")]
477    pub summary: Option<String>,
478
479    /// Hash of the native payload, never the raw payload bytes.
480    #[arg(long = "payload-hash")]
481    pub payload_hash: Option<String>,
482}
483
484/// Arguments for `heddle timeline record-start`.
485#[derive(Clone, Debug, clap::Args)]
486pub struct TimelineRecordStartArgs {
487    #[command(flatten)]
488    pub tool: TimelineRecordToolArgs,
489
490    /// Stable tool name such as `bash`, `edit`, or `read`.
491    #[arg(long = "tool-name", default_value = "tool")]
492    pub tool_name: String,
493}
494
495/// Arguments for `heddle timeline record-finish`.
496#[derive(Clone, Debug, clap::Args)]
497pub struct TimelineRecordFinishArgs {
498    #[command(flatten)]
499    pub tool: TimelineRecordToolArgs,
500
501    /// Tool result status: succeeded, failed, or cancelled.
502    #[arg(long, default_value = "succeeded")]
503    pub status: String,
504}
505
506/// Arguments for the `retro` command.
507///
508/// `heddle retro --since <marker-or-state>` summarizes a working
509/// session by combining oplog, agent registry, marker, and context
510/// annotation reads into one structured payload. Replaces the
511/// reconstruct-from-`heddle log` boilerplate agents wrote before.
512#[derive(Clone, Debug, clap::Args)]
513pub struct RetroArgs {
514    /// Lower bound: marker name or state id (short or full). When
515    /// omitted, the verb walks back to the most recent `Claude Code
516    /// turn`-shaped intent or to one hour ago, whichever is more
517    /// recent.
518    #[arg(long)]
519    pub since: Option<String>,
520
521    /// Include merge entries in the output payload (off by default
522    /// because merges are noisy in agent retros).
523    #[arg(long)]
524    pub include_merges: bool,
525
526    /// Include undo entries in the output payload (off by default
527    /// because undos are noisy in agent retros).
528    #[arg(long)]
529    pub include_undos: bool,
530
531    /// Render full annotation/intent content rather than excerpts.
532    /// Aliased as `--full` because the global `-v/--verbose` flag is
533    /// already wired as a u8 verbosity counter on `Cli`.
534    #[arg(long = "full", alias = "expand")]
535    pub full: bool,
536}
537
538/// Arguments for the `diff` command.
539#[derive(Clone, Debug, clap::Args)]
540#[command(after_help = "\
541Examples:
542  heddle diff                     # worktree vs HEAD
543  heddle diff NOTES.md            # only that path
544  heddle diff -- NOTES.md         # same, after the path separator
545  heddle diff --path NOTES.md     # same, explicit path filter
546  heddle diff HEAD~1 HEAD -- src  # two states, filtered to src/
547
548Path-shaped positionals and arguments after `--` are worktree/path filters,
549not missing states. `log --path` uses the same filter spelling.
550
551Restore:
552  Heddle does not restore one file from a saved state (no restore/checkout/reset).
553  Materialize one state in a new checkout: heddle start <name> --from <state> --path <dir>
554  Apply the inverse of one state to this worktree: heddle revert <state> [--no-commit]
555  Restore the tree preserved by the last undo: heddle undo --recover
556
557Patch compatibility:
558  --patch output uses Git-compatible unified diff, including extended headers for type and mode changes.
559")]
560pub struct DiffArgs {
561    /// Base state (default: HEAD). A path-shaped value is a worktree filter.
562    pub from: Option<String>,
563
564    /// Target state (default: worktree). A path-shaped value is a worktree filter.
565    pub to: Option<String>,
566
567    /// Restrict the diff to these repository-relative paths.
568    #[arg(long = "path", value_name = "PATH")]
569    pub path_filters: Vec<String>,
570
571    /// Paths after `--`. Always treated as worktree/path filters.
572    #[arg(last = true, value_name = "PATH")]
573    pub paths: Vec<String>,
574
575    /// Show semantic changes.
576    #[arg(long)]
577    pub semantic: bool,
578
579    /// Show diffstat summary only.
580    #[arg(long)]
581    pub stat: bool,
582
583    /// Show only changed file names.
584    #[arg(long)]
585    pub name_only: bool,
586
587    /// Number of surrounding context lines to include in each hunk.
588    #[arg(short = 'U', long = "unified", default_value_t = 3)]
589    pub unified: usize,
590
591    /// Show concise applicable context alongside diff output.
592    #[arg(long)]
593    pub context: bool,
594
595    /// Output a Git-compatible unified diff.
596    #[arg(short = 'p', long = "patch")]
597    pub patch: bool,
598}
599
600/// Arguments for the `revert` command.
601#[derive(Clone, Debug, clap::Args)]
602#[command(after_help = "\
603Restore:
604  `revert` applies the inverse of one state's changes. It is not a single-file
605  restore, and Heddle has no restore/checkout/reset verb.
606  Materialize one state in a new checkout: heddle start <name> --from <state> --path <dir>
607  Restore the tree preserved by the last undo: heddle undo --recover
608  Heddle cannot put one file back from a saved state.
609")]
610pub struct RevertArgs {
611    /// State to revert.
612    pub state: String,
613
614    /// Commit message for the revert.
615    #[arg(short = 'm', long)]
616    pub message: Option<String>,
617
618    /// Apply the inverse to the worktree without capturing a new state.
619    #[arg(long)]
620    pub no_commit: bool,
621}
622
623/// Arguments for the `undo` command.
624#[derive(Clone, Debug, clap::Args)]
625#[command(after_help = "\
626Examples:
627  heddle undo --preview      # inspect the most recent operation
628  heddle undo --hard --preview  # preview the worktree rewind --hard would apply
629  heddle undo --hard         # roll it back and rewind the worktree
630  heddle undo -n 3 --hard    # roll back the last three operations
631  heddle undo --recover      # restore the state preserved by the last undo
632  heddle undo --list         # preview undoable operations on this thread
633  heddle undo --dry-run      # show what would change without applying
634
635Restore:
636  `--recover` restores only the last undo's preserved tree as worktree changes.
637  Heddle does not restore one arbitrary file or an arbitrary saved state
638  (no restore/checkout/reset). Materialize a state with
639  `heddle start <name> --from <state> --path <dir>`, or invert one with
640  `heddle revert <state>`.
641
642Undoable operations:
643  - heddle capture           (restores HEAD to the pre-capture parent)
644  - heddle land (non-FF)     (restores HEAD + both thread refs)
645  - heddle land (FF)         (restores HEAD + the landed-into thread ref to
646                              the pre-merge tip; the merged-in thread is
647                              untouched.)
648  - heddle thread switch     (restores HEAD to the previous thread state)
649  - heddle thread create/drop/rename
650  - heddle thread marker create/drop
651  - heddle redact apply               (with --allow-redact-undo; removes the
652                                       redaction record so future materializes
653                                       restore the original blob bytes. Refused
654                                       when a Purge has destroyed the bytes.)
655  - heddle undo --redo                re-apply the most recently undone operation
656
657Not undoable (file a follow-up if you need one):
658  - heddle push / pull                (remote-affecting; out of scope)
659  - heddle redact purge apply         (destructive by design; irreversible)
660  - heddle start <name> --path <dir>  (refused while the materialized worktree
661                                       still exists — run `heddle thread drop
662                                       <name> --delete-thread` first, then
663                                       re-run `heddle undo`)
664  - cross-worktree shared-backend undo (no worktree registry yet; single-
665                                        worktree usage is the supported
666                                        configuration for 0.3)
667")]
668pub struct UndoArgs {
669    /// Undo N operations.
670    #[arg(short = 'n', long, default_value = "1")]
671    pub steps: usize,
672
673    /// List recent operations without undoing.
674    #[arg(long)]
675    pub list: bool,
676
677    /// Number of batches to list.
678    #[arg(long, default_value = "20")]
679    pub depth: usize,
680
681    /// Preview operations without undoing. `--dry-run` is an accepted
682    /// alias kept for muscle memory from git/other VCS tooling.
683    #[arg(long, visible_alias = "dry-run")]
684    pub preview: bool,
685
686    /// Permit undo to rewind worktree files to the selected operation's prior
687    /// state. Without this explicit opt-in, an undo that would rewrite the
688    /// worktree refuses before changing repository state or files.
689    #[arg(long, conflicts_with_all = ["list", "redo", "recover"])]
690    pub hard: bool,
691
692    /// Re-apply operations that a prior `undo` rewound.
693    #[arg(long, conflicts_with = "list")]
694    pub redo: bool,
695
696    /// Restore the checkout-local state preserved by the most recent undo as
697    /// worktree changes. HEAD and the current thread remain unchanged.
698    #[arg(
699        long,
700        conflicts_with_all = ["steps", "list", "preview", "hard", "redo", "allow_redact_undo"]
701    )]
702    pub recover: bool,
703
704    /// Explicit opt-in for undoing a `heddle redact apply`. The inverse
705    /// removes the redaction record so subsequent materializes restore
706    /// the original blob bytes — i.e. previously-hidden content
707    /// becomes readable again. Without this flag, a `heddle undo`
708    /// chain that crosses a Redact refuses loudly rather than silently
709    /// re-exposing the content. Refused regardless of the flag when
710    /// a Purge has destroyed the bytes: Purge is irreversible.
711    #[arg(long)]
712    pub allow_redact_undo: bool,
713}
714
715/// User-facing `--workspace` flag values. Vocabulary is the same as
716/// [`repo::ThreadMode`] (and the on-wire
717/// `thread.mode` JSON field) so a single name carries through the
718/// CLI, the daemon, and the thread record on disk. See
719/// `docs/design/clonefile-threads.md` for the rationale.
720#[derive(Clone, Copy, Debug, clap::ValueEnum, PartialEq, Eq)]
721pub enum WorkspaceModeArg {
722    /// Let Heddle choose the right checkout mode.
723    Auto,
724    /// Create a disk checkout with shared extents when the filesystem supports it.
725    Materialized,
726    /// Use a virtual filesystem checkout when the mount feature is available.
727    Virtualized,
728    /// Copy full files into an isolated checkout.
729    Solid,
730}
731
732/// Arguments for the `thread start` and top-level `start` commands.
733#[derive(Clone, Debug, clap::Args)]
734#[command(after_help = "\
735Examples:
736  heddle start feature/auth --path ../feature-auth  # create an isolated checkout
737  heddle start scratch --path ../scratch            # place the checkout explicitly
738  heddle start fix-flake --path ../fix-flake --task 'fix CI flake'
739
740`--path` is required when workspace is omitted or `auto`. Without it, start
741refuses instead of hiding a checkout under `.heddle/threads/<name>/`.
742`--workspace auto` is the same default and still requires `--path`.
743Pass `--path ../<name>`, or an explicit `--workspace solid|materialized|virtualized`
744if you want the managed layout. To stay on this checkout, use
745`heddle thread create <name>` then `heddle thread switch <name>`.
746
747Isolated 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.
748
749`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 promote <name> --path <dir>` materializes it later. Use the split form only when you intentionally need ref-first, checkout-later staging.
750
751Advanced (hidden) flags:
752  --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.
753")]
754pub struct ThreadStartArgs {
755    /// Thread name to create or resume.
756    pub name: String,
757
758    /// Base state for the thread (default: HEAD).
759    #[arg(long)]
760    pub from: Option<String>,
761
762    /// Filesystem path for the isolated checkout. Required so the checkout
763    /// is not hidden under `.heddle/threads/`.
764    #[arg(long)]
765    pub path: Option<std::path::PathBuf>,
766
767    /// Workspace mode for the thread. Omitted or `auto` requires `--path`
768    /// so the checkout is not hidden under `.heddle/threads/`.
769    #[arg(long, value_enum)]
770    pub workspace: Option<WorkspaceModeArg>,
771
772    /// AI provider name for the registered agent thread.
773    #[arg(long, hide = true)]
774    pub agent_provider: Option<String>,
775
776    /// AI model name for the registered agent thread.
777    #[arg(long, hide = true)]
778    pub agent_model: Option<String>,
779
780    /// First-class task/goal metadata for the thread.
781    #[arg(long)]
782    pub task: Option<String>,
783
784    /// Parent thread identifier for delegated child work.
785    #[arg(long, hide = true)]
786    pub parent_thread: Option<String>,
787
788    /// Internal hint that this thread was started by automation rather than a direct CLI flow.
789    #[arg(long, hide = true)]
790    pub automated: bool,
791
792    /// Print only the new thread's absolute checkout path to stdout and exit.
793    ///
794    /// Designed for shell wrappers that want to cd into the new checkout:
795    ///   dir=$(heddle start foo --print-cd-path) && cd "$dir"
796    /// Skips all other output (no JSON, no styling, no extra lines) so the
797    /// stdout is a clean path. Mutually exclusive with `--watch`-style flows.
798    #[arg(long, hide = true, conflicts_with_all = ["agent_provider", "agent_model"])]
799    pub print_cd_path: bool,
800
801    /// For `--workspace virtualized`: hand the filesystem mount off to the
802    /// long-lived `heddled` daemon (default). The daemon owns the
803    /// mount across CLI invocations, so the mount survives `heddle
804    /// thread start` exiting. Linux-only; no-op for heavy
805    /// workspaces. Pass `--no-daemon` to keep the mount in-process
806    /// instead.
807    #[arg(
808        long,
809        overrides_with = "no_daemon",
810        action = clap::ArgAction::SetTrue,
811        default_value_t = true,
812        hide = true,
813    )]
814    pub daemon: bool,
815
816    /// For `--workspace virtualized`: keep the filesystem mount in this CLI
817    /// process instead of handing it to the `heddled` daemon. The
818    /// mount unmounts when this `heddle thread start` exits — useful
819    /// for one-shot inspections, debugging the in-process mount path,
820    /// or environments where the daemon can't run.
821    #[arg(
822        long,
823        overrides_with = "daemon",
824        action = clap::ArgAction::SetTrue,
825        hide = true,
826    )]
827    pub no_daemon: bool,
828
829    /// Allow this invocation to open System Settings and wait briefly for
830    /// FSKit approval. Requires an interactive terminal; otherwise setup
831    /// fails before opening a GUI.
832    #[arg(long)]
833    pub interactive_setup: bool,
834
835    /// Redirect cargo's `target/` directory to a workspace-wide shared
836    /// path (`.heddle/targets/<workspace-fingerprint>/`) instead of
837    /// letting cargo create a per-thread `target/`. Saves multiples of
838    /// gigabytes when several materialized threads coexist in a Rust
839    /// workspace. Implemented by writing `.cargo/config.toml` inside
840    /// the new thread checkout — transparent to any `cargo` invocation
841    /// in that directory.
842    ///
843    /// Default: on for solid/materialized threads when the repository
844    /// root has a `Cargo.toml`. Pass `--no-shared-target` to opt out.
845    /// Explicit `--shared-target` forces the attempt on (still a no-op
846    /// without a top-level `Cargo.toml`). Has no effect on virtualized
847    /// (mounted) threads.
848    #[arg(
849        long,
850        overrides_with = "no_shared_target",
851        action = clap::ArgAction::SetTrue,
852        hide = true,
853    )]
854    pub shared_target: bool,
855
856    /// Opt out of the default shared cargo `target/` redirect for
857    /// solid/materialized threads in Rust workspaces. See
858    /// `--shared-target`.
859    #[arg(
860        long,
861        overrides_with = "shared_target",
862        action = clap::ArgAction::SetTrue,
863        hide = true,
864    )]
865    pub no_shared_target: bool,
866
867    /// Symlink the origin checkout's top-level ignored dependency
868    /// directories (`node_modules`, `.venv`, `target`, …) into this
869    /// isolated checkout so it's immediately buildable — run
870    /// `tsc`/`eslint`/tests without reinstalling deps from scratch.
871    ///
872    /// The links point back at the origin's directories and stay
873    /// ignored, so the deps are never captured into heddle. Admin dirs
874    /// (`.git`, `.heddle`) are excluded; only top-level ignored
875    /// directories are linked. Has no effect on virtualized (mounted)
876    /// threads.
877    #[arg(long)]
878    pub hydrate: bool,
879}
880
881/// Arguments for the `try` command — atomic-ephemeral-thread sugar.
882///
883/// Implements item 3.1 from the heddle 6→8 plan: spin up an ephemeral
884/// thread, run `<cmd>` inside that thread's checkout, capture on
885/// success and drop on failure. The parent's working tree is never
886/// touched, regardless of whether the command succeeds or fails — the
887/// ephemeral thread is a sandbox.
888#[derive(Clone, Debug, clap::Args)]
889pub struct TryArgs {
890    /// Optional thread name. When omitted, defaults to
891    /// `try-<short-hash>` derived from the command and a timestamp.
892    #[arg(long)]
893    pub name: Option<String>,
894
895    /// Workspace mode for the ephemeral thread. Defaults to `materialized`
896    /// (a real isolated checkout) so `<cmd>` runs against a proper
897    /// filesystem. Pass `auto`, `virtualized`, or `solid` to use a different
898    /// workspace strategy.
899    #[arg(long, value_enum, default_value_t = WorkspaceModeArg::Materialized)]
900    pub workspace: WorkspaceModeArg,
901    /// On zero exit, automatically land the resulting thread into
902    /// the current thread. Default: off.
903    #[arg(long = "auto-merge")]
904    pub auto_merge: bool,
905
906    /// Keep the ephemeral thread on success even if `--auto-merge`
907    /// would otherwise drop it after merging. Has no effect on the
908    /// failure path (failed attempts are always dropped).
909    #[arg(long = "keep-on-success")]
910    pub keep_on_success: bool,
911
912    /// Allow Heddle global-option spellings after `--` and pass them
913    /// unchanged to the inner command.
914    #[arg(long)]
915    pub allow_heddle_global_args: bool,
916
917    /// The command to run. Everything after `--` lands here. The
918    /// first token is the program; the rest are its arguments.
919    #[arg(required = true, trailing_var_arg = true, allow_hyphen_values = true)]
920    pub command: Vec<String>,
921}
922
923/// Arguments for the `run` command.
924#[derive(Clone, Debug, clap::Args)]
925pub struct RunArgs {
926    /// Thread to execute within.
927    #[arg(long = "thread")]
928    pub thread: Option<String>,
929
930    /// Command to run inside the thread execution root.
931    #[arg(required = true, trailing_var_arg = true, allow_hyphen_values = true)]
932    pub command: Vec<String>,
933}
934
935/// Arguments for the `ready` command.
936#[derive(Clone, Debug, clap::Args)]
937pub struct ReadyArgs {
938    /// Thread to evaluate for integration readiness.
939    #[arg(long = "thread")]
940    pub thread: Option<String>,
941
942    /// Intent/message to use if `ready` needs to capture outstanding work first.
943    #[arg(short = 'm', long)]
944    pub message: Option<String>,
945
946    /// Honest confidence estimate (0.0-1.0) if `ready` captures outstanding work.
947    #[arg(long, value_parser = parse_confidence)]
948    pub confidence: Option<f32>,
949
950    /// Preview the readiness decision (integration target, conflicts, verify
951    /// verdicts, would-be thread transition) without capturing work or moving
952    /// the thread to Ready/Blocked. No mutation occurs.
953    #[arg(long)]
954    pub dry_run: bool,
955}
956
957/// Arguments for the `sync` command.
958#[derive(Clone, Debug, clap::Args)]
959pub struct SyncArgs {
960    /// Optional sync target. Omit for operator/thread sync.
961    #[cfg(feature = "git-overlay")]
962    #[command(subcommand)]
963    pub command: Option<SyncCommands>,
964
965    /// Thread to refresh (default: current thread).
966    #[arg(long = "thread")]
967    pub thread: Option<String>,
968}
969
970/// Arguments for the `land` command.
971#[derive(Clone, Debug, clap::Args)]
972pub struct LandArgs {
973    /// Thread to capture and integrate (default: current thread).
974    #[arg(long = "thread")]
975    pub thread: Option<String>,
976
977    /// Peer threads to land in order. When `--thread` is also supplied, that
978    /// thread is landed first. Comma-separated, e.g.
979    /// `--threads alpha,beta,gamma`. Each peer is refreshed and landed against
980    /// the live target tip.
981    #[arg(long = "threads", value_delimiter = ',')]
982    pub threads: Vec<String>,
983
984    /// Intent/message to use if land needs to capture outstanding work first.
985    #[arg(short = 'm', long)]
986    pub message: Option<String>,
987
988    /// Preserve per-State Git export instead of squashing the landed thread.
989    #[arg(long)]
990    pub no_squash: bool,
991
992    /// Preview the integration (thread -> target, merge relation, conflicts,
993    /// verify verdicts) without capturing work, syncing, or merging. No
994    /// mutation occurs and no server round-trip is made.
995    #[arg(long)]
996    pub dry_run: bool,
997}
998
999/// Arguments for `thread show`.
1000#[derive(Clone, Debug, clap::Args)]
1001pub struct ThreadShowArgs {
1002    /// Thread identifier. Defaults to the current thread when omitted.
1003    pub thread: Option<String>,
1004
1005    /// Continuously refresh thread status.
1006    #[arg(long)]
1007    pub watch: bool,
1008
1009    /// Internal helper for tests: stop after N watch updates.
1010    #[arg(long, hide = true)]
1011    pub watch_iterations: Option<usize>,
1012
1013    /// Internal helper for tests: polling interval in milliseconds.
1014    #[arg(long, hide = true)]
1015    pub watch_interval_ms: Option<u64>,
1016}
1017
1018/// Arguments for `thread captures`.
1019#[derive(Clone, Debug, clap::Args)]
1020pub struct ThreadCapturesArgs {
1021    /// Thread identifier. Defaults to the current thread when omitted.
1022    pub thread: Option<String>,
1023
1024    /// Maximum captures to show.
1025    #[arg(long, default_value_t = 20)]
1026    pub limit: usize,
1027}
1028
1029/// Arguments for commands that take a thread identifier. Omitting the
1030/// positional resolves to the current thread when one can be inferred
1031/// from the working checkout.
1032#[derive(Clone, Debug, clap::Args)]
1033pub struct ThreadNameArgs {
1034    /// Thread identifier. Defaults to the current thread when omitted.
1035    pub thread: Option<String>,
1036}
1037
1038/// Arguments for `thread rename`.
1039#[derive(Clone, Debug, clap::Args)]
1040pub struct ThreadRenameArgs {
1041    /// Existing thread identifier.
1042    pub old: String,
1043
1044    /// New thread identifier.
1045    pub new: String,
1046}
1047
1048/// Arguments for `thread promote`.
1049#[derive(Clone, Debug, clap::Args)]
1050pub struct ThreadPromoteArgs {
1051    /// Thread identifier.
1052    pub thread: String,
1053
1054    /// Materialized checkout path.
1055    #[arg(long)]
1056    pub path: Option<std::path::PathBuf>,
1057
1058    /// Discard dirty work in the source checkout while promoting.
1059    #[arg(long)]
1060    pub force: bool,
1061}
1062
1063/// Arguments for `thread move`.
1064#[derive(Clone, Debug, clap::Args)]
1065pub struct ThreadMoveArgs {
1066    /// Source thread identifier.
1067    pub from: String,
1068
1069    /// Destination thread identifier.
1070    pub to: String,
1071
1072    /// Repository-relative path prefix to move.
1073    #[arg(long = "path", required = true, value_name = "PATH")]
1074    pub paths: Vec<String>,
1075
1076    /// Intent/message for the snapshots created by the move.
1077    #[arg(short = 'm', long)]
1078    pub message: Option<String>,
1079}
1080
1081/// Arguments for `thread absorb`.
1082#[derive(Clone, Debug, clap::Args)]
1083pub struct ThreadAbsorbArgs {
1084    /// Child thread to absorb.
1085    pub thread: String,
1086
1087    /// Parent thread to absorb into (default: the thread's recorded parent).
1088    #[arg(long)]
1089    pub into: Option<String>,
1090
1091    /// Commit message for the absorb merge.
1092    #[arg(short = 'm', long)]
1093    pub message: Option<String>,
1094
1095    /// Show the absorb preview without applying it.
1096    #[arg(long)]
1097    pub preview: bool,
1098}
1099
1100/// Arguments for `thread resolve`.
1101#[derive(Clone, Debug, clap::Args)]
1102pub struct ThreadResolveArgs {
1103    /// Thread identifier.
1104    pub thread: String,
1105}
1106
1107/// Arguments for `thread drop`.
1108#[derive(Clone, Debug, clap::Args)]
1109pub struct ThreadDropArgs {
1110    /// Thread identifier.
1111    pub thread: String,
1112
1113    /// Also delete the attached thread ref.
1114    #[arg(long)]
1115    pub delete_thread: bool,
1116
1117    /// Discard uncommitted changes in the thread checkout before dropping it.
1118    #[arg(short, long)]
1119    pub force: bool,
1120}
1121
1122/// Arguments for `thread approve` — record an approval for a
1123/// `<source> -> <target>` merge against the source thread's
1124/// current state.
1125#[derive(Clone, Debug, clap::Args)]
1126pub struct ThreadApproveArgs {
1127    /// Source thread identifier (the change set being merged).
1128    pub source: String,
1129
1130    /// Target thread identifier (where the merge would land).
1131    pub target: String,
1132
1133    /// Optional human note attached to the approval.
1134    #[arg(long)]
1135    pub note: Option<String>,
1136
1137    /// Hosted remote name (default: `origin`).
1138    #[arg(long, default_value = "origin")]
1139    pub remote: String,
1140}
1141
1142/// Arguments for `thread approvals` — list every approval recorded
1143/// for `<source> -> <target>`.
1144#[derive(Clone, Debug, clap::Args)]
1145pub struct ThreadApprovalsArgs {
1146    pub source: String,
1147    pub target: String,
1148    #[arg(long, default_value = "origin")]
1149    pub remote: String,
1150}
1151
1152/// Arguments for `thread revoke-approval` — remove a recorded
1153/// approval by id.
1154#[derive(Clone, Debug, clap::Args)]
1155pub struct ThreadRevokeApprovalArgs {
1156    /// UUID of the approval row to revoke.
1157    pub id: String,
1158    #[arg(long, default_value = "origin")]
1159    pub remote: String,
1160}
1161
1162/// Arguments for `thread check-merge` — query the merge gate
1163/// without recording anything. Returns the unmet requirements.
1164#[derive(Clone, Debug, clap::Args)]
1165pub struct ThreadCheckMergeArgs {
1166    pub source: String,
1167    pub target: String,
1168
1169    /// 'merge' (default), 'force_push', or 'complete'.
1170    #[arg(long, default_value = "merge")]
1171    pub gated_action: String,
1172
1173    /// File paths the diff touches, repeat or comma-separate. Empty =
1174    /// "we don't know" (every path-conditional policy fires).
1175    #[arg(long = "path", value_delimiter = ',')]
1176    pub changed_paths: Vec<String>,
1177
1178    #[arg(long, default_value = "origin")]
1179    pub remote: String,
1180}
1181
1182/// Arguments for the `collapse` command.
1183#[derive(Clone, Debug, clap::Args)]
1184pub struct CollapseArgs {
1185    /// States to collapse.
1186    #[arg(required = true)]
1187    pub states: Vec<String>,
1188
1189    /// Intent/name for the resulting state.
1190    #[arg(long)]
1191    pub into: String,
1192
1193    /// Confidence for the resulting state (0.0-1.0).
1194    #[arg(long)]
1195    pub confidence: Option<f32>,
1196}
1197
1198/// Arguments for the `expand` command.
1199#[derive(Clone, Debug, clap::Args)]
1200pub struct ExpandArgs {
1201    /// Git OID, state spec, or thread name for the squashed land.
1202    pub reference: String,
1203}
1204
1205/// Arguments for the `resolve` command.
1206#[derive(Clone, Debug, clap::Args)]
1207pub struct ResolveArgs {
1208    /// File to resolve.
1209    pub path: Option<String>,
1210
1211    /// Resolve all conflicts.
1212    #[arg(long)]
1213    pub all: bool,
1214
1215    /// List unresolved conflicts.
1216    #[arg(long)]
1217    pub list: bool,
1218
1219    /// Use our version (current thread).
1220    #[arg(long, conflicts_with = "theirs")]
1221    pub ours: bool,
1222
1223    /// Use their version (merged thread).
1224    #[arg(long, conflicts_with = "ours")]
1225    pub theirs: bool,
1226
1227    /// Mark the path resolved even if conflict markers are still present.
1228    #[arg(long)]
1229    pub force: bool,
1230}
1231
1232/// The `(remote, thread)` pair shared by remote commands that use an
1233/// option-only thread selector.
1234#[derive(Clone, Debug, clap::Args)]
1235pub struct RemoteOperationArgs {
1236    /// Heddle remote name, native repository path, or hosted address.
1237    pub remote: Option<String>,
1238
1239    /// Thread to act on.
1240    #[arg(short, long)]
1241    pub thread: Option<String>,
1242
1243    /// Allow cleartext (non-TLS) connections to non-loopback hosts.
1244    /// Prefer enabling TLS; use this only for intentional lab/VPN testing.
1245    #[arg(long)]
1246    pub insecure: bool,
1247}
1248
1249/// Arguments for the `push` command.
1250#[derive(Clone, Debug, clap::Args)]
1251#[command(after_help = "\
1252Git Overlay refs:
1253  A normal push writes refs/heads/<thread> and refs/notes/heddle.
1254  --all-threads writes every refs/heads/<thread> and refs/tags/<tag>, plus refs/notes/heddle.
1255  JSON output lists changed refs in refs_written; verify with git ls-remote <remote>.
1256")]
1257pub struct PushArgs {
1258    /// Heddle remote name, native repository path, or hosted address.
1259    pub remote: Option<String>,
1260
1261    /// Thread to push.
1262    #[arg(short, long, conflicts_with = "thread_arg")]
1263    pub thread: Option<String>,
1264
1265    /// Thread to push; alias for `--thread`.
1266    #[arg(value_name = "THREAD")]
1267    pub thread_arg: Option<String>,
1268
1269    /// State to push (default: HEAD).
1270    #[arg(short, long)]
1271    pub state: Option<String>,
1272
1273    /// Force push.
1274    #[arg(short, long)]
1275    pub force: bool,
1276
1277    /// Push every thread. In Git Overlay, also include every local Git tag.
1278    #[arg(long)]
1279    pub all_threads: bool,
1280
1281    /// Allow cleartext (non-TLS) connections to non-loopback hosts.
1282    /// Prefer enabling TLS; use this only for intentional lab/VPN testing.
1283    #[arg(long)]
1284    pub insecure: bool,
1285
1286    /// Preview the push plan (target, thread/track ref, state that would be
1287    /// published, force status) without pushing, moving refs, capturing work,
1288    /// running hooks, or contacting the server for anything beyond read/plan.
1289    #[arg(long)]
1290    pub dry_run: bool,
1291}
1292
1293impl PushArgs {
1294    pub fn thread_name(&self) -> Option<String> {
1295        self.thread.clone().or_else(|| self.thread_arg.clone())
1296    }
1297}
1298
1299/// Arguments for the `pull` command.
1300#[derive(Clone, Debug, clap::Args)]
1301#[command(after_help = "\
1302Advanced (hidden) flags:
1303  --lazy leaves blob content absent by design and hydrates it explicitly later. Hosted/network Heddle remotes only.
1304")]
1305pub struct PullArgs {
1306    #[command(flatten)]
1307    pub remote_op: RemoteOperationArgs,
1308
1309    /// Local thread to update.
1310    #[arg(short, long)]
1311    pub local_thread: Option<String>,
1312
1313    /// Leave blob content absent by design and hydrate it explicitly later.
1314    #[arg(long, hide = true)]
1315    pub lazy: bool,
1316}
1317
1318/// Arguments for the `clone` command.
1319///
1320/// Help style budget (heddle#652): `--help` carries the signature, flags,
1321/// a one-screen Behavior summary, and the hidden-flag breadcrumb
1322/// (heddle#646). The full default-thread fallback chain and --depth
1323/// exposition moved to `heddle help clone` (help.rs CLONE_TOPIC); keep
1324/// flag docs single-line so clap renders the compact help layout.
1325#[derive(Clone, Debug, clap::Args)]
1326#[command(after_help = "\
1327Behavior:
1328  Clones native Heddle or Git repositories. Native clones follow the remote default thread; `--thread` overrides. Git clones check out the selected default branch. Git transport runs through Sley and does not require a Git executable. Never prompts. Full details: `heddle help clone`.
1329
1330Advanced/planned flags: see `heddle help clone`.
1331
1332Examples:
1333  heddle clone ../native-repo ./clone                # local native Heddle repository
1334  heddle clone heddle://host/repo ./clone --depth 1   # shallow Heddle clone: tip plus immediate parents
1335")]
1336pub struct CloneArgs {
1337    /// Remote repository path.
1338    pub remote: String,
1339
1340    /// Local directory to clone into.
1341    pub local: String,
1342
1343    /// Thread to check out after cloning.
1344    #[arg(long)]
1345    pub thread: Option<String>,
1346
1347    /// Create a shallow clone with the specified depth. `0` means full history.
1348    #[arg(long)]
1349    pub depth: Option<u32>,
1350
1351    // Hosted/network remotes only. The user-facing exposition lives in the
1352    // after-help breadcrumb above and `heddle help clone`.
1353    /// Leave blob content absent by design and hydrate it explicitly later.
1354    #[arg(long, hide = true)]
1355    pub lazy: bool,
1356
1357    /// Allow cleartext (non-TLS) connections to non-loopback hosts.
1358    #[arg(long)]
1359    pub insecure: bool,
1360
1361    // Only `blob:none` is accepted (a synonym for --lazy on hosted
1362    // remotes); git-style filters such as `tree:0` or `blob:limit=…` are
1363    // rejected at parse time. See the after-help breadcrumb and `heddle help
1364    // clone`.
1365    /// Partial-clone filter spec (`blob:none` only).
1366    #[arg(long, hide = true, value_name = "SPEC", value_parser = parse_clone_filter_spec)]
1367    pub filter: Option<String>,
1368
1369    /// Clone a whole hosted monorepo: resolve the root spool's child tree and
1370    /// clone every child spool at its anchored state into its mount path.
1371    /// Hosted/network remotes only. (Alias: --monorepo.)
1372    #[arg(long, visible_alias = "monorepo")]
1373    pub recursive: bool,
1374}
1375
1376fn parse_clone_filter_spec(s: &str) -> Result<String, String> {
1377    match s {
1378        "blob:none" => Ok(s.to_string()),
1379        other => Err(format!(
1380            "unsupported --filter spec `{other}`; only `blob:none` is supported today"
1381        )),
1382    }
1383}
1384
1385/// Arguments for `agent provenance begin`.
1386#[derive(Clone, Debug, clap::Args)]
1387pub struct AgentProvenanceBeginArgs {
1388    /// Provider name (e.g., "anthropic", "openai").
1389    #[arg(long)]
1390    pub provider: String,
1391
1392    /// Model identifier (e.g., "claude-opus-4").
1393    #[arg(long)]
1394    pub model: String,
1395
1396    /// Policy or prompt template ID.
1397    #[arg(long)]
1398    pub policy: Option<String>,
1399}
1400
1401/// Arguments for `agent provenance segment`.
1402#[derive(Clone, Debug, clap::Args)]
1403pub struct AgentProvenanceSegmentArgs {
1404    /// Provider name (e.g., "anthropic", "openai").
1405    #[arg(long)]
1406    pub provider: String,
1407
1408    /// Model identifier (e.g., "claude-opus-4").
1409    #[arg(long)]
1410    pub model: String,
1411
1412    /// Policy or prompt template ID.
1413    #[arg(long)]
1414    pub policy: Option<String>,
1415}
1416
1417/// Arguments for `agent provenance end`.
1418#[derive(Clone, Debug, clap::Args)]
1419pub struct AgentProvenanceEndArgs {
1420    /// Session ID to end (default: current session).
1421    pub session_id: Option<String>,
1422}
1423
1424/// Arguments for `agent provenance show`.
1425#[derive(Clone, Debug, clap::Args)]
1426pub struct AgentProvenanceShowArgs {
1427    /// Session ID to show (default: current session).
1428    pub session_id: Option<String>,
1429}
1430
1431/// Arguments for `agent provenance list`.
1432#[derive(Clone, Debug, clap::Args)]
1433pub struct AgentProvenanceListArgs {
1434    /// Show only active sessions.
1435    #[arg(long)]
1436    pub active: bool,
1437}
1438
1439/// Arguments for the `worktree add` command.
1440#[derive(Clone, Debug, clap::Args)]
1441pub struct WorktreeAddArgs {
1442    /// Path to the new agent checkout directory.
1443    pub path: std::path::PathBuf,
1444
1445    /// Thread name for the agent (created if absent, default: HEAD thread).
1446    #[arg(long)]
1447    pub thread: Option<String>,
1448
1449    /// Base state to materialize (default: HEAD).
1450    #[arg(long)]
1451    pub from: Option<String>,
1452}
1453
1454/// Arguments for the `worktree remove` command.
1455#[derive(Clone, Debug, clap::Args)]
1456pub struct WorktreeRemoveArgs {
1457    /// Path to the isolated checkout directory to remove.
1458    pub path: std::path::PathBuf,
1459
1460    /// Also delete the associated thread ref, if this checkout is attached.
1461    #[arg(long)]
1462    pub delete_thread: bool,
1463}
1464
1465/// Arguments for `presence list`.
1466#[derive(Clone, Debug, clap::Args)]
1467pub struct AgentPresenceListArgs {
1468    /// Show only active actors.
1469    #[arg(long)]
1470    pub active: bool,
1471}
1472
1473/// Arguments for `presence show`.
1474#[derive(Clone, Debug, clap::Args)]
1475pub struct AgentPresenceShowArgs {
1476    /// Session ID to show (default: current thread actor).
1477    pub session: Option<String>,
1478}
1479
1480/// Arguments for `presence explain`.
1481#[derive(Clone, Debug, clap::Args)]
1482pub struct AgentPresenceExplainArgs {
1483    /// Session ID to explain (default: current thread actor).
1484    pub session: Option<String>,
1485}
1486
1487/// Arguments for `presence complete`.
1488#[derive(Clone, Debug, clap::Args)]
1489pub struct AgentPresenceCompleteArgs {
1490    /// Session ID to mark as complete (default: current thread actor).
1491    #[arg(long)]
1492    pub session: Option<String>,
1493}
1494
1495/// Arguments for `agent reserve`.
1496#[derive(Clone, Debug, clap::Args)]
1497pub struct AgentReserveArgs {
1498    /// Thread to reserve.
1499    #[arg(long)]
1500    pub thread: String,
1501
1502    /// Anchor state spec (default: current HEAD).
1503    #[arg(long)]
1504    pub anchor: Option<String>,
1505
1506    /// Optional task description.
1507    #[arg(long)]
1508    pub task: Option<String>,
1509
1510    /// Local agent task assignment id to attach to this reservation.
1511    #[arg(long)]
1512    pub task_id: Option<String>,
1513
1514    /// Reap the lease early when this long-lived owner process exits.
1515    #[arg(long, value_name = "PID")]
1516    pub hold_for_pid: Option<u32>,
1517}
1518
1519/// Arguments for `agent heartbeat`.
1520#[derive(Clone, Debug, clap::Args)]
1521pub struct AgentHeartbeatArgs {
1522    /// Writer lease id returned by `agent reserve`.
1523    #[arg(long)]
1524    pub lease: String,
1525
1526    /// Bearer token returned by `agent reserve`.
1527    #[arg(long, env = "HEDDLE_RESERVATION_TOKEN", hide_env_values = true)]
1528    pub token: String,
1529}
1530
1531/// Arguments for `agent release`.
1532#[derive(Clone, Debug, clap::Args)]
1533pub struct AgentReleaseArgs {
1534    /// Writer lease id returned by `agent reserve`.
1535    #[arg(long)]
1536    pub lease: String,
1537
1538    /// Bearer token returned by `agent reserve`.
1539    #[arg(long, env = "HEDDLE_RESERVATION_TOKEN", hide_env_values = true)]
1540    pub token: String,
1541
1542    /// Terminal status to record.
1543    #[arg(long, default_value = "complete")]
1544    pub status: AgentReleaseStatusArg,
1545}
1546
1547#[derive(Clone, Debug, clap::ValueEnum)]
1548pub enum AgentReleaseStatusArg {
1549    Complete,
1550    Abandoned,
1551}
1552
1553/// Arguments for `agent list`.
1554#[derive(Clone, Debug, clap::Args)]
1555pub struct AgentApiListArgs {
1556    /// Filter by thread.
1557    #[arg(long)]
1558    pub thread: Option<String>,
1559
1560    /// Show only active reservations.
1561    #[arg(long)]
1562    pub alive_only: bool,
1563}
1564
1565#[derive(Clone, Debug, clap::ValueEnum)]
1566pub enum AgentTaskStatusArg {
1567    Open,
1568    InProgress,
1569    Blocked,
1570    Complete,
1571    Abandoned,
1572}
1573
1574/// Arguments for `agent task create`.
1575#[derive(Clone, Debug, clap::Args)]
1576pub struct AgentTaskCreateArgs {
1577    /// Optional caller-provided task id (default: generated task UUIDv7 id).
1578    #[arg(long)]
1579    pub task_id: Option<String>,
1580
1581    /// Human-readable task title.
1582    #[arg(long)]
1583    pub title: String,
1584
1585    /// Detailed task body.
1586    #[arg(long)]
1587    pub body: Option<String>,
1588
1589    /// Thread this task targets.
1590    #[arg(long)]
1591    pub thread: String,
1592
1593    /// Optional base state id this task was delegated from.
1594    #[arg(long)]
1595    pub base_state: Option<String>,
1596
1597    /// Optional base root id this task was delegated from.
1598    #[arg(long)]
1599    pub base_root: Option<String>,
1600
1601    /// Optional parent task id.
1602    #[arg(long)]
1603    pub parent_task_id: Option<String>,
1604
1605    /// Optional coordination discussion id.
1606    #[arg(long)]
1607    pub coordination_discussion_id: Option<String>,
1608
1609    /// Allow this task to continue without hosted connectivity.
1610    #[arg(long)]
1611    pub allow_offline: bool,
1612
1613    /// Principal or agent that delegated this task.
1614    #[arg(long)]
1615    pub delegated_by: Option<String>,
1616}
1617
1618/// Arguments for `agent task list`.
1619#[derive(Clone, Debug, clap::Args)]
1620pub struct AgentTaskListArgs {
1621    /// Filter by target thread.
1622    #[arg(long)]
1623    pub thread: Option<String>,
1624
1625    /// Filter by task status.
1626    #[arg(long)]
1627    pub status: Option<AgentTaskStatusArg>,
1628}
1629
1630/// Arguments for `agent task show`.
1631#[derive(Clone, Debug, clap::Args)]
1632pub struct AgentTaskShowArgs {
1633    /// Task id to show.
1634    pub task_id: String,
1635}
1636
1637/// Arguments for `agent task update`.
1638#[derive(Clone, Debug, clap::Args)]
1639pub struct AgentTaskUpdateArgs {
1640    /// Task id to update.
1641    pub task_id: String,
1642
1643    /// Replace the task title.
1644    #[arg(long)]
1645    pub title: Option<String>,
1646
1647    /// Replace the task body.
1648    #[arg(long)]
1649    pub body: Option<String>,
1650
1651    /// Replace the task status.
1652    #[arg(long)]
1653    pub status: Option<AgentTaskStatusArg>,
1654
1655    /// Replace the target thread.
1656    #[arg(long)]
1657    pub thread: Option<String>,
1658
1659    /// Replace the base state id.
1660    #[arg(long)]
1661    pub base_state: Option<String>,
1662
1663    /// Replace the base root id.
1664    #[arg(long)]
1665    pub base_root: Option<String>,
1666
1667    /// Replace the parent task id.
1668    #[arg(long)]
1669    pub parent_task_id: Option<String>,
1670
1671    /// Replace the coordination discussion id.
1672    #[arg(long)]
1673    pub coordination_discussion_id: Option<String>,
1674
1675    /// Allow this task to continue without hosted connectivity.
1676    #[arg(long, conflicts_with = "no_allow_offline")]
1677    pub allow_offline: bool,
1678
1679    /// Disallow offline continuation for this task.
1680    #[arg(long, conflicts_with = "allow_offline")]
1681    pub no_allow_offline: bool,
1682
1683    /// Replace the delegating principal or agent label.
1684    #[arg(long)]
1685    pub delegated_by: Option<String>,
1686}
1687
1688/// Arguments shared by `agent fanout plan` and `agent fanout start`.
1689#[derive(Clone, Debug, clap::Args)]
1690pub struct AgentFanoutPlanArgs {
1691    /// Parent coordination task title.
1692    #[arg(long)]
1693    pub title: String,
1694
1695    /// Lane spec: `<thread>=<path>:<title>`. Repeat once per child lane.
1696    #[arg(long, value_name = "THREAD=PATH:TITLE")]
1697    pub lane: Vec<String>,
1698
1699    /// Optional collaboration discussion id to store on task assignments.
1700    #[arg(long)]
1701    pub coordination_discussion_id: Option<String>,
1702}
1703
1704/// Arguments for `agent fanout start`.
1705#[derive(Clone, Debug, clap::Args)]
1706pub struct AgentFanoutStartArgs {
1707    /// Parent coordination task title.
1708    #[arg(long)]
1709    pub title: String,
1710
1711    /// Lane spec: `<thread>=<path>:<title>`. Repeat once per child lane.
1712    #[arg(long, value_name = "THREAD=PATH:TITLE")]
1713    pub lane: Vec<String>,
1714
1715    /// Optional collaboration discussion id to store on task assignments.
1716    #[arg(long)]
1717    pub coordination_discussion_id: Option<String>,
1718}
1719
1720/// Arguments for `agent capture` under a current reservation lease.
1721#[derive(Clone, Debug, clap::Args)]
1722pub struct AgentCaptureArgs {
1723    /// Writer lease id returned by `agent reserve`.
1724    #[arg(long)]
1725    pub lease: String,
1726
1727    /// Bearer token returned by `agent reserve`.
1728    #[arg(long, env = "HEDDLE_RESERVATION_TOKEN", hide_env_values = true)]
1729    pub token: String,
1730
1731    /// Capture intent / commit message.
1732    #[arg(long, short = 'm', alias = "intent")]
1733    pub message: Option<String>,
1734
1735    /// Honest confidence estimate (0.0–1.0).
1736    #[arg(long, value_parser = parse_confidence)]
1737    pub confidence: Option<f32>,
1738}
1739
1740/// Arguments for `agent ready` under a writer lease.
1741#[derive(Clone, Debug, clap::Args)]
1742pub struct AgentReadyArgs {
1743    /// Writer lease id returned by `agent reserve`.
1744    #[arg(long)]
1745    pub lease: String,
1746
1747    /// Bearer token returned by `agent reserve`.
1748    #[arg(long, env = "HEDDLE_RESERVATION_TOKEN", hide_env_values = true)]
1749    pub token: String,
1750
1751    /// Optional summary message.
1752    #[arg(long, short = 'm')]
1753    pub message: Option<String>,
1754
1755    /// Honest confidence estimate (0.0-1.0) if `agent ready` captures outstanding work.
1756    #[arg(long, value_parser = parse_confidence)]
1757    pub confidence: Option<f32>,
1758}
1759
1760/// Arguments for the `watch` command.
1761///
1762/// Streams live oplog activity (snapshots, merges, thread create/update,
1763/// markers, etc.) as it happens. Default behavior tails forever and exits
1764/// on Ctrl-C. `--since 5m` replays the last N before tailing live;
1765/// `--filter` restricts output to the named kinds; `--output json` emits one
1766/// JSON object per line for piping to `jq`.
1767#[derive(Clone, Debug, clap::Args)]
1768pub struct WatchArgs {
1769    /// Replay events from this duration ago (e.g. `30s`, `5m`, `1h`,
1770    /// `2d`) before tailing live. When unset, only new events are
1771    /// emitted.
1772    #[arg(long, value_name = "DURATION")]
1773    pub since: Option<String>,
1774
1775    /// Comma-separated event kinds to include
1776    /// (`snapshot,merge,thread_create,thread_update,thread_delete,
1777    /// collapse,thread_marker_create,thread_marker_delete`).
1778    #[arg(long, value_name = "KINDS")]
1779    pub filter: Option<String>,
1780
1781    /// Internal helper for tests: stop after the oplog file produces
1782    /// this many modify events (still drains pending entries first).
1783    #[arg(long, hide = true)]
1784    pub max_iterations: Option<usize>,
1785
1786    /// Internal helper for tests: poll interval in milliseconds for
1787    /// the `notify` watcher's debounce check (default 200ms).
1788    #[arg(long, hide = true)]
1789    pub poll_interval_ms: Option<u64>,
1790}
1791
1792// `AgentCaptureArgs` and `AgentReadyArgs` defined earlier in this
1793// file. A second copy was left here by the rebase (the workstreams
1794// commit added them twice when the cherry-pick had lost the
1795// originals and we re-added them mid-rebase). Removed.
1796
1797#[cfg(test)]
1798mod capture_message_alias_tests {
1799    use clap::Parser;
1800
1801    use crate::cli::{Cli, Commands, SnapshotArgs};
1802
1803    fn parse_capture(extra: &[&str]) -> Result<SnapshotArgs, clap::Error> {
1804        let mut argv: Vec<&str> = vec!["heddle", "capture"];
1805        argv.extend_from_slice(extra);
1806        let cli = Cli::try_parse_from(argv)?;
1807        match cli.command {
1808            Commands::Capture(args) => Ok(args),
1809            _ => panic!("expected Commands::Capture"),
1810        }
1811    }
1812
1813    #[test]
1814    fn capture_accepts_message_alias() {
1815        let args = parse_capture(&["--message", "my change"]).expect("--message should parse");
1816        assert_eq!(args.intent.as_deref(), Some("my change"));
1817    }
1818
1819    #[test]
1820    fn capture_accepts_intent_long_form() {
1821        let args = parse_capture(&["--intent", "my change"]).expect("--intent should parse");
1822        assert_eq!(args.intent.as_deref(), Some("my change"));
1823    }
1824
1825    #[test]
1826    fn capture_accepts_short_m() {
1827        let args = parse_capture(&["-m", "my change"]).expect("-m should parse");
1828        assert_eq!(args.intent.as_deref(), Some("my change"));
1829    }
1830
1831    #[test]
1832    fn capture_parses_without_intent_so_the_refuse_can_fire() {
1833        let args =
1834            parse_capture(&[]).expect("omitted -m is a semantic refuse, not a clap usage error");
1835        assert!(args.intent.is_none());
1836    }
1837
1838    #[test]
1839    fn capture_rejects_non_finite_or_out_of_range_confidence() {
1840        for value in ["NaN", "inf", "-0.1", "1.7"] {
1841            let confidence_arg = format!("--confidence={value}");
1842            let err = parse_capture(&["-m", "bad confidence", &confidence_arg])
1843                .expect_err("invalid confidence should fail to parse");
1844            assert!(
1845                err.to_string()
1846                    .contains("confidence must be a finite number from 0.0 to 1.0"),
1847                "unexpected parse error for {value}: {err}"
1848            );
1849        }
1850    }
1851}
1852
1853#[cfg(test)]
1854mod clone_filter_tests {
1855    use clap::Parser;
1856
1857    use crate::cli::{Cli, CloneArgs, Commands};
1858
1859    fn parse_clone(extra: &[&str]) -> Result<CloneArgs, clap::Error> {
1860        let mut argv: Vec<&str> = vec!["heddle", "clone", "remote", "local"];
1861        argv.extend_from_slice(extra);
1862        let cli = Cli::try_parse_from(argv)?;
1863        match cli.command {
1864            Commands::Clone(args) => Ok(args),
1865            _ => panic!("expected Commands::Clone"),
1866        }
1867    }
1868
1869    #[test]
1870    fn parses_clone_filter_blob_none() {
1871        let args = parse_clone(&["--filter", "blob:none"]).expect("parse --filter blob:none");
1872        assert_eq!(args.filter.as_deref(), Some("blob:none"));
1873        assert!(!args.lazy);
1874    }
1875
1876    #[test]
1877    fn rejects_unknown_filter_spec() {
1878        let err = parse_clone(&["--filter", "tree:0"])
1879            .expect_err("unknown --filter spec should fail to parse");
1880        let msg = err.to_string();
1881        assert!(
1882            msg.contains("tree:0") && msg.contains("blob:none"),
1883            "error should name the bad spec and the supported one: {msg}"
1884        );
1885    }
1886}