onevcs 0.40.1

Version control and remote-host abstraction for agent workflows: host-neutral change requests, sessions, and a rules system.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
//! The command-line argument surface.
//!
//! This is the parser only: it validates what a user typed and nothing else. What
//! each command then does is `app.rs`'s, reached through `crate::run`; a seam with
//! no body behind it answers exit code 70 from there, never from here.

use std::path::PathBuf;
use std::time::Duration;

use clap::{Parser, Subcommand, ValueEnum};
use url::Url;

use crate::releases::TargetName;
use crate::rules::MergePolicy;
use crate::sweep;
use crate::workspaces::{Bound, Span};

/// Version control and its remote host, behind one host-neutral vocabulary.
#[derive(Debug, Clone, PartialEq, Eq, Parser)]
#[command(name = "onevcs", version, about, long_about = None)]
pub struct Cli {
    /// What to do.
    #[command(subcommand)]
    pub command: Command,
}

/// The top-level commands.
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
pub enum Command {
    /// Register a checkout, resolving its origin to a repository identity.
    Register(RegisterArgs),
    /// List the registered repositories.
    Repos(ReposArgs),
    /// Resolve a repository to its identity.
    Resolve(ResolveArgs),
    /// Open, adopt, or close a session.
    Session {
        /// Which part of a session's life cycle.
        #[command(subcommand)]
        command: SessionCommand,
    },
    /// Verify a session's work and publish it under its policy.
    Publish(PublishArgs),
    /// Verify and publish a completed branch no session holds.
    PublishBranch(PublishBranchArgs),
    /// Read, describe, or ready a session's own change request.
    Change {
        /// Which thing to do to the change request.
        #[command(subcommand)]
        command: ChangeCommand,
    },
    /// Put an unpublished branch on its identity's origin, without publishing it.
    Preserve(PreserveArgs),
    /// Verify and publish a preserved branch that was left behind.
    Recover(RecoverArgs),
    /// List preserved work that has not been published.
    Recoverable(RecoverableArgs),
    /// Report everything onevcs knows about one piece of work.
    Status(StatusArgs),
    /// Make a branch reachable from an identity's registered checkouts.
    Import(ImportArgs),
    /// Merge finished branches into their base, in order.
    Integrate(IntegrateArgs),
    /// Fast-forward a publication checkout to its origin.
    Sync(SyncArgs),
    /// Reclaim the publication workspaces this host has finished with.
    Sweep(SweepArgs),
    /// Read a session's event stream.
    Events(EventsArgs),
    /// Work with stored artifacts.
    Artifact {
        /// What to do with an artifact.
        #[command(subcommand)]
        command: ArtifactCommand,
    },
    /// Work with the rules file.
    Rules {
        /// What to do with the rules.
        #[command(subcommand)]
        command: RulesCommand,
    },
    /// Ask about the releases that follow a landed change.
    Release {
        /// Which release question.
        #[command(subcommand)]
        command: ReleaseCommand,
    },
    /// Report, prune or maintain a repository's pool of warm worktree slots.
    Pool {
        /// Which pool question.
        #[command(subcommand)]
        command: PoolCommand,
    },
    /// Delete a branch everywhere this host holds it, where it provably holds no
    /// work beyond its base.
    Retire(RetireArgs),
    /// Delete a branch a retry superseded and landed, discarding what it still
    /// differs from the base in.
    Reclaim(RetireArgs),
    /// Retire every branch in scope that provably holds no work beyond its base.
    RetireFinished(RetireFinishedArgs),
    /// Record that a branch was superseded by a retry that landed.
    Supersede(SupersedeArgs),
}

/// Arguments for `onevcs retire` and `onevcs reclaim`, which take the same ones.
#[derive(Debug, Clone, PartialEq, Eq, Parser)]
pub struct RetireArgs {
    /// The branch to retire.
    // llmlint: ignore[invalid_states_unrepresentable] a branch name is valid when `git
    // check-ref-format` says so, which is a subprocess argument parsing must not run;
    // `retire` is the boundary that refuses one, as `PreserveArgs::branch` says.
    pub branch: String,
    /// The repository it belongs to: an identity key, a registered alias, an origin
    /// URL, or a path. Omitted, the one identity anything on this host holds it in.
    // llmlint: ignore[invalid_states_unrepresentable] the four forms cannot be told apart
    // by a parser — an alias and a key are registry lookups and a path is `canonicalize`
    // — so `store::resolve` is the one boundary that decides, as `PreserveArgs::repo` says.
    #[arg(long)]
    pub repo: Option<String>,
    /// Report what would be retired, and change nothing.
    #[arg(long)]
    pub dry_run: bool,
    /// Report as JSON rather than as prose.
    #[arg(long)]
    pub json: bool,
}

