Compare commits

...

1 Commits

Author SHA1 Message Date
kimi
9dee9ed2a8 feat: add focus-day presets for Daily Run and work selection
Some checks failed
Tests / lint (pull_request) Has been cancelled
Tests / test (pull_request) Has been cancelled
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
2026-03-21 15:34:10 -04:00
4 changed files with 268 additions and 28 deletions

View File

@@ -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": []
}

View File

@@ -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",

View File

@@ -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 <name>` 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.

View File

@@ -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