sb-tracker 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
sb_tracker/__init__.py ADDED
@@ -0,0 +1,11 @@
1
+ """
2
+ Simple Beads (sb) - A minimal, standalone issue tracker for individuals.
3
+ No git hooks, no complex dependencies, just one JSON file.
4
+ """
5
+
6
+ __version__ = "0.1.0"
7
+ __author__ = "Simple Beads Contributors"
8
+
9
+ from .cli import main
10
+
11
+ __all__ = ["main"]
sb_tracker/cli.py ADDED
@@ -0,0 +1,552 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Simple Beads (sb) - A minimal, standalone issue tracker for individuals.
4
+ No git hooks, no complex dependencies, just one JSON file.
5
+ """
6
+
7
+ import json
8
+ import os
9
+ import sys
10
+ from datetime import datetime
11
+
12
+ def find_db_path():
13
+ """Walk up the directory tree to find .sb.json."""
14
+ cwd = os.getcwd()
15
+ while cwd != os.path.dirname(cwd): # Stop at root
16
+ potential_path = os.path.join(cwd, ".sb.json")
17
+ if os.path.exists(potential_path):
18
+ return potential_path
19
+ # Also stop at git root to keep it project-local
20
+ if os.path.exists(os.path.join(cwd, ".git")):
21
+ return os.path.join(cwd, ".sb.json")
22
+ cwd = os.path.dirname(cwd)
23
+ return os.path.join(os.getcwd(), ".sb.json")
24
+
25
+ DB_FILE = find_db_path()
26
+
27
+ def load_db():
28
+ if not os.path.exists(DB_FILE):
29
+ return {"issues": []}
30
+ try:
31
+ with open(DB_FILE, "r") as f:
32
+ return json.load(f)
33
+ except (json.JSONDecodeError, IOError):
34
+ return {"issues": []}
35
+
36
+ def save_db(db):
37
+ with open(DB_FILE, "w") as f:
38
+ json.dump(db, f, indent=2)
39
+
40
+ def init():
41
+ if os.path.exists(DB_FILE):
42
+ print(f"Error: {DB_FILE} already exists.")
43
+ return
44
+ save_db({"issues": []})
45
+ print(f"Initialized Simple Beads in {DB_FILE}")
46
+
47
+ # Create or append to AGENTS.md
48
+ agents_md_content = """## Using SB Tracker
49
+
50
+ This project uses [SB Tracker](https://github.com/sirius-cc-wu/sb-tracker) for task tracking.
51
+
52
+ **Agents**: Please use the `sb` command to track work:
53
+ - `sb add "Task title" [priority] [description]` - Create a task
54
+ - `sb list` - View open tasks
55
+ - `sb ready` - See tasks ready to work on
56
+ - `sb done <id>` - Mark a task as complete
57
+ - `sb promote <id>` - Generate a summary of task progress
58
+
59
+ ### Priority Levels (Required Numeric Values)
60
+
61
+ When using `sb add`, specify priority as a **numeric value** (0-3):
62
+ - **0** = P0 (Critical) - Blocking other work
63
+ - **1** = P1 (High) - Important, do soon
64
+ - **2** = P2 (Medium) - Normal priority (default)
65
+ - **3** = P3 (Low) - Nice to have
66
+
67
+ Example: `sb add "Fix critical bug" 0 "This blocks release"`
68
+
69
+ Run `sb --help` or check the README for more commands.
70
+
71
+ ### Landing the Plane (Session Completion)
72
+
73
+ **When ending a work session**, complete these steps:
74
+
75
+ 1. **File remaining work** - Create issues for any follow-up tasks
76
+ 2. **Update task status** - Mark completed work as done with `sb done <id>`
77
+ 3. **Promote for handoff** - Run `sb promote <id>` on significant tasks to document progress
78
+ 4. **Clean up** - Run `sb compact` to archive closed tasks and keep the tracker lean
79
+
80
+ **CRITICAL RULES:**
81
+ - Always update task status before ending a session
82
+ - Use `sb promote` to hand off context about what was accomplished
83
+ - Never leave tasks in an ambiguous state—close them or create sub-tasks
84
+ """
85
+
86
+ agents_md_path = os.path.join(os.path.dirname(DB_FILE), "AGENTS.md")
87
+
88
+ if os.path.exists(agents_md_path):
89
+ # Check if "Using SB Tracker" section already exists
90
+ with open(agents_md_path, "r") as f:
91
+ content = f.read()
92
+
93
+ if "## Using SB Tracker" not in content:
94
+ # Append to existing file
95
+ with open(agents_md_path, "a") as f:
96
+ if not content.endswith("\n"):
97
+ f.write("\n")
98
+ f.write("\n" + agents_md_content)
99
+ print(f"Appended SB Tracker instructions to {agents_md_path}")
100
+ # else: already has "Using SB Tracker" section, don't duplicate
101
+ else:
102
+ # Create new AGENTS.md
103
+ with open(agents_md_path, "w") as f:
104
+ f.write(agents_md_content)
105
+ print(f"Created {agents_md_path} with SB Tracker instructions")
106
+
107
+ def search_issues(keyword, as_json=False):
108
+ db = load_db()
109
+ keyword = keyword.lower()
110
+ results = []
111
+ for i in db["issues"]:
112
+ if keyword in i["title"].lower() or keyword in i.get("description", "").lower():
113
+ results.append(i)
114
+
115
+ if as_json:
116
+ print(json.dumps(results, indent=2))
117
+ return
118
+
119
+ if not results:
120
+ print(f"No results found for '{keyword}'")
121
+ return
122
+
123
+ print(f"Search results for '{keyword}':")
124
+ print(f"{'ID':<12} {'Status':<12} {'Title'}")
125
+ print("-" * 60)
126
+ for i in results:
127
+ print(f"{i['id']:<12} {i['status']:<12} {i['title']}")
128
+
129
+ def update_issue(issue_id, title=None, description=None, priority=None, parent_id=None):
130
+ db = load_db()
131
+ issue = next((i for i in db["issues"] if i["id"] == issue_id), None)
132
+ if not issue:
133
+ print(f"Error: Issue {issue_id} not found.")
134
+ return
135
+
136
+ changes = {}
137
+ if title:
138
+ changes["title"] = (issue["title"], title)
139
+ issue["title"] = title
140
+ if description is not None:
141
+ changes["description"] = "updated"
142
+ issue["description"] = description
143
+ if priority is not None:
144
+ changes["priority"] = (issue.get("priority", 2), priority)
145
+ issue["priority"] = priority
146
+ if parent_id is not None:
147
+ # Hierarchy change
148
+ old_parent = issue.get("parent")
149
+ if parent_id == "": # Remove parent
150
+ if "parent" in issue: del issue["parent"]
151
+ changes["parent"] = (old_parent, None)
152
+ else:
153
+ issue["parent"] = parent_id
154
+ changes["parent"] = (old_parent, parent_id)
155
+
156
+ if changes:
157
+ log_event(issue, "updated", {"changes": changes})
158
+ save_db(db)
159
+ print(f"Updated {issue_id}")
160
+ else:
161
+ print("No changes specified.")
162
+
163
+ def promote_issue(issue_id):
164
+ db = load_db()
165
+ issue = next((i for i in db["issues"] if i["id"] == issue_id), None)
166
+ if not issue:
167
+ print(f"Error: Issue {issue_id} not found.")
168
+ return
169
+
170
+ children = [i for i in db["issues"] if i.get("parent") == issue_id]
171
+
172
+ print(f"### [{issue['id']}] {issue['title']}")
173
+ print(f"**Status:** {issue['status']} | **Priority:** P{issue.get('priority', 2)}")
174
+ if issue.get("description"):
175
+ print(f"\n{issue['description']}")
176
+
177
+ if children:
178
+ print("\n#### Sub-tasks")
179
+ for child in children:
180
+ check = "x" if child["status"] == "closed" else " "
181
+ print(f"- [{check}] {child['id']}: {child['title']}")
182
+
183
+ if issue.get("events"):
184
+ print("\n#### Activity Log")
185
+ for e in issue["events"]:
186
+ ts = e["timestamp"].split("T")[0]
187
+ if e["type"] == "created":
188
+ print(f"- {ts}: Created")
189
+ elif e["type"] == "status_changed":
190
+ print(f"- {ts}: {e['old']} -> {e['new']}")
191
+ elif e["type"] == "updated":
192
+ print(f"- {ts}: Details updated")
193
+
194
+ def log_event(issue, event_type, details=None):
195
+ event = {
196
+ "type": event_type,
197
+ "timestamp": datetime.now().isoformat(),
198
+ }
199
+ if details:
200
+ event.update(details)
201
+ if "events" not in issue:
202
+ issue["events"] = []
203
+ issue["events"].append(event)
204
+
205
+ def add(title, description="", priority=2, depends_on=None, parent_id=None):
206
+ db = load_db()
207
+
208
+ if parent_id:
209
+ parent = next((i for i in db["issues"] if i["id"] == parent_id), None)
210
+ if not parent:
211
+ print(f"Error: Parent issue {parent_id} not found.")
212
+ return
213
+
214
+ prefix = parent_id + "."
215
+ children = [i for i in db["issues"] if i["id"].startswith(prefix)]
216
+ max_sub = 0
217
+ for child in children:
218
+ sub_part = child["id"][len(prefix):]
219
+ if "." not in sub_part:
220
+ try:
221
+ val = int(sub_part)
222
+ if val > max_sub:
223
+ max_sub = val
224
+ except ValueError: continue
225
+ new_id = f"{prefix}{max_sub + 1}"
226
+ else:
227
+ max_id = 0
228
+ for issue in db["issues"]:
229
+ if "." not in issue["id"]:
230
+ try:
231
+ if "-" in issue["id"]:
232
+ val = int(issue["id"].split("-")[1])
233
+ if val > max_id: max_id = val
234
+ except (IndexError, ValueError): continue
235
+ new_id = f"sb-{max_id + 1}"
236
+
237
+ issue = {
238
+ "id": new_id,
239
+ "title": title,
240
+ "description": description,
241
+ "priority": priority,
242
+ "status": "open",
243
+ "depends_on": depends_on or [],
244
+ "events": [],
245
+ "created_at": datetime.now().isoformat()
246
+ }
247
+ if parent_id:
248
+ issue["parent"] = parent_id
249
+
250
+ log_event(issue, "created", {"title": title})
251
+ db["issues"].append(issue)
252
+ save_db(db)
253
+ print(f"Created {new_id}: {title} (P{priority})")
254
+
255
+ def add_dependency(child_id, parent_id):
256
+ db = load_db()
257
+ child = next((i for i in db["issues"] if i["id"] == child_id), None)
258
+ parent = next((i for i in db["issues"] if i["id"] == parent_id), None)
259
+
260
+ if not child:
261
+ print(f"Error: Child issue {child_id} not found.")
262
+ return
263
+ if not parent:
264
+ print(f"Error: Parent issue {parent_id} not found.")
265
+ return
266
+
267
+ if parent_id not in child["depends_on"]:
268
+ child["depends_on"].append(parent_id)
269
+ log_event(child, "dep_added", {"parent": parent_id})
270
+ save_db(db)
271
+ print(f"Linked {child_id} -> depends on -> {parent_id}")
272
+ else:
273
+ print(f"Already linked.")
274
+
275
+ def is_ready(issue, all_issues):
276
+ if issue["status"] != "open":
277
+ return False
278
+
279
+ # Check if all dependencies are closed
280
+ for dep_id in issue.get("depends_on", []):
281
+ dep = next((i for i in all_issues if i["id"] == dep_id), None)
282
+ if dep and dep["status"] != "closed":
283
+ return False
284
+ return True
285
+
286
+ def list_issues(show_all=False, as_json=False, ready_only=False):
287
+ db = load_db()
288
+ all_issues = db["issues"]
289
+
290
+ if ready_only:
291
+ issues = [i for i in all_issues if is_ready(i, all_issues)]
292
+ elif not show_all:
293
+ issues = [i for i in all_issues if i["status"] == "open"]
294
+ else:
295
+ issues = all_issues
296
+
297
+ # Sort by ID (to keep hierarchy together), then priority
298
+ issues.sort(key=lambda x: (x["id"], x.get("priority", 2)))
299
+
300
+ if as_json:
301
+ # Include compaction log in JSON if it exists
302
+ output = {"issues": issues}
303
+ if db.get("compaction_log"):
304
+ output["compaction_log"] = db["compaction_log"]
305
+ print(json.dumps(output, indent=2))
306
+ return
307
+
308
+ if not issues:
309
+ print("No issues found matching criteria.")
310
+ if db.get("compaction_log"):
311
+ print("\nCompaction Log (Archived):")
312
+ for entry in db["compaction_log"]:
313
+ print(f" - {entry['summary']}")
314
+ return
315
+
316
+ print(f"{'ID':<12} {'P':<2} {'Status':<12} {'Deps':<10} {'Title'}")
317
+ print("-" * 80)
318
+ for i in issues:
319
+ status = i["status"]
320
+ deps = ",".join(i.get("depends_on", []))
321
+ if len(deps) > 10: deps = deps[:7] + "..."
322
+ # Indent children
323
+ indent = " " * i["id"].count(".")
324
+ print(f"{i['id']:<12} {i.get('priority', 2):<2} {status:<12} {deps:<10} {indent}{i['title']}")
325
+
326
+ def show_stats():
327
+ db = load_db()
328
+ issues = db["issues"]
329
+
330
+ total = len(issues)
331
+ open_count = len([i for i in issues if i["status"] == "open"])
332
+ closed_count = len([i for i in issues if i["status"] == "closed"])
333
+ ready_count = len([i for i in issues if is_ready(i, issues)])
334
+
335
+ p_counts = {}
336
+ for i in issues:
337
+ p = f"P{i.get('priority', 2)}"
338
+ p_counts[p] = p_counts.get(p, 0) + 1
339
+
340
+ print("════════════════════════════════════════")
341
+ print(" SB Tracker Statistics")
342
+ print("════════════════════════════════════════")
343
+ print(f"Total Issues: {total}")
344
+ print(f"Open: {open_count}")
345
+ print(f"Ready: {ready_count}")
346
+ print(f"Closed: {closed_count}")
347
+ print("----------------------------------------")
348
+ print("Priority Breakdown:")
349
+ for p in sorted(p_counts.keys()):
350
+ print(f" {p}: {p_counts[p]}")
351
+
352
+ if db.get("compaction_log"):
353
+ print("----------------------------------------")
354
+ print(f"Archived via Compaction: {len(db['compaction_log'])} entries")
355
+ print("════════════════════════════════════════")
356
+
357
+ def compact():
358
+ db = load_db()
359
+ closed_issues = [i for i in db["issues"] if i["status"] == "closed"]
360
+
361
+ if not closed_issues:
362
+ print("No closed issues to compact.")
363
+ return
364
+
365
+ summary_parts = []
366
+ for i in closed_issues:
367
+ summary_parts.append(f"{i['id']}: {i['title']}")
368
+
369
+ summary_text = f"Compacted {len(closed_issues)} issues on {datetime.now().strftime('%Y-%m-%d %H:%M')}: " + ", ".join(summary_parts)
370
+
371
+ if "compaction_log" not in db:
372
+ db["compaction_log"] = []
373
+
374
+ db["compaction_log"].append({
375
+ "timestamp": datetime.now().isoformat(),
376
+ "count": len(closed_issues),
377
+ "summary": summary_text
378
+ })
379
+
380
+ # Remove closed issues
381
+ db["issues"] = [i for i in db["issues"] if i["status"] != "closed"]
382
+
383
+ save_db(db)
384
+ print(f"Successfully compacted {len(closed_issues)} issues.")
385
+ print(f"Archive entry added to compaction_log.")
386
+
387
+ def update_status(issue_id, status):
388
+ db = load_db()
389
+ for i in db["issues"]:
390
+ if i["id"] == issue_id:
391
+ old_status = i["status"]
392
+ if old_status == status: return
393
+ i["status"] = status
394
+ log_event(i, "status_changed", {"old": old_status, "new": status})
395
+ if status == "closed":
396
+ i["closed_at"] = datetime.now().isoformat()
397
+ save_db(db)
398
+ print(f"Updated {issue_id} status to {status}")
399
+ return
400
+ print(f"Error: Issue {issue_id} not found.")
401
+
402
+ def delete_issue(issue_id):
403
+ db = load_db()
404
+ original_count = len(db["issues"])
405
+ db["issues"] = [i for i in db["issues"] if i["id"] != issue_id]
406
+ if len(db["issues"]) < original_count:
407
+ save_db(db)
408
+ print(f"Deleted {issue_id}")
409
+ else:
410
+ print(f"Error: Issue {issue_id} not found.")
411
+
412
+ def show_issue(issue_id, as_json=False):
413
+ db = load_db()
414
+ for i in db["issues"]:
415
+ if i["id"] == issue_id:
416
+ if as_json:
417
+ print(json.dumps(i, indent=2))
418
+ else:
419
+ print(f"ID: {i['id']}")
420
+ print(f"Title: {i['title']}")
421
+ print(f"Priority: P{i.get('priority', 2)}")
422
+ print(f"Status: {i['status']}")
423
+ print(f"Created: {i['created_at']}")
424
+ print(f"Depends On: {', '.join(i.get('depends_on', [])) or 'None'}")
425
+
426
+ dependents = [dep['id'] for dep in db["issues"] if i['id'] in dep.get('depends_on', [])]
427
+ print(f"Blocking: {', '.join(dependents) or 'None'}")
428
+
429
+ if i.get("description"):
430
+ print(f"\nDescription:\n{i['description']}")
431
+
432
+ if i.get("events"):
433
+ print("\nAudit Log:")
434
+ for e in i["events"]:
435
+ ts = e["timestamp"].split("T")[1][:8]
436
+ if e["type"] == "created":
437
+ print(f" [{ts}] Created")
438
+ elif e["type"] == "status_changed":
439
+ print(f" [{ts}] Status: {e['old']} -> {e['new']}")
440
+ elif e["type"] == "dep_added":
441
+ print(f" [{ts}] Dependency added: {e['parent']}")
442
+ return
443
+ print(f"Error: Issue {issue_id} not found.")
444
+
445
+ def main():
446
+ if len(sys.argv) < 2:
447
+ print("Usage: sb <command> [args]")
448
+ print("Commands:")
449
+ print(" init Initialize .sb.json")
450
+ print(" add <title> [p] [desc] [parent] Add issue")
451
+ print(" list [--all] [--json] List issues")
452
+ print(" ready [--json] List issues with no open blockers")
453
+ print(" search <keyword> [--json] Search titles and descriptions")
454
+ print(" stats Show task statistics")
455
+ print(" compact Archive closed issues")
456
+ print(" dep <child> <parent> Add dependency")
457
+ print(" update <id> [field=val] Update title, desc, p, parent")
458
+ print(" promote <id> Export task as Markdown")
459
+ print(" show <id> [--json] Show issue details")
460
+ print(" done <id> Close issue")
461
+ print(" rm <id> Delete issue")
462
+ return
463
+
464
+ cmd = sys.argv[1]
465
+ if cmd == "init":
466
+ init()
467
+ elif cmd == "add":
468
+ if len(sys.argv) < 3:
469
+ print("Usage: sb add <title> [priority] [description] [parent_id]")
470
+ else:
471
+ title = sys.argv[2]
472
+ p = 2
473
+ desc = ""
474
+ parent = None
475
+
476
+ args = sys.argv[3:]
477
+ if args:
478
+ try:
479
+ p = int(args[0])
480
+ args = args[1:]
481
+ except ValueError: pass
482
+
483
+ if args:
484
+ desc = args[0]
485
+ args = args[1:]
486
+
487
+ if args:
488
+ parent = args[0]
489
+
490
+ add(title, desc, p, parent_id=parent)
491
+ elif cmd == "list":
492
+ show_all = "--all" in sys.argv
493
+ as_json = "--json" in sys.argv
494
+ list_issues(show_all, as_json)
495
+ elif cmd == "ready":
496
+ as_json = "--json" in sys.argv
497
+ list_issues(as_json=as_json, ready_only=True)
498
+ elif cmd == "search":
499
+ if len(sys.argv) < 3:
500
+ print("Usage: sb search <keyword> [--json]")
501
+ else:
502
+ as_json = "--json" in sys.argv
503
+ search_issues(sys.argv[2], as_json)
504
+ elif cmd == "update":
505
+ if len(sys.argv) < 3:
506
+ print("Usage: sb update <id> [title=...] [desc=...] [p=...] [parent=...]")
507
+ else:
508
+ issue_id = sys.argv[2]
509
+ kwargs = {}
510
+ for arg in sys.argv[3:]:
511
+ if "=" in arg:
512
+ k, v = arg.split("=", 1)
513
+ if k == "p": kwargs["priority"] = int(v)
514
+ elif k == "title": kwargs["title"] = v
515
+ elif k == "desc": kwargs["description"] = v
516
+ elif k == "parent": kwargs["parent_id"] = v
517
+ update_issue(issue_id, **kwargs)
518
+ elif cmd == "promote":
519
+ if len(sys.argv) < 3:
520
+ print("Usage: sb promote <id>")
521
+ else:
522
+ promote_issue(sys.argv[2])
523
+ elif cmd == "stats":
524
+ show_stats()
525
+ elif cmd == "compact":
526
+ compact()
527
+ elif cmd == "dep":
528
+ if len(sys.argv) < 4:
529
+ print("Usage: sb dep <child_id> <parent_id>")
530
+ else:
531
+ add_dependency(sys.argv[2], sys.argv[3])
532
+ elif cmd == "show":
533
+ if len(sys.argv) < 3:
534
+ print("Usage: sb show <id> [--json]")
535
+ else:
536
+ as_json = "--json" in sys.argv
537
+ show_issue(sys.argv[2], as_json)
538
+ elif cmd == "done":
539
+ if len(sys.argv) < 3:
540
+ print("Usage: sb done <id>")
541
+ else:
542
+ update_status(sys.argv[2], "closed")
543
+ elif cmd == "rm":
544
+ if len(sys.argv) < 3:
545
+ print("Usage: sb rm <id>")
546
+ else:
547
+ delete_issue(sys.argv[2])
548
+ else:
549
+ print(f"Unknown command: {cmd}")
550
+
551
+ if __name__ == "__main__":
552
+ main()
@@ -0,0 +1,294 @@
1
+ Metadata-Version: 2.4
2
+ Name: sb-tracker
3
+ Version: 0.1.0
4
+ Summary: A minimal, standalone issue tracker for individuals. No git hooks, no complex dependencies, just one JSON file.
5
+ Home-page: https://github.com/sirius-cc-wu/sb-tracker
6
+ Author: Simple Beads Contributors
7
+ License: MIT
8
+ Project-URL: Homepage, https://github.com/sirius-cc-wu/sb-tracker
9
+ Project-URL: Documentation, https://github.com/sirius-cc-wu/sb-tracker#readme
10
+ Keywords: task-tracker,issues,simple,standalone
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.7
18
+ Classifier: Programming Language :: Python :: 3.8
19
+ Classifier: Programming Language :: Python :: 3.9
20
+ Classifier: Programming Language :: Python :: 3.10
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
23
+ Requires-Python: >=3.7
24
+ Description-Content-Type: text/markdown
25
+ License-File: LICENSE
26
+ Dynamic: home-page
27
+ Dynamic: license-file
28
+ Dynamic: requires-python
29
+
30
+ # SB Tracker - Simple Beads
31
+
32
+ A lightweight, standalone task tracker that stores state in a local `.sb.json` file. Perfect for individuals and agents to maintain context and track long-running or multi-step tasks without external dependencies.
33
+
34
+ ## Features
35
+
36
+ - **Zero Dependencies**: Pure Python, uses only stdlib (json, os, sys, datetime)
37
+ - **Standalone**: One JSON file stores all state locally
38
+ - **Hierarchical Tasks**: Support for sub-tasks with parent-child relationships
39
+ - **Priority Levels**: Tasks support P0-P3 priority levels
40
+ - **Task Status Tracking**: Open/closed status with timestamps
41
+ - **Dependencies**: Link tasks with blocking dependencies
42
+ - **Audit Log**: Track all changes to each task with timestamps
43
+ - **JSON Export**: Machine-readable output for integration
44
+ - **Compaction**: Archive closed tasks to keep context efficient
45
+
46
+ ## Installation
47
+
48
+ ### From PyPI (when published)
49
+
50
+ ```bash
51
+ pip install sb-tracker
52
+ ```
53
+
54
+ ### From Source
55
+
56
+ ```bash
57
+ git clone https://github.com/sirius-cc-wu/sb-tracker.git
58
+ cd sb-tracker
59
+ pip install -e .
60
+ ```
61
+
62
+ ## Quick Start
63
+
64
+ Initialize a new task tracker:
65
+
66
+ ```bash
67
+ sb init
68
+ ```
69
+
70
+ Add a task:
71
+
72
+ ```bash
73
+ sb add "My first task"
74
+ sb add "High priority task" 0 "This is urgent"
75
+ ```
76
+
77
+ List tasks:
78
+
79
+ ```bash
80
+ sb list # Show open tasks
81
+ sb list --all # Show all tasks
82
+ sb list --json # Machine-readable output
83
+ ```
84
+
85
+ Complete a task:
86
+
87
+ ```bash
88
+ sb done sb-1
89
+ ```
90
+
91
+ ## Commands
92
+
93
+ ### Create and Modify
94
+
95
+ - **`init`**: Initialize `.sb.json` in the current git repository root
96
+ - **`add <title> [priority] [description] [parent_id]`**
97
+ - Example: `sb add "Setup database" 1 "Configure PostgreSQL" sb-1`
98
+ - **`update <id> [field=value ...]`**
99
+ - Fields: `title`, `desc`, `p` (priority), `parent`
100
+ - Example: `sb update sb-1 p=0 desc="New description"`
101
+ - **`dep <child_id> <parent_id>`**: Add a blocking dependency
102
+ - Example: `sb dep sb-2 sb-1` (sb-2 blocked by sb-1)
103
+
104
+ ### List and Search
105
+
106
+ - **`list [--all] [--json]`**: Show open (or all) tasks with hierarchy
107
+ - **`ready [--json]`**: Show tasks with no open blockers
108
+ - **`search <keyword> [--json]`**: Search titles and descriptions
109
+
110
+ ### Reporting and Maintenance
111
+
112
+ - **`show <id> [--json]`**: Display task details with audit log
113
+ - **`promote <id>`**: Generate Markdown summary of task and sub-tasks
114
+ - **`stats`**: Overview of progress and priority breakdown
115
+ - **`compact`**: Archive closed tasks to save space
116
+ - **`done <id>`**: Mark task as closed
117
+ - **`rm <id>`**: Permanently delete task
118
+
119
+ ## Workflow
120
+
121
+ ### For Individual Sessions
122
+
123
+ 1. **Breakdown**: Create tasks with hierarchies for complex work
124
+ ```bash
125
+ sb add "Implement feature X" # Creates sb-1
126
+ sb add "Write unit tests" 1 "" sb-1 # Creates sb-1.1
127
+ sb add "Write integration tests" 1 "" sb-1 # Creates sb-1.2
128
+ ```
129
+
130
+ 2. **Execute**: Focus on high-priority ready tasks
131
+ ```bash
132
+ sb ready # Show tasks with no blockers
133
+ ```
134
+
135
+ 3. **Track Progress**: Update as you complete steps
136
+ ```bash
137
+ sb done sb-1.1
138
+ sb done sb-1.2
139
+ ```
140
+
141
+ 4. **Report**: Generate summary when handing off
142
+ ```bash
143
+ sb promote sb-1
144
+ ```
145
+
146
+ ### Task ID Format
147
+
148
+ - **Root tasks**: `sb-1`, `sb-2`, etc.
149
+ - **Sub-tasks**: `sb-1.1`, `sb-1.2`, `sb-1.1.1`, etc.
150
+ - **Parent relationship**: Use parent ID in `add` or `update`
151
+
152
+ ## Priority Levels
153
+
154
+ - **P0**: Critical, blocking everything
155
+ - **P1**: High priority, do soon
156
+ - **P2**: Normal priority (default)
157
+ - **P3**: Low priority, nice to have
158
+
159
+ ## Database Format
160
+
161
+ Tasks are stored in `.sb.json` (found in git repository root) with this schema:
162
+
163
+ ```json
164
+ {
165
+ "issues": [
166
+ {
167
+ "id": "sb-1",
168
+ "title": "Task title",
169
+ "description": "Optional description",
170
+ "priority": 1,
171
+ "status": "open",
172
+ "depends_on": ["sb-2"],
173
+ "parent": "sb-1",
174
+ "created_at": "2026-02-04T18:40:10.692Z",
175
+ "closed_at": "2026-02-04T19:40:10.692Z",
176
+ "events": [
177
+ {
178
+ "type": "created",
179
+ "timestamp": "2026-02-04T18:40:10.692Z",
180
+ "title": "Task title"
181
+ }
182
+ ]
183
+ }
184
+ ],
185
+ "compaction_log": []
186
+ }
187
+ ```
188
+
189
+ ## Examples
190
+
191
+ ### Hierarchical Task Breakdown
192
+
193
+ ```bash
194
+ $ sb add "Build authentication system"
195
+ Created sb-1: Build authentication system (P2)
196
+
197
+ $ sb add "Design schema" 1 "" sb-1
198
+ Created sb-1.1: Design schema (P1)
199
+
200
+ $ sb add "Implement login endpoint" 1 "" sb-1
201
+ Created sb-1.2: Implement login endpoint (P1)
202
+
203
+ $ sb add "Write tests" 2 "" sb-1
204
+ Created sb-1.3: Write tests (P2)
205
+
206
+ $ sb list
207
+ ID P Status Deps Title
208
+ sb-1 2 open Build authentication system
209
+ sb-1.1 1 open Design schema
210
+ sb-1.2 1 open Implement login endpoint
211
+ sb-1.3 2 open Write tests
212
+ ```
213
+
214
+ ### Blocking Dependencies
215
+
216
+ ```bash
217
+ $ sb add "Deploy to production" 1
218
+ Created sb-2: Deploy to production (P1)
219
+
220
+ $ sb dep sb-2 sb-1
221
+ Linked sb-2 -> depends on -> sb-1
222
+
223
+ $ sb ready
224
+ No issues found matching criteria.
225
+
226
+ $ sb done sb-1.1
227
+ Updated sb-1.1 status to closed
228
+
229
+ $ sb done sb-1.2
230
+ Updated sb-1.2 status to closed
231
+
232
+ $ sb done sb-1.3
233
+ Updated sb-1.3 status to closed
234
+
235
+ $ sb done sb-1
236
+ Updated sb-1 status to closed
237
+
238
+ $ sb ready
239
+ ID P Status Deps Title
240
+ sb-2 1 open Deploy to production
241
+ ```
242
+
243
+ ### Task Reporting
244
+
245
+ ```bash
246
+ $ sb promote sb-1
247
+ ### [sb-1] Build authentication system
248
+ **Status:** closed | **Priority:** P2
249
+
250
+ #### Sub-tasks
251
+ - [x] sb-1.1: Design schema
252
+ - [x] sb-1.2: Implement login endpoint
253
+ - [x] sb-1.3: Write tests
254
+
255
+ #### Activity Log
256
+ - 2026-02-04: Created
257
+ - 2026-02-04: Status: open -> closed
258
+ ```
259
+
260
+ ## License
261
+
262
+ MIT License - See LICENSE file for details
263
+
264
+ ## Contributing
265
+
266
+ Contributions are welcome! Please feel free to submit a Pull Request.
267
+
268
+ ## Troubleshooting
269
+
270
+ ### `.sb.json` not found
271
+
272
+ The tracker looks for `.sb.json` starting from the current directory and walking up the directory tree until it finds a `.git` directory (to keep data project-local). If not found, it creates `.sb.json` in the current working directory.
273
+
274
+ To initialize:
275
+ ```bash
276
+ cd /your/project
277
+ sb init
278
+ ```
279
+
280
+ ### Task not found error
281
+
282
+ Make sure you're using the correct task ID:
283
+ ```bash
284
+ $ sb list --json # See all task IDs
285
+ ```
286
+
287
+ ### Compaction
288
+
289
+ Archive old tasks to reduce token context:
290
+ ```bash
291
+ sb compact
292
+ ```
293
+
294
+ This moves all closed tasks to a `compaction_log` and keeps them accessible via `list --all` or `list --json`.
@@ -0,0 +1,8 @@
1
+ sb_tracker/__init__.py,sha256=iQoBnwcQOqWI8K8J6qURQ9I4YV6-nEjtI3_f-fXOg0M,247
2
+ sb_tracker/cli.py,sha256=3qfGgrVNdWgFmnRntNcQhBC8OzloB1zDK6U77SSWvGk,19574
3
+ sb_tracker-0.1.0.dist-info/licenses/LICENSE,sha256=CM9NfNlJghMvmE336gJnEmv0cGmb-nnU03Jr8sK2DpU,1082
4
+ sb_tracker-0.1.0.dist-info/METADATA,sha256=iGrIkGJgH1XGsSHpeJGx-amfvJEytKrPtOIwAkeOPTw,7607
5
+ sb_tracker-0.1.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
6
+ sb_tracker-0.1.0.dist-info/entry_points.txt,sha256=moL2ZfRCx4VBG1nTWva3OsOv5NMO2vw64C1iDiMOHm4,43
7
+ sb_tracker-0.1.0.dist-info/top_level.txt,sha256=qbDo8V63QunqlNdVT4QtGaTAcDB4Gk7gr5j4IwyiaXo,11
8
+ sb_tracker-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.10.2)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ sb = sb_tracker.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Simple Beads Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ sb_tracker