/// Arguments for `onevcs retire-finished`.
#[derive(Debug, Clone, PartialEq, Eq, Parser)]
pub struct RetireFinishedArgs {
    /// The repository to examine. Omitted, every registered identity.
    #[arg(long)]
    pub repo: Option<String>,
    /// A branch to leave alone, whatever it is; repeatable.
    // llmlint: ignore[invalid_states_unrepresentable] a branch name is valid when `git
    // check-ref-format` says so, a subprocess argument parsing must not run; the pass
    // refuses an exclusion naming no valid branch where it arrives, by name.
    #[arg(long, value_name = "BRANCH")]
    pub exclude: Vec<String>,
    /// Report what would be retired, and change nothing.
    #[arg(long)]
    pub dry_run: bool,
    /// Report as JSON rather than as prose.
    #[arg(long)]
    pub json: bool,
}

/// Arguments for `onevcs supersede`.
#[derive(Debug, Clone, PartialEq, Eq, Parser)]
pub struct SupersedeArgs {
    // llmlint: ignore-block[invalid_states_unrepresentable] this module is the parser only,
    // and every field here is decided by a check it must not run: a branch name by `git
    // check-ref-format`, a repository by the registry, a landing by whether it is a full
    // commit id or an http(s) URL. `record_supersession` is the boundary that refuses each
    // by name, and the command renders exactly the request that operation takes.
    /// The branch that was superseded.
    pub branch: String,
    /// The repository it belongs to: an identity key, a registered alias, an origin
    /// URL, or a path.
    #[arg(long)]
    pub repo: String,
    /// The branch that superseded it.
    #[arg(long, value_name = "BRANCH")]
    pub by: String,
    /// Where that branch landed: a full commit id, or a change request's URL.
    #[arg(long, value_name = "SHA-OR-URL")]
    pub landing: String,
    /// A label to record with it, as KEY=VALUE; repeatable, one value per key.
    #[arg(long, value_name = "KEY=VALUE")]
    pub label: Vec<String>,
    // llmlint: ignore-end[invalid_states_unrepresentable]
}

/// The `onevcs pool` subcommands.
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
pub enum PoolCommand {
    /// Report a repository's capacity and every slot of its pool.
    Status(PoolStatusArgs),
    /// Remove every idle slot whose clone retains no branch, and say why the rest
    /// were kept.
    Prune(PoolPruneArgs),
    /// Run the host's maintain command in each idle slot, one slot at a time, and
    /// record the attempt on the slot.
    Maintain(PoolMaintainArgs),
}

/// Arguments for `onevcs pool status`.
#[derive(Debug, Clone, PartialEq, Eq, Parser)]
pub struct PoolStatusArgs {
    /// An identity key, a registered alias, an origin URL, or a path.
    pub repo: String,
    /// Report as JSON rather than as a human table.
    #[arg(long)]
    pub json: bool,
}

/// Arguments for `onevcs pool prune`.
#[derive(Debug, Clone, PartialEq, Eq, Parser)]
pub struct PoolPruneArgs {
    /// An identity key, a registered alias, an origin URL, or a path.
    pub repo: String,
    /// Report as JSON rather than as a human table.
    #[arg(long)]
    pub json: bool,
}

/// Arguments for `onevcs pool maintain`.
#[derive(Debug, Clone, PartialEq, Eq, Parser)]
pub struct PoolMaintainArgs {
    /// An identity key, a registered alias, an origin URL, or a path. Omitted, every
    /// registered identity.
    pub repo: Option<String>,
    /// Skip a slot maintained within this span (`7d`, `36h`, `90m`, `600s`). Omitted,
    /// every idle slot is due; a slot never maintained is always due.
    #[arg(long, value_name = "SPAN")]
    pub older_than: Option<Span>,
    /// Report as JSON rather than as a human report.
    #[arg(long)]
    pub json: bool,
}

/// Arguments for `onevcs register`.
#[derive(Debug, Clone, PartialEq, Eq, Parser)]
pub struct RegisterArgs {
    /// The checkout to register.
    pub path: PathBuf,
    /// The origin to resolve the identity from, when the checkout's own remote
    /// is not the one to use.
    #[arg(long, value_name = "URL")]
    pub origin: Option<Url>,
}

