frame 0.1.5

A markdown task tracker with a terminal UI for humans and a CLI for agents
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
use clap::{Args, Parser, Subcommand};

#[derive(Parser)]
#[command(name = "fr", about = concat!("[>] frame v", env!("CARGO_PKG_VERSION"), " - your backlog is plain text"), version)]
pub struct Cli {
    #[command(subcommand)]
    pub command: Option<Commands>,

    /// Output as JSON
    #[arg(long, global = true)]
    pub json: bool,

    /// Run against a different project directory
    #[arg(short = 'C', long = "project-dir", global = true)]
    pub project_dir: Option<String>,
}

#[derive(Subcommand)]
pub enum Commands {
    /// Initialize a new frame project in the current directory
    Init(InitArgs),
    /// List tasks in a track
    List(ListArgs),
    /// Show task details
    Show(ShowArgs),
    /// Show ready (unblocked) tasks
    Ready(ReadyArgs),
    /// Show blocked tasks and their blockers
    Blocked,
    /// Search tasks by regex
    Search(SearchArgs),
    /// List inbox items, or add a new one
    Inbox(InboxCmd),
    /// List all tracks
    Tracks,
    /// Show task statistics
    Stats(StatsArgs),
    /// Show recently completed tasks
    Recent(RecentArgs),
    /// Show dependency tree for a task
    Deps(DepsArgs),
    /// Validate project integrity
    Check,
    /// Add a task to a track's backlog (bottom)
    Add(AddArgs),
    /// Push a task to top of a track's backlog
    Push(PushArgs),
    /// Add a subtask
    Sub(SubArgs),
    /// Change task state
    State(StateArgs),
    /// Start a task (shortcut for state <ID> active)
    Start(StartArgs),
    /// Mark a task done (shortcut for state <ID> done)
    Done(DoneArgs),
    /// Add or remove tags
    Tag(TagArgs),
    /// Add or remove dependencies
    Dep(DepArgs),
    /// Set task note
    Note(NoteArgs),
    /// Add file reference
    Ref(RefArgs),
    /// Set spec reference
    Spec(SpecArgs),
    /// Change task title
    Title(TitleArgs),
    /// Move a task (reorder or cross-track)
    Mv(MvArgs),
    /// Triage an inbox item to a track
    Triage(TriageArgs),
    /// Track management
    Track(TrackCmd),
    /// Run maintenance and cleanup
    Clean(CleanArgs),
    /// Import tasks from a markdown file
    Import(ImportArgs),
    /// Permanently delete tasks
    Delete(DeleteArgs),
    /// Manage project registry
    Projects(ProjectsCmd),
    /// View or manage the recovery log
    Recovery(RecoveryCmd),
}

// ---------------------------------------------------------------------------
// Init args
// ---------------------------------------------------------------------------

#[derive(Args)]
pub struct InitArgs {
    /// Project name (default: inferred from directory name)
    #[arg(long)]
    pub name: Option<String>,
    /// Create an initial track: --track <id> "name" (repeatable)
    #[arg(long, num_args = 2, value_names = ["ID", "NAME"], action = clap::ArgAction::Append)]
    pub track: Vec<String>,
    /// Reinitialize even if frame/ already exists
    #[arg(long)]
    pub force: bool,
}

// ---------------------------------------------------------------------------
// Read command args
// ---------------------------------------------------------------------------

#[derive(Args)]
pub struct ListArgs {
    /// Track to list (default: all active tracks)
    pub track: Option<String>,
    /// Filter by state (todo, active, blocked, done, parked)
    #[arg(long)]
    pub state: Option<String>,
    /// Filter by tag
    #[arg(long)]
    pub tag: Option<String>,
    /// Include all tracks (shelved, archived)
    #[arg(long)]
    pub all: bool,
}

#[derive(Args)]
pub struct ShowArgs {
    /// Task ID to show
    pub id: String,
    /// Include ancestor context (parent chain)
    #[arg(long)]
    pub context: bool,
}

#[derive(Args)]
pub struct ReadyArgs {
    /// Show only cc-tagged tasks on cc-focus track
    #[arg(long)]
    pub cc: bool,
    /// Filter to specific track
    #[arg(long)]
    pub track: Option<String>,
    /// Filter by tag
    #[arg(long)]
    pub tag: Option<String>,
}

