#!/usr/bin/env python3 import json import sys from datetime import date from pathlib import Path ROOT = Path(__file__).resolve().parents[1] BACKLOG = ROOT / 'backlog' / 'features.json' TYPE_CHOICES = ('feature', 'fix', 'bug', 'chore') LEVEL_CHOICES = ('low', 'med', 'high') def ask(prompt, default=''): value = input(f"{prompt}{' [' + default + ']' if default else ''}: ").strip() return value if value else default def ask_choice(prompt, choices, default): while True: value = ask(prompt, default).lower() if value in choices: return value print(f"Invalid value. Use one of: {', '.join(choices)}") def ask_list(prompt, default_csv=''): raw = ask(prompt, default_csv) return [item.strip() for item in raw.split(',') if item.strip()] def next_id(features): nums = [] for feature in features: fid = str(feature.get('id', '')) if fid.startswith('F-') and fid[2:].isdigit(): nums.append(int(fid[2:])) return f"F-{(max(nums) + 1) if nums else 1:03d}" def normalize_deprecated_gates(data): """Replace the legacy `review` gate key without changing feature state.""" changed = 0 for feature in data.get('features', []): gates = feature.get('gates') if not isinstance(gates, dict) or 'review' not in gates: continue gates.setdefault('reviewer', gates['review']) del gates['review'] changed += 1 return changed def start_feature(data, feature_id): """Promote one pending feature to in_progress while enforcing exclusivity.""" active = [ feature.get('id') for feature in data.get('features', []) if feature.get('status') == 'in_progress' and feature.get('id') != feature_id ] if active: raise SystemExit(f"Cannot start {feature_id}; already in progress: {', '.join(active)}") for feature in data.get('features', []): if feature.get('id') != feature_id: continue if feature.get('status') not in {'pending', 'in_progress'}: raise SystemExit(f"Cannot start {feature_id} from status {feature.get('status')}") feature['status'] = 'in_progress' return raise SystemExit(f'Feature not found: {feature_id}') def main(): data = json.loads(BACKLOG.read_text(encoding='utf-8')) if '--normalize-gates' in sys.argv[1:]: changed = normalize_deprecated_gates(data) BACKLOG.write_text(json.dumps(data, indent=2, ensure_ascii=False) + '\n', encoding='utf-8') print(f'Normalized deprecated gates in {changed} feature(s)') return if '--start' in sys.argv[1:]: index = sys.argv.index('--start') if index + 1 >= len(sys.argv): raise SystemExit('Usage: new_ticket.py --start ') feature_id = sys.argv[index + 1] start_feature(data, feature_id) BACKLOG.write_text(json.dumps(data, indent=2, ensure_ascii=False) + '\n', encoding='utf-8') print(f'Started {feature_id}') return # CLI flags (non-interactive mode) cli_args = sys.argv[1:] if any(a.startswith('--id') or a.startswith('--type') for a in cli_args): import argparse p = argparse.ArgumentParser(add_help=False) p.add_argument('--id') p.add_argument('--type', choices=TYPE_CHOICES, default='fix') p.add_argument('--title', default='Fix TODO') p.add_argument('--description', default='Need change') p.add_argument('--priority', choices=LEVEL_CHOICES, default='med') p.add_argument('--risk', choices=LEVEL_CHOICES, default='low') p.add_argument('--phase', default=None) ns, _ = p.parse_known_args(cli_args) features = data['features'] fid = ns.id or next_id(features) new_feature = { 'id': fid, 'type': ns.type, 'title': ns.title, 'description': ns.description, 'priority': ns.priority, 'risk': ns.risk, 'status': 'pending', 'created_at': str(date.today()), 'gates': {'reviewer': False, 'security': False, 'qa': False}, } if ns.phase: new_feature['phase'] = ns.phase features.append(new_feature) data['features'] = features BACKLOG.write_text(json.dumps(data, indent=2, ensure_ascii=False) + '\n', encoding='utf-8') print(f'Created {fid}: {ns.title}') return features = data.get('features', []) print('Create ticket (English caveman style).') ticket_type = ask_choice('Type (feature/fix/bug/chore)', TYPE_CHOICES, 'feature') title = ask('Title (short EN)', f'{ticket_type.capitalize()} TODO') problem = ask('Problem (short EN)', 'Need change') goal = ask('Goal (short EN)', 'Make flow better') scope_in = ask_list('Scope IN (comma list EN)', 'Core flow') scope_out = ask_list('Scope OUT (comma list EN)', 'No redesign') risk = ask_choice('Risk (low/med/high)', LEVEL_CHOICES, 'low') priority = ask_choice('Priority (low/med/high)', LEVEL_CHOICES, 'med') print('Acceptance bullets (EN caveman). Empty line to end.') acceptance = [] while True: line = input('- ').strip() if not line: break acceptance.append(line) if not acceptance: acceptance = [ 'Flow works end to end', 'No break old behavior', 'verify.sh is green', ] fid = next_id(features) desc = ( f"Problem: {problem}. " f"Goal: {goal}. " f"Scope IN: {', '.join(scope_in) or 'none'}. " f"Scope OUT: {', '.join(scope_out) or 'none'}. " f"Type: {ticket_type}. Priority: {priority}. Risk: {risk}." ) features.append({ 'id': fid, 'type': ticket_type, 'title': title, 'problem': problem, 'goal': goal, 'scope_in': scope_in, 'scope_out': scope_out, 'priority': priority, 'risk': risk, 'description': desc, 'acceptance': acceptance, 'status': 'pending', 'created_at': str(date.today()), 'gates': {'reviewer': False, 'security': False, 'qa': False}, }) data['features'] = features rules = data.setdefault('rules', {}) rules.setdefault('valid_types', list(TYPE_CHOICES)) BACKLOG.write_text(json.dumps(data, indent=2, ensure_ascii=False) + '\n', encoding='utf-8') print(f'Created {fid}: {title}') if __name__ == '__main__': main()