/// Arguments for `onevcs repos`.
#[derive(Debug, Clone, PartialEq, Eq, Parser)]
pub struct ReposArgs {
    /// Also report, per identity, each check its host requires before a merge, and
    /// per checkout the policy it publishes under and what on its merge path runs a
    /// gate.
    #[arg(long)]
    pub audit_gates: bool,
}

/// Arguments for `onevcs resolve`.
#[derive(Debug, Clone, PartialEq, Eq, Parser)]
pub struct ResolveArgs {
    /// An identity key, a registered alias, an origin URL, or a path.
    pub repo: String,
}

/// The `onevcs session` subcommands.
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
pub enum SessionCommand {
    /// Open a session over a clone and worktree: a warm pool slot where the host
    /// keeps one, else one cut for this run.
    Open(SessionOpenArgs),
    /// Re-attach to an existing session.
    Adopt(SessionTokenArgs),
    /// Release a session's worktree and its occupancy lease.
    Close(SessionTokenArgs),
    /// List every session recorded for a repository.
    Holders(SessionHoldersArgs),
}

/// Arguments for `onevcs session open`.
#[derive(Debug, Clone, PartialEq, Eq, Parser)]
pub struct SessionOpenArgs {
    /// An identity key, a registered alias, an origin URL, or a path.
    pub repo: String,
    /// The branch to work on. One that already exists is continued from its own
    /// tip; one that does not is cut from the base. Omitted, one is derived.
    #[arg(long, value_name = "B")]
    pub branch: Option<String>,
    // llmlint: ignore[invalid_states_unrepresentable] a proposal is arbitrary text by
    // design — it is rendered by something that knows about tickets and plans rather
    // than about git, and making it a name git accepts is this crate's half of that
    // division of labour (`branches::sanitize`). There is no invalid value to make
    // unrepresentable: every string is a proposal this crate can answer for, and the
    // only one it refuses is the one nothing usable is left of, which is a property
    // of the sanitized result rather than of the input.
    /// A name to cut a branch at, which --branch is not: this one is sanitized,
    /// prefixed, and given the first free of -2, -3, … where something already
    /// carries it. Refused together with --branch.
    #[arg(long, value_name = "N")]
    pub branch_name: Option<String>,
    // llmlint: ignore[invalid_states_unrepresentable] what makes a prefix usable is
    // whether git accepts a ref starting with it, and deciding that runs `git
    // check-ref-format` — a subprocess, which argument parsing must not run, for the
    // reason `PublishBranchArgs::branch` gives. `branches::resolve` is the one
    // boundary that decides it, and it is also the only place that can name *which*
    // of the three layers set the value, which is what the refusal owes an operator.
    /// The prefix every branch this open cuts is put in front of, over the host's
    /// configuration and ONEVCS_BRANCH_PREFIX. An empty value cuts unprefixed.
    #[arg(long, value_name = "P")]
    pub branch_prefix: Option<String>,
    /// The branch this work is merged with and published into, and the one a new
    /// branch is cut from. Omitted, the identity's registered base is used.
    #[arg(long, value_name = "B")]
    pub base: Option<String>,
    /// Which registered checkout to clone from.
    #[arg(long, value_name = "ALIAS")]
    pub execution_checkout: Option<String>,
    /// The pool size this open places against, over the host's configuration: 0 cuts
    /// this session fresh under runs/ (and still spends the overflow), N may cut a
    /// slot while fewer than N exist. It removes no slot.
    #[arg(long, value_name = "N")]
    pub pool: Option<u32>,
    /// The overflow bound this open is admitted against, over the host's
    /// configuration: an integer, or `unlimited` to opt this open out of the cap.
    #[arg(long, value_name = "N|unlimited")]
    pub overflow: Option<Bound>,
    /// A label to stamp on the session record, as KEY=VALUE; repeatable, one value
    /// per key. What a key means is the caller's — a run, a node, a launcher — and
    /// `session holders` and `recoverable` report and filter by it.
    #[arg(long, value_name = "KEY=VALUE")]
    pub label: Vec<String>,
    /// Refuse a continued branch whose base conflicts with it (exit 3, the branch
    /// untouched) rather than open the session with the merge left in progress for it
    /// to conclude.
    // llmlint: ignore[invalid_states_unrepresentable] a presence flag, which is all the
    // amendment's `--refuse-conflicts` spells, rendered straight into the contract-fixed
    // `SessionRequest::refuse_conflicts: bool`; an enum here would be a value-taking
    // option the contract does not name.
    #[arg(long)]
    pub refuse_conflicts: bool,
}

/// A session token, for the commands that take nothing else.
#[derive(Debug, Clone, PartialEq, Eq, Parser)]
pub struct SessionTokenArgs {
    /// The token `onevcs session open` printed.
    pub token: String,
}

