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)]
1027pub struct ResolveArgs {
1028    /// File to resolve.
1029    pub path: Option<String>,
1030
1031    /// Resolve all conflicts.
1032    #[arg(long)]
1033    pub all: bool,
1034
1035    /// List unresolved conflicts.
1036    #[arg(long)]
1037    pub list: bool,
1038
1039    /// Use our version (current thread).
1040    #[arg(long, conflicts_with = "theirs")]
1041    pub ours: bool,
1042
1043    /// Use their version (merged thread).
1044    #[arg(long, conflicts_with = "ours")]
1045    pub theirs: bool,
1046
1047    /// Mark the path resolved even if conflict markers are still present.
1048    #[arg(long)]
1049    pub force: bool,
1050}
1051
1052/// The `(remote, thread)` pair shared by remote commands that use an
1053/// option-only thread selector.
1054#[derive(Clone, Debug, clap::Args)]
1055pub struct RemoteOperationArgs {
1056    /// Heddle remote name, native repository path, or hosted address.
1057    pub remote: Option<String>,
1058
1059    /// Thread to act on.
1060    #[arg(short, long)]
1061    pub thread: Option<String>,
1062
1063    /// Allow cleartext (non-TLS) connections to non-loopback hosts.
1064    /// Prefer enabling TLS; use this only for intentional lab/VPN testing.
1065    #[arg(long)]
1066    pub insecure: bool,
1067}
1068
1069/// Arguments for the `push` command.
1070#[derive(Clone, Debug, clap::Args)]
1071#[command(after_help = "\
1072Git Overlay refs:
1073  A normal push writes refs/heads/<thread> and refs/notes/heddle.
1074  --all-threads writes every refs/heads/<thread> and refs/tags/<tag>, plus refs/notes/heddle.
1075  JSON output lists changed refs in refs_written; verify with git ls-remote <remote>.
1076")]
1077pub struct PushArgs {
1078    /// Heddle remote name, native repository path, or hosted address.
1079    pub remote: Option<String>,
1080
1081    /// Thread to push.
1082    #[arg(short, long, conflicts_with = "thread_arg")]
1083    pub thread: Option<String>,
1084
1085    /// Thread to push; alias for `--thread`.
1086    #[arg(value_name = "THREAD")]
1087    pub thread_arg: Option<String>,
1088
1089    /// State to push (default: HEAD).
1090    #[arg(short, long)]
1091    pub state: Option<String>,
1092
1093    /// Force push.
1094    #[arg(short, long)]
1095    pub force: bool,
1096
1097    /// Push every thread. In Git Overlay, also include every local Git tag.
1098    #[arg(long)]
1099    pub all_threads: bool,
1100
1101    /// Allow cleartext (non-TLS) connections to non-loopback hosts.
1102    /// Prefer enabling TLS; use this only for intentional lab/VPN testing.
1103    #[arg(long)]
1104    pub insecure: bool,
1105
1106    #[command(flatten)]
1107    pub dry_run: super::DryRunArgs,
1108}
1109
1110impl PushArgs {
1111    pub fn thread_name(&self) -> Option<String> {
1112        self.thread.clone().or_else(|| self.thread_arg.clone())
1113    }
1114}
1115
1116/// Arguments for the `pull` command.
1117#[derive(Clone, Debug, clap::Args)]
1118#[command(after_help = "\
1119Advanced (hidden) flags:
1120  --lazy is reserved for hosted lazy hydration and is rejected until end-to-end support lands.
1121")]
1122pub struct PullArgs {
1123    #[command(flatten)]
1124    pub remote_op: RemoteOperationArgs,
1125
1126    /// Local thread to update.
1127    #[arg(short, long)]
1128    pub local_thread: Option<String>,
1129
1130    /// Request lazy blobs. Hosted pull currently rejects this planned mode.
1131    #[arg(long, hide = true)]
1132    pub lazy: bool,
1133}
1134
1135/// Arguments for the `clone` command.
1136///
1137/// Help style budget (heddle#652): `--help` carries the signature, flags,
1138/// a one-screen Behavior summary, and the hidden-flag breadcrumb
1139/// (heddle#646). The full default-thread fallback chain and --depth
1140/// exposition moved to `heddle help clone` (help.rs CLONE_TOPIC); keep
1141/// flag docs single-line so clap renders the compact help layout.
1142#[derive(Clone, Debug, clap::Args)]
1143#[command(after_help = "\
1144Behavior:
1145  `--source git|heddle` selects the protocol; omitted, `.git` URLs are Git and other HTTPS URLs are hosted Heddle. No protocol retry on failure. Convert a local Git checkout with `heddle import local`. Full details: `heddle help clone`.
1146
1147Advanced/planned flags: see `heddle help clone`.
1148")]
1149pub struct CloneArgs {
1150    /// Remote repository URL or path.
1151    pub remote: String,
1152
1153    /// Local directory to clone into. Optional when the source has an unambiguous basename.
1154    pub local: Option<String>,
1155
1156    /// Clone protocol (`git` or `heddle`). Failures are not retried on the other protocol.
1157    #[arg(long, value_enum, hide_possible_values = true)]
1158    pub source: Option<super::CloneSourceArg>,
1159
1160    /// Thread to check out after cloning.
1161    #[arg(long)]
1162    pub thread: Option<String>,
1163
1164    /// Request a history depth. Hosted native clones currently accept only `0` (full history).
1165    #[arg(long)]
1166    pub depth: Option<u32>,
1167
1168    // Planned hosted syntax. The user-facing exposition lives in the after-help
1169    // breadcrumb above and `heddle help clone`.
1170    /// Request lazy blobs. Hosted native clones currently reject this planned mode.
1171    #[arg(long, hide = true)]
1172    pub lazy: bool,
1173
1174    /// Allow cleartext (non-TLS) connections to non-loopback hosts.
1175    #[arg(long)]
1176    pub insecure: bool,
1177
1178    // Only the planned `blob:none` spelling is accepted. Git-style filters
1179    // such as `tree:0` or `blob:limit=…` are
1180    // rejected at parse time. See the after-help breadcrumb and `heddle help
1181    // clone`.
1182    /// Request lazy blobs (`blob:none` only). Hosted native clones currently reject it.
1183    #[arg(long, hide = true, value_name = "SPEC", value_parser = parse_clone_filter_spec)]
1184    pub filter: Option<String>,
1185
1186    /// Clone a whole hosted monorepo: resolve the root spool's child tree and
1187    /// clone every child spool at its anchored state into its mount path.
1188    /// Hosted/network remotes only. (Alias: --monorepo.)
1189    #[arg(long, visible_alias = "monorepo")]
1190    pub recursive: bool,
1191}
1192
1193impl CloneArgs {
1194    /// Destination directory: explicit `DIR`, or a safe basename derived from
1195    /// the source. Errors when the basename is missing or ambiguous.
1196    pub fn destination_dir(&self) -> Result<String, String> {
1197        if let Some(local) = self
1198            .local
1199            .as_deref()
1200            .map(str::trim)
1201            .filter(|value| !value.is_empty())
1202        {
1203            return Ok(local.to_string());
1204        }
1205        super::safe_clone_destination_basename(&self.remote).ok_or_else(|| {
1206            "clone destination is required when the source has no unambiguous basename; pass a directory".to_string()
1207        })
1208    }
1209}
1210
1211fn parse_clone_filter_spec(s: &str) -> Result<String, String> {
1212    match s {
1213        "blob:none" => Ok(s.to_string()),
1214        other => Err(format!(
1215            "unsupported --filter spec `{other}`; only `blob:none` is supported today"
1216        )),
1217    }
1218}
1219
1220/// Arguments for `agent provenance begin`.
1221#[derive(Clone, Debug, clap::Args)]
1222pub struct AgentProvenanceBeginArgs {
1223    /// Provider name (e.g., "anthropic", "openai").
1224    #[arg(long)]
1225    pub provider: String,
1226
1227    /// Model identifier (e.g., "claude-opus-4").
1228    #[arg(long)]
1229    pub model: String,
1230
1231    /// Policy or prompt template ID.
1232    #[arg(long)]
1233    pub policy: Option<String>,
1234}
1235
1236/// Arguments for `agent provenance segment`.
1237#[derive(Clone, Debug, clap::Args)]
1238pub struct AgentProvenanceSegmentArgs {
1239    /// Provider name (e.g., "anthropic", "openai").
1240    #[arg(long)]
1241    pub provider: String,
1242
1243    /// Model identifier (e.g., "claude-opus-4").
1244    #[arg(long)]
1245    pub model: String,
1246
1247    /// Policy or prompt template ID.
1248    #[arg(long)]
1249    pub policy: Option<String>,
1250}
1251
1252/// Arguments for `agent provenance end`.
1253#[derive(Clone, Debug, clap::Args)]
1254pub struct AgentProvenanceEndArgs {
1255    /// Session ID to end (default: current session).
1256    pub session_id: Option<String>,
1257}
1258
1259/// Arguments for `agent provenance show`.
1260#[derive(Clone, Debug, clap::Args)]
1261pub struct AgentProvenanceShowArgs {
1262    /// Session ID to show (default: current session).
1263    pub session_id: Option<String>,
1264}
1265
1266/// Arguments for `agent provenance list`.
1267#[derive(Clone, Debug, clap::Args)]
1268pub struct AgentProvenanceListArgs {
1269    /// Show only active sessions.
1270    #[arg(long)]
1271    pub active: bool,
1272}
1273
1274/// Arguments for the `worktree add` command.
1275#[derive(Clone, Debug, clap::Args)]
1276pub struct WorktreeAddArgs {
1277    /// Path to the new agent checkout directory.
1278    pub path: std::path::PathBuf,
1279
1280    /// Thread name for the agent (created if absent, default: HEAD thread).
1281    #[arg(long)]
1282    pub thread: Option<String>,
1283
1284    /// Base state to materialize (default: HEAD).
1285    #[arg(long)]
1286    pub from: Option<String>,
1287}
1288
1289/// Arguments for the `worktree remove` command.
1290#[derive(Clone, Debug, clap::Args)]
1291pub struct WorktreeRemoveArgs {
1292    /// Path to the isolated checkout directory to remove.
1293    pub path: std::path::PathBuf,
1294
1295    /// Also delete the associated thread ref, if this checkout is attached.
1296    #[arg(long)]
1297    pub delete_thread: bool,
1298}
1299
1300/// Arguments for `presence list`.
1301#[derive(Clone, Debug, clap::Args)]
1302pub struct AgentPresenceListArgs {
1303    /// Show only active actors.
1304    #[arg(long)]
1305    pub active: bool,
1306}
1307
1308/// Arguments for `presence show`.
1309#[derive(Clone, Debug, clap::Args)]
1310pub struct AgentPresenceShowArgs {
1311    /// Session ID to show (default: current thread actor).
1312    pub session: Option<String>,
1313}
1314
1315/// Arguments for `presence explain`.
1316#[derive(Clone, Debug, clap::Args)]
1317pub struct AgentPresenceExplainArgs {
1318    /// Session ID to explain (default: current thread actor).
1319    pub session: Option<String>,
1320}
1321
1322/// Arguments for `presence complete`.
1323#[derive(Clone, Debug, clap::Args)]
1324pub struct AgentPresenceCompleteArgs {
1325    /// Session ID to mark as complete (default: current thread actor).
1326    #[arg(long)]
1327    pub session: Option<String>,
1328}
1329
1330/// Arguments for `agent reserve`.
1331#[derive(Clone, Debug, clap::Args)]
1332pub struct AgentReserveArgs {
1333    /// Thread to reserve.
1334    #[arg(long)]
1335    pub thread: String,
1336
1337    /// Anchor state spec (default: current HEAD).
1338    #[arg(long)]
1339    pub anchor: Option<String>,
1340
1341    /// Optional task description.
1342    #[arg(long)]
1343    pub task: Option<String>,
1344
1345    /// Local agent task assignment id to attach to this reservation.
1346    #[arg(long)]
1347    pub task_id: Option<String>,
1348
1349    /// Reap the lease early when this long-lived owner process exits.
1350    #[arg(long, value_name = "PID")]
1351    pub hold_for_pid: Option<u32>,
1352}
1353
1354/// Arguments for `agent heartbeat`.
1355#[derive(Clone, Debug, clap::Args)]
1356pub struct AgentHeartbeatArgs {
1357    /// Writer lease id returned by `agent reserve`.
1358    #[arg(long)]
1359    pub lease: String,
1360
1361    /// Bearer token returned by `agent reserve`.
1362    #[arg(long, env = "HEDDLE_RESERVATION_TOKEN", hide_env_values = true)]
1363    pub token: String,
1364}
1365
1366/// Arguments for `agent release`.
1367#[derive(Clone, Debug, clap::Args)]
1368pub struct AgentReleaseArgs {
1369    /// Writer lease id returned by `agent reserve`.
1370    #[arg(long)]
1371    pub lease: String,
1372
1373    /// Bearer token returned by `agent reserve`.
1374    #[arg(long, env = "HEDDLE_RESERVATION_TOKEN", hide_env_values = true)]
1375    pub token: String,
1376
1377    /// Terminal status to record.
1378    #[arg(long, default_value = "complete")]
1379    pub status: AgentReleaseStatusArg,
1380}
1381
1382#[derive(Clone, Debug, clap::ValueEnum)]
1383pub enum AgentReleaseStatusArg {
1384    Complete,
1385    Abandoned,
1386}
1387
1388/// Arguments for `agent list`.
1389#[derive(Clone, Debug, clap::Args)]
1390pub struct AgentApiListArgs {
1391    /// Filter by thread.
1392    #[arg(long)]
1393    pub thread: Option<String>,
1394
1395    /// Show only active reservations.
1396    #[arg(long)]
1397    pub alive_only: bool,
1398}
1399
1400#[derive(Clone, Debug, clap::ValueEnum)]
1401pub enum AgentTaskStatusArg {
1402    Open,
1403    InProgress,
1404    Blocked,
1405    Complete,
1406    Abandoned,
1407}
1408
1409/// Arguments for `agent task create`.
1410#[derive(Clone, Debug, clap::Args)]
1411pub struct AgentTaskCreateArgs {
1412    /// Optional caller-provided task id (default: generated task UUIDv7 id).
1413    #[arg(long)]
1414    pub task_id: Option<String>,
1415
1416    /// Human-readable task title.
1417    #[arg(long)]
1418    pub title: String,
1419
1420    /// Detailed task body.
1421    #[arg(long)]
1422    pub body: Option<String>,
1423
1424    /// Thread this task targets.
1425    #[arg(long)]
1426    pub thread: String,
1427
1428    /// Optional base state id this task was delegated from.
1429    #[arg(long)]
1430    pub base_state: Option<String>,
1431
1432    /// Optional base root id this task was delegated from.
1433    #[arg(long)]
1434    pub base_root: Option<String>,
1435
1436    /// Optional parent task id.
1437    #[arg(long)]
1438    pub parent_task_id: Option<String>,
1439
1440    /// Optional coordination discussion id.
1441    #[arg(long)]
1442    pub coordination_discussion_id: Option<String>,
1443
1444    /// Allow this task to continue without hosted connectivity.
1445    #[arg(long)]
1446    pub allow_offline: bool,
1447
1448    /// Principal or agent that delegated this task.
1449    #[arg(long)]
1450    pub delegated_by: Option<String>,
1451}
1452
1453/// Arguments for `agent task list`.
1454#[derive(Clone, Debug, clap::Args)]
1455pub struct AgentTaskListArgs {
1456    /// Filter by target thread.
1457    #[arg(long)]
1458    pub thread: Option<String>,
1459
1460    /// Filter by task status.
1461    #[arg(long)]
1462    pub status: Option<AgentTaskStatusArg>,
1463}
1464
1465/// Arguments for `agent task show`.
1466#[derive(Clone, Debug, clap::Args)]
1467pub struct AgentTaskShowArgs {
1468    /// Task id to show.
1469    pub task_id: String,
1470}
1471
1472/// Arguments for `agent task update`.
1473#[derive(Clone, Debug, clap::Args)]
1474pub struct AgentTaskUpdateArgs {
1475    /// Task id to update.
1476    pub task_id: String,
1477
1478    /// Replace the task title.
1479    #[arg(long)]
1480    pub title: Option<String>,
1481
1482    /// Replace the task body.
1483    #[arg(long)]
1484    pub body: Option<String>,
1485
1486    /// Replace the task status.
1487    #[arg(long)]
1488    pub status: Option<AgentTaskStatusArg>,
1489
1490    /// Replace the target thread.
1491    #[arg(long)]
1492    pub thread: Option<String>,
1493
1494    /// Replace the base state id.
1495    #[arg(long)]
1496    pub base_state: Option<String>,
1497
1498    /// Replace the base root id.
1499    #[arg(long)]
1500    pub base_root: Option<String>,
1501
1502    /// Replace the parent task id.
1503    #[arg(long)]
1504    pub parent_task_id: Option<String>,
1505
1506    /// Replace the coordination discussion id.
1507    #[arg(long)]
1508    pub coordination_discussion_id: Option<String>,
1509
1510    /// Allow this task to continue without hosted connectivity.
1511    #[arg(long, conflicts_with = "no_allow_offline")]
1512    pub allow_offline: bool,
1513
1514    /// Disallow offline continuation for this task.
1515    #[arg(long, conflicts_with = "allow_offline")]
1516    pub no_allow_offline: bool,
1517
1518    /// Replace the delegating principal or agent label.
1519    #[arg(long)]
1520    pub delegated_by: Option<String>,
1521}
1522
1523/// Arguments shared by `agent fanout plan` and `agent fanout start`.
1524#[derive(Clone, Debug, clap::Args)]
1525pub struct AgentFanoutPlanArgs {
1526    /// Parent coordination task title.
1527    #[arg(long)]
1528    pub title: String,
1529
1530    /// Child Thread spec: `<thread>=<title>`. Heddle manages its checkout.
1531    #[arg(long, value_name = "THREAD=TITLE")]
1532    pub lane: Vec<String>,
1533
1534    /// Optional collaboration discussion id to store on task assignments.
1535    #[arg(long)]
1536    pub coordination_discussion_id: Option<String>,
1537}
1538
1539/// Arguments for `agent fanout start`.
1540#[derive(Clone, Debug, clap::Args)]
1541pub struct AgentFanoutStartArgs {
1542    /// Parent coordination task title.
1543    #[arg(long)]
1544    pub title: String,
1545
1546    /// Child Thread spec: `<thread>=<title>`. Heddle manages its checkout.
1547    #[arg(long, value_name = "THREAD=TITLE")]
1548    pub lane: Vec<String>,
1549
1550    /// Optional collaboration discussion id to store on task assignments.
1551    #[arg(long)]
1552    pub coordination_discussion_id: Option<String>,
1553}
1554
1555/// Arguments for `agent capture` under a current reservation lease.
1556#[derive(Clone, Debug, clap::Args)]
1557pub struct AgentCaptureArgs {
1558    /// Writer lease id returned by `agent reserve`.
1559    #[arg(long)]
1560    pub lease: String,
1561
1562    /// Bearer token returned by `agent reserve`.
1563    #[arg(long, env = "HEDDLE_RESERVATION_TOKEN", hide_env_values = true)]
1564    pub token: String,
1565
1566    /// Capture intent / commit message.
1567    #[arg(long, short = 'm', alias = "intent")]
1568    pub message: Option<String>,
1569
1570    /// Honest confidence estimate (0.0–1.0).
1571    #[arg(long, value_parser = parse_confidence)]
1572    pub confidence: Option<f32>,
1573}
1574
1575/// Arguments for `agent ready` under a writer lease.
1576#[derive(Clone, Debug, clap::Args)]
1577pub struct AgentReadyArgs {
1578    /// Writer lease id returned by `agent reserve`.
1579    #[arg(long)]
1580    pub lease: String,
1581
1582    /// Bearer token returned by `agent reserve`.
1583    #[arg(long, env = "HEDDLE_RESERVATION_TOKEN", hide_env_values = true)]
1584    pub token: String,
1585
1586    /// Optional summary message.
1587    #[arg(long, short = 'm')]
1588    pub message: Option<String>,
1589
1590    /// Honest confidence estimate (0.0-1.0) if `agent ready` captures outstanding work.
1591    #[arg(long, value_parser = parse_confidence)]
1592    pub confidence: Option<f32>,
1593}
1594
1595/// Arguments for the `watch` command.
1596///
1597/// Streams live oplog activity (snapshots, merges, thread create/update,
1598/// markers, etc.) as it happens. Default behavior tails forever and exits
1599/// on Ctrl-C. `--since 5m` replays the last N before tailing live;
1600/// `--filter` restricts output to the named kinds; `--output json` emits one
1601/// JSON object per line for piping to `jq`.
1602#[derive(Clone, Debug, clap::Args)]
1603pub struct WatchArgs {
1604    /// Replay events from this duration ago (e.g. `30s`, `5m`, `1h`,
1605    /// `2d`) before tailing live. When unset, only new events are
1606    /// emitted.
1607    #[arg(long, value_name = "DURATION")]
1608    pub since: Option<String>,
1609
1610    /// Comma-separated event kinds to include
1611    /// (`snapshot,merge,thread_create,thread_update,thread_delete,
1612    /// collapse,thread_marker_create,thread_marker_delete`).
1613    #[arg(long, value_name = "KINDS")]
1614    pub filter: Option<String>,
1615
1616    /// Internal helper for tests: stop after the oplog file produces
1617    /// this many modify events (still drains pending entries first).
1618    #[arg(long, hide = true)]
1619    pub max_iterations: Option<usize>,
1620
1621    /// Internal helper for tests: poll interval in milliseconds for
1622    /// the `notify` watcher's debounce check (default 200ms).
1623    #[arg(long, hide = true)]
1624    pub poll_interval_ms: Option<u64>,
1625}
1626
1627// `AgentCaptureArgs` and `AgentReadyArgs` defined earlier in this
1628// file. A second copy was left here by the rebase (the workstreams
1629// commit added them twice when the cherry-pick had lost the
1630// originals and we re-added them mid-rebase). Removed.
1631
1632#[cfg(test)]
1633mod capture_message_alias_tests {
1634    use clap::Parser;
1635
1636    use crate::cli::{Cli, Commands, SnapshotArgs};
1637
1638    fn parse_capture(extra: &[&str]) -> Result<SnapshotArgs, clap::Error> {
1639        let mut argv: Vec<&str> = vec!["heddle", "capture"];
1640        argv.extend_from_slice(extra);
1641        let cli = Cli::try_parse_from(argv)?;
1642        match cli.command {
1643            Commands::Capture(args) => Ok(args),
1644            _ => panic!("expected Commands::Capture"),
1645        }
1646    }
1647
1648    #[test]
1649    fn capture_accepts_message_alias() {
1650        let args = parse_capture(&["--message", "my change"]).expect("--message should parse");
1651        assert_eq!(args.intent.as_deref(), Some("my change"));
1652    }
1653
1654    #[test]
1655    fn capture_accepts_intent_long_form() {
1656        let args = parse_capture(&["--intent", "my change"]).expect("--intent should parse");
1657        assert_eq!(args.intent.as_deref(), Some("my change"));
1658    }
1659
1660    #[test]
1661    fn capture_accepts_short_m() {
1662        let args = parse_capture(&["-m", "my change"]).expect("-m should parse");
1663        assert_eq!(args.intent.as_deref(), Some("my change"));
1664    }
1665
1666    #[test]
1667    fn capture_parses_without_intent_so_the_refuse_can_fire() {
1668        let args =
1669            parse_capture(&[]).expect("omitted -m is a semantic refuse, not a clap usage error");
1670        assert!(args.intent.is_none());
1671    }
1672
1673    #[test]
1674    fn capture_rejects_non_finite_or_out_of_range_confidence() {
1675        for value in ["NaN", "inf", "-0.1", "1.7"] {
1676            let confidence_arg = format!("--confidence={value}");
1677            let err = parse_capture(&["-m", "bad confidence", &confidence_arg])
1678                .expect_err("invalid confidence should fail to parse");
1679            assert!(
1680                err.to_string()
1681                    .contains("confidence must be a finite number from 0.0 to 1.0"),
1682                "unexpected parse error for {value}: {err}"
1683            );
1684        }
1685    }
1686}
1687
1688#[cfg(test)]
1689mod clone_filter_tests {
1690    use clap::Parser;
1691
1692    use crate::cli::{Cli, CloneArgs, Commands};
1693
1694    fn parse_clone(extra: &[&str]) -> Result<CloneArgs, clap::Error> {
1695        let mut argv: Vec<&str> = vec!["heddle", "clone", "remote", "local"];
1696        argv.extend_from_slice(extra);
1697        let cli = Cli::try_parse_from(argv)?;
1698        match cli.command {
1699            Commands::Clone(args) => Ok(args),
1700            _ => panic!("expected Commands::Clone"),
1701        }
1702    }
1703
1704    #[test]
1705    fn parses_clone_filter_blob_none() {
1706        let args = parse_clone(&["--filter", "blob:none"]).expect("parse --filter blob:none");
1707        assert_eq!(args.filter.as_deref(), Some("blob:none"));
1708        assert!(!args.lazy);
1709    }
1710
1711    #[test]
1712    fn rejects_unknown_filter_spec() {
1713        let err = parse_clone(&["--filter", "tree:0"])
1714            .expect_err("unknown --filter spec should fail to parse");
1715        let msg = err.to_string();
1716        assert!(
1717            msg.contains("tree:0") && msg.contains("blob:none"),
1718            "error should name the bad spec and the supported one: {msg}"
1719        );
1720    }
1721}