Skip to main content

heddle_cli_args/cli/cli_args/
commands_args.rs

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