/// Arguments for `onevcs session holders`.
#[derive(Debug, Clone, PartialEq, Eq, Parser)]
pub struct SessionHoldersArgs {
    /// An identity key, a registered alias, an origin URL, or a path.
    pub repo: String,
    /// Only the sessions whose labels carry this KEY=VALUE; repeatable, and every
    /// pair given must match.
    #[arg(long, value_name = "KEY=VALUE")]
    pub label: Vec<String>,
    /// Report the holders as a JSON array.
    #[arg(long)]
    pub json: bool,
}

/// Arguments for `onevcs publish`.
#[derive(Debug, Clone, PartialEq, Eq, Parser)]
pub struct PublishArgs {
    /// The token of the session to publish.
    pub token: String,
    /// Override the policy the rules chose. It may narrow the stored policy but
    /// never widen it past requiring approvals.
    #[arg(long, value_name = "P")]
    pub policy: Option<MergePolicy>,
    /// The change request's title.
    #[arg(long, value_name = "T")]
    pub title: Option<String>,
    // llmlint: ignore-block[invalid_states_unrepresentable] the two body options are
    // deliberately representable together, and refused by name in `app::explicit_body`
    // — which is where the refusal can say which two were given, which one to keep,
    // and the invocation that keeps it. A clap `conflicts_with` would answer the same
    // mistake with usage text, and every other argument this command takes is checked
    // at dispatch for that reason.
    /// The change request's body. Omitted, it is opened with none.
    #[arg(long, value_name = "TEXT")]
    pub body: Option<String>,
    /// A file holding the change request's body. A body is prose, so this is the
    /// form a caller with a real one uses.
    #[arg(long, value_name = "PATH")]
    pub body_file: Option<PathBuf>,
    // llmlint: ignore-end[invalid_states_unrepresentable]
    /// Open the change request as a draft the session holds while its work is still
    /// being made. `change ready` lifts it; a later `publish` without `--draft` lifts or
    /// keeps it on its checks' verdict.
    #[arg(long)]
    pub draft: bool,
    /// Why the session is holding the change request as a draft, on one line.
    /// Omitted, a sentence saying the session is holding it while its work is still
    /// being made. Refused without `--draft`.
    // llmlint: ignore[invalid_states_unrepresentable] a reason without `--draft` is
    // deliberately representable and refused by name in `app::held_draft`, where the
    // refusal can say which option was missing; a clap `requires` would answer the
    // same mistake with usage text, and every other argument this command takes is
    // checked at dispatch for that reason.
    #[arg(long, value_name = "TEXT")]
    pub draft_reason: Option<String>,
}

/// The `onevcs change` subcommands: a session's own change request, after it exists.
///
/// Every one takes a session token and nothing that names a change request: the
/// change request is the one open from the session's branch into its base, so no
/// caller ever names a URL.
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
pub enum ChangeCommand {
    /// Report the session's change request as the host holds it.
    Show(ChangeShowArgs),
    /// Replace the session's change request's description.
    Describe(ChangeDescribeArgs),
    /// Mark the session's change request ready for review.
    Ready(ChangeReadyArgs),
}

/// Arguments for `onevcs change show`.
#[derive(Debug, Clone, PartialEq, Eq, Parser)]
pub struct ChangeShowArgs {
    /// The token of the session whose change request to report.
    pub token: String,
    /// Report as JSON rather than as a human table.
    #[arg(long)]
    pub json: bool,
}

/// Arguments for `onevcs change describe`.
#[derive(Debug, Clone, PartialEq, Eq, Parser)]
pub struct ChangeDescribeArgs {
    /// The token of the session whose change request to describe.
    pub token: String,
    // llmlint: ignore-block[invalid_states_unrepresentable] the same pair `PublishArgs`
    // carries, representable together for the same reason — and representable *absent*
    // together, because `app::described_body` is where the refusal can say that a
    // description is a body and name the two ways to hand one over.
    /// The change request's body, replacing what it has.
    #[arg(long, value_name = "TEXT")]
    pub body: Option<String>,
    /// A file holding the change request's body. A body is prose, so this is the
    /// form a caller with a real one uses.
    #[arg(long, value_name = "PATH")]
    pub body_file: Option<PathBuf>,
    // llmlint: ignore-end[invalid_states_unrepresentable]
    /// The change request's title, replacing what it has. Omitted, the title is left.
    // llmlint: ignore[invalid_states_unrepresentable] typed text for the reason given on
    // `PublishBranchArgs::title`: it becomes a `Subject` in `app::explicit_title`, where
    // the refusal names the title the operator typed rather than clap's usage text.
    #[arg(long, value_name = "T")]
    pub title: Option<String>,
    /// Report the change as it stands after the write as JSON.
    #[arg(long)]
    pub json: bool,
}

