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