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/// Timeline navigation action commands.
314#[derive(Clone, Debug, clap::Subcommand)]
315pub enum TimelineCommands {
316    /// Show the current timeline cursor, counts, and recovery status.
317    Status(TimelineStatusArgs),
318
319    /// Record the start of a native tool timeline step.
320    #[command(name = "record-start")]
321    RecordStart(TimelineRecordStartArgs),
322
323    /// Record the finish of a native tool timeline step.
324    #[command(name = "record-finish")]
325    RecordFinish(TimelineRecordFinishArgs),
326
327    /// Fork a timeline branch from a step or native harness tool call.
328    #[command(after_help = "\
329Examples:
330  heddle agent timeline fork --step tls-abc --branch tlb-experiment
331  heddle agent timeline fork --tool-call call_123 --session ses_456 --branch tlb-alt
332")]
333    Fork(TimelineForkArgs),
334
335    /// Reset the logical timeline cursor, optionally materializing checkout files.
336    #[command(after_help = "\
337Examples:
338  heddle agent timeline reset --step tls-abc
339  heddle agent timeline reset --tool-call call_123 --materialize
340")]
341    Reset(TimelineResetArgs),
342
343    /// Recover a pending timeline materialization after an interrupted reset/seek.
344    Recover(TimelineRecoverArgs),
345}
346
347/// Shared selector arguments for timeline action commands.
348#[derive(Clone, Debug, clap::Args)]
349pub struct TimelineTargetArgs {
350    /// Timeline thread to target.
351    #[arg(long, default_value = "main")]
352    pub thread: String,
353
354    /// Constrain the target to this branch when selecting by step/current cursor.
355    #[arg(long = "from-branch", value_name = "BRANCH")]
356    pub from_branch: Option<String>,
357
358    /// Target a timeline step id.
359    #[arg(long, conflicts_with_all = ["tool_call", "undo", "redo", "current"])]
360    pub step: Option<String>,
361
362    /// Target a native harness tool call id, such as an OpenCode tool call id.
363    #[arg(long = "tool-call", conflicts_with_all = ["step", "undo", "redo", "current"])]
364    pub tool_call: Option<String>,
365
366    /// Native harness name for `--tool-call`.
367    #[arg(long, default_value = "opencode")]
368    pub harness: String,
369
370    /// Native harness session id for `--tool-call`.
371    #[arg(long)]
372    pub session: Option<String>,
373
374    /// Native harness message id for `--tool-call`.
375    #[arg(long)]
376    pub message: Option<String>,
377
378    /// Target the previous step from the current cursor.
379    #[arg(long, conflicts_with_all = ["step", "tool_call", "redo", "current"])]
380    pub undo: bool,
381
382    /// Target the next step from the current cursor.
383    #[arg(long, conflicts_with_all = ["step", "tool_call", "undo", "current"])]
384    pub redo: bool,
385
386    /// Target the current logical cursor.
387    #[arg(long, conflicts_with_all = ["step", "tool_call", "undo", "redo"])]
388    pub current: bool,
389}
390
391/// Arguments for `heddle agent timeline fork`.
392#[derive(Clone, Debug, clap::Args)]
393pub struct TimelineForkArgs {
394    #[command(flatten)]
395    pub target: TimelineTargetArgs,
396
397    /// New timeline branch id. Generated when omitted.
398    #[arg(long, value_name = "BRANCH")]
399    pub branch: Option<String>,
400
401    /// Branch reason: explicit-fork, edit-from-rewound-cursor, retry, fan-out.
402    #[arg(long, default_value = "explicit-fork")]
403    pub reason: String,
404}
405
406/// Arguments for `heddle agent timeline reset`.
407#[derive(Clone, Debug, clap::Args)]
408pub struct TimelineResetArgs {
409    #[command(flatten)]
410    pub target: TimelineTargetArgs,
411
412    /// Materialize checkout files to the target state after moving the cursor.
413    #[arg(long)]
414    pub materialize: bool,
415
416    /// Materialization mode: fail-if-dirty or capture-current-then-seek.
417    #[arg(long, default_value = "fail-if-dirty")]
418    pub mode: String,
419}
420
421/// Arguments for `heddle agent timeline recover`.
422#[derive(Clone, Debug, clap::Args)]
423pub struct TimelineRecoverArgs {
424    /// Timeline thread to recover.
425    #[arg(long, default_value = "main")]
426    pub thread: String,
427}
428
429/// Arguments for `heddle agent timeline status`.
430#[derive(Clone, Debug, clap::Args)]
431pub struct TimelineStatusArgs {
432    /// Timeline thread to inspect.
433    #[arg(long, default_value = "main")]
434    pub thread: String,
435}
436
437/// Shared scrubbed native tool-call identity for timeline recording commands.
438#[derive(Clone, Debug, clap::Args)]
439pub struct TimelineRecordToolArgs {
440    /// Timeline thread to record into.
441    #[arg(long, default_value = "main")]
442    pub thread: String,
443
444    /// Native harness name.
445    #[arg(long, default_value = "opencode")]
446    pub harness: String,
447
448    /// Native harness session id.
449    #[arg(long)]
450    pub session: Option<String>,
451
452    /// Native harness message id.
453    #[arg(long)]
454    pub message: Option<String>,
455
456    /// Native harness tool-call id.
457    #[arg(long = "tool-call")]
458    pub tool_call: String,
459
460    /// Explicit timeline step id. When omitted, Heddle derives one from the native identity.
461    #[arg(long = "step-id")]
462    pub step_id: Option<String>,
463
464    /// Explicit timeline branch id. Defaults to the current timeline branch or `tlb-main`.
465    #[arg(long = "branch")]
466    pub branch: Option<String>,
467
468    /// Scrubbed human summary for the native payload.
469    #[arg(long = "summary")]
470    pub summary: Option<String>,
471
472    /// Hash of the native payload, never the raw payload bytes.
473    #[arg(long = "payload-hash")]
474    pub payload_hash: Option<String>,
475}
476
477/// Arguments for `heddle agent timeline record-start`.
478#[derive(Clone, Debug, clap::Args)]
479pub struct TimelineRecordStartArgs {
480    #[command(flatten)]
481    pub tool: TimelineRecordToolArgs,
482
483    /// Stable tool name such as `bash`, `edit`, or `read`.
484    #[arg(long = "tool-name", default_value = "tool")]
485    pub tool_name: String,
486}
487
488/// Arguments for `heddle agent timeline record-finish`.
489#[derive(Clone, Debug, clap::Args)]
490pub struct TimelineRecordFinishArgs {
491    #[command(flatten)]
492    pub tool: TimelineRecordToolArgs,
493
494    /// Tool result status: succeeded, failed, or cancelled.
495    #[arg(long, default_value = "succeeded")]
496    pub status: String,
497}
498
499/// Arguments for the `diff` command.
500#[derive(Clone, Debug, clap::Args)]
501#[command(after_help = "\
502Examples:
503  heddle diff                     # worktree vs HEAD
504  heddle diff --base last-turn    # worktree vs this agent peer's turn start
505  heddle diff NOTES.md            # only that path
506  heddle diff -- NOTES.md         # same, after the path separator
507  heddle diff --path NOTES.md     # same, explicit path filter
508  heddle diff HEAD~1 HEAD -- src  # two states, filtered to src/
509
510Path-shaped positionals and arguments after `--` are worktree/path filters,
511not missing states. `log --path` uses the same filter spelling.
512
513Restore:
514  Heddle does not restore one file from a saved state (no restore/checkout/reset).
515  Materialize one state in a new checkout: heddle start <name> --from <state> --path <dir>
516  Apply the inverse of one state to this worktree: heddle revert <state> [--no-commit]
517  Restore the tree preserved by the last undo: heddle undo --recover
518
519Patch compatibility:
520  --patch output uses Git-compatible unified diff, including extended headers for type and mode changes.
521")]
522pub struct DiffArgs {
523    /// Base state (default: HEAD). A path-shaped value is a worktree filter.
524    pub from: Option<String>,
525
526    /// Target state (default: worktree). A path-shaped value is a worktree filter.
527    pub to: Option<String>,
528
529    /// Select an additional diff base. `last-turn` uses the first capture in
530    /// the live harness session on this thread. With this set, one state
531    /// positional names the target rather than another base.
532    #[arg(long, value_enum)]
533    pub base: Option<DiffBaseArg>,
534
535    /// Restrict the diff to these repository-relative paths.
536    #[arg(long = "path", value_name = "PATH")]
537    pub path_filters: Vec<String>,
538
539    /// Paths after `--`. Always treated as worktree/path filters.
540    #[arg(last = true, value_name = "PATH")]
541    pub paths: Vec<String>,
542
543    /// Show semantic changes.
544    #[arg(long)]
545    pub semantic: bool,
546
547    /// Show diffstat summary only.
548    #[arg(long)]
549    pub stat: bool,
550
551    /// Show only changed file names.
552    #[arg(long)]
553    pub name_only: bool,
554
555    /// Number of surrounding context lines to include in each hunk.
556    #[arg(short = 'U', long = "unified", default_value_t = 3)]
557    pub unified: usize,
558
559    /// Show concise applicable context alongside diff output.
560    #[arg(long)]
561    pub context: bool,
562
563    /// Output a Git-compatible unified diff.
564    #[arg(short = 'p', long = "patch")]
565    pub patch: bool,
566}
567
568/// Named bases shared by `diff` and review change selection.
569#[derive(Clone, Copy, Debug, PartialEq, Eq, clap::ValueEnum)]
570pub enum DiffBaseArg {
571    /// First capture in the current harness session on this thread.
572    LastTurn,
573}
574
575impl DiffBaseArg {
576    pub fn as_str(self) -> &'static str {
577        match self {
578            Self::LastTurn => "last-turn",
579        }
580    }
581}
582
583/// Arguments for the `revert` command.
584#[derive(Clone, Debug, clap::Args)]
585#[command(after_help = "\
586Restore:
587  `revert` applies the inverse of one state's changes. It is not a single-file
588  restore, and Heddle has no restore/checkout/reset verb.
589  Materialize one state in a new checkout: heddle start <name> --from <state> --path <dir>
590  Restore the tree preserved by the last undo: heddle undo --recover
591  Heddle cannot put one file back from a saved state.
592")]
593pub struct RevertArgs {
594    /// State to revert.
595    pub state: String,
596
597    /// Commit message for the revert.
598    #[arg(short = 'm', long)]
599    pub message: Option<String>,
600
601    /// Apply the inverse to the worktree without capturing a new state.
602    #[arg(long)]
603    pub no_commit: bool,
604}
605
606/// Arguments for the `undo` command.
607#[derive(Clone, Debug, clap::Args)]
608#[command(after_help = "\
609Examples:
610  heddle undo --preview      # inspect the most recent operation
611  heddle undo --hard --preview  # preview the worktree rewind --hard would apply
612  heddle undo --hard         # roll it back and rewind the worktree
613  heddle undo -n 3 --hard    # roll back the last three operations
614  heddle undo --recover      # restore the state preserved by the last undo
615  heddle undo --list         # preview undoable operations on this thread
616  heddle undo --dry-run      # show what would change without applying
617
618Restore:
619  `--recover` restores only the last undo's preserved tree as worktree changes.
620  Heddle does not restore one arbitrary file or an arbitrary saved state
621  (no restore/checkout/reset). Materialize a state with
622  `heddle start <name> --from <state> --path <dir>`, or invert one with
623  `heddle revert <state>`.
624
625Undoable operations:
626  - heddle capture           (restores HEAD to the pre-capture parent)
627  - heddle land (non-FF)     (restores HEAD + both thread refs)
628  - heddle land (FF)         (restores HEAD + the landed-into thread ref to
629                              the pre-merge tip; the merged-in thread is
630                              untouched.)
631  - heddle thread switch     (restores HEAD to the previous thread state)
632  - heddle thread create/drop/rename
633  - heddle thread marker create/drop
634  - heddle redact apply               (with --allow-redact-undo; removes the
635                                       redaction record so future materializes
636                                       restore the original blob bytes. Refused
637                                       when a Purge has destroyed the bytes.)
638  - heddle undo --redo                re-apply the most recently undone operation
639
640Not undoable (file a follow-up if you need one):
641  - heddle push / pull                (remote-affecting; out of scope)
642  - heddle redact purge apply         (destructive by design; irreversible)
643  - heddle start <name> --path <dir>  (refused while the materialized worktree
644                                       still exists — run `heddle thread drop
645                                       <name> --delete-thread` first, then
646                                       re-run `heddle undo`)
647  - cross-worktree shared-backend undo (no worktree registry yet; single-
648                                        worktree usage is the supported
649                                        configuration for 0.3)
650")]
651pub struct UndoArgs {
652    /// Undo N operations.
653    #[arg(short = 'n', long, default_value = "1")]
654    pub steps: usize,
655
656    /// List recent operations without undoing.
657    #[arg(long)]
658    pub list: bool,
659
660    /// Number of batches to list.
661    #[arg(long, default_value = "20")]
662    pub depth: usize,
663
664    /// Preview operations without undoing. `--dry-run` is an accepted
665    /// alias kept for muscle memory from git/other VCS tooling.
666    #[arg(long, visible_alias = "dry-run")]
667    pub preview: bool,
668
669    /// Permit undo to rewind worktree files to the selected operation's prior
670    /// state. Without this explicit opt-in, an undo that would rewrite the
671    /// worktree refuses before changing repository state or files.
672    #[arg(long, conflicts_with_all = ["list", "redo", "recover"])]
673    pub hard: bool,
674
675    /// Re-apply operations that a prior `undo` rewound.
676    #[arg(long, conflicts_with = "list")]
677    pub redo: bool,
678
679    /// Restore the checkout-local state preserved by the most recent undo as
680    /// worktree changes. HEAD and the current thread remain unchanged.
681    #[arg(
682        long,
683        conflicts_with_all = ["steps", "list", "preview", "hard", "redo", "allow_redact_undo"]
684    )]
685    pub recover: bool,
686
687    /// Explicit opt-in for undoing a `heddle redact apply`. The inverse
688    /// removes the redaction record so subsequent materializes restore
689    /// the original blob bytes — i.e. previously-hidden content
690    /// becomes readable again. Without this flag, a `heddle undo`
691    /// chain that crosses a Redact refuses loudly rather than silently
692    /// re-exposing the content. Refused regardless of the flag when
693    /// a Purge has destroyed the bytes: Purge is irreversible.
694    #[arg(long)]
695    pub allow_redact_undo: bool,
696}
697
698/// User-facing `--workspace` flag values. Vocabulary is the same as
699/// [`repo::ThreadMode`] (and the on-wire
700/// `thread.mode` JSON field) so a single name carries through the
701/// CLI, the daemon, and the thread record on disk. See
702/// `docs/design/clonefile-threads.md` for the rationale.
703#[derive(Clone, Copy, Debug, clap::ValueEnum, PartialEq, Eq)]
704pub enum WorkspaceModeArg {
705    /// Let Heddle choose the right checkout mode.
706    Auto,
707    /// Create a disk checkout with shared extents when the filesystem supports it.
708    Materialized,
709    /// Use a virtual filesystem checkout when the mount feature is available.
710    Virtualized,
711    /// Copy full files into an isolated checkout.
712    Solid,
713}
714
715/// Arguments for the `thread start` and top-level `start` commands.
716#[derive(Clone, Debug, clap::Args)]
717#[command(after_help = "\
718Examples:
719  heddle start feature/auth --path ../feature-auth  # create an isolated checkout
720  heddle start scratch --path ../scratch            # place the checkout explicitly
721  heddle start fix-flake --path ../fix-flake --task 'fix CI flake'
722
723`--path` is required when workspace is omitted or `auto`. Without it, start
724refuses instead of hiding a checkout under `.heddle/threads/<name>/`.
725`--workspace auto` is the same default and still requires `--path`.
726Pass `--path ../<name>`, or an explicit `--workspace solid|materialized|virtualized`
727if you want the managed layout. To stay on this checkout, use
728`heddle thread create <name>` then `heddle thread switch <name>`.
729
730Isolated 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.
731
732`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.
733
734Advanced (hidden) flags:
735  --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.
736")]
737pub struct ThreadStartArgs {
738    /// Thread name to create or resume.
739    pub name: String,
740
741    /// Base state for the thread (default: HEAD).
742    #[arg(long)]
743    pub from: Option<String>,
744
745    /// Filesystem path for the isolated checkout. Required so the checkout
746    /// is not hidden under `.heddle/threads/`.
747    #[arg(long)]
748    pub path: Option<std::path::PathBuf>,
749
750    /// Workspace mode for the thread. Omitted or `auto` requires `--path`
751    /// so the checkout is not hidden under `.heddle/threads/`.
752    #[arg(long, value_enum)]
753    pub workspace: Option<WorkspaceModeArg>,
754
755    /// AI provider name for the registered agent thread.
756    #[arg(long, hide = true)]
757    pub agent_provider: Option<String>,
758
759    /// AI model name for the registered agent thread.
760    #[arg(long, hide = true)]
761    pub agent_model: Option<String>,
762
763    /// First-class task/goal metadata for the thread.
764    #[arg(long)]
765    pub task: Option<String>,
766
767    /// Parent thread identifier for delegated child work.
768    #[arg(long, hide = true)]
769    pub parent_thread: Option<String>,
770
771    /// Internal hint that this thread was started by automation rather than a direct CLI flow.
772    #[arg(long, hide = true)]
773    pub automated: bool,
774
775    /// Print only the new thread's absolute checkout path to stdout and exit.
776    ///
777    /// Designed for shell wrappers that want to cd into the new checkout:
778    ///   dir=$(heddle start foo --print-cd-path) && cd "$dir"
779    /// Skips all other output (no JSON, no styling, no extra lines) so the
780    /// stdout is a clean path. Mutually exclusive with `--watch`-style flows.
781    #[arg(long, hide = true, conflicts_with_all = ["agent_provider", "agent_model"])]
782    pub print_cd_path: bool,
783
784    /// For `--workspace virtualized`: hand the filesystem mount off to the
785    /// long-lived `heddled` daemon (default). The daemon owns the
786    /// mount across CLI invocations, so the mount survives `heddle
787    /// thread start` exiting. Linux-only; no-op for heavy
788    /// workspaces. Pass `--no-daemon` to keep the mount in-process
789    /// instead.
790    #[arg(
791        long,
792        overrides_with = "no_daemon",
793        action = clap::ArgAction::SetTrue,
794        default_value_t = true,
795        hide = true,
796    )]
797    pub daemon: bool,
798
799    /// For `--workspace virtualized`: keep the filesystem mount in this CLI
800    /// process instead of handing it to the `heddled` daemon. The
801    /// mount unmounts when this `heddle thread start` exits — useful
802    /// for one-shot inspections, debugging the in-process mount path,
803    /// or environments where the daemon can't run.
804    #[arg(
805        long,
806        overrides_with = "daemon",
807        action = clap::ArgAction::SetTrue,
808        hide = true,
809    )]
810    pub no_daemon: bool,
811
812    /// Allow this invocation to open System Settings and wait briefly for
813    /// FSKit approval. Requires an interactive terminal; otherwise setup
814    /// fails before opening a GUI.
815    #[arg(long)]
816    pub interactive_setup: bool,
817
818    /// Redirect cargo's `target/` directory to a workspace-wide shared
819    /// path (`.heddle/targets/<workspace-fingerprint>/`) instead of
820    /// letting cargo create a per-thread `target/`. Saves multiples of
821    /// gigabytes when several materialized threads coexist in a Rust
822    /// workspace. Implemented by writing `.cargo/config.toml` inside
823    /// the new thread checkout — transparent to any `cargo` invocation
824    /// in that directory.
825    ///
826    /// Default: on for solid/materialized threads when the repository
827    /// root has a `Cargo.toml`. Pass `--no-shared-target` to opt out.
828    /// Explicit `--shared-target` forces the attempt on (still a no-op
829    /// without a top-level `Cargo.toml`). Has no effect on virtualized
830    /// (mounted) threads.
831    #[arg(
832        long,
833        overrides_with = "no_shared_target",
834        action = clap::ArgAction::SetTrue,
835        hide = true,
836    )]
837    pub shared_target: bool,
838
839    /// Opt out of the default shared cargo `target/` redirect for
840    /// solid/materialized threads in Rust workspaces. See
841    /// `--shared-target`.
842    #[arg(
843        long,
844        overrides_with = "shared_target",
845        action = clap::ArgAction::SetTrue,
846        hide = true,
847    )]
848    pub no_shared_target: bool,
849
850    /// Symlink the origin checkout's top-level ignored dependency
851    /// directories (`node_modules`, `.venv`, `target`, …) into this
852    /// isolated checkout so it's immediately buildable — run
853    /// `tsc`/`eslint`/tests without reinstalling deps from scratch.
854    ///
855    /// The links point back at the origin's directories and stay
856    /// ignored, so the deps are never captured into heddle. Admin dirs
857    /// (`.git`, `.heddle`) are excluded; only top-level ignored
858    /// directories are linked. Has no effect on virtualized (mounted)
859    /// threads.
860    #[arg(long)]
861    pub hydrate: bool,
862}
863
864/// Arguments for the `ready` command.
865#[derive(Clone, Debug, clap::Args)]
866pub struct ReadyArgs {
867    /// Thread to evaluate for integration readiness.
868    #[arg(long = "thread")]
869    pub thread: Option<String>,
870
871    /// Intent/message to use if `ready` needs to capture outstanding work first.
872    #[arg(short = 'm', long)]
873    pub message: Option<String>,
874
875    /// Honest confidence estimate (0.0-1.0) if `ready` captures outstanding work.
876    #[arg(long, value_parser = parse_confidence)]
877    pub confidence: Option<f32>,
878
879    /// Preview the readiness decision (integration target, conflicts, verify
880    /// verdicts, would-be thread transition) without capturing work or moving
881    /// the thread to Ready/Blocked. No mutation occurs.
882    #[arg(long)]
883    pub dry_run: bool,
884}
885
886/// Arguments for the `sync` command.
887#[derive(Clone, Debug, clap::Args)]
888pub struct SyncArgs {
889    /// Optional sync target. Omit for operator/thread sync.
890    #[cfg(feature = "git-overlay")]
891    #[command(subcommand)]
892    pub command: Option<SyncCommands>,
893
894    /// Thread to refresh (default: current thread).
895    #[arg(long = "thread")]
896    pub thread: Option<String>,
897}
898
899/// Arguments for the `land` command.
900#[derive(Clone, Debug, clap::Args)]
901pub struct LandArgs {
902    /// Thread to capture and integrate (default: current thread).
903    #[arg(long = "thread")]
904    pub thread: Option<String>,
905
906    /// Peer threads to land in order. When `--thread` is also supplied, that
907    /// thread is landed first. Comma-separated, e.g.
908    /// `--threads alpha,beta,gamma`. Each peer is refreshed and landed against
909    /// the live target tip.
910    #[arg(long = "threads", value_delimiter = ',')]
911    pub threads: Vec<String>,
912
913    /// Intent/message to use if land needs to capture outstanding work first.
914    #[arg(short = 'm', long)]
915    pub message: Option<String>,
916
917    /// Preserve per-State Git export instead of squashing the landed thread.
918    #[arg(long)]
919    pub no_squash: bool,
920
921    /// Preview the integration (thread -> target, merge relation, conflicts,
922    /// verify verdicts) without capturing work, syncing, or merging. No
923    /// mutation occurs and no server round-trip is made.
924    #[arg(long)]
925    pub dry_run: bool,
926}
927
928/// Arguments for `thread show`.
929#[derive(Clone, Debug, clap::Args)]
930pub struct ThreadShowArgs {
931    /// Thread identifier. Defaults to the current thread when omitted.
932    pub thread: Option<String>,
933
934    /// Continuously refresh thread status.
935    #[arg(long)]
936    pub watch: bool,
937
938    /// Internal helper for tests: stop after N watch updates.
939    #[arg(long, hide = true)]
940    pub watch_iterations: Option<usize>,
941
942    /// Internal helper for tests: polling interval in milliseconds.
943    #[arg(long, hide = true)]
944    pub watch_interval_ms: Option<u64>,
945}
946
947/// Arguments for `thread captures`.
948#[derive(Clone, Debug, clap::Args)]
949pub struct ThreadCapturesArgs {
950    /// Thread identifier. Defaults to the current thread when omitted.
951    pub thread: Option<String>,
952
953    /// Maximum captures to show.
954    #[arg(long, default_value_t = 20)]
955    pub limit: usize,
956}
957
958/// Arguments for commands that take a thread identifier. Omitting the
959/// positional resolves to the current thread when one can be inferred
960/// from the working checkout.
961#[derive(Clone, Debug, clap::Args)]
962pub struct ThreadNameArgs {
963    /// Thread identifier. Defaults to the current thread when omitted.
964    pub thread: Option<String>,
965}
966
967/// Arguments for `thread rename`.
968#[derive(Clone, Debug, clap::Args)]
969pub struct ThreadRenameArgs {
970    /// Existing thread identifier.
971    pub old: String,
972
973    /// New thread identifier.
974    pub new: String,
975}
976
977/// Arguments for `thread promote`.
978#[derive(Clone, Debug, clap::Args)]
979pub struct ThreadPromoteArgs {
980    /// Thread identifier.
981    pub thread: String,
982
983    /// Materialized checkout path.
984    #[arg(long)]
985    pub path: Option<std::path::PathBuf>,
986
987    /// Discard dirty work in the source checkout while promoting.
988    #[arg(long)]
989    pub force: bool,
990}
991
992/// Arguments for `thread move`.
993#[derive(Clone, Debug, clap::Args)]
994pub struct ThreadMoveArgs {
995    /// Source thread identifier.
996    pub from: String,
997
998    /// Destination thread identifier.
999    pub to: String,
1000
1001    /// Repository-relative path prefix to move.
1002    #[arg(long = "path", required = true, value_name = "PATH")]
1003    pub paths: Vec<String>,
1004
1005    /// Intent/message for the snapshots created by the move.
1006    #[arg(short = 'm', long)]
1007    pub message: Option<String>,
1008}
1009
1010/// Arguments for `thread absorb`.
1011#[derive(Clone, Debug, clap::Args)]
1012pub struct ThreadAbsorbArgs {
1013    /// Child thread to absorb.
1014    pub thread: String,
1015
1016    /// Parent thread to absorb into (default: the thread's recorded parent).
1017    #[arg(long)]
1018    pub into: Option<String>,
1019
1020    /// Commit message for the absorb merge.
1021    #[arg(short = 'm', long)]
1022    pub message: Option<String>,
1023
1024    /// Show the absorb preview without applying it.
1025    #[arg(long)]
1026    pub preview: bool,
1027}
1028
1029/// Arguments for `thread resolve`.
1030#[derive(Clone, Debug, clap::Args)]
1031pub struct ThreadResolveArgs {
1032    /// Thread identifier.
1033    pub thread: String,
1034}
1035
1036/// Arguments for `thread drop`.
1037#[derive(Clone, Debug, clap::Args)]
1038pub struct ThreadDropArgs {
1039    /// Thread identifier.
1040    pub thread: String,
1041
1042    /// Also delete the attached thread ref.
1043    #[arg(long)]
1044    pub delete_thread: bool,
1045
1046    /// Discard uncommitted changes in the thread checkout before dropping it.
1047    #[arg(short, long)]
1048    pub force: bool,
1049}
1050
1051/// Arguments for `thread approve` — record an approval for a
1052/// `<source> -> <target>` merge against the source thread's
1053/// current state.
1054#[derive(Clone, Debug, clap::Args)]
1055pub struct ThreadApproveArgs {
1056    /// Source thread identifier (the change set being merged).
1057    pub source: String,
1058
1059    /// Target thread identifier (where the merge would land).
1060    pub target: String,
1061
1062    /// Optional human note attached to the approval.
1063    #[arg(long)]
1064    pub note: Option<String>,
1065
1066    /// Hosted remote name (default: `origin`).
1067    #[arg(long, default_value = "origin")]
1068    pub remote: String,
1069}
1070
1071/// Arguments for `thread approvals` — list every approval recorded
1072/// for `<source> -> <target>`.
1073#[derive(Clone, Debug, clap::Args)]
1074pub struct ThreadApprovalsArgs {
1075    pub source: String,
1076    pub target: String,
1077    #[arg(long, default_value = "origin")]
1078    pub remote: String,
1079}
1080
1081/// Arguments for `thread revoke-approval` — remove a recorded
1082/// approval by id.
1083#[derive(Clone, Debug, clap::Args)]
1084pub struct ThreadRevokeApprovalArgs {
1085    /// UUID of the approval row to revoke.
1086    pub id: String,
1087    #[arg(long, default_value = "origin")]
1088    pub remote: String,
1089}
1090
1091/// Arguments for `thread check-merge` — query the merge gate
1092/// without recording anything. Returns the unmet requirements.
1093#[derive(Clone, Debug, clap::Args)]
1094pub struct ThreadCheckMergeArgs {
1095    pub source: String,
1096    pub target: String,
1097
1098    /// 'merge' (default), 'force_push', or 'complete'.
1099    #[arg(long, default_value = "merge")]
1100    pub gated_action: String,
1101
1102    /// File paths the diff touches, repeat or comma-separate. Empty =
1103    /// "we don't know" (every path-conditional policy fires).
1104    #[arg(long = "path", value_delimiter = ',')]
1105    pub changed_paths: Vec<String>,
1106
1107    #[arg(long, default_value = "origin")]
1108    pub remote: String,
1109}
1110
1111/// Arguments for the `collapse` command.
1112#[derive(Clone, Debug, clap::Args)]
1113pub struct CollapseArgs {
1114    /// States to collapse.
1115    #[arg(required = true)]
1116    pub states: Vec<String>,
1117
1118    /// Intent/name for the resulting state.
1119    #[arg(long)]
1120    pub into: String,
1121
1122    /// Confidence for the resulting state (0.0-1.0).
1123    #[arg(long)]
1124    pub confidence: Option<f32>,
1125}
1126
1127/// Arguments for the `expand` command.
1128#[derive(Clone, Debug, clap::Args)]
1129pub struct ExpandArgs {
1130    /// Git OID, state spec, or thread name for the squashed land.
1131    pub reference: String,
1132}
1133
1134/// Arguments for the `resolve` command.
1135#[derive(Clone, Debug, clap::Args)]
1136pub struct ResolveArgs {
1137    /// File to resolve.
1138    pub path: Option<String>,
1139
1140    /// Resolve all conflicts.
1141    #[arg(long)]
1142    pub all: bool,
1143
1144    /// List unresolved conflicts.
1145    #[arg(long)]
1146    pub list: bool,
1147
1148    /// Use our version (current thread).
1149    #[arg(long, conflicts_with = "theirs")]
1150    pub ours: bool,
1151
1152    /// Use their version (merged thread).
1153    #[arg(long, conflicts_with = "ours")]
1154    pub theirs: bool,
1155
1156    /// Mark the path resolved even if conflict markers are still present.
1157    #[arg(long)]
1158    pub force: bool,
1159}
1160
1161/// The `(remote, thread)` pair shared by remote commands that use an
1162/// option-only thread selector.
1163#[derive(Clone, Debug, clap::Args)]
1164pub struct RemoteOperationArgs {
1165    /// Heddle remote name, native repository path, or hosted address.
1166    pub remote: Option<String>,
1167
1168    /// Thread to act on.
1169    #[arg(short, long)]
1170    pub thread: Option<String>,
1171
1172    /// Allow cleartext (non-TLS) connections to non-loopback hosts.
1173    /// Prefer enabling TLS; use this only for intentional lab/VPN testing.
1174    #[arg(long)]
1175    pub insecure: bool,
1176}
1177
1178/// Arguments for the `push` command.
1179#[derive(Clone, Debug, clap::Args)]
1180#[command(after_help = "\
1181Git Overlay refs:
1182  A normal push writes refs/heads/<thread> and refs/notes/heddle.
1183  --all-threads writes every refs/heads/<thread> and refs/tags/<tag>, plus refs/notes/heddle.
1184  JSON output lists changed refs in refs_written; verify with git ls-remote <remote>.
1185")]
1186pub struct PushArgs {
1187    /// Heddle remote name, native repository path, or hosted address.
1188    pub remote: Option<String>,
1189
1190    /// Thread to push.
1191    #[arg(short, long, conflicts_with = "thread_arg")]
1192    pub thread: Option<String>,
1193
1194    /// Thread to push; alias for `--thread`.
1195    #[arg(value_name = "THREAD")]
1196    pub thread_arg: Option<String>,
1197
1198    /// State to push (default: HEAD).
1199    #[arg(short, long)]
1200    pub state: Option<String>,
1201
1202    /// Force push.
1203    #[arg(short, long)]
1204    pub force: bool,
1205
1206    /// Push every thread. In Git Overlay, also include every local Git tag.
1207    #[arg(long)]
1208    pub all_threads: bool,
1209
1210    /// Allow cleartext (non-TLS) connections to non-loopback hosts.
1211    /// Prefer enabling TLS; use this only for intentional lab/VPN testing.
1212    #[arg(long)]
1213    pub insecure: bool,
1214
1215    /// Preview the push plan (target, thread/track ref, state that would be
1216    /// published, force status) without pushing, moving refs, capturing work,
1217    /// running hooks, or contacting the server for anything beyond read/plan.
1218    #[arg(long)]
1219    pub dry_run: bool,
1220}
1221
1222impl PushArgs {
1223    pub fn thread_name(&self) -> Option<String> {
1224        self.thread.clone().or_else(|| self.thread_arg.clone())
1225    }
1226}
1227
1228/// Arguments for the `pull` command.
1229#[derive(Clone, Debug, clap::Args)]
1230#[command(after_help = "\
1231Advanced (hidden) flags:
1232  --lazy leaves blob content absent by design and hydrates it explicitly later. Hosted/network Heddle remotes only.
1233")]
1234pub struct PullArgs {
1235    #[command(flatten)]
1236    pub remote_op: RemoteOperationArgs,
1237
1238    /// Local thread to update.
1239    #[arg(short, long)]
1240    pub local_thread: Option<String>,
1241
1242    /// Leave blob content absent by design and hydrate it explicitly later.
1243    #[arg(long, hide = true)]
1244    pub lazy: bool,
1245}
1246
1247/// Arguments for the `clone` command.
1248///
1249/// Help style budget (heddle#652): `--help` carries the signature, flags,
1250/// a one-screen Behavior summary, and the hidden-flag breadcrumb
1251/// (heddle#646). The full default-thread fallback chain and --depth
1252/// exposition moved to `heddle help clone` (help.rs CLONE_TOPIC); keep
1253/// flag docs single-line so clap renders the compact help layout.
1254#[derive(Clone, Debug, clap::Args)]
1255#[command(after_help = "\
1256Behavior:
1257  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`.
1258
1259Advanced/planned flags: see `heddle help clone`.
1260
1261Examples:
1262  heddle clone ../native-repo ./clone                # local native Heddle repository
1263  heddle clone heddle://host/repo ./clone --depth 1   # shallow Heddle clone: tip plus immediate parents
1264")]
1265pub struct CloneArgs {
1266    /// Remote repository path.
1267    pub remote: String,
1268
1269    /// Local directory to clone into.
1270    pub local: String,
1271
1272    /// Thread to check out after cloning.
1273    #[arg(long)]
1274    pub thread: Option<String>,
1275
1276    /// Create a shallow clone with the specified depth. `0` means full history.
1277    #[arg(long)]
1278    pub depth: Option<u32>,
1279
1280    // Hosted/network remotes only. The user-facing exposition lives in the
1281    // after-help breadcrumb above and `heddle help clone`.
1282    /// Leave blob content absent by design and hydrate it explicitly later.
1283    #[arg(long, hide = true)]
1284    pub lazy: bool,
1285
1286    /// Allow cleartext (non-TLS) connections to non-loopback hosts.
1287    #[arg(long)]
1288    pub insecure: bool,
1289
1290    // Only `blob:none` is accepted (a synonym for --lazy on hosted
1291    // remotes); git-style filters such as `tree:0` or `blob:limit=…` are
1292    // rejected at parse time. See the after-help breadcrumb and `heddle help
1293    // clone`.
1294    /// Partial-clone filter spec (`blob:none` only).
1295    #[arg(long, hide = true, value_name = "SPEC", value_parser = parse_clone_filter_spec)]
1296    pub filter: Option<String>,
1297
1298    /// Clone a whole hosted monorepo: resolve the root spool's child tree and
1299    /// clone every child spool at its anchored state into its mount path.
1300    /// Hosted/network remotes only. (Alias: --monorepo.)
1301    #[arg(long, visible_alias = "monorepo")]
1302    pub recursive: bool,
1303}
1304
1305fn parse_clone_filter_spec(s: &str) -> Result<String, String> {
1306    match s {
1307        "blob:none" => Ok(s.to_string()),
1308        other => Err(format!(
1309            "unsupported --filter spec `{other}`; only `blob:none` is supported today"
1310        )),
1311    }
1312}
1313
1314/// Arguments for `agent provenance begin`.
1315#[derive(Clone, Debug, clap::Args)]
1316pub struct AgentProvenanceBeginArgs {
1317    /// Provider name (e.g., "anthropic", "openai").
1318    #[arg(long)]
1319    pub provider: String,
1320
1321    /// Model identifier (e.g., "claude-opus-4").
1322    #[arg(long)]
1323    pub model: String,
1324
1325    /// Policy or prompt template ID.
1326    #[arg(long)]
1327    pub policy: Option<String>,
1328}
1329
1330/// Arguments for `agent provenance segment`.
1331#[derive(Clone, Debug, clap::Args)]
1332pub struct AgentProvenanceSegmentArgs {
1333    /// Provider name (e.g., "anthropic", "openai").
1334    #[arg(long)]
1335    pub provider: String,
1336
1337    /// Model identifier (e.g., "claude-opus-4").
1338    #[arg(long)]
1339    pub model: String,
1340
1341    /// Policy or prompt template ID.
1342    #[arg(long)]
1343    pub policy: Option<String>,
1344}
1345
1346/// Arguments for `agent provenance end`.
1347#[derive(Clone, Debug, clap::Args)]
1348pub struct AgentProvenanceEndArgs {
1349    /// Session ID to end (default: current session).
1350    pub session_id: Option<String>,
1351}
1352
1353/// Arguments for `agent provenance show`.
1354#[derive(Clone, Debug, clap::Args)]
1355pub struct AgentProvenanceShowArgs {
1356    /// Session ID to show (default: current session).
1357    pub session_id: Option<String>,
1358}
1359
1360/// Arguments for `agent provenance list`.
1361#[derive(Clone, Debug, clap::Args)]
1362pub struct AgentProvenanceListArgs {
1363    /// Show only active sessions.
1364    #[arg(long)]
1365    pub active: bool,
1366}
1367
1368/// Arguments for the `worktree add` command.
1369#[derive(Clone, Debug, clap::Args)]
1370pub struct WorktreeAddArgs {
1371    /// Path to the new agent checkout directory.
1372    pub path: std::path::PathBuf,
1373
1374    /// Thread name for the agent (created if absent, default: HEAD thread).
1375    #[arg(long)]
1376    pub thread: Option<String>,
1377
1378    /// Base state to materialize (default: HEAD).
1379    #[arg(long)]
1380    pub from: Option<String>,
1381}
1382
1383/// Arguments for the `worktree remove` command.
1384#[derive(Clone, Debug, clap::Args)]
1385pub struct WorktreeRemoveArgs {
1386    /// Path to the isolated checkout directory to remove.
1387    pub path: std::path::PathBuf,
1388
1389    /// Also delete the associated thread ref, if this checkout is attached.
1390    #[arg(long)]
1391    pub delete_thread: bool,
1392}
1393
1394/// Arguments for `presence list`.
1395#[derive(Clone, Debug, clap::Args)]
1396pub struct AgentPresenceListArgs {
1397    /// Show only active actors.
1398    #[arg(long)]
1399    pub active: bool,
1400}
1401
1402/// Arguments for `presence show`.
1403#[derive(Clone, Debug, clap::Args)]
1404pub struct AgentPresenceShowArgs {
1405    /// Session ID to show (default: current thread actor).
1406    pub session: Option<String>,
1407}
1408
1409/// Arguments for `presence explain`.
1410#[derive(Clone, Debug, clap::Args)]
1411pub struct AgentPresenceExplainArgs {
1412    /// Session ID to explain (default: current thread actor).
1413    pub session: Option<String>,
1414}
1415
1416/// Arguments for `presence complete`.
1417#[derive(Clone, Debug, clap::Args)]
1418pub struct AgentPresenceCompleteArgs {
1419    /// Session ID to mark as complete (default: current thread actor).
1420    #[arg(long)]
1421    pub session: Option<String>,
1422}
1423
1424/// Arguments for `agent reserve`.
1425#[derive(Clone, Debug, clap::Args)]
1426pub struct AgentReserveArgs {
1427    /// Thread to reserve.
1428    #[arg(long)]
1429    pub thread: String,
1430
1431    /// Anchor state spec (default: current HEAD).
1432    #[arg(long)]
1433    pub anchor: Option<String>,
1434
1435    /// Optional task description.
1436    #[arg(long)]
1437    pub task: Option<String>,
1438
1439    /// Local agent task assignment id to attach to this reservation.
1440    #[arg(long)]
1441    pub task_id: Option<String>,
1442
1443    /// Reap the lease early when this long-lived owner process exits.
1444    #[arg(long, value_name = "PID")]
1445    pub hold_for_pid: Option<u32>,
1446}
1447
1448/// Arguments for `agent heartbeat`.
1449#[derive(Clone, Debug, clap::Args)]
1450pub struct AgentHeartbeatArgs {
1451    /// Writer lease id returned by `agent reserve`.
1452    #[arg(long)]
1453    pub lease: String,
1454
1455    /// Bearer token returned by `agent reserve`.
1456    #[arg(long, env = "HEDDLE_RESERVATION_TOKEN", hide_env_values = true)]
1457    pub token: String,
1458}
1459
1460/// Arguments for `agent release`.
1461#[derive(Clone, Debug, clap::Args)]
1462pub struct AgentReleaseArgs {
1463    /// Writer lease id returned by `agent reserve`.
1464    #[arg(long)]
1465    pub lease: String,
1466
1467    /// Bearer token returned by `agent reserve`.
1468    #[arg(long, env = "HEDDLE_RESERVATION_TOKEN", hide_env_values = true)]
1469    pub token: String,
1470
1471    /// Terminal status to record.
1472    #[arg(long, default_value = "complete")]
1473    pub status: AgentReleaseStatusArg,
1474}
1475
1476#[derive(Clone, Debug, clap::ValueEnum)]
1477pub enum AgentReleaseStatusArg {
1478    Complete,
1479    Abandoned,
1480}
1481
1482/// Arguments for `agent list`.
1483#[derive(Clone, Debug, clap::Args)]
1484pub struct AgentApiListArgs {
1485    /// Filter by thread.
1486    #[arg(long)]
1487    pub thread: Option<String>,
1488
1489    /// Show only active reservations.
1490    #[arg(long)]
1491    pub alive_only: bool,
1492}
1493
1494#[derive(Clone, Debug, clap::ValueEnum)]
1495pub enum AgentTaskStatusArg {
1496    Open,
1497    InProgress,
1498    Blocked,
1499    Complete,
1500    Abandoned,
1501}
1502
1503/// Arguments for `agent task create`.
1504#[derive(Clone, Debug, clap::Args)]
1505pub struct AgentTaskCreateArgs {
1506    /// Optional caller-provided task id (default: generated task UUIDv7 id).
1507    #[arg(long)]
1508    pub task_id: Option<String>,
1509
1510    /// Human-readable task title.
1511    #[arg(long)]
1512    pub title: String,
1513
1514    /// Detailed task body.
1515    #[arg(long)]
1516    pub body: Option<String>,
1517
1518    /// Thread this task targets.
1519    #[arg(long)]
1520    pub thread: String,
1521
1522    /// Optional base state id this task was delegated from.
1523    #[arg(long)]
1524    pub base_state: Option<String>,
1525
1526    /// Optional base root id this task was delegated from.
1527    #[arg(long)]
1528    pub base_root: Option<String>,
1529
1530    /// Optional parent task id.
1531    #[arg(long)]
1532    pub parent_task_id: Option<String>,
1533
1534    /// Optional coordination discussion id.
1535    #[arg(long)]
1536    pub coordination_discussion_id: Option<String>,
1537
1538    /// Allow this task to continue without hosted connectivity.
1539    #[arg(long)]
1540    pub allow_offline: bool,
1541
1542    /// Principal or agent that delegated this task.
1543    #[arg(long)]
1544    pub delegated_by: Option<String>,
1545}
1546
1547/// Arguments for `agent task list`.
1548#[derive(Clone, Debug, clap::Args)]
1549pub struct AgentTaskListArgs {
1550    /// Filter by target thread.
1551    #[arg(long)]
1552    pub thread: Option<String>,
1553
1554    /// Filter by task status.
1555    #[arg(long)]
1556    pub status: Option<AgentTaskStatusArg>,
1557}
1558
1559/// Arguments for `agent task show`.
1560#[derive(Clone, Debug, clap::Args)]
1561pub struct AgentTaskShowArgs {
1562    /// Task id to show.
1563    pub task_id: String,
1564}
1565
1566/// Arguments for `agent task update`.
1567#[derive(Clone, Debug, clap::Args)]
1568pub struct AgentTaskUpdateArgs {
1569    /// Task id to update.
1570    pub task_id: String,
1571
1572    /// Replace the task title.
1573    #[arg(long)]
1574    pub title: Option<String>,
1575
1576    /// Replace the task body.
1577    #[arg(long)]
1578    pub body: Option<String>,
1579
1580    /// Replace the task status.
1581    #[arg(long)]
1582    pub status: Option<AgentTaskStatusArg>,
1583
1584    /// Replace the target thread.
1585    #[arg(long)]
1586    pub thread: Option<String>,
1587
1588    /// Replace the base state id.
1589    #[arg(long)]
1590    pub base_state: Option<String>,
1591
1592    /// Replace the base root id.
1593    #[arg(long)]
1594    pub base_root: Option<String>,
1595
1596    /// Replace the parent task id.
1597    #[arg(long)]
1598    pub parent_task_id: Option<String>,
1599
1600    /// Replace the coordination discussion id.
1601    #[arg(long)]
1602    pub coordination_discussion_id: Option<String>,
1603
1604    /// Allow this task to continue without hosted connectivity.
1605    #[arg(long, conflicts_with = "no_allow_offline")]
1606    pub allow_offline: bool,
1607
1608    /// Disallow offline continuation for this task.
1609    #[arg(long, conflicts_with = "allow_offline")]
1610    pub no_allow_offline: bool,
1611
1612    /// Replace the delegating principal or agent label.
1613    #[arg(long)]
1614    pub delegated_by: Option<String>,
1615}
1616
1617/// Arguments shared by `agent fanout plan` and `agent fanout start`.
1618#[derive(Clone, Debug, clap::Args)]
1619pub struct AgentFanoutPlanArgs {
1620    /// Parent coordination task title.
1621    #[arg(long)]
1622    pub title: String,
1623
1624    /// Lane spec: `<thread>=<path>:<title>`. Repeat once per child lane.
1625    #[arg(long, value_name = "THREAD=PATH:TITLE")]
1626    pub lane: Vec<String>,
1627
1628    /// Optional collaboration discussion id to store on task assignments.
1629    #[arg(long)]
1630    pub coordination_discussion_id: Option<String>,
1631}
1632
1633/// Arguments for `agent fanout start`.
1634#[derive(Clone, Debug, clap::Args)]
1635pub struct AgentFanoutStartArgs {
1636    /// Parent coordination task title.
1637    #[arg(long)]
1638    pub title: String,
1639
1640    /// Lane spec: `<thread>=<path>:<title>`. Repeat once per child lane.
1641    #[arg(long, value_name = "THREAD=PATH:TITLE")]
1642    pub lane: Vec<String>,
1643
1644    /// Optional collaboration discussion id to store on task assignments.
1645    #[arg(long)]
1646    pub coordination_discussion_id: Option<String>,
1647}
1648
1649/// Arguments for `agent capture` under a current reservation lease.
1650#[derive(Clone, Debug, clap::Args)]
1651pub struct AgentCaptureArgs {
1652    /// Writer lease id returned by `agent reserve`.
1653    #[arg(long)]
1654    pub lease: String,
1655
1656    /// Bearer token returned by `agent reserve`.
1657    #[arg(long, env = "HEDDLE_RESERVATION_TOKEN", hide_env_values = true)]
1658    pub token: String,
1659
1660    /// Capture intent / commit message.
1661    #[arg(long, short = 'm', alias = "intent")]
1662    pub message: Option<String>,
1663
1664    /// Honest confidence estimate (0.0–1.0).
1665    #[arg(long, value_parser = parse_confidence)]
1666    pub confidence: Option<f32>,
1667}
1668
1669/// Arguments for `agent ready` under a writer lease.
1670#[derive(Clone, Debug, clap::Args)]
1671pub struct AgentReadyArgs {
1672    /// Writer lease id returned by `agent reserve`.
1673    #[arg(long)]
1674    pub lease: String,
1675
1676    /// Bearer token returned by `agent reserve`.
1677    #[arg(long, env = "HEDDLE_RESERVATION_TOKEN", hide_env_values = true)]
1678    pub token: String,
1679
1680    /// Optional summary message.
1681    #[arg(long, short = 'm')]
1682    pub message: Option<String>,
1683
1684    /// Honest confidence estimate (0.0-1.0) if `agent ready` captures outstanding work.
1685    #[arg(long, value_parser = parse_confidence)]
1686    pub confidence: Option<f32>,
1687}
1688
1689/// Arguments for the `watch` command.
1690///
1691/// Streams live oplog activity (snapshots, merges, thread create/update,
1692/// markers, etc.) as it happens. Default behavior tails forever and exits
1693/// on Ctrl-C. `--since 5m` replays the last N before tailing live;
1694/// `--filter` restricts output to the named kinds; `--output json` emits one
1695/// JSON object per line for piping to `jq`.
1696#[derive(Clone, Debug, clap::Args)]
1697pub struct WatchArgs {
1698    /// Replay events from this duration ago (e.g. `30s`, `5m`, `1h`,
1699    /// `2d`) before tailing live. When unset, only new events are
1700    /// emitted.
1701    #[arg(long, value_name = "DURATION")]
1702    pub since: Option<String>,
1703
1704    /// Comma-separated event kinds to include
1705    /// (`snapshot,merge,thread_create,thread_update,thread_delete,
1706    /// collapse,thread_marker_create,thread_marker_delete`).
1707    #[arg(long, value_name = "KINDS")]
1708    pub filter: Option<String>,
1709
1710    /// Internal helper for tests: stop after the oplog file produces
1711    /// this many modify events (still drains pending entries first).
1712    #[arg(long, hide = true)]
1713    pub max_iterations: Option<usize>,
1714
1715    /// Internal helper for tests: poll interval in milliseconds for
1716    /// the `notify` watcher's debounce check (default 200ms).
1717    #[arg(long, hide = true)]
1718    pub poll_interval_ms: Option<u64>,
1719}
1720
1721// `AgentCaptureArgs` and `AgentReadyArgs` defined earlier in this
1722// file. A second copy was left here by the rebase (the workstreams
1723// commit added them twice when the cherry-pick had lost the
1724// originals and we re-added them mid-rebase). Removed.
1725
1726#[cfg(test)]
1727mod capture_message_alias_tests {
1728    use clap::Parser;
1729
1730    use crate::cli::{Cli, Commands, SnapshotArgs};
1731
1732    fn parse_capture(extra: &[&str]) -> Result<SnapshotArgs, clap::Error> {
1733        let mut argv: Vec<&str> = vec!["heddle", "capture"];
1734        argv.extend_from_slice(extra);
1735        let cli = Cli::try_parse_from(argv)?;
1736        match cli.command {
1737            Commands::Capture(args) => Ok(args),
1738            _ => panic!("expected Commands::Capture"),
1739        }
1740    }
1741
1742    #[test]
1743    fn capture_accepts_message_alias() {
1744        let args = parse_capture(&["--message", "my change"]).expect("--message should parse");
1745        assert_eq!(args.intent.as_deref(), Some("my change"));
1746    }
1747
1748    #[test]
1749    fn capture_accepts_intent_long_form() {
1750        let args = parse_capture(&["--intent", "my change"]).expect("--intent should parse");
1751        assert_eq!(args.intent.as_deref(), Some("my change"));
1752    }
1753
1754    #[test]
1755    fn capture_accepts_short_m() {
1756        let args = parse_capture(&["-m", "my change"]).expect("-m should parse");
1757        assert_eq!(args.intent.as_deref(), Some("my change"));
1758    }
1759
1760    #[test]
1761    fn capture_parses_without_intent_so_the_refuse_can_fire() {
1762        let args =
1763            parse_capture(&[]).expect("omitted -m is a semantic refuse, not a clap usage error");
1764        assert!(args.intent.is_none());
1765    }
1766
1767    #[test]
1768    fn capture_rejects_non_finite_or_out_of_range_confidence() {
1769        for value in ["NaN", "inf", "-0.1", "1.7"] {
1770            let confidence_arg = format!("--confidence={value}");
1771            let err = parse_capture(&["-m", "bad confidence", &confidence_arg])
1772                .expect_err("invalid confidence should fail to parse");
1773            assert!(
1774                err.to_string()
1775                    .contains("confidence must be a finite number from 0.0 to 1.0"),
1776                "unexpected parse error for {value}: {err}"
1777            );
1778        }
1779    }
1780}
1781
1782#[cfg(test)]
1783mod clone_filter_tests {
1784    use clap::Parser;
1785
1786    use crate::cli::{Cli, CloneArgs, Commands};
1787
1788    fn parse_clone(extra: &[&str]) -> Result<CloneArgs, clap::Error> {
1789        let mut argv: Vec<&str> = vec!["heddle", "clone", "remote", "local"];
1790        argv.extend_from_slice(extra);
1791        let cli = Cli::try_parse_from(argv)?;
1792        match cli.command {
1793            Commands::Clone(args) => Ok(args),
1794            _ => panic!("expected Commands::Clone"),
1795        }
1796    }
1797
1798    #[test]
1799    fn parses_clone_filter_blob_none() {
1800        let args = parse_clone(&["--filter", "blob:none"]).expect("parse --filter blob:none");
1801        assert_eq!(args.filter.as_deref(), Some("blob:none"));
1802        assert!(!args.lazy);
1803    }
1804
1805    #[test]
1806    fn rejects_unknown_filter_spec() {
1807        let err = parse_clone(&["--filter", "tree:0"])
1808            .expect_err("unknown --filter spec should fail to parse");
1809        let msg = err.to_string();
1810        assert!(
1811            msg.contains("tree:0") && msg.contains("blob:none"),
1812            "error should name the bad spec and the supported one: {msg}"
1813        );
1814    }
1815}