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