Skip to main content

heddle_cli_args/cli/cli_args/
commands_main.rs

1// SPDX-License-Identifier: Apache-2.0
2//! Top-level CLI commands.
3
4use clap::{Args, Subcommand};
5
6#[cfg(feature = "git-overlay")]
7use super::BridgeCommands;
8#[cfg(feature = "semantic")]
9use super::SemanticCommands;
10use super::{
11    AgentCommands, BlameArgs, CompletionSubject, ContextCommands, DiscussArgs, EnvCommands,
12    HookCommands, ImportArgs, IntegrationCommands, OplogCommands, QueryArgs, RedactCommands,
13    RemoteCommands, ReviewCommands, ShellCommands, ThreadCommands, VisibilityCommands,
14    commands_args::{
15        CloneArgs, DiffArgs, DoctorArgs, IMPORT_VERB, INIT_VERB, InitArgs, LandArgs, LogArgs,
16        PullArgs, PushArgs, ReadyArgs, ResolveArgs, RevertArgs, SnapshotArgs, SyncArgs,
17        ThreadStartArgs, UndoArgs, WatchArgs,
18    },
19};
20#[cfg(feature = "client")]
21use super::{AuthCommands, AuthInviteCommands, ClaimArgs, GrantCommands, PromoteArgs};
22
23#[derive(Clone, Debug, Args)]
24pub struct FsckArgs {
25    /// Full check (includes content verification).
26    #[arg(long)]
27    pub full: bool,
28
29    /// Run slower graph and signature integrity checks.
30    #[arg(long)]
31    pub thorough: bool,
32
33    /// Verify offline authorship identity and review-signature chains.
34    #[arg(long, requires = "thorough")]
35    pub provenance: bool,
36
37    /// Include Git projection, mapping, notes, and checkout checks.
38    #[arg(long)]
39    pub git: bool,
40
41    #[command(subcommand)]
42    pub command: Option<FsckCommands>,
43}
44
45#[derive(Clone, Debug, Subcommand)]
46pub enum FsckCommands {
47    /// Repair an integrity surface, then verify it.
48    Repair {
49        #[command(subcommand)]
50        target: FsckRepairCommands,
51    },
52}
53
54#[derive(Clone, Debug, Subcommand)]
55pub enum FsckRepairCommands {
56    /// Reconcile Git projection metadata or one projected ref.
57    Git(FsckRepairGitArgs),
58}
59
60#[derive(Clone, Debug, Args)]
61pub struct FsckRepairGitArgs {
62    /// Git ref to reconcile. Required for native repositories.
63    #[arg(long = "ref", value_name = "BRANCH")]
64    pub ref_name: Option<String>,
65
66    /// Assert the intended authority direction.
67    #[arg(long, value_parser = ["git", "heddle"])]
68    pub prefer: Option<String>,
69
70    #[command(flatten)]
71    pub dry_run: super::DryRunArgs,
72}
73
74#[derive(Subcommand)]
75pub enum Commands {
76    /// Initialize Heddle in a directory or existing Git checkout.
77    #[command(name = INIT_VERB)]
78    Init(InitArgs),
79
80    /// Bring an existing Git repository into Heddle.
81    #[command(name = IMPORT_VERB, verbatim_doc_comment)]
82    Import(ImportArgs),
83
84    /// Curated, progressive-disclosure help.
85    ///
86    /// `heddle help` prints the task map. `heddle help --all` prints the
87    /// full command tree. `heddle help <topic>` prints the topic page
88    /// (e.g. `model`, `advanced`, `git-concepts`). `heddle help
89    /// <command path>` falls through to that command's `--help`.
90    Help {
91        /// Print the full command tree instead of the task map.
92        #[arg(long)]
93        all: bool,
94        /// Topic name (`model`, `advanced`, `git-concepts`, …) or command
95        /// path. When omitted, prints the curated default.
96        #[arg(value_name = "TOPIC_OR_COMMAND")]
97        topics: Vec<String>,
98    },
99
100    /// Show what needs attention and the next safe Heddle action.
101    #[command(after_help = "\
102Clean describes the worktree and local checks. Push checks hosted identity and write authority.
103
104Examples:
105  heddle status               # current thread, dirty paths, recommended next step
106  heddle status --short       # one-line summary for shell prompts
107  heddle status --watch       # live dashboard that refreshes in place
108")]
109    Status {
110        /// Short format.
111        #[arg(short, long)]
112        short: bool,
113
114        /// Continuously refresh status.
115        #[arg(long)]
116        watch: bool,
117
118        /// Internal helper for tests: stop after N watch updates.
119        #[arg(long, hide = true)]
120        watch_iterations: Option<usize>,
121
122        /// Internal helper for tests: polling interval in milliseconds.
123        #[arg(long, hide = true)]
124        watch_interval_ms: Option<u64>,
125    },
126
127    /// Stream live oplog activity.
128    ///
129    /// Tails the repository's append-only oplog file like `tail -f`,
130    /// emitting snapshots, merges, and thread events as they happen.
131    /// Exits on Ctrl-C.
132    Watch(WatchArgs),
133
134    /// Verify this workspace; exits nonzero until every check is clean.
135    #[command(after_help = "\
136Checks: Git mapping, worktree, remote, operation, clone verification, machine contract.
137
138Examples:
139  heddle verify                # strict verification gate and next recovery step
140  heddle verify --verbose      # full proof rows and machine-contract details
141  heddle verify --output json  # proof JSON when clean; error envelope when blocked
142")]
143    Verify {
144        /// Verify each state's offline authorship and review-signature chain.
145        #[arg(long)]
146        provenance: bool,
147    },
148
149    /// Explain repository health, or run targeted doctor checks.
150    ///
151    /// `heddle doctor` (no subcommand) reports repository health and
152    /// the next recovery step. `heddle doctor docs` diff-checks markdown
153    /// documentation against
154    /// the actual CLI surface and exits non-zero on drift — wire it
155    /// into CI to stop docs from going stale.
156    Doctor(DoctorArgs),
157
158    /// Create or resume an isolated thread for focused work.
159    Start(ThreadStartArgs),
160
161    /// Run CI checks locally or record signed evidence on hosted.
162    #[cfg(feature = "ci")]
163    Ci {
164        #[command(subcommand)]
165        command: super::CiCommands,
166    },
167
168    /// Automation/workflow command: refresh the current thread onto its target when safe.
169    Sync(SyncArgs),
170
171    /// Continue the active operation without remembering the specific subcommand.
172    Continue,
173
174    /// Abort the active operation without remembering the specific subcommand.
175    Abort,
176
177    /// Integrate into the local target thread; push to publish.
178    ///
179    /// `land` is the local integration verb: capture outstanding work if needed,
180    /// refresh against the target when safe, and land the thread. It fails
181    /// closed when conflicts or other blockers exist. Pair it with `ready`
182    /// when you want the verdict and next action before landing anything.
183    Land(LandArgs),
184
185    /// Check this checkout before local integration.
186    ///
187    /// `ready` captures outstanding work if needed, checks conflicts,
188    /// blockers, freshness, and semantic risk, then marks the thread
189    /// ready or blocked and prints the next action. It never lands,
190    /// checkpoints, or pushes; use it when you want Heddle's verdict
191    /// before integrating the work.
192    Ready(ReadyArgs),
193
194    /// Capture a recoverable Heddle step for undo, provenance, and review.
195    Capture(SnapshotArgs),
196
197    /// Show state history.
198    ///
199    /// By default, when a thread name is given (e.g. `heddle log master`),
200    /// the walk is *first-parent only* — equivalent to `git log
201    /// --first-parent <branch>`. To see every ancestor reachable through
202    /// merge commits, pass `--graph` (which renders the full DAG) or
203    /// `--all` (which lists every state regardless of ancestry).
204    #[command(visible_alias = "history")]
205    Log(LogArgs),
206
207    /// Show state details.
208    Show {
209        /// State by physical state ID, logical change ID, or unambiguous prefix.
210        /// Defaults to HEAD.
211        state: Option<String>,
212    },
213
214    /// Show what changed in the worktree, a thread, or two states.
215    Diff(DiffArgs),
216
217    /// Show line-by-line attribution for a tracked file.
218    ///
219    /// Names the state that last changed each line, with the same
220    /// structured principal / agent shape as `log` and `show`.
221    /// `heddle query --attribution <path>` remains as the equivalent
222    /// query form.
223    #[command(after_help = "\
224Examples:
225  heddle blame src/auth.rs
226  heddle blame src/auth.rs --state HEAD
227  heddle blame src/auth.rs --context
228  heddle blame src/auth.rs --output json
229")]
230    Blame(BlameArgs),
231
232    /// Open or resolve discussions anchored to code.
233    ///
234    /// Subcommands: `new`, `reply`, `resolve`, `reopen`, `list`, `show`,
235    /// `wait`. `--path` / `--symbol` / `--line` are the code anchor;
236    /// `--state` is the historical revision; `--body` / `--file` is the
237    /// markdown body.
238    ///
239    /// Native Heddle only. Discussions live in `.heddle` and travel
240    /// over `heddle push` / `heddle pull` to a Heddle remote. They are
241    /// not projected into Git, so `git clone` does not carry them; in
242    /// Git Overlay mode they are local to that working copy.
243    #[command(after_help = "\
244Scope:
245  Native Heddle only. Discussions are stored in `.heddle` and move over
246  `heddle push` / `heddle pull`. Git does not carry them: a `git clone` of a
247  Git Overlay repository arrives with no discussions and no Heddle store.
248
249Examples:
250  heddle discuss new --path src/lib.rs --symbol greet --body \"why greet?\"
251  heddle discuss new --path src/lib.rs --file why.md
252  heddle discuss reply disc-01a0afc6 --body \"second thought\"
253  heddle discuss reply disc-01a0afc6 --turn 2 --body \"reply to that turn\"
254  heddle discuss resolve <id> --mode by-edit --state HEAD
255")]
256    Discuss(DiscussArgs),
257
258    /// Structured query over the operation log. Filter by
259    /// actor, time window, signal kind, symbol, thread, verbs. Returns
260    /// structured results consumable by agents.
261    Query(QueryArgs),
262
263    /// Review and approve an exact hosted Thread comparison.
264    ///
265    /// `show`, `approve`, and `list` default to the current Thread.
266    /// `readiness` checks the exact source and target comparison before
267    /// hosted landing. Remote selection is explicit, then the configured
268    /// default, and fails when neither is available.
269    #[command(after_help = "\
270Examples:
271  heddle review show feature
272  heddle review approve feature -m \"Looks good\"
273  heddle review list feature
274  heddle review revoke <review-id> --thread feature
275  heddle review readiness feature --into main
276")]
277    Review {
278        #[command(subcommand)]
279        command: ReviewCommands,
280    },
281
282    /// Redact a sensitive blob in a state so reads return a stub
283    /// instead of the content.
284    ///
285    /// `heddle redact apply` declares a redaction; the blob bytes stay
286    /// on disk and reads return the operator-supplied stub. `heddle
287    /// redact purge` afterward physically removes the bytes. Both are signed,
288    /// attributed, oplog-audited operations. See
289    /// `docs/PRINCIPLES.md` (the honesty principle) for context.
290    ///
291    /// Redaction is path/blob hide inside history. It is not `heddle env`
292    /// (runtime secrets; literal `.env` capture is reserved, exit 65) and
293    /// not `heddle visibility` (per-state audience, downward-closed).
294    Redact {
295        #[command(subcommand)]
296        command: RedactCommands,
297    },
298
299    /// Declare and inspect a state's audience visibility tier.
300    ///
301    /// `heddle visibility set` binds a tier to a state; `promote` lifts it to
302    /// a less-restrictive tier via a superseding record; `show` reports the
303    /// effective declared tier; `list` enumerates non-public sidecar records
304    /// in this store. Capture binds the inherited
305    /// `[review.discussion] default_visibility` automatically (Invariant A)
306    /// — these verbs are the explicit operator overrides.
307    ///
308    /// Private is per-state and downward-closed: a public descendant that
309    /// still names private-ancestor blobs is withheld from lesser audiences.
310    /// It does not hide one path inside a later public tip. Runtime secrets
311    /// belong in `heddle env` (literal `.env` capture is reserved, exit 65);
312    /// path-level hide of bytes already in history is `heddle redact`.
313    /// See `heddle help visibility`.
314    Visibility {
315        #[command(subcommand)]
316        command: VisibilityCommands,
317    },
318
319    /// Run a child with a confidential runtime profile.
320    ///
321    /// `heddle env run --profile <name> -- <cmd>` asks the local policy
322    /// broker to unwrap named slots and injects them into the child
323    /// environment only. Values never land in the worktree, the store, or
324    /// command JSON. Same-UID callers are cooperative; OS isolation is later.
325    ///
326    /// Literal `.env` / `.env.local` files are reserved (capture exits 65).
327    /// `heddle visibility` does not replace this for secrets beside a public
328    /// tip; `heddle redact` stubs a blob already in history.
329    #[command(after_help = "\
330Examples:
331  heddle env list
332  heddle env create --name local --from-env DATABASE_URL
333  heddle env run --profile local -- printenv DATABASE_URL
334
335Literal `.env` capture is reserved (exit 65). Use this verb for runtime
336secrets. `heddle visibility` embargoes a state and its descendants;
337`heddle redact` hides a blob already in history. See `heddle help visibility`.
338")]
339    Env {
340        #[command(subcommand)]
341        command: EnvCommands,
342    },
343
344    /// Revert changes from a state.
345    Revert(RevertArgs),
346
347    /// Undo the last Heddle operation.
348    Undo(UndoArgs),
349
350    /// Manage threads.
351    Thread {
352        #[command(subcommand)]
353        command: ThreadCommands,
354    },
355
356    /// Shell integration helpers (auto-cd on thread start/switch/cd).
357    Shell {
358        #[command(subcommand)]
359        command: ShellCommands,
360    },
361
362    /// Print a tab-completion script for bash, zsh, or fish.
363    ///
364    /// With no shell, prints install lines. With `bash`, `zsh`, or `fish`,
365    /// emits the same script as `heddle shell completion`.
366    Completions {
367        /// Shell to generate completion for: bash, zsh, or fish.
368        #[arg(value_name = "SHELL")]
369        shell: Option<String>,
370    },
371
372    /// Internal shell-completion candidate helper.
373    #[command(name = "complete", alias = "__complete", hide = true)]
374    Complete {
375        /// Candidate set to print, one candidate per line.
376        #[arg(value_enum)]
377        subject: CompletionSubject,
378    },
379
380    /// Resolve merge conflicts.
381    Resolve(ResolveArgs),
382
383    /// Explicit interoperability with other version-control formats.
384    #[cfg(feature = "git-overlay")]
385    Bridge {
386        #[command(subcommand)]
387        command: BridgeCommands,
388    },
389
390    /// Push the source-authoritative history to a remote.
391    Push(PushArgs),
392
393    /// Pull source-authoritative history from a remote.
394    Pull(PullArgs),
395
396    /// Manage remote repositories.
397    Remote {
398        #[command(subcommand)]
399        command: RemoteCommands,
400    },
401
402    /// Authenticate with a Heddle server.
403    #[cfg(feature = "client")]
404    Auth {
405        #[command(subcommand)]
406        command: AuthCommands,
407    },
408
409    /// Create or list signup invites (thin alias of `auth invite`).
410    ///
411    /// Signup-only: this mints an account-creation code. It does not grant
412    /// another principal access to a spool. Use `heddle grant` to add a
413    /// collaborator.
414    #[cfg(feature = "client")]
415    #[command(args_conflicts_with_subcommands = true)]
416    #[command(after_help = "\
417Signup-only. `heddle invite` is the same as `heddle auth invite`.
418It does not grant spool access. Add a collaborator with:
419
420  heddle grant create --spool <path|url> --principal <handle> --role writer
421")]
422    Invite {
423        /// Bind the new invite to an email address.
424        #[arg(long)]
425        email: Option<String>,
426
427        /// Heddle server address. Omit to use the configured default
428        /// (`api.heddle.sh` when none is stored).
429        #[arg(long, global = true)]
430        server: Option<String>,
431
432        #[command(subcommand)]
433        command: Option<AuthInviteCommands>,
434    },
435
436    /// Grant a principal access to a hosted spool.
437    ///
438    /// Separate from `heddle auth invite`, which is signup-only. Create,
439    /// list, and delete collaborator grants on a spool you can administer.
440    /// Agents may grant writer or below; maintainer, admin, and owner stay human-verified.
441    #[cfg(feature = "client")]
442    Grant {
443        #[command(subcommand)]
444        command: GrantCommands,
445    },
446
447    /// Promote a personal hosted spool to a root-level spool.
448    ///
449    /// Moves `spool/<your-handle>/<name>` to `spool/<name>` after the server
450    /// confirms the root slug is free, the account is claimed/verified, and
451    /// you hold an owner grant. Clone a bare name still prefers your personal
452    /// copy first.
453    #[cfg(feature = "client")]
454    Promote(PromoteArgs),
455
456    /// Offer this agent account for a human to claim.
457    ///
458    /// Prints a short-lived bearer link, then keeps the agent's Iroh endpoint
459    /// online until the human finishes, the offer expires, or Ctrl-C stops it.
460    #[cfg(feature = "client")]
461    #[command(after_help = "\
462Examples:
463  heddle claim
464  heddle claim --timeout 30m
465  heddle claim --server weft.example --web-origin https://heddle.example
466")]
467    Claim(ClaimArgs),
468
469    /// Report the capture actor, then hosted auth.
470    ///
471    /// The capture actor is who the next capture is attributed to
472    /// (`user_config`, `init --principal-*`, or `HEDDLE_PRINCIPAL_*`).
473    /// Hosted auth shows the account, handle, claim state, and current credential.
474    /// These are different objects. `heddle auth login` does not set the
475    /// local actor. `whoami` only reads; it never attaches a credential.
476    #[cfg(feature = "client")]
477    #[command(after_help = "\
478The capture actor and hosted auth are different objects:
479  capture actor  who the next capture is attributed to
480                 (user_config, init --principal-*, or HEDDLE_PRINCIPAL_*)
481  hosted auth    whether this machine has a credential for the server
482                 (heddle auth login). whoami never attaches a credential.
483
484Examples:
485  heddle whoami                       # capture actor first, then hosted auth
486  heddle whoami --output json         # machine-readable, stable output_kind shape
487  heddle whoami --server api.heddle.sh")]
488    Whoami {
489        /// Heddle server address (defaults to the configured server).
490        #[arg(long)]
491        server: Option<String>,
492    },
493
494    /// Manage code context annotations.
495    ///
496    /// Native Heddle only. Annotations live in `.heddle`, and travel
497    /// over `heddle push` / `heddle pull` to a Heddle remote. They are
498    /// deliberately not projected into Git — not into `refs/notes/*`,
499    /// not into a tracked file — so `git push` and `git clone` do not
500    /// carry them. In Git Overlay mode annotations still work and are
501    /// still useful; they are simply local to that working copy.
502    #[command(after_help = "\
503Scope:
504  Native Heddle only. Annotations are stored in `.heddle` and move over
505  `heddle push` / `heddle pull`. Git does not carry them: a `git clone` of a
506  Git Overlay repository arrives with no annotations and no Heddle store.
507
508Examples:
509  heddle context set --path src/auth.rs --symbol verify --kind invariant -m 'returns false on timing mismatch'
510  heddle context get --path src/auth.rs --symbol verify
511  heddle context --for-thread <THREAD>              # pre-turn briefing
512  heddle context history --path src/auth.rs      # same --path as set, or pass the id
513  heddle context list --prefix src/auth          # everything attached under a path
514  heddle context check --path src/auth.rs        # surface annotations for editor tooling
515")]
516    Context {
517        /// Show a bounded pre-turn briefing for a Thread.
518        #[arg(long, value_name = "THREAD")]
519        for_thread: Option<String>,
520        #[command(subcommand)]
521        command: Option<ContextCommands>,
522    },
523
524    /// Manage ambient harness integrations.
525    Integration {
526        #[command(subcommand)]
527        command: IntegrationCommands,
528    },
529
530    /// Semantic analysis queries (call-graph hot-spots, churn,
531    /// signature-stability surfaces).
532    #[cfg(feature = "semantic")]
533    Semantic {
534        #[command(subcommand)]
535        command: SemanticCommands,
536    },
537
538    /// FUSE mount-daemon control plane — distinct from `agent`.
539    ///
540    /// `heddle daemon serve` runs a foreground mount daemon that
541    /// owns FUSE sessions for `--workspace virtualized --daemon`
542    /// threads. It is normally spawned on demand by the per-thread
543    /// CLI; running it interactively is for debugging.
544    /// `status` reports liveness/uptime/mount count without spawning;
545    /// `stop` asks a running daemon to drain mounts and exit.
546    Daemon {
547        #[command(subcommand)]
548        command: DaemonCommands,
549    },
550
551    /// Box-scoped network daemon control plane — distinct from
552    /// `daemon` (FUSE mounts).
553    ///
554    /// `heddle netd serve` runs a long-lived async daemon that binds
555    /// the machine's single persistent Iroh endpoint on the persisted
556    /// device node id and keeps it relay-reachable, so outstanding
557    /// claim links keep resolving across restarts. Hosted verbs
558    /// (`whoami`, `push`, `pull`, `clone`) reuse that endpoint's warm
559    /// weft session when netd is running. Unlike `daemon`, it is not
560    /// gated on Linux/FUSE and never idle-exits. `status` reports
561    /// liveness and the advertised node id; `stop` asks a running
562    /// daemon to close its endpoint and exit.
563    Netd {
564        #[command(subcommand)]
565        command: NetdCommands,
566    },
567
568    /// Agent reservation and one-shot orchestration API.
569    ///
570    /// `heddle agent reserve|capture|ready|release|list|heartbeat` is the stable
571    /// JSON contract orchestrators use to coordinate parallel
572    /// writers. `heddle daemon` remains the distinct FUSE mount control plane.
573    Agent {
574        #[command(subcommand)]
575        command: AgentCommands,
576    },
577
578    /// Inspect and refresh rebuildable performance sidecars.
579    Maintenance {
580        #[command(subcommand)]
581        command: MaintenanceCommands,
582    },
583
584    /// Download an existing repository into a local directory.
585    Clone(CloneArgs),
586
587    /// Manage repository hooks.
588    Hook {
589        #[command(subcommand)]
590        command: HookCommands,
591    },
592}
593
594/// Maintenance subcommands.
595#[derive(Clone, Debug, clap::Subcommand)]
596pub enum MaintenanceCommands {
597    /// Verify repository integrity or explicitly repair one surface.
598    Fsck(FsckArgs),
599
600    /// Inspect repository performance sidecars and repo shape.
601    Inspect,
602
603    /// Refresh repository performance sidecars without changing repository meaning.
604    Refresh,
605
606    /// Repack native objects now through the resource-controlled scheduler.
607    Repack,
608
609    /// Garbage collect unreachable objects.
610    Gc {
611        /// Prune unreachable objects.
612        #[arg(long)]
613        prune: bool,
614
615        /// Aggressive garbage collection.
616        #[arg(long)]
617        aggressive: bool,
618
619        /// Show what would be removed without removing.
620        #[arg(long)]
621        dry_run: bool,
622    },
623
624    /// Inspect and repair the operation log.
625    ///
626    /// `heddle maintenance oplog recover` explicitly salvages a truncated or
627    /// torn oplog, reporting what was recovered — the operator-facing
628    /// entrypoint over the same recovery the everyday read path runs
629    /// automatically.
630    Oplog {
631        #[command(subcommand)]
632        command: OplogCommands,
633    },
634}
635
636/// Daemon control plane subcommands. See `Commands::Daemon`.
637#[derive(Clone, Debug, clap::Subcommand)]
638pub enum DaemonCommands {
639    /// Run a foreground mount daemon for this repository.
640    ///
641    /// Normally spawned on demand by the per-thread CLI when
642    /// `--daemon` is passed. Running interactively is for
643    /// debugging the daemon protocol.
644    Serve,
645
646    /// Report daemon liveness, version, uptime, and active mount
647    /// count. No-op success when the daemon isn't running.
648    Status,
649
650    /// Ask the running daemon to drain its mounts and exit. Sweeps
651    /// any leftover registry entries with `fusermount -u` as a
652    /// safety net before returning.
653    Stop,
654}
655
656/// Box-scoped network daemon subcommands. See `Commands::Netd`.
657#[derive(Clone, Debug, clap::Subcommand)]
658pub enum NetdCommands {
659    /// Run the foreground network daemon: bind the persistent device
660    /// endpoint, keep relays online, hold warm weft sessions for hosted
661    /// CLI verbs, and serve same-uid control RPCs. Never idle-exits.
662    Serve,
663
664    /// Report network-daemon liveness and the advertised device node
665    /// id. No-op success when the daemon isn't running.
666    Status,
667
668    /// Ask the running network daemon to close its endpoint and exit.
669    Stop,
670}