From 9dee9ed2a88eb7a64011196634821c6915cb9d3f Mon Sep 17 00:00:00 2001 From: kimi Date: Sat, 21 Mar 2026 15:34:10 -0400 Subject: [PATCH] feat: add focus-day presets for Daily Run and work selection Add five focus-day presets that bias Daily Run agenda generation: - tests-day: Focus on test-related work - triage-day: Issue triage and backlog grooming - economy-day: Payment and economic features - docs-day: Documentation and guides - refactor-day: Code cleanup and refactoring Changes: - Add focus_day_presets configuration to daily_run.json - Add --preset CLI argument to orchestrator.py - Add --list-presets to show available presets - Update fetch_candidates() to use preset label filters - Update score_issue() to boost preset-matching issues - Update generate_agenda() to include preset metadata - Add comprehensive documentation to README.md Fixes #716 --- timmy_automations/config/automations.json | 11 +- timmy_automations/config/daily_run.json | 42 +++++ timmy_automations/daily_run/README.md | 68 +++++++- timmy_automations/daily_run/orchestrator.py | 175 +++++++++++++++++--- 4 files changed, 268 insertions(+), 28 deletions(-) diff --git a/timmy_automations/config/automations.json b/timmy_automations/config/automations.json index 48dc7af2..b147c4be 100644 --- a/timmy_automations/config/automations.json +++ b/timmy_automations/config/automations.json @@ -196,7 +196,7 @@ { "id": "daily_run_orchestrator", "name": "Daily Run Orchestrator", - "description": "The 10-minute ritual โ€” fetches candidate issues and produces a concise Daily Run agenda plus day summary", + "description": "The 10-minute ritual โ€” fetches candidate issues and produces a concise Daily Run agenda plus day summary. Supports focus-day presets for themed work sessions.", "script": "timmy_automations/daily_run/orchestrator.py", "category": "daily_run", "enabled": true, @@ -208,7 +208,14 @@ "size_labels": ["size:XS", "size:S"], "max_agenda_items": 3, "lookback_hours": 24, - "agenda_time_minutes": 10 + "agenda_time_minutes": 10, + "focus_day_presets": [ + "tests-day", + "triage-day", + "economy-day", + "docs-day", + "refactor-day" + ] }, "outputs": [] } diff --git a/timmy_automations/config/daily_run.json b/timmy_automations/config/daily_run.json index 8db1909e..4075aacc 100644 --- a/timmy_automations/config/daily_run.json +++ b/timmy_automations/config/daily_run.json @@ -1,6 +1,48 @@ { "version": "1.0.0", "description": "Daily run schedule configuration", + "focus_day_presets": { + "tests-day": { + "description": "Focus on test-related work", + "candidate_labels": ["test", "testing", "tests", "coverage"], + "size_labels": ["size:XS", "size:S", "size:M"], + "title_keywords": ["test", "tests", "testing", "coverage", "pytest", "tox"], + "priority_boost": ["test", "testing", "tests"], + "agenda_title": "๐Ÿงช Tests Day โ€” Focus on Quality" + }, + "triage-day": { + "description": "Focus on issue triage and backlog grooming", + "candidate_labels": ["triage", "backlog", "needs-review", "grooming"], + "size_labels": ["size:XS", "size:S", "size:M", "size:L"], + "title_keywords": ["triage", "review", "categorize", "organize"], + "priority_boost": ["triage", "backlog"], + "agenda_title": "๐Ÿ“‹ Triage Day โ€” Organize and Prioritize" + }, + "economy-day": { + "description": "Focus on payment, pricing, and economic features", + "candidate_labels": ["economy", "payment", "pricing", "l402", "lightning", "bitcoin"], + "size_labels": ["size:XS", "size:S", "size:M"], + "title_keywords": ["payment", "price", "economy", "invoice", "lightning", "bitcoin", "l402"], + "priority_boost": ["economy", "payment", "lightning"], + "agenda_title": "โšก Economy Day โ€” Build the Circular Economy" + }, + "docs-day": { + "description": "Focus on documentation and guides", + "candidate_labels": ["docs", "documentation", "readme", "guide"], + "size_labels": ["size:XS", "size:S", "size:M"], + "title_keywords": ["doc", "docs", "documentation", "readme", "guide", "tutorial"], + "priority_boost": ["docs", "documentation"], + "agenda_title": "๐Ÿ“š Docs Day โ€” Knowledge and Clarity" + }, + "refactor-day": { + "description": "Focus on code cleanup and refactoring", + "candidate_labels": ["refactor", "cleanup", "debt", "tech-debt"], + "size_labels": ["size:XS", "size:S", "size:M"], + "title_keywords": ["refactor", "cleanup", "simplify", "organize", "restructure"], + "priority_boost": ["refactor", "cleanup", "debt"], + "agenda_title": "๐Ÿ”ง Refactor Day โ€” Clean Code, Clear Mind" + } + }, "schedules": { "every_cycle": { "description": "Run before/after every dev cycle", diff --git a/timmy_automations/daily_run/README.md b/timmy_automations/daily_run/README.md index d7769d17..81d57bd1 100644 --- a/timmy_automations/daily_run/README.md +++ b/timmy_automations/daily_run/README.md @@ -33,6 +33,12 @@ python3 timmy_automations/daily_run/orchestrator.py --review # Output as JSON python3 timmy_automations/daily_run/orchestrator.py --review --json + +# Use a focus-day preset (see Focus-Day Presets section below) +python3 timmy_automations/daily_run/orchestrator.py --preset tests-day + +# List available presets +python3 timmy_automations/daily_run/orchestrator.py --list-presets ``` ## Daily Run Orchestrator @@ -42,6 +48,7 @@ The orchestrator script connects to local Gitea and: 1. **Fetches candidate issues** matching configured labels (default: `daily-run` + `size:XS`/`size:S`) 2. **Generates a concise agenda** with up to 3 items for approximately 10 minutes of work 3. **Review mode** (`--review`): Summarizes the last 24 hours โ€” issues/PRs touched, items closed/merged, test failures +4. **Focus-Day Presets** (`--preset`): Biases the agenda toward specific types of work ### Configuration @@ -98,6 +105,65 @@ Candidates considered: 5 **Review mode (`--review`):** Adds a day summary section showing issues touched, closed, PRs merged, and any test failures. +### Focus-Day Presets + +Focus-day presets bias the Daily Run agenda toward specific types of work. Use `--preset ` to activate a preset. + +| Preset | Description | Candidate Labels | Use When | +|--------|-------------|------------------|----------| +| `tests-day` | Focus on test-related work | `test`, `testing`, `tests`, `coverage` | Improving test coverage, fixing flaky tests | +| `triage-day` | Issue triage and backlog grooming | `triage`, `backlog`, `needs-review`, `grooming` | Organizing the backlog, reviewing stale issues | +| `economy-day` | Payment and economic features | `economy`, `payment`, `pricing`, `l402`, `lightning`, `bitcoin` | Working on Lightning/L402 integration | +| `docs-day` | Documentation and guides | `docs`, `documentation`, `readme`, `guide` | Writing docs, updating READMEs | +| `refactor-day` | Code cleanup and refactoring | `refactor`, `cleanup`, `debt`, `tech-debt` | Paying down technical debt | + +**How Presets Work:** + +1. **Label Filtering**: Each preset defines its own `candidate_labels` โ€” issues with any of these labels are fetched +2. **Size Filtering**: Presets can include larger sizes (e.g., `tests-day` includes `size:M` for bigger test refactors) +3. **Priority Boosting**: Issues matching preset priority labels get a scoring bonus (+15 for labels, +8 for title keywords) +4. **Title Filtering**: Some presets filter by title keywords to find relevant issues even without labels +5. **Custom Agenda Title**: The output header reflects the focus (e.g., "๐Ÿงช Tests Day โ€” Focus on Quality") + +**Example:** + +```bash +# Run with tests-day preset +$ python3 timmy_automations/daily_run/orchestrator.py --preset tests-day + +============================================================ +๐Ÿงช Tests Day โ€” Focus on Quality +============================================================ +Generated: 2026-03-21T15:16:02+00:00 +Time budget: 10 minutes +Candidates considered: 5 +Focus preset: tests-day + +1. #123 [M] [infra] + Title: Add integration tests for payment flow + Action: TEST + URL: http://localhost:3000/rockachopa/Timmy-time-dashboard/issues/123 +``` + +### Extending Presets + +Presets are defined in `timmy_automations/config/daily_run.json` under `focus_day_presets`. To add a new preset: + +```json +{ + "focus_day_presets": { + "my-preset": { + "description": "What this preset is for", + "candidate_labels": ["label-1", "label-2"], + "size_labels": ["size:XS", "size:S"], + "title_keywords": ["keyword1", "keyword2"], + "priority_boost": ["label-1"], + "agenda_title": "๐ŸŽฏ My Preset โ€” Description" + } + } +} +``` + ## Configuration -See `../config/automations.json` for automation manifests and `../config/daily_run.json` for scheduling and orchestrator settings. +See `../config/automations.json` for automation manifests and `../config/daily_run.json` for scheduling, orchestrator settings, and focus-day presets. diff --git a/timmy_automations/daily_run/orchestrator.py b/timmy_automations/daily_run/orchestrator.py index de117b4a..bf8bb5dd 100755 --- a/timmy_automations/daily_run/orchestrator.py +++ b/timmy_automations/daily_run/orchestrator.py @@ -4,10 +4,10 @@ Connects to local Gitea, fetches candidate issues, and produces a concise agenda plus a day summary (review mode). -Run: python3 timmy_automations/daily_run/orchestrator.py [--review] +Run: python3 timmy_automations/daily_run/orchestrator.py [--review] [--preset NAME] Env: See timmy_automations/config/daily_run.json for configuration -Refs: #703 +Refs: #703, #716 """ from __future__ import annotations @@ -40,17 +40,42 @@ DEFAULT_CONFIG = { } -def load_config() -> dict: - """Load configuration from config file with fallback to defaults.""" +def load_config(preset: str | None = None) -> dict: + """Load configuration from config file with fallback to defaults. + + If a preset is specified, merge preset configuration with defaults. + """ config = DEFAULT_CONFIG.copy() + presets = {} + if CONFIG_PATH.exists(): try: file_config = json.loads(CONFIG_PATH.read_text()) if "orchestrator" in file_config: config.update(file_config["orchestrator"]) + # Load presets if available + presets = file_config.get("focus_day_presets", {}) except (json.JSONDecodeError, OSError) as exc: print(f"[orchestrator] Warning: Could not load config: {exc}", file=sys.stderr) + # Apply preset configuration if specified and exists + if preset: + if preset not in presets: + available = ", ".join(presets.keys()) if presets else "none defined" + print( + f"[orchestrator] Warning: Preset '{preset}' not found. " + f"Available: {available}", + file=sys.stderr, + ) + else: + preset_config = presets[preset] + config["_preset"] = preset + config["_preset_title"] = preset_config.get("agenda_title", f"Focus: {preset}") + # Override config with preset values + for key in ["candidate_labels", "size_labels", "title_keywords", "priority_boost"]: + if key in preset_config: + config[f"_preset_{key}"] = preset_config[key] + # Environment variable overrides if os.environ.get("TIMMY_GITEA_API"): config["gitea_api"] = os.environ.get("TIMMY_GITEA_API") @@ -185,10 +210,20 @@ def suggest_action_type(issue: dict) -> str: return "review" -def score_issue(issue: dict) -> int: +def score_issue(issue: dict, config: dict) -> int: """Score an issue for prioritization (higher = more suitable for daily run).""" score = 0 labels = [l.get("name", "").lower() for l in issue.get("labels", [])] + title = issue.get("title", "").lower() + + # Preset-specific scoring: boost issues matching preset priority labels + preset_priority = config.get("_preset_priority_boost", []) + if preset_priority: + for priority_label in preset_priority: + if priority_label.lower() in labels: + score += 15 # Strong boost for preset-matching labels + if priority_label.lower() in title: + score += 8 # Medium boost for preset-matching title # Prefer smaller sizes if "size:xs" in labels: @@ -198,8 +233,8 @@ def score_issue(issue: dict) -> int: elif "size:m" in labels: score += 2 - # Prefer daily-run labeled issues - if "daily-run" in labels: + # Prefer daily-run labeled issues (when not using a preset) + if "daily-run" in labels and not config.get("_preset"): score += 3 # Prefer issues with clear type labels @@ -217,31 +252,57 @@ def score_issue(issue: dict) -> int: def fetch_candidates(client: GiteaClient, config: dict) -> list[dict]: """Fetch issues matching candidate criteria.""" - candidate_labels = config["candidate_labels"] - size_labels = config.get("size_labels", []) - all_labels = candidate_labels + size_labels + # Use preset labels if available, otherwise fall back to config defaults + candidate_labels = config.get("_preset_candidate_labels") or config["candidate_labels"] + size_labels = config.get("_preset_size_labels") or config.get("size_labels", []) - # Build label filter (OR logic via multiple label queries doesn't work well, - # so we fetch by candidate label and filter sizes client-side) - params = {"state": "open", "sort": "created", "labels": ",".join(candidate_labels)} + all_issues = [] - try: - issues = client.get_paginated("issues", params) - except (HTTPError, URLError) as exc: - print(f"[orchestrator] Warning: Failed to fetch issues: {exc}", file=sys.stderr) - return [] + # Fetch issues for each candidate label separately and combine + for label in candidate_labels: + params = {"state": "open", "sort": "created", "labels": label} + try: + issues = client.get_paginated("issues", params) + all_issues.extend(issues) + except (HTTPError, URLError) as exc: + print( + f"[orchestrator] Warning: Failed to fetch issues for label '{label}': {exc}", + file=sys.stderr, + ) + + # Remove duplicates (in case issues have multiple matching labels) + seen = set() + unique_issues = [] + for issue in all_issues: + issue_id = issue.get("number") + if issue_id not in seen: + seen.add(issue_id) + unique_issues.append(issue) # Filter by size labels if specified if size_labels: filtered = [] size_names = {s.lower() for s in size_labels} - for issue in issues: + for issue in unique_issues: issue_labels = {l.get("name", "").lower() for l in issue.get("labels", [])} if issue_labels & size_names: filtered.append(issue) - issues = filtered + unique_issues = filtered - return issues + # Additional filtering by title keywords if preset specifies them + title_keywords = config.get("_preset_title_keywords", []) + if title_keywords: + keyword_filtered = [] + keywords = [k.lower() for k in title_keywords] + for issue in unique_issues: + title = issue.get("title", "").lower() + if any(kw in title for kw in keywords): + keyword_filtered.append(issue) + # Only apply keyword filter if it doesn't eliminate all candidates + if keyword_filtered: + unique_issues = keyword_filtered + + return unique_issues def generate_agenda(issues: list[dict], config: dict) -> dict: @@ -250,7 +311,7 @@ def generate_agenda(issues: list[dict], config: dict) -> dict: agenda_time = config.get("agenda_time_minutes", 10) # Score and sort issues - scored = [(score_issue(issue), issue) for issue in issues] + scored = [(score_issue(issue, config), issue) for issue in issues] scored.sort(key=lambda x: (-x[0], x[1].get("number", 0))) selected = scored[:max_items] @@ -267,7 +328,7 @@ def generate_agenda(issues: list[dict], config: dict) -> dict: } items.append(item) - return { + agenda = { "generated_at": datetime.now(timezone.utc).isoformat(), "time_budget_minutes": agenda_time, "item_count": len(items), @@ -275,15 +336,27 @@ def generate_agenda(issues: list[dict], config: dict) -> dict: "candidates_considered": len(issues), } + # Include preset info if active + if config.get("_preset"): + agenda["preset"] = config["_preset"] + agenda["preset_title"] = config.get("_preset_title", f"Focus: {config['_preset']}") + + return agenda + def print_agenda(agenda: dict) -> None: """Print a formatted agenda to stdout.""" + # Use preset title if available, otherwise default + title = agenda.get("preset_title", "๐Ÿ“‹ DAILY RUN AGENDA") + print("=" * 60) - print("๐Ÿ“‹ DAILY RUN AGENDA") + print(title) print("=" * 60) print(f"Generated: {agenda['generated_at']}") print(f"Time budget: {agenda['time_budget_minutes']} minutes") print(f"Candidates considered: {agenda['candidates_considered']}") + if agenda.get("preset"): + print(f"Focus preset: {agenda['preset']}") print() if not agenda["items"]: @@ -487,12 +560,64 @@ def parse_args() -> argparse.Namespace: default=None, help="Override max agenda items", ) + p.add_argument( + "--preset", "-p", + type=str, + default=None, + metavar="NAME", + help="Use a focus-day preset (tests-day, triage-day, economy-day, docs-day, refactor-day)", + ) + p.add_argument( + "--list-presets", + action="store_true", + help="List available focus-day presets and exit", + ) return p.parse_args() +def list_presets() -> None: + """List available focus-day presets.""" + if not CONFIG_PATH.exists(): + print("No configuration file found.", file=sys.stderr) + return + + try: + file_config = json.loads(CONFIG_PATH.read_text()) + presets = file_config.get("focus_day_presets", {}) + + if not presets: + print("No focus-day presets configured.") + return + + print("=" * 60) + print("๐ŸŽฏ AVAILABLE FOCUS-DAY PRESETS") + print("=" * 60) + print() + + for name, config in presets.items(): + title = config.get("agenda_title", f"Focus: {name}") + description = config.get("description", "No description") + labels = config.get("candidate_labels", []) + + print(f"{name}") + print(f" {title}") + print(f" {description}") + print(f" Labels: {', '.join(labels) if labels else 'none'}") + print() + + except (json.JSONDecodeError, OSError) as exc: + print(f"Error loading presets: {exc}", file=sys.stderr) + + def main() -> int: args = parse_args() - config = load_config() + + # Handle --list-presets + if args.list_presets: + list_presets() + return 0 + + config = load_config(preset=args.preset) if args.max_items: config["max_agenda_items"] = args.max_items -- 2.43.0