aiag-cli 2.2.2 → 2.3.0

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.
Files changed (77) hide show
  1. package/README.md +72 -37
  2. package/dist/cli.js +30 -2
  3. package/dist/cli.js.map +1 -1
  4. package/dist/commands/auto.js +45 -41
  5. package/dist/commands/auto.js.map +1 -1
  6. package/dist/commands/feature.d.ts +11 -0
  7. package/dist/commands/feature.d.ts.map +1 -0
  8. package/dist/commands/feature.js +153 -0
  9. package/dist/commands/feature.js.map +1 -0
  10. package/dist/commands/init.d.ts +1 -1
  11. package/dist/commands/init.d.ts.map +1 -1
  12. package/dist/commands/init.js +29 -78
  13. package/dist/commands/init.js.map +1 -1
  14. package/dist/commands/prd.d.ts +12 -0
  15. package/dist/commands/prd.d.ts.map +1 -0
  16. package/dist/commands/prd.js +179 -0
  17. package/dist/commands/prd.js.map +1 -0
  18. package/dist/prompts/coding.d.ts.map +1 -1
  19. package/dist/prompts/coding.js +12 -0
  20. package/dist/prompts/coding.js.map +1 -1
  21. package/dist/prompts/index.d.ts +2 -0
  22. package/dist/prompts/index.d.ts.map +1 -1
  23. package/dist/prompts/index.js +2 -0
  24. package/dist/prompts/index.js.map +1 -1
  25. package/dist/prompts/initializer.d.ts.map +1 -1
  26. package/dist/prompts/initializer.js +6 -0
  27. package/dist/prompts/initializer.js.map +1 -1
  28. package/dist/prompts/prd.d.ts +28 -0
  29. package/dist/prompts/prd.d.ts.map +1 -0
  30. package/dist/prompts/prd.js +105 -0
  31. package/dist/prompts/prd.js.map +1 -0
  32. package/dist/skills/index.d.ts +12 -0
  33. package/dist/skills/index.d.ts.map +1 -0
  34. package/dist/skills/index.js +12 -0
  35. package/dist/skills/index.js.map +1 -0
  36. package/dist/skills/installer.d.ts +38 -0
  37. package/dist/skills/installer.d.ts.map +1 -0
  38. package/dist/skills/installer.js +153 -0
  39. package/dist/skills/installer.js.map +1 -0
  40. package/dist/skills/loader.d.ts +34 -0
  41. package/dist/skills/loader.d.ts.map +1 -0
  42. package/dist/skills/loader.js +134 -0
  43. package/dist/skills/loader.js.map +1 -0
  44. package/dist/skills/runner.d.ts +14 -0
  45. package/dist/skills/runner.d.ts.map +1 -0
  46. package/dist/skills/runner.js +238 -0
  47. package/dist/skills/runner.js.map +1 -0
  48. package/dist/types.d.ts +127 -0
  49. package/dist/types.d.ts.map +1 -1
  50. package/dist/utils/prd.d.ts +21 -0
  51. package/dist/utils/prd.d.ts.map +1 -1
  52. package/dist/utils/prd.js +69 -0
  53. package/dist/utils/prd.js.map +1 -1
  54. package/dist/utils/taskmasterConverter.d.ts +72 -0
  55. package/dist/utils/taskmasterConverter.d.ts.map +1 -0
  56. package/dist/utils/taskmasterConverter.js +401 -0
  57. package/dist/utils/taskmasterConverter.js.map +1 -0
  58. package/dist/utils/taskmasterParser.d.ts +35 -0
  59. package/dist/utils/taskmasterParser.d.ts.map +1 -0
  60. package/dist/utils/taskmasterParser.js +259 -0
  61. package/dist/utils/taskmasterParser.js.map +1 -0
  62. package/package.json +1 -1
  63. package/templates/skills/prd-taskmaster/.taskmaster/docs/prd.md +2571 -0
  64. package/templates/skills/prd-taskmaster/.taskmaster/scripts/execution-state.py +87 -0
  65. package/templates/skills/prd-taskmaster/.taskmaster/scripts/learn-accuracy.py +113 -0
  66. package/templates/skills/prd-taskmaster/.taskmaster/scripts/rollback.sh +71 -0
  67. package/templates/skills/prd-taskmaster/.taskmaster/scripts/security-audit.py +130 -0
  68. package/templates/skills/prd-taskmaster/.taskmaster/scripts/track-time.py +133 -0
  69. package/templates/skills/prd-taskmaster/LICENSE +21 -0
  70. package/templates/skills/prd-taskmaster/README.md +608 -0
  71. package/templates/skills/prd-taskmaster/SKILL.md +1258 -0
  72. package/templates/skills/prd-taskmaster/reference/taskmaster-integration-guide.md +645 -0
  73. package/templates/skills/prd-taskmaster/reference/validation-checklist.md +394 -0
  74. package/templates/skills/prd-taskmaster/scripts/setup-taskmaster.sh +112 -0
  75. package/templates/skills/prd-taskmaster/templates/CLAUDE.md.template +635 -0
  76. package/templates/skills/prd-taskmaster/templates/taskmaster-prd-comprehensive.md +983 -0
  77. package/templates/skills/prd-taskmaster/templates/taskmaster-prd-minimal.md +103 -0