#[derive(Args)]
pub struct SearchArgs {
    /// Regex pattern to search for
    pub pattern: String,
    /// Limit search to specific track
    #[arg(long)]
    pub track: Option<String>,
    /// Also search archived tasks
    #[arg(short, long)]
    pub archive: bool,
}

#[derive(Args)]
pub struct InboxCmd {
    /// Text to add (if omitted, lists inbox items)
    pub text: Option<String>,
    /// Tag(s) to add to the new inbox item
    #[arg(long)]
    pub tag: Vec<String>,
    /// Note body for the new inbox item
    #[arg(long)]
    pub note: Option<String>,
}

#[derive(Args)]
pub struct StatsArgs {
    /// Include shelved tracks
    #[arg(long)]
    pub all: bool,
}

#[derive(Args)]
pub struct RecentArgs {
    /// Maximum number of recent items to show
    #[arg(long, default_value = "20")]
    pub limit: usize,
}

#[derive(Args)]
pub struct DepsArgs {
    /// Task ID to show dependency tree for
    pub id: String,
}

// ---------------------------------------------------------------------------
// Write command args
// ---------------------------------------------------------------------------

#[derive(Args)]
pub struct AddArgs {
    /// Track to add the task to
    pub track: String,
    /// Task title
    pub title: String,
    /// Insert after this task ID
    #[arg(long)]
    pub after: Option<String>,
    /// Note that this task was found while working on another task
    #[arg(long)]
    pub found_from: Option<String>,
}

#[derive(Args)]
pub struct PushArgs {
    /// Track to push the task to
    pub track: String,
    /// Task title
    pub title: String,
}

#[derive(Args)]
pub struct SubArgs {
    /// Parent task ID
    pub id: String,
    /// Subtask title
    pub title: String,
}

#[derive(Args)]
pub struct StateArgs {
    /// Task ID
    pub id: String,
    /// New state (todo, active, blocked, done, parked)
    pub state: String,
}

#[derive(Args)]
pub struct StartArgs {
    /// Task ID
    pub id: String,
}

#[derive(Args)]
pub struct DoneArgs {
    /// Task ID
    pub id: String,
}

#[derive(Args)]
pub struct TagArgs {
    /// Task ID
    pub id: String,
    /// Action: "add" or "rm"
    pub action: String,
    /// Tag name
    pub tag: String,
}

#[derive(Args)]
pub struct DepArgs {
    /// Task ID
    pub id: String,
    /// Action: "add" or "rm"
    pub action: String,
    /// Dependency task ID
    pub dep_id: String,
}

#[derive(Args)]
pub struct NoteArgs {
    /// Task ID
    pub id: String,
    /// Note text
    pub text: String,
    /// Replace existing note instead of appending
    #[arg(long)]
    pub replace: bool,
}

#[derive(Args)]
pub struct RefArgs {
    /// Task ID
    pub id: String,
    /// File path
    pub path: String,
}

#[derive(Args)]
pub struct SpecArgs {
    /// Task ID
    pub id: String,
    /// Spec path (e.g., doc/spec.md#section)
    pub path: String,
}

#[derive(Args)]
pub struct TitleArgs {
    /// Task ID
    pub id: String,
    /// New title
    pub title: String,
}

#[derive(Args)]
pub struct MvArgs {
    /// Task ID
    pub id: String,
    /// Numeric position (0-indexed)
    pub position: Option<usize>,
    /// Move to top of backlog
    #[arg(long)]
    pub top: bool,
    /// Move after this task ID
    #[arg(long)]
    pub after: Option<String>,
    /// Move to a different track
    #[arg(long)]
    pub track: Option<String>,
    /// Promote subtask to top-level
    #[arg(long)]
    pub promote: bool,
    /// Reparent under the given task ID
    #[arg(long)]
    pub parent: Option<String>,
}

#[derive(Args)]
pub struct TriageArgs {
    /// Inbox item index (1-based)
    pub index: usize,
    /// Target track
    #[arg(long)]
    pub track: String,
    /// Insert at top of backlog
    #[arg(long)]
    pub top: bool,
    /// Insert at bottom of backlog (default)
    #[arg(long)]
    pub bottom: bool,
    /// Insert after this task ID
    #[arg(long)]
    pub after: Option<String>,
}

#[derive(Args)]
pub struct DeleteArgs {
    /// Task IDs to delete
    #[arg(required = true)]
    pub ids: Vec<String>,
    /// Skip confirmation prompt
    #[arg(long)]
    pub yes: bool,
}