/// Arguments for `onevcs change ready`.
#[derive(Debug, Clone, PartialEq, Eq, Parser)]
pub struct ChangeReadyArgs {
    /// The token of the session whose change request to mark ready for review.
    pub token: String,
    /// Report the change as it stands as JSON.
    #[arg(long)]
    pub json: bool,
}

/// Arguments for `onevcs publish-branch`.
///
/// A branch and a title arrive as typed text, as they do on every other command
/// that takes one, and are converted at dispatch — into the crate's validated ref
/// and [`Subject`](crate::Subject) — where a refusal can name what to do about them.
#[derive(Debug, Clone, PartialEq, Eq, Parser)]
pub struct PublishBranchArgs {
    /// The completed branch to verify and publish.
    // llmlint: ignore[invalid_states_unrepresentable] this module is the parser only,
    // and what makes a branch name valid is `git check-ref-format` — a subprocess,
    // which argument parsing must not run. `branch::prepare` is the one boundary that
    // decides it, for both verbs, and its refusal names `onevcs recoverable`;
    // `tests/e2e/publish_branch.rs` holds it there.
    pub branch: String,
    /// The checkout the branch can be reached from.
    #[arg(long, value_name = "PATH")]
    pub repo: PathBuf,
    /// The change request's title.
    // llmlint: ignore[invalid_states_unrepresentable] `Subject` is what this becomes,
    // by the same conversion the library surface uses, in `app::explicit_title` —
    // before anything is cloned or committed. It is spelled the way `PublishArgs`
    // spells the same option, so one option does not meet two refusals depending on
    // which command took it: a title clap rejected would answer with usage text where
    // `onevcs publish` answers with the title the operator typed.
    #[arg(long, value_name = "T")]
    pub title: Option<String>,
    /// Override the policy the rules chose. It may narrow the stored policy but
    /// never widen it past requiring approvals.
    #[arg(long, value_name = "P")]
    pub policy: Option<MergePolicy>,
    // llmlint: ignore-block[invalid_states_unrepresentable] the same pair `PublishArgs`
    // carries, representable together for the same reason: see the note there.
    /// The change request's body. Omitted, it is opened with none.
    #[arg(long, value_name = "TEXT")]
    pub body: Option<String>,
    /// A file holding the change request's body. A body is prose, so this is the
    /// form a caller with a real one uses.
    #[arg(long, value_name = "PATH")]
    pub body_file: Option<PathBuf>,
    // llmlint: ignore-end[invalid_states_unrepresentable]
}

/// Arguments for `onevcs preserve`.
#[derive(Debug, Clone, PartialEq, Eq, Parser)]
pub struct PreserveArgs {
    /// The unpublished branch to put on its identity's origin under its own name.
    // llmlint: ignore[invalid_states_unrepresentable] this module is the parser only,
    // and what makes a branch name valid is `git check-ref-format` — a subprocess,
    // which argument parsing must not run. `preserve::run` is the boundary that decides
    // it, and its refusal names `onevcs recoverable`, exactly as
    // `PublishBranchArgs::branch`'s does.
    pub branch: String,
    /// The repository the branch belongs to: an identity key, a registered alias, an
    /// origin URL, or a path — read exactly as `publish-branch --repo` reads one.
    // llmlint: ignore[invalid_states_unrepresentable] the four forms cannot be told
    // apart by a parser — an alias and a key are registry lookups and a path is
    // `canonicalize` — so `store::resolve` is the one boundary that decides, refusing a
    // value that names nothing by name. Typed as the text `PreserveRequest::repo` is,
    // because the library form takes the same four spellings and the command is a
    // rendering of it.
    #[arg(long, value_name = "REPO")]
    pub repo: String,
}

