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
use clap::{Parser, Subcommand, ValueEnum};
#[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>,
},
/// 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 identifier; common values:
/// task, bug, epic, feature, chore, spike, question,
/// discussion, retro. 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,
},
/// 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>,
/// Include closed tasks
#[arg(long)]
all: 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 the number of entries shown. Text mode appends a
/// `... and N more` footer when the queue is longer; JSON
/// returns an array of at most this length. 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,
},
/// 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,
},
/// Close a reviewed task: archive and remove worktree. Must run from repo root.
Close {
id: String,
/// Reviewer message, embedded in the state-branch close
/// commit body. Repeatable, like `git commit -m … -m …`:
/// each value becomes its own paragraph.
#[arg(short = 'm', long = "message")]
message: Vec<String>,
#[arg(long = "as")]
identity: Option<String>,
/// Force a remote round-trip on this close. Mirrors
/// `bl claim --sync`.
#[arg(long, conflicts_with = "no_sync")]
sync: bool,
/// Skip any configured remote round-trip on this close.
#[arg(long, conflicts_with = "sync")]
no_sync: bool,
},
/// 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>,
},
/// 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,
},
/// 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,
},
/// 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,
},
}
#[derive(Clone, Debug, ValueEnum)]
pub enum ShellArg {
Bash,
Zsh,
Fish,
}
#[derive(Subcommand, Debug)]
pub enum DepCmd {
/// Add a dependency: TASK depends on DEPENDS_ON.
Add { task: String, depends_on: String },
/// Remove a dependency.
Rm { task: String, depends_on: String },
/// Print parent/child tree with box-drawing. Deps and gates show
/// as inline annotations, never as nesting. Without ID, prints
/// every parentless task as its own root.
Tree {
id: Option<String>,
/// Emit a nested JSON tree instead of the box-drawn text.
#[arg(long)]
json: bool,
},
}
#[derive(Subcommand, Debug)]
pub enum LinkCmd {
/// Add a typed link: relates_to, duplicates, supersedes, replies_to, gates.
/// `gates` blocks the source task from closing until the target closes.
Add {
task: String,
link_type: String,
target: String,
},
/// Remove a typed link.
Rm {
task: String,
link_type: String,
target: String,
},
}