Compare commits
1 Commits
main
...
kimi/issue
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9dee9ed2a8 |
@@ -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": []
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 = []
|
||||
|
||||
# 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: {exc}", file=sys.stderr)
|
||||
return []
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user