/// Arguments for `onevcs recover`.
#[derive(Debug, Clone, PartialEq, Eq, Parser)]
pub struct RecoverArgs {
    /// The preserved branch to verify and publish.
    pub branch: String,
    /// The checkout the branch can be reached from.
    #[arg(long, value_name = "PATH")]
    pub repo: PathBuf,
    /// The published change's title, which replaces the subject synthesized from
    /// the branch.
    // llmlint: ignore[invalid_states_unrepresentable] typed text for the reason given
    // on `PublishBranchArgs::title`: it becomes a `Subject` in `app::explicit_title`,
    // and spelling it as one here would answer a blank title with clap's usage text
    // where the other two commands name the title itself.
    #[arg(long, value_name = "T")]
    pub title: Option<String>,
    // llmlint: ignore-block[invalid_states_unrepresentable] the same pair `PublishArgs`
    // carries, representable together for the same reason: see the note there.
    /// The change request's body. Omitted, it is opened with none.
    #[arg(long, value_name = "TEXT")]
    pub body: Option<String>,
    /// A file holding the change request's body. A body is prose, so this is the
    /// form a caller with a real one uses.
    #[arg(long, value_name = "PATH")]
    pub body_file: Option<PathBuf>,
    // llmlint: ignore-end[invalid_states_unrepresentable]
}

/// Arguments for `onevcs recoverable`.
#[derive(Debug, Clone, PartialEq, Eq, Parser)]
pub struct RecoverableArgs {
    // llmlint: ignore-block[invalid_states_unrepresentable,names_match_behavior] this is
    // `PublishBranchArgs::repo`'s type and spelling on purpose, so the two verbs read one
    // value one way. Which of the four forms a value is cannot be decided by a parser —
    // an alias and a key are registry lookups and a path is `canonicalize` — so
    // `store::resolve_path` is the one boundary that decides it for both, refusing a
    // value that names nothing by name; and `PATH` is the placeholder the pinned
    // `recoverable [--repo <PATH>]` usage block spells.
    /// Answer for the one identity this names, wherever it is run: a registered
    /// alias, a registered checkout's path, an identity key, or an origin — read
    /// exactly as `publish-branch --repo` reads it.
    #[arg(long, value_name = "PATH")]
    pub repo: Option<PathBuf>,
    // llmlint: ignore-end[invalid_states_unrepresentable,names_match_behavior]
    /// List every preserved branch, including the ones whose work reached their
    /// base and the ones nothing here can decide about.
    #[arg(long)]
    pub all: bool,
    /// Only the branches of sessions whose labels carry this KEY=VALUE; repeatable,
    /// and every pair given must match. Combines with `--repo` and `--session`.
    #[arg(long, value_name = "KEY=VALUE")]
    pub label: Vec<String>,
    /// Only the branches this session holds or held; repeatable. A token no session
    /// record on this host names is refused by name.
    // llmlint: ignore[invalid_states_unrepresentable] a token is typed text here for
    // the reason a branch name is on `PublishBranchArgs`: this module is the parser
    // only, and what makes a token one this host knows is a read of the session
    // records — which argument parsing must not do. `vcs::asked` is the one boundary
    // that decides it, and it refuses an unknown token by name rather than with
    // clap's usage text.
    #[arg(long, value_name = "TOKEN")]
    pub session: Vec<String>,
    /// Report as JSON rather than as a human table.
    #[arg(long)]
    pub json: bool,
}

/// Arguments for `onevcs status`.
#[derive(Debug, Clone, PartialEq, Eq, Parser)]
pub struct StatusArgs {
    /// The work to report on: a change request's URL, a session token, a branch
    /// name, or a commit — read in that order.
    // llmlint: ignore[invalid_states_unrepresentable] four spellings share this one
    // operand deliberately, and which one a value is cannot be decided by a parser: a
    // session token names a file under the state root, a branch name is decided by
    // `git check-ref-format`, and a commit is one a repository has. `status::resolve`
    // is the boundary that decides, and its refusal names every candidate rather
    // than answering with clap's usage text.
    pub reference: String,
    /// Report as JSON rather than as a human table.
    #[arg(long)]
    pub json: bool,
}

/// Arguments for `onevcs import`.
#[derive(Debug, Clone, PartialEq, Eq, Parser)]
pub struct ImportArgs {
    /// The branch to make reachable.
    // llmlint: ignore[invalid_states_unrepresentable] a branch name is decided by
    // `git check-ref-format`, which is a subprocess argument parsing must not run —
    // the same reason `PublishBranchArgs::branch` is typed text. `import::run` is the
    // one boundary that decides it, and its refusal names the command that lists the
    // branches there are.
    pub branch: String,
    /// The checkout whose identity the branch is imported into.
    #[arg(long, value_name = "PATH")]
    pub repo: PathBuf,
    /// Where to read it from: the path of a checkout or a run clone, or a remote
    /// ref. Omitted, everywhere this identity keeps work is searched.
    #[arg(long, value_name = "SOURCE")]
    pub from: Option<String>,
    /// An alternate local name to import it under, for when the original is spent.
    // llmlint: ignore[invalid_states_unrepresentable] the same boundary as `branch`
    // above, and the same reason: git's own parser decides it, in `import::run`,
    // where the refusal can name the option that carried it.
    #[arg(long, value_name = "NAME")]
    pub r#as: Option<String>,
}

