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
use clap::{Parser, Subcommand};
#[derive(Parser, Debug)]
#[command(name = "bl", version, about = "Git-native task tracker", long_about = None)]
pub struct Cli {
/// Force plain output: no color, no Unicode glyphs. Overrides
/// terminal detection.
#[arg(long, global = true)]
pub plain: bool,
#[command(subcommand)]
pub command: Command,
}
#[derive(Subcommand, Debug)]
pub enum Command {
/// Initialize balls in the current git repository.
Init {
/// Stealth mode: store tasks outside the repo (not git-tracked).
#[arg(long)]
stealth: bool,
/// Custom absolute path for task storage. Implies --stealth.
#[arg(long)]
tasks_dir: Option<String>,
/// Bootstrap a bare central hub: bare-clone SOURCE into
/// HUBDIR/.git and reconstruct the loose store there.
/// Idempotent. SOURCE's `main` must already be
/// balls-initialized (run `bl init` in a working clone and
/// push first). Mutually exclusive with --stealth/--tasks-dir.
#[arg(long, num_args = 2, value_names = ["SOURCE", "HUBDIR"])]
bare: Option<Vec<String>>,
},
/// Create a new task.
Create {
/// Task title
title: String,
/// Priority: 1 (highest) to 4 (lowest)
#[arg(short = 'p', long, default_value_t = 3)]
priority: u8,
/// Task type label (free-form: task, bug, epic, feature,
/// chore, …). Only `epic` has special rendering.
#[arg(short = 't', long, default_value = "task")]
task_type: String,
/// Parent task ID
#[arg(long)]
parent: Option<String>,
/// Dependency task ID (repeatable)
#[arg(long = "dep")]
dep: Vec<String>,
/// Tag (repeatable)
#[arg(long = "tag")]
tag: Vec<String>,
/// Description
#[arg(short = 'd', long, default_value = "")]
description: String,
/// Integration branch this task's `bl review` squashes into,
/// overriding the repo `target_branch` and current-branch
/// fallback (e.g. a hotfix targeting `main` in a develop repo).
#[arg(long = "target-branch")]
target_branch: Option<String>,
#[command(flatten)]
participant: ParticipantFlags,
},
/// List tasks.
List {
/// Filter by status
#[arg(long)]
status: Option<String>,
/// Filter by priority
#[arg(short = 'p', long)]
priority: Option<u8>,
/// Filter by parent
#[arg(long)]
parent: Option<String>,
/// Filter by tag
#[arg(long)]
tag: Option<String>,
/// Open and closed tasks together (closed ones reconstructed
/// from `balls/tasks` history — high-volume on old repos).
#[arg(long)]
all: bool,
/// Only closed/archived tasks, reconstructed from the
/// `balls/tasks` history.
#[arg(long)]
closed: bool,
/// JSON output
#[arg(long)]
json: bool,
},
/// Show details of a task. For closed/review tasks, prints a `delivered:` line resolving the squash-merge commit on main.
Show {
id: String,
#[arg(long)]
json: bool,
/// Append absolute ISO timestamps next to the relative ones.
#[arg(long)]
verbose: bool,
},
/// Show tasks ready to be claimed.
Ready {
#[arg(long)]
json: bool,
#[arg(long)]
no_fetch: bool,
/// Cap entries shown (text mode appends a `... and N more`
/// footer; JSON returns at most this many). Must be >= 1.
#[arg(long)]
limit: Option<usize>,
},
/// Claim a task: update the task file and create a worktree.
Claim {
id: String,
#[arg(long = "as")]
identity: Option<String>,
/// Claim without creating a git worktree (status flip only).
#[arg(long)]
no_worktree: bool,
/// Force a remote round-trip on this claim. Overrides repo
/// and per-clone config. Closes the offline-agent claim race
/// at the cost of requiring network.
#[arg(long, conflicts_with = "no_sync")]
sync: bool,
/// Skip any configured remote round-trip on this claim. Lets
/// you claim offline against a repo whose maintainer set
/// `require_remote_on_claim`.
#[arg(long, conflicts_with = "sync")]
no_sync: bool,
#[command(flatten)]
participant: ParticipantFlags,
},
/// Submit work for review: merge to main, keep worktree for rework.
Review {
id: String,
/// Commit message. Repeatable, like `git commit -m … -m …`:
/// the first `-m` is the title (under ~50 chars), each later
/// `-m` becomes a body paragraph with a blank line between.
/// A single value may also span multiple lines. The `[bl-id]`
/// delivery tag is appended to the title automatically.
#[arg(short = 'm', long = "message")]
message: Vec<String>,
#[arg(long = "as")]
identity: Option<String>,
/// Force a remote round-trip on this review. Mirrors
/// `bl claim --sync`; flips the per-event sync policy on for
/// just this invocation.
#[arg(long, conflicts_with = "no_sync")]
sync: bool,
/// Skip any configured remote round-trip on this review.
#[arg(long, conflicts_with = "sync")]
no_sync: bool,
#[command(flatten)]
participant: ParticipantFlags,
},
/// Close a reviewed task: archive and remove worktree. Must run from repo root.
Close {
id: String,
#[command(flatten)]
args: CloseArgs,
},
/// Drop a claim: reset task and remove worktree.
Drop {
id: String,
#[arg(long)]
force: bool,
},
/// Update fields of a task.
Update {
id: String,
/// field=value pairs
assignments: Vec<String>,
#[arg(long)]
note: Option<String>,
#[arg(long = "as")]
identity: Option<String>,
#[command(flatten)]
participant: ParticipantFlags,
},
/// Manage dependencies.
Dep {
#[command(subcommand)]
sub: DepCmd,
},
/// Manage typed links (relates_to, duplicates, supersedes, replies_to, gates).
Link {
#[command(subcommand)]
sub: LinkCmd,
},
/// Sync with remote: fetch, merge, resolve, push.
Sync {
#[arg(long, default_value = "origin")]
remote: String,
/// Sync a single task by local ID or remote key.
#[arg(long)]
task: Option<String>,
/// Stage every plugin sync report for human review under
/// `.balls/local/pending-sync/sync/` instead of applying it.
/// Use `--apply <id>` or `--discard <id>` to act on a staged
/// entry afterward.
#[arg(long, conflicts_with_all = ["apply", "discard", "list_staged"])]
review: bool,
/// Apply a previously staged sync report by id and remove the
/// staged file.
#[arg(long, value_name = "ID", conflicts_with_all = ["discard", "list_staged"])]
apply: Option<String>,
/// Drop a staged sync report without applying it.
#[arg(long, value_name = "ID", conflicts_with = "list_staged")]
discard: Option<String>,
/// List staged sync reports awaiting review.
#[arg(long)]
list_staged: bool,
},
/// Merge a task file with git conflict markers using balls' field-level rules. Rarely needed — `bl sync` runs this automatically.
Resolve { file: String },
/// Prime an agent session: sync and print ready + in-progress tasks.
Prime {
#[arg(long = "as")]
identity: Option<String>,
#[arg(long)]
json: bool,
},
/// Read-only health check: report repo/bl state drift and the
/// command that fixes each. Changes nothing.
Doctor,
/// Scan and repair malformed task files and orphaned state.
Repair {
#[arg(long)]
fix: bool,
/// Retract a stale half-push warning for ID. Writes a
/// `state: forget-half-push <id>` commit on the state branch
/// so the detector stops flagging it. ID must currently be
/// flagged. Repeatable.
#[arg(long = "forget-half-push", value_name = "ID")]
forget_half_push: Vec<String>,
/// Retract every half-push warning currently detected.
#[arg(
long = "forget-all-half-pushes",
conflicts_with = "forget_half_push"
)]
forget_all_half_pushes: bool,
},
/// Re-point this repo's task state branch at TARGET (a configured
/// git remote) and reconcile local-only tasks onto it. The link
/// is per-clone unless `--commit`. `--detach` instead forks the
/// current branch into a fresh local orphan and goes standalone.
Remaster {
/// Git remote whose `balls/tasks` becomes authoritative.
/// Omit only with `--detach`.
target: Option<String>,
/// Write the link to the committed `.balls/config.json`
/// (project-wide) instead of the per-clone local override.
#[arg(long, conflicts_with = "detach")]
commit: bool,
/// Sever shared history and clear the link: the repo becomes
/// a standalone local task store again.
#[arg(long)]
detach: bool,
},
/// Print the agent skill guide (SKILL.md).
Skill,
/// Generate shell completions, or install/uninstall to ~/.local/share.
Completions {
/// Shell to generate completions for. Omit when using `--install`/`--uninstall`.
shell: Option<ShellArg>,
/// Install bash, zsh, and fish completions to standard XDG paths.
#[arg(long, conflicts_with_all = ["shell", "uninstall"])]
install: bool,
/// Remove completions previously written by `--install`.
#[arg(long, conflicts_with_all = ["shell", "install"])]
uninstall: bool,
},
}
pub use crate::cli_sub::{CloseArgs, DepCmd, LinkCmd, ParticipantFlags, ShellArg};