// ---------------------------------------------------------------------------
// Track management
// ---------------------------------------------------------------------------

#[derive(Args)]
pub struct TrackCmd {
    #[command(subcommand)]
    pub action: TrackAction,
}

#[derive(Subcommand)]
pub enum TrackAction {
    /// Create a new track
    New(TrackNewArgs),
    /// Shelve a track
    Shelve(TrackIdArg),
    /// Activate a track
    Activate(TrackIdArg),
    /// Archive a track
    Archive(TrackIdArg),
    /// Delete an empty track
    Delete(TrackIdArg),
    /// Move (reorder) a track
    Mv(TrackMvArgs),
    /// Set or clear the cc-focus track
    CcFocus(CcFocusArgs),
    /// Rename a track (name, id, or prefix)
    Rename(TrackRenameArgs),
}

#[derive(Args)]
pub struct TrackNewArgs {
    /// Track ID (short identifier)
    pub id: String,
    /// Track name
    pub name: String,
}

#[derive(Args)]
pub struct TrackIdArg {
    /// Track ID
    pub id: String,
}

#[derive(Args)]
pub struct CcFocusArgs {
    /// Track ID (omit with --clear)
    pub id: Option<String>,
    /// Clear the cc-focus setting
    #[arg(long)]
    pub clear: bool,
}

#[derive(Args)]
pub struct TrackMvArgs {
    /// Track ID
    pub id: String,
    /// New position (0-indexed among active tracks)
    pub position: usize,
}

#[derive(Args)]
pub struct TrackRenameArgs {
    /// Track ID
    pub id: String,
    /// New display name
    #[arg(long)]
    pub name: Option<String>,
    /// New track ID
    #[arg(long, value_name = "NEW_ID")]
    pub new_id: Option<String>,
    /// New prefix (bulk-rewrites task IDs)
    #[arg(long)]
    pub prefix: Option<String>,
    /// Preview changes without writing
    #[arg(long)]
    pub dry_run: bool,
    /// Auto-confirm prefix rename
    #[arg(long, short)]
    pub yes: bool,
}

// ---------------------------------------------------------------------------
// Maintenance
// ---------------------------------------------------------------------------

#[derive(Args)]
pub struct CleanArgs {
    /// Show what would be done without making changes
    #[arg(long)]
    pub dry_run: bool,
}

#[derive(Args)]
pub struct ImportArgs {
    /// Markdown file to import
    pub file: String,
    /// Target track
    #[arg(long)]
    pub track: String,
    /// Insert at top of backlog
    #[arg(long)]
    pub top: bool,
    /// Insert after this task ID
    #[arg(long)]
    pub after: Option<String>,
}

// ---------------------------------------------------------------------------
// Project registry
// ---------------------------------------------------------------------------

#[derive(Args)]
pub struct ProjectsCmd {
    #[command(subcommand)]
    pub action: Option<ProjectsAction>,
}

#[derive(Subcommand)]
pub enum ProjectsAction {
    /// List registered projects (default)
    List,
    /// Register a project by path
    Add(ProjectsAddArgs),
    /// Remove a project from the registry
    Remove(ProjectsRemoveArgs),
}

#[derive(Args)]
pub struct ProjectsAddArgs {
    /// Path to the project directory
    pub path: String,
}

#[derive(Args)]
pub struct ProjectsRemoveArgs {
    /// Project name or path
    pub name_or_path: String,
}

// ---------------------------------------------------------------------------
// Recovery log
// ---------------------------------------------------------------------------

#[derive(Args)]
pub struct RecoveryCmd {
    #[command(subcommand)]
    pub action: Option<RecoveryAction>,
    /// Maximum number of entries to show (default: 10)
    #[arg(long)]
    pub limit: Option<usize>,
    /// Show entries after this timestamp (ISO-8601)
    #[arg(long)]
    pub since: Option<String>,
    /// Output as JSON
    #[arg(long)]
    pub json: bool,
}

#[derive(Subcommand)]
pub enum RecoveryAction {
    /// Remove old entries
    Prune(RecoveryPruneArgs),
    /// Print the absolute path to the recovery log
    Path,
}

#[derive(Args)]
pub struct RecoveryPruneArgs {
    /// Remove entries older than this timestamp (default: 30 days ago)
    #[arg(long)]
    pub before: Option<String>,
    /// Remove all entries
    #[arg(long)]
    pub all: bool,
}