Skip to main content

heddle_cli_args/cli/cli_args/
commands_thread.rs

1// SPDX-License-Identifier: Apache-2.0
2//! Thread command definitions.
3
4use clap::{Args, Subcommand};
5
6use super::{
7    CollapseArgs, ExpandArgs, ThreadAbsorbArgs, ThreadCapturesArgs, ThreadCheckoutArgs,
8    ThreadDropArgs, ThreadMoveArgs, ThreadNameArgs, ThreadRenameArgs, ThreadResolveArgs,
9    ThreadShowArgs,
10};
11
12#[derive(Subcommand, Clone)]
13pub enum ThreadCommands {
14    /// Create a thread ref at the current state.
15    #[command(after_help = "\
16Advanced split form:
17  heddle start <name> --path <dir> is the normal one-step isolated-checkout path.
18  heddle thread create <name> only creates the thread ref. Pair it later with
19  heddle thread checkout <name> --path <dir> when you intentionally need to
20  create the ref now and materialize the checkout later.
21")]
22    Create {
23        /// Thread identifier.
24        name: String,
25        /// Mark the thread ephemeral. Auto-collapses after `--ttl` if not
26        /// promoted. The collapse is recorded as
27        /// `OpRecord::EphemeralThreadCollapse`; underlying states stay
28        /// addressable. (W1/A13.)
29        #[arg(long)]
30        ephemeral: bool,
31        /// TTL in seconds. Defaults to 24h when `--ephemeral` is set
32        /// without `--ttl`.
33        #[arg(long, requires = "ephemeral")]
34        ttl_secs: Option<u32>,
35    },
36
37    /// Print the name of the current thread (the thread the working
38    /// checkout is attached to). Read-only — no state change.
39    /// Useful in shell pipelines: `cd "$(heddle thread cd "$(heddle thread current)")"`.
40    Current,
41
42    /// Switch the current checkout to an existing thread ref.
43    Switch {
44        /// Thread identifier.
45        name: String,
46        /// Print only the target thread's checkout path on stdout and
47        /// exit. Used by the shell hook (`heddle shell init`) to auto-cd
48        /// into the new thread:
49        ///   dir=$(heddle thread switch X --print-cd-path) && cd "$dir"
50        /// Auto-capture still runs; rich output is suppressed.
51        #[arg(long, hide_short_help = true)]
52        print_cd_path: bool,
53        /// Discard uncommitted changes in the current checkout before switching.
54        #[arg(short, long)]
55        force: bool,
56    },
57
58    /// Print the on-disk path for a thread. Read-only — no state change,
59    /// no auto-capture. Without a shell hook:
60    ///   cd "$(heddle thread cd X)"
61    /// With `heddle shell init`, `heddle thread cd X` becomes `cd <path>`.
62    Cd {
63        /// Thread identifier.
64        name: String,
65    },
66
67    /// List threads.
68    List(ThreadListArgs),
69
70    /// Show one thread with actor and workflow context.
71    Show(ThreadShowArgs),
72
73    /// Show granular captures on a thread.
74    Captures(ThreadCapturesArgs),
75
76    /// Rename a thread ref.
77    Rename(ThreadRenameArgs),
78
79    /// Refresh a thread onto its target thread.
80    Refresh(ThreadNameArgs),
81
82    /// Move selected captured paths from one thread into another.
83    Move(ThreadMoveArgs),
84
85    /// Absorb a child thread into its parent or another thread.
86    Absorb(ThreadAbsorbArgs),
87
88    /// Guide a blocked or stale thread toward its next clean state.
89    Resolve(ThreadResolveArgs),
90
91    /// Inspect or explicitly transfer a Thread's native ownership.
92    Ownership {
93        #[command(subcommand)]
94        command: ThreadOwnershipCommands,
95    },
96
97    /// Create a working checkout for this thread.
98    #[command(after_help = "\
99`heddle start <name>` is the one-step default: it creates the thread ref and
100isolated checkout together. `thread checkout <name> --path <dir>` is the
101second step after `heddle thread create <name>` when you intentionally
102created the ref first and want to materialize it later.
103")]
104    Checkout(ThreadCheckoutArgs),
105
106    /// Drop a thread and mark it abandoned.
107    #[command(visible_alias = "delete")]
108    Drop(ThreadDropArgs),
109
110    /// Sweep merged, stale auto-created, or abandoned threads.
111    #[command(
112        long_about = "\
113Sweep threads that have outlived their usefulness. Cleanup removes recorded checkouts, marks matching thread records abandoned, and prunes live thread refs so everyday thread lists stay focused.
114
115Modes:
116  - --merged: clean up threads recorded as merged.
117  - --auto --older-than <duration>: clean up harness-created threads that have not been touched in the given duration.
118  - --abandoned: remove live refs and checkout residue left by abandoned threads; retain their records, states, and audit history.
119
120The three modes can be combined. Pair with --dry-run to preview the work without changing anything on disk.",
121        after_help = "\
122Examples:
123  heddle thread cleanup --merged --dry-run
124  heddle thread cleanup --merged
125  heddle thread cleanup --auto --older-than 7d --dry-run
126  heddle thread cleanup --abandoned --dry-run
127"
128    )]
129    Cleanup(ThreadCleanupArgs),
130
131    /// Collapse (squash) multiple states into one.
132    Collapse(CollapseArgs),
133
134    /// Expand a squashed land into the captures it collapsed.
135    Expand(ExpandArgs),
136
137    /// Manage named state markers under the thread namespace.
138    Marker {
139        #[command(subcommand)]
140        command: ThreadMarkerCommands,
141    },
142}
143
144#[derive(Subcommand, Clone)]
145pub enum ThreadOwnershipCommands {
146    /// Show the local owner or the complete signed conflict.
147    Status { thread: Option<String> },
148    /// Explicitly transfer a local-key Thread to the currently authorized account.
149    Claim { thread: Option<String> },
150    /// Original local owner chooses one accepted claim; the current account accepts.
151    Resolve {
152        thread: Option<String>,
153        /// Full claim ID shown by `heddle thread ownership status`.
154        #[arg(long)]
155        claim: String,
156    },
157}
158
159#[cfg(test)]
160mod ownership_tests {
161    use clap::Parser;
162
163    use super::*;
164    use crate::cli::cli_args::{Cli, Commands};
165
166    #[test]
167    fn ownership_commands_select_named_or_current_thread_and_require_explicit_winner() {
168        let status = Cli::try_parse_from(["heddle", "thread", "ownership", "status"])
169            .expect("current Thread status");
170        assert!(matches!(
171            status.command,
172            Commands::Thread {
173                command: ThreadCommands::Ownership {
174                    command: ThreadOwnershipCommands::Status { thread: None }
175                }
176            }
177        ));
178        let resolve = Cli::try_parse_from([
179            "heddle",
180            "thread",
181            "ownership",
182            "resolve",
183            "feature",
184            "--claim",
185            "ab12",
186        ])
187        .expect("named Thread and explicit claim");
188        assert!(
189            matches!(resolve.command, Commands::Thread { command: ThreadCommands::Ownership {
190            command: ThreadOwnershipCommands::Resolve { thread: Some(_), claim }
191        } } if claim == "ab12")
192        );
193        assert!(
194            Cli::try_parse_from(["heddle", "thread", "ownership", "resolve", "feature"]).is_err()
195        );
196    }
197}
198
199#[derive(Subcommand, Clone)]
200pub enum ThreadMarkerCommands {
201    /// List markers, optionally filtered by name prefix.
202    ///
203    /// Pass `--filter <PREFIX>` to return only markers whose name
204    /// starts with the given prefix. The match is a literal
205    /// `starts_with` check, not a glob.
206    List {
207        /// Return only markers whose name starts with this prefix.
208        #[arg(long, value_name = "PREFIX")]
209        filter: Option<String>,
210    },
211
212    /// Create marker at current state.
213    Create {
214        /// Marker name.
215        name: String,
216    },
217
218    /// Delete marker(s).
219    ///
220    /// Pass an exact marker name, or `--prefix <PFX>` to delete every marker
221    /// whose name starts with the given prefix. Exactly one of `<NAME>` or
222    /// `--prefix` must be supplied.
223    Delete {
224        /// Marker name (exact match). Mutually exclusive with `--prefix`.
225        #[arg(required_unless_present = "prefix", conflicts_with = "prefix")]
226        name: Option<String>,
227
228        /// Delete every marker whose name starts with this prefix.
229        #[arg(long)]
230        prefix: Option<String>,
231    },
232
233    /// Show marker details.
234    Show {
235        /// Marker name.
236        name: String,
237    },
238}
239
240/// Arguments for `heddle thread list`.
241///
242/// The default view hides harness-auto-created and abandoned threads.
243/// Pass the corresponding include flags to surface them.
244#[derive(Args, Clone, Debug, Default)]
245pub struct ThreadListArgs {
246    /// Include threads created automatically by harness integrations
247    /// (e.g. Claude Code segment-rotation). Hidden by default to keep
248    /// the view focused on threads the user explicitly created.
249    #[arg(long)]
250    pub include_auto: bool,
251
252    /// Include threads whose lifecycle state is abandoned. Hidden by
253    /// default because they are no longer actionable.
254    #[arg(long)]
255    pub include_abandoned: bool,
256}
257
258/// Arguments for `heddle thread cleanup`.
259///
260/// At least one cleanup mode must be set; otherwise the command refuses
261/// with a clear message. `--older-than` is required when `--auto` is set.
262#[derive(Args, Clone, Debug)]
263pub struct ThreadCleanupArgs {
264    /// Clean up threads whose recorded state is `merged`.
265    #[arg(long)]
266    pub merged: bool,
267
268    /// Drop harness-auto-created threads (those tagged `auto: true`).
269    /// Combine with `--older-than` to gate the sweep on staleness.
270    #[arg(long)]
271    pub auto: bool,
272
273    /// Clean abandoned threads that still have a live ref or checkout
274    /// residue. States and audit history are retained.
275    #[arg(long)]
276    pub abandoned: bool,
277
278    /// Maximum age (since `updated_at`) for an auto-thread to be
279    /// considered live. Threads older than this are eligible for
280    /// sweep when `--auto` is set. Accepts a Go-style duration like
281    /// `7d`, `24h`, `30m`, `15s` (or a raw integer interpreted as
282    /// seconds).
283    #[arg(long, value_name = "DURATION")]
284    pub older_than: Option<String>,
285
286    /// Print what would be dropped without actually dropping it.
287    #[arg(long)]
288    pub dry_run: bool,
289}