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 +11 -0
- sb_tracker/cli.py +552 -0
- sb_tracker-0.1.0.dist-info/METADATA +294 -0
- sb_tracker-0.1.0.dist-info/RECORD +8 -0
- sb_tracker-0.1.0.dist-info/WHEEL +5 -0
- sb_tracker-0.1.0.dist-info/entry_points.txt +2 -0
- sb_tracker-0.1.0.dist-info/licenses/LICENSE +21 -0
- sb_tracker-0.1.0.dist-info/top_level.txt +1 -0
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,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
|