@@ -0,0 +1,87 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Task execution state manager for crash recovery.
4
+ """
5
+
6
+ import json
7
+ from pathlib import Path
8
+ from datetime import datetime, timezone
9
+
10
+ class ExecutionState:
11
+ def __init__(self):
12
+ self.state_file = Path(".taskmaster/state/execution-state.json")
13
+ self.state_file.parent.mkdir(parents=True, exist_ok=True)
14
+ self.load_state()
15
+
16
+ def load_state(self):
17
+ """Load current execution state."""
18
+ if self.state_file.exists():
19
+ with open(self.state_file, 'r') as f:
20
+ self.state = json.load(f)
21
+ else:
22
+ self.state = {
23
+ "mode": None,
24
+ "current_task": None,
25
+ "current_subtask": None,
26
+ "tasks_completed": [],
27
+ "last_update": None
28
+ }
29
+
30
+ def save_state(self):
31
+ """Persist current state to disk."""
32
+ self.state["last_update"] = datetime.now(timezone.utc).isoformat()
33
+ with open(self.state_file, 'w') as f:
34
+ json.dump(self.state, f, indent=2)
35
+
36
+ def set_mode(self, mode):
37
+ """Set execution mode (sequential, parallel, etc.)."""
38
+ self.state["mode"] = mode
39
+ self.save_state()
40
+
41
+ def start_task(self, task_id):
42
+ """Mark task as started."""
43
+ self.state["current_task"] = task_id
44
+ self.state["current_subtask"] = None
45
+ self.save_state()
46
+
47
+ def start_subtask(self, task_id, subtask_id):
48
+ """Mark subtask as started."""
49
+ self.state["current_task"] = task_id
50
+ self.state["current_subtask"] = subtask_id
51
+ self.save_state()
52
+
53
+ def complete_task(self, task_id):
54
+ """Mark task as completed."""
55
+ if task_id not in self.state["tasks_completed"]:
56
+ self.state["tasks_completed"].append(task_id)
57
+ self.state["current_task"] = None
58
+ self.state["current_subtask"] = None
59
+ self.save_state()
60
+
61
+ def has_incomplete_work(self):
62
+ """Check if there's incomplete work from previous session."""
63
+ return self.state["current_task"] is not None
64
+
65
+ def get_resume_point(self):
66
+ """Get information about where to resume."""
67
+ if not self.has_incomplete_work():
68
+ return None
69
+
70
+ return {
71
+ "mode": self.state["mode"],
72
+ "task": self.state["current_task"],
73
+ "subtask": self.state["current_subtask"],
74
+ "last_update": self.state["last_update"],
75
+ "completed_tasks": self.state["tasks_completed"]
76
+ }
77
+
78
+ if __name__ == "__main__":
79
+ state = ExecutionState()
80
+ if state.has_incomplete_work():
81
+ resume = state.get_resume_point()
82
+ print(f"Incomplete work detected:")
83
+ print(f" Task: {resume['task']}")
84
+ print(f" Subtask: {resume['subtask']}")
85
+ print(f" Last update: {resume['last_update']}")
86
+ else:
87
+ print("No incomplete work found.")
@@ -0,0 +1,113 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Estimation accuracy learning system.
4
+ Analyzes actual vs estimated times and improves future estimates.
5
+ """
6
+
7
+ import json
8
+ from pathlib import Path
9
+ import sys
10
+ sys.path.insert(0, str(Path(__file__).parent))
11
+ from track_time import get_all_task_times, calculate_accuracy
12
+
13
+ class AccuracyLearner:
14
+ def __init__(self):
15
+ self.reports_dir = Path(".taskmaster/reports")
16
+ self.reports_dir.mkdir(parents=True, exist_ok=True)
17
+ self.accuracy_file = self.reports_dir / "estimation-accuracy.json"
18
+ self.load_accuracy_data()
19
+
20
+ def load_accuracy_data(self):
21
+ """Load historical accuracy data."""
22
+ if self.accuracy_file.exists():
23
+ with open(self.accuracy_file, 'r') as f:
24
+ self.data = json.load(f)
25
+ else:
26
+ self.data = {
27
+ "tasks_analyzed": 0,
28
+ "average_accuracy": 100.0,
29
+ "adjustment_factor": 1.0,
30
+ "history": []
31
+ }
32
+
33
+ def save_accuracy_data(self):
34
+ """Save accuracy data."""
35
+ with open(self.accuracy_file, 'w') as f:
36
+ json.dump(self.data, f, indent=2)
37
+
38
+ def analyze_task(self, task_id, estimated_minutes, actual_minutes):
39
+ """Analyze a single task's accuracy."""
40
+ accuracy = calculate_accuracy(estimated_minutes, actual_minutes)
41
+
42
+ entry = {
43
+ "task_id": task_id,
44
+ "estimated_minutes": estimated_minutes,
45
+ "actual_minutes": actual_minutes,
46
+ "accuracy_percent": accuracy,
47
+ "variance_percent": accuracy - 100
48
+ }
49
+
50
+ self.data["history"].append(entry)
51
+ self.data["tasks_analyzed"] += 1
52
+
53
+ # Recalculate average
54
+ total_accuracy = sum(h["accuracy_percent"] for h in self.data["history"])
55
+ self.data["average_accuracy"] = round(total_accuracy / len(self.data["history"]), 1)
56
+
57
+ # Update adjustment factor
58
+ self.data["adjustment_factor"] = round(self.data["average_accuracy"] / 100, 2)
59
+
60
+ self.save_accuracy_data()
61
+
62
+ return entry
63
+
64
+ def get_adjusted_estimate(self, base_estimate_minutes):
65
+ """Apply learned adjustment to new estimate."""
66
+ adjusted = base_estimate_minutes * self.data["adjustment_factor"]
67
+ return round(adjusted, 0)
68
+
69
+ def generate_report(self):
70
+ """Generate human-readable accuracy report."""
71
+ if self.data["tasks_analyzed"] < 3:
72
+ return "❌ Need at least 3 completed tasks for accuracy analysis."
73
+
74
+ history = self.data["history"]
75
+ avg_accuracy = self.data["average_accuracy"]
76
+ adjustment = self.data["adjustment_factor"]
77
+
78
+ # Find outliers
79
+ outliers = [h for h in history if abs(h["variance_percent"]) > 50]
80
+
81
+ report = f"""
82
+ 📊 Estimation Accuracy Report
83
+
84
+ Tasks Analyzed: {self.data["tasks_analyzed"]}
85
+ Average Accuracy: {avg_accuracy}%
86
+ Adjustment Factor: {adjustment}x
87
+
88
+ Recent Tasks:
89
+ """
90
+
91
+ for h in history[-10:]: # Last 10 tasks
92
+ status = "✅" if 90 <= h["accuracy_percent"] <= 110 else "⚠️"
93
+ report += f" Task {h['task_id']}: {h['actual_minutes']}min (est: {h['estimated_minutes']}min) - {h['accuracy_percent']}% {status}\n"
94
+
95
+ if avg_accuracy < 90:
96
+ report += f"\n⚠️ You're completing tasks {100 - avg_accuracy:.0f}% faster than estimated.\n"
97
+ report += f"🎯 Recommendation: Reduce future estimates by {(1 - adjustment) * 100:.0f}%\n"
98
+ elif avg_accuracy > 110:
99
+ report += f"\n⚠️ You're taking {avg_accuracy - 100:.0f}% longer than estimated.\n"
100
+ report += f"🎯 Recommendation: Increase future estimates by {(adjustment - 1) * 100:.0f}%\n"
101
+ else:
102
+ report += f"\n✅ Your estimates are accurate! Keep it up.\n"
103
+
104
+ if outliers:
105
+ report += f"\n⚠️ {len(outliers)} outlier tasks found (>50% variance):\n"
106
+ for o in outliers:
107
+ report += f" Task {o['task_id']}: {o['variance_percent']:+.0f}% variance\n"
108
+
109
+ return report
110
+
111
+ if __name__ == "__main__":
112
+ learner = AccuracyLearner()
113
+ print(learner.generate_report())
@@ -0,0 +1,71 @@
1
+ #!/bin/bash
2
+ # .taskmaster/scripts/rollback.sh
3
+ # Rollback to any task checkpoint
4
+
5
+ TASK_ID=$1
6
+
7
+ if [ -z "$TASK_ID" ]; then
8
+ echo "❌ Error: Task ID required"
9
+ echo "Usage: rollback.sh <task_id>"
10
+ exit 1
11
+ fi
12
+
13
+ TAG="checkpoint-task-${TASK_ID}"
14
+
15
+ # Check if tag exists
16
+ if ! git tag -l | grep -q "^${TAG}$"; then
17
+ echo "❌ Error: Checkpoint tag '${TAG}' not found"
18
+ echo "Available checkpoints:"
19
+ git tag -l "checkpoint-task-*" | sort -V
20
+ exit 1
21
+ fi
22
+
23
+ echo "🔄 Rolling back to ${TAG}..."
24
+
25
+ # Safety check
26
+ echo "⚠️ This will:"
27
+ echo " - Discard all changes after Task ${TASK_ID}"
28
+ echo " - Reset to checkpoint-task-${TASK_ID}"
29
+ echo " - Preserve current work in rollback-backup branch"
30
+ echo ""
31
+ echo "Continue? (yes/no)"
32
+ read -r CONFIRM
33
+
34
+ if [ "$CONFIRM" != "yes" ]; then
35
+ echo "❌ Rollback cancelled"
36
+ exit 0
37
+ fi
38
+
39
+ # Create backup branch of current state
40
+ BACKUP_BRANCH="rollback-backup-$(date +%Y%m%d-%H%M%S)"
41
+ git checkout -b "$BACKUP_BRANCH"
42
+ git checkout main
43
+
44
+ echo "💾 Backed up current state to: ${BACKUP_BRANCH}"
45
+
46
+ # Reset to checkpoint
47
+ git reset --hard "${TAG}"
48
+
49
+ echo "✅ Rolled back to Task ${TASK_ID} completion state"
50
+ echo "📝 Updating progress.md..."
51
+
52
+ # Log rollback
53
+ cat >> .taskmaster/docs/progress.md <<EOF
54
+
55
+ ---
56
+ ## ROLLBACK PERFORMED
57
+ **Timestamp**: $(date -u +"%Y-%m-%d %H:%M:%S UTC")
58
+ **Rolled back to**: Task ${TASK_ID} (${TAG})
59
+ **Backup branch**: ${BACKUP_BRANCH}
60
+ **Reason**: User-initiated rollback
61
+
62
+ Tasks after ${TASK_ID} discarded. Ready to resume from Task ${TASK_ID}.
63
+ ---
64
+
65
+ EOF
66
+
67
+ echo ""
68
+ echo "🎯 Next steps:"
69
+ echo " 1. Resume from Task $((TASK_ID + 1))"
70
+ echo " 2. Redo Task ${TASK_ID} differently"
71
+ echo " 3. Review backup: git checkout ${BACKUP_BRANCH}"
@@ -0,0 +1,130 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Auto-generate security audit checklist based on implemented features.
4
+ """
5
+
6
+ import re
7
+ from pathlib import Path
8
+
9
+ def detect_security_concerns(codebase_path="."):
10
+ """Scan code for security-relevant features."""
11
+ concerns = []
12
+
13
+ # Scan for authentication-related code
14
+ for file_path in Path(codebase_path).rglob("*.{js,ts,py,go,java}"):
15
+ try:
16
+ content = file_path.read_text()
17
+
18
+ # Check for password handling
19
+ if re.search(r'password|passwd|pwd', content, re.I):
20
+ concerns.append({
21
+ "category": "Authentication",
22
+ "item": "Password hashing",
23
+ "check": "Passwords hashed with bcrypt/argon2 (cost ≥ 10)",
24
+ "file": str(file_path)
25
+ })
26
+
27
+ # Check for OAuth
28
+ if re.search(r'oauth|openid', content, re.I):
29
+ concerns.append({
30
+ "category": "OAuth",
31
+ "item": "OAuth token security",
32
+ "check": "OAuth tokens encrypted at rest and in transit",
33
+ "file": str(file_path)
34
+ })
35
+
36
+ # Check for database queries
37
+ if re.search(r'SELECT.*FROM|INSERT INTO|UPDATE.*SET', content, re.I):
38
+ concerns.append({
39
+ "category": "Database",
40
+ "item": "SQL injection prevention",
41
+ "check": "All queries use parameterized statements (no string concatenation)",
42
+ "file": str(file_path)
43
+ })
44
+
45
+ # Check for user input handling
46
+ if re.search(r'req\.body|request\.form|input\(', content):
47
+ concerns.append({
48
+ "category": "Input Validation",
49
+ "item": "XSS prevention",
50
+ "check": "All user input sanitized before rendering",
51
+ "file": str(file_path)
52
+ })
53
+ except:
54
+ pass # Skip files that can't be read
55
+
56
+ # Deduplicate
57
+ unique_concerns = []
58
+ seen = set()
59
+ for c in concerns:
60
+ key = (c["category"], c["item"])
61
+ if key not in seen:
62
+ unique_concerns.append(c)
63
+ seen.add(key)
64
+
65
+ return unique_concerns
66
+
67
+ def generate_security_checklist():
68
+ """Generate comprehensive security audit checklist."""
69
+ concerns = detect_security_concerns()
70
+
71
+ checklist = """
72
+ 🔒 SECURITY AUDIT CHECKLIST
73
+
74
+ Auto-generated based on your implementation.
75
+
76
+ """
77
+
78
+ # Group by category
79
+ by_category = {}
80
+ for c in concerns:
81
+ cat = c["category"]
82
+ if cat not in by_category:
83
+ by_category[cat] = []
84
+ by_category[cat].append(c)
85
+
86
+ for category, items in sorted(by_category.items()):
87
+ checklist += f"\n### {category}\n"
88
+ for item in items:
89
+ checklist += f"- [ ] {item['check']}\n"
90
+ checklist += f" File: {item['file']}\n"
91
+
92
+ # Add standard checks
93
+ checklist += """
94
+
95
+ ### General Security
96
+ - [ ] HTTPS enforced in production
97
+ - [ ] CSRF protection enabled
98
+ - [ ] Rate limiting on sensitive endpoints
99
+ - [ ] Security headers set (CSP, X-Frame-Options, etc.)
100
+ - [ ] Dependencies audited (npm audit / pip-audit)
101
+ - [ ] Secrets not committed to git (.env in .gitignore)
102
+ - [ ] Error messages don't leak sensitive info
103
+ - [ ] Logging doesn't include passwords/tokens
104
+
105
+ ### Compliance (if applicable)
106
+ - [ ] GDPR: User data deletion implemented
107
+ - [ ] SOC2: Audit logs for all auth events
108
+ - [ ] PCI-DSS: No credit card data stored
109
+
110
+ """
111
+
112
+ checklist += """
113
+ 🧪 Automated Security Scans Available:
114
+
115
+ 1. npm audit (Node.js dependencies)
116
+ 2. pip-audit (Python dependencies)
117
+ 3. OWASP ZAP (web application scan)
118
+ 4. Snyk (vulnerability scanning)
119
+
120
+ Run automated scans?
121
+ 1. Yes, run npm audit + recommended scans
122
+ 2. Manual review only
123
+ 3. Skip for now
124
+
125
+ """
126
+
127
+ return checklist
128
+
129
+ if __name__ == "__main__":
130
+ print(generate_security_checklist())
@@ -0,0 +1,133 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Task time tracking with real datetime stamps.
4
+ Auto-generated by PRD-Taskmaster skill.
5
+ """
6
+
7
+ from datetime import datetime, timezone
8
+ import json
9
+ import os
10
+ from pathlib import Path
11
+
12
+ class TaskTimer:
13
+ """Track task execution time with precise datetime stamps."""
14
+
15
+ def __init__(self, task_id, subtask_id=None):
16
+ self.task_id = task_id
17
+ self.subtask_id = subtask_id
18
+ self.state_dir = Path(".taskmaster/state")
19
+ self.state_dir.mkdir(parents=True, exist_ok=True)
20
+
21
+ if subtask_id:
22
+ self.state_file = self.state_dir / f"task-{task_id}-subtask-{subtask_id}.json"
23
+ else:
24
+ self.state_file = self.state_dir / f"task-{task_id}.json"
25
+
26
+ def start(self):
27
+ """Mark task/subtask as started with current UTC timestamp."""
28
+ data = {
29
+ "task_id": self.task_id,
30
+ "subtask_id": self.subtask_id,
31
+ "start_time": datetime.now(timezone.utc).isoformat(),
32
+ "status": "in_progress"
33
+ }
34
+ with open(self.state_file, 'w') as f:
35
+ json.dump(data, f, indent=2)
36
+
37
+ start_time = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC")
38
+ print(f"📅 Started: {start_time}")
39
+
40
+ def complete(self, notes=""):
41
+ """Mark task/subtask as complete and calculate actual duration."""
42
+ with open(self.state_file, 'r') as f:
43
+ data = json.load(f)
44
+
45
+ start = datetime.fromisoformat(data['start_time'])
46
+ end = datetime.now(timezone.utc)
47
+
48
+ duration_seconds = (end - start).total_seconds()
49
+ duration_minutes = duration_seconds / 60
50
+ duration_hours = duration_minutes / 60
51
+
52
+ data.update({
53
+ "end_time": end.isoformat(),
54
+ "status": "completed",
55
+ "actual_duration_seconds": int(duration_seconds),
56
+ "actual_duration_minutes": round(duration_minutes, 2),
57
+ "actual_duration_hours": round(duration_hours, 2),
58
+ "notes": notes
59
+ })
60
+
61
+ with open(self.state_file, 'w') as f:
62
+ json.dump(data, f, indent=2)
63
+
64
+ # Format output
65
+ if duration_hours >= 1:
66
+ duration_str = f"{int(duration_hours)}h {int(duration_minutes % 60)}min"
67
+ else:
68
+ duration_str = f"{int(duration_minutes)} min"
69
+
70
+ end_time = end.strftime("%Y-%m-%d %H:%M:%S UTC")
71
+ print(f"✅ COMPLETED")
72
+ print(f"📅 Ended: {end_time}")
73
+ print(f"⏱️ Actual: {duration_str} ({duration_minutes:.1f} min)")
74
+
75
+ return duration_minutes
76
+
77
+ def get_duration(self):
78
+ """Get current duration for in-progress task."""
79
+ if not self.state_file.exists():
80
+ return 0
81
+
82
+ with open(self.state_file, 'r') as f:
83
+ data = json.load(f)
84
+
85
+ if data['status'] == 'completed':
86
+ return data['actual_duration_minutes']
87
+
88
+ start = datetime.fromisoformat(data['start_time'])
89
+ now = datetime.now(timezone.utc)
90
+ duration_minutes = (now - start).total_seconds() / 60
91
+ return round(duration_minutes, 2)
92
+
93
+ def get_all_task_times():
94
+ """Get timing data for all tasks."""
95
+ state_dir = Path(".taskmaster/state")
96
+ if not state_dir.exists():
97
+ return []
98
+
99
+ times = []
100
+ for state_file in state_dir.glob("task-*.json"):
101
+ with open(state_file, 'r') as f:
102
+ data = json.load(f)
103
+ if data['status'] == 'completed':
104
+ times.append(data)
105
+
106
+ return times
107
+
108
+ def calculate_accuracy(estimated_minutes, actual_minutes):
109
+ """Calculate estimation accuracy percentage."""
110
+ if estimated_minutes == 0:
111
+ return 0
112
+ accuracy = (actual_minutes / estimated_minutes) * 100
113
+ return round(accuracy, 1)
114
+
115
+ if __name__ == "__main__":
116
+ # CLI usage
117
+ import sys
118
+
119
+ if len(sys.argv) < 3:
120
+ print("Usage: python track-time.py <command> <task_id> [subtask_id]")
121
+ print("Commands: start, complete")
122
+ sys.exit(1)
123
+
124
+ command = sys.argv[1]
125
+ task_id = sys.argv[2]
126
+ subtask_id = sys.argv[3] if len(sys.argv) > 3 else None
127
+
128
+ timer = TaskTimer(task_id, subtask_id)
129
+
130
+ if command == "start":
131
+ timer.start()
132
+ elif command == "complete":
133
+ timer.complete()
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 anombyte93
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.