/// Arguments for `onevcs integrate`.
#[derive(Debug, Clone, PartialEq, Eq, Parser)]
pub struct IntegrateArgs {
    /// The branches to merge, in the order they should land.
    #[arg(required = true, num_args = 1..)]
    pub branches: Vec<String>,
    /// Push the base once every branch has landed.
    #[arg(long)]
    pub push: bool,
}

/// Arguments for `onevcs sync`.
#[derive(Debug, Clone, PartialEq, Eq, Parser)]
pub struct SyncArgs {
    /// The branch to fast-forward. Omitted, the registered base is used.
    pub branch: Option<String>,
}

/// Arguments for `onevcs sweep`.
///
/// Both the spelling and the default are shared with `oneagentgraph sweep`, because
/// one composing caller forwards its own arguments to each unchanged. Neither side
/// may depart from them alone.
// llmlint: ignore-block[contracts_have_one_source_or_a_drift_gate] the default is not
// restated here: it comes from `sweep::DEFAULT_MIN_AGE_HOURS`, which is the crate's one
// source for it and is gated against `docs/inferred-surface.md`. Why the other side of
// the surface cannot be gated from this repository is written out at that constant.
#[derive(Debug, Clone, PartialEq, Eq, Parser)]
pub struct SweepArgs {
    /// Report what would be reclaimed and remove nothing.
    #[arg(long)]
    pub dry_run: bool,
    /// Leave anything written inside this many hours alone.
    // A window rather than a number, so nothing past the parser can be handed hours
    // that are negative, infinite, or not a number: `sweep::hours` refuses those
    // here, where clap's own usage error names the option that carried them.
    #[arg(
        long,
        value_name = "HOURS",
        default_value = sweep::DEFAULT_MIN_AGE_HOURS,
        value_parser = sweep::hours,
    )]
    pub min_age_hours: Duration,
    /// How to write the report: prose, or one JSON object a consumer reads by field
    /// name.
    // `--format` rather than this crate's usual `--json`, because the option is
    // forwarded to `oneagentgraph sweep` unchanged by the same composing caller as the
    // two above and that verb spells it this way.
    #[arg(long, value_enum, value_name = "FORMAT", default_value_t = SweepFormat::Text)]
    pub format: SweepFormat,
}
// llmlint: ignore-end[contracts_have_one_source_or_a_drift_gate]

/// How `onevcs sweep` writes its report.
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum SweepFormat {
    /// The human report: what it did, then what it kept and why.
    Text,
    /// One JSON object carrying the same report.
    Json,
}

/// Arguments for `onevcs events`.
#[derive(Debug, Clone, PartialEq, Eq, Parser)]
pub struct EventsArgs {
    /// The token of the session whose stream to read.
    pub token: String,
    /// Keep reading as the session writes.
    #[arg(long)]
    pub follow: bool,
    /// Report only the events a filter spec admits: the spec inline as JSON when
    /// it opens with `{`, otherwise the path of a file holding one.
    #[arg(long, value_name = "SPEC")]
    pub filter: Option<String>,
}

/// The `onevcs artifact` subcommands.
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
pub enum ArtifactCommand {
    /// Write a stored artifact to stdout.
    Cat(ArtifactCatArgs),
}

/// Arguments for `onevcs artifact cat`.
#[derive(Debug, Clone, PartialEq, Eq, Parser)]
pub struct ArtifactCatArgs {
    /// The artifact id an event referenced.
    pub id: String,
}

/// The `onevcs rules` subcommands.
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
pub enum RulesCommand {
    /// Report which rule a repository matches, and the policy that follows.
    Check(RulesCheckArgs),
}

/// Arguments for `onevcs rules check`.
#[derive(Debug, Clone, PartialEq, Eq, Parser)]
pub struct RulesCheckArgs {
    /// An identity key, a registered alias, an origin URL, or a path.
    pub repo: String,
}

/// The `onevcs release` subcommands.
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
pub enum ReleaseCommand {
    /// Report which release targets a repository has, and what it adopts.
    Targets(ReleaseTargetsArgs),
    /// Report every target a repository has and what each has released right now.
    Discover(ReleaseDiscoverArgs),
    /// Report what version of a target is released right now.
    Latest(ReleaseLatestArgs),
    /// Report whether the release carrying a landed change is out yet.
    Status(ReleaseStatusArgs),
    /// Record that somebody performed a human-step release.
    Acknowledge(ReleaseAcknowledgeArgs),
    /// Report what a repository's own `release-targets.toml` declares it publishes.
    Declaration(ReleaseDeclarationArgs),
}

/// Arguments for `onevcs release declaration`.
#[derive(Debug, Clone, PartialEq, Eq, Parser)]
pub struct ReleaseDeclarationArgs {
    /// A repository's root, or the `release-targets.toml` in it.
    ///
    /// A path rather than the identity spelling every other verb takes: this
    /// reads a file a *checkout* carries, and a repository this host has never
    /// registered is exactly the case a consumer asks about.
    // llmlint: ignore[invalid_states_unrepresentable] whether a path is a directory
    // or the document itself is decided by looking at the filesystem, which a parser
    // cannot do, and `declaration::read` is the one boundary that decides it.
    pub path: PathBuf,
    /// Report as JSON rather than as a human table.
    ///
    /// The two renderings this verb has, and deliberately not a third: rendering a
    /// declaration back *as TOML* is a library call
    /// (`onevcs::render_release_declaration`) and not a verb, because a producer's
    /// comments are not this crate's to keep, and a verb that redirected a rendering
    /// over a repository's own `release-targets.toml` would delete the reasoning that
    /// is the most valuable thing in it. A caller *producing* a declaration has no
    /// comments to lose and reaches it through the library.
    #[arg(long)]
    pub json: bool,
}

/// Arguments for `onevcs release targets`.
#[derive(Debug, Clone, PartialEq, Eq, Parser)]
pub struct ReleaseTargetsArgs {
    /// An identity key, a registered alias, an origin URL, or a path.
    pub repo: String,
    /// Report as JSON rather than as a human table.
    #[arg(long)]
    pub json: bool,
}

/// Arguments for `onevcs release discover`.
#[derive(Debug, Clone, PartialEq, Eq, Parser)]
pub struct ReleaseDiscoverArgs {
    /// An identity key, a registered alias, an origin URL, or a path.
    pub repo: String,
    /// Report as JSON rather than as a human table.
    #[arg(long)]
    pub json: bool,
}

/// Arguments for `onevcs release latest`.
#[derive(Debug, Clone, PartialEq, Eq, Parser)]
pub struct ReleaseLatestArgs {
    /// An identity key, a registered alias, an origin URL, or a path.
    pub repo: String,
    /// Which target to ask about. Omitted, the repository's `default_target` is
    /// used.
    #[arg(long, value_name = "NAME")]
    pub target: Option<TargetName>,
    /// Report as JSON rather than as a human table.
    #[arg(long)]
    pub json: bool,
}

/// Arguments for `onevcs release status`.
#[derive(Debug, Clone, PartialEq, Eq, Parser)]
pub struct ReleaseStatusArgs {
    /// The work to report on: a change request's URL, a session token, a branch
    /// name, or a commit — the same four spellings `onevcs status` takes.
    // llmlint: ignore[invalid_states_unrepresentable] the same operand `StatusArgs`
    // carries, for the same reason: which of the four spellings a value is cannot be
    // decided by a parser, and `status::resolve` is the one boundary that decides it.
    pub reference: String,
    /// Which target to ask about. Omitted, the repository's `default_target` is
    /// used.
    #[arg(long, value_name = "NAME")]
    pub target: Option<TargetName>,
    /// Report as JSON rather than as a human table.
    #[arg(long)]
    pub json: bool,
}

/// Arguments for `onevcs release acknowledge`.
#[derive(Debug, Clone, PartialEq, Eq, Parser)]
pub struct ReleaseAcknowledgeArgs {
    /// The landed work the release carries, in the same four spellings
    /// `onevcs status` takes.
    // llmlint: ignore[invalid_states_unrepresentable] as on `ReleaseStatusArgs` above.
    pub reference: String,
    /// The target that was released. Required: this operation records a fact
    /// somebody performed, and which artifact they released is not a thing to
    /// infer.
    #[arg(long, value_name = "NAME")]
    pub target: TargetName,
    /// The version that was released.
    // llmlint: ignore[invalid_states_unrepresentable] whether a value is a semantic
    // version is decided in `release::acknowledge`, beside the three other refusals
    // this operation makes, so an operator meets one vocabulary of refusal rather
    // than clap's usage text for one of the four and prose for the rest.
    #[arg(long, value_name = "VERSION")]
    pub version: String,
    /// Replace a different version already recorded for this landing, keeping the
    /// old one in the record's own history.
    #[arg(long)]
    pub supersede: bool,
    /// Report as JSON rather than as a human table.
    #[arg(long)]
    pub json: bool,
}