Compare commits
1 Commits
main
...
kimi/issue
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9dee9ed2a8 |
@@ -196,7 +196,7 @@
|
|||||||
{
|
{
|
||||||
"id": "daily_run_orchestrator",
|
"id": "daily_run_orchestrator",
|
||||||
"name": "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",
|
"script": "timmy_automations/daily_run/orchestrator.py",
|
||||||
"category": "daily_run",
|
"category": "daily_run",
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
@@ -208,7 +208,14 @@
|
|||||||
"size_labels": ["size:XS", "size:S"],
|
"size_labels": ["size:XS", "size:S"],
|
||||||
"max_agenda_items": 3,
|
"max_agenda_items": 3,
|
||||||
"lookback_hours": 24,
|
"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": []
|
"outputs": []
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,48 @@
|
|||||||
{
|
{
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "Daily run schedule configuration",
|
"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": {
|
"schedules": {
|
||||||
"every_cycle": {
|
"every_cycle": {
|
||||||
"description": "Run before/after every dev cycle",
|
"description": "Run before/after every dev cycle",
|
||||||
|
|||||||
@@ -33,6 +33,12 @@ python3 timmy_automations/daily_run/orchestrator.py --review
|
|||||||
|
|
||||||
# Output as JSON
|
# Output as JSON
|
||||||
python3 timmy_automations/daily_run/orchestrator.py --review --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
|
## 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`)
|
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
|
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
|
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
|
### Configuration
|
||||||
|
|
||||||
@@ -98,6 +105,65 @@ Candidates considered: 5
|
|||||||
**Review mode (`--review`):**
|
**Review mode (`--review`):**
|
||||||
Adds a day summary section showing issues touched, closed, PRs merged, and any test failures.
|
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
|
## 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
|
Connects to local Gitea, fetches candidate issues, and produces a concise agenda
|
||||||
plus a day summary (review mode).
|
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
|
Env: See timmy_automations/config/daily_run.json for configuration
|
||||||
|
|
||||||
Refs: #703
|
Refs: #703, #716
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -40,17 +40,42 @@ DEFAULT_CONFIG = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def load_config() -> dict:
|
def load_config(preset: str | None = None) -> dict:
|
||||||
"""Load configuration from config file with fallback to defaults."""
|
"""Load configuration from config file with fallback to defaults.
|
||||||
|
|
||||||
|
If a preset is specified, merge preset configuration with defaults.
|
||||||
|
"""
|
||||||
config = DEFAULT_CONFIG.copy()
|
config = DEFAULT_CONFIG.copy()
|
||||||
|
presets = {}
|
||||||
|
|
||||||
if CONFIG_PATH.exists():
|
if CONFIG_PATH.exists():
|
||||||
try:
|
try:
|
||||||
file_config = json.loads(CONFIG_PATH.read_text())
|
file_config = json.loads(CONFIG_PATH.read_text())
|
||||||
if "orchestrator" in file_config:
|
if "orchestrator" in file_config:
|
||||||
config.update(file_config["orchestrator"])
|
config.update(file_config["orchestrator"])
|
||||||
|
# Load presets if available
|
||||||
|
presets = file_config.get("focus_day_presets", {})
|
||||||
except (json.JSONDecodeError, OSError) as exc:
|
except (json.JSONDecodeError, OSError) as exc:
|
||||||
print(f"[orchestrator] Warning: Could not load config: {exc}", file=sys.stderr)
|
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
|
# Environment variable overrides
|
||||||
if os.environ.get("TIMMY_GITEA_API"):
|
if os.environ.get("TIMMY_GITEA_API"):
|
||||||
config["gitea_api"] = 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"
|
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 an issue for prioritization (higher = more suitable for daily run)."""
|
||||||
score = 0
|
score = 0
|
||||||
labels = [l.get("name", "").lower() for l in issue.get("labels", [])]
|
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
|
# Prefer smaller sizes
|
||||||
if "size:xs" in labels:
|
if "size:xs" in labels:
|
||||||
@@ -198,8 +233,8 @@ def score_issue(issue: dict) -> int:
|
|||||||
elif "size:m" in labels:
|
elif "size:m" in labels:
|
||||||
score += 2
|
score += 2
|
||||||
|
|
||||||
# Prefer daily-run labeled issues
|
# Prefer daily-run labeled issues (when not using a preset)
|
||||||
if "daily-run" in labels:
|
if "daily-run" in labels and not config.get("_preset"):
|
||||||
score += 3
|
score += 3
|
||||||
|
|
||||||
# Prefer issues with clear type labels
|
# 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]:
|
def fetch_candidates(client: GiteaClient, config: dict) -> list[dict]:
|
||||||
"""Fetch issues matching candidate criteria."""
|
"""Fetch issues matching candidate criteria."""
|
||||||
candidate_labels = config["candidate_labels"]
|
# Use preset labels if available, otherwise fall back to config defaults
|
||||||
size_labels = config.get("size_labels", [])
|
candidate_labels = config.get("_preset_candidate_labels") or config["candidate_labels"]
|
||||||
all_labels = candidate_labels + size_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,
|
all_issues = []
|
||||||
# so we fetch by candidate label and filter sizes client-side)
|
|
||||||
params = {"state": "open", "sort": "created", "labels": ",".join(candidate_labels)}
|
|
||||||
|
|
||||||
try:
|
# Fetch issues for each candidate label separately and combine
|
||||||
issues = client.get_paginated("issues", params)
|
for label in candidate_labels:
|
||||||
except (HTTPError, URLError) as exc:
|
params = {"state": "open", "sort": "created", "labels": label}
|
||||||
print(f"[orchestrator] Warning: Failed to fetch issues: {exc}", file=sys.stderr)
|
try:
|
||||||
return []
|
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
|
# Filter by size labels if specified
|
||||||
if size_labels:
|
if size_labels:
|
||||||
filtered = []
|
filtered = []
|
||||||
size_names = {s.lower() for s in size_labels}
|
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", [])}
|
issue_labels = {l.get("name", "").lower() for l in issue.get("labels", [])}
|
||||||
if issue_labels & size_names:
|
if issue_labels & size_names:
|
||||||
filtered.append(issue)
|
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:
|
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)
|
agenda_time = config.get("agenda_time_minutes", 10)
|
||||||
|
|
||||||
# Score and sort issues
|
# 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)))
|
scored.sort(key=lambda x: (-x[0], x[1].get("number", 0)))
|
||||||
|
|
||||||
selected = scored[:max_items]
|
selected = scored[:max_items]
|
||||||
@@ -267,7 +328,7 @@ def generate_agenda(issues: list[dict], config: dict) -> dict:
|
|||||||
}
|
}
|
||||||
items.append(item)
|
items.append(item)
|
||||||
|
|
||||||
return {
|
agenda = {
|
||||||
"generated_at": datetime.now(timezone.utc).isoformat(),
|
"generated_at": datetime.now(timezone.utc).isoformat(),
|
||||||
"time_budget_minutes": agenda_time,
|
"time_budget_minutes": agenda_time,
|
||||||
"item_count": len(items),
|
"item_count": len(items),
|
||||||
@@ -275,15 +336,27 @@ def generate_agenda(issues: list[dict], config: dict) -> dict:
|
|||||||
"candidates_considered": len(issues),
|
"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:
|
def print_agenda(agenda: dict) -> None:
|
||||||
"""Print a formatted agenda to stdout."""
|
"""Print a formatted agenda to stdout."""
|
||||||
|
# Use preset title if available, otherwise default
|
||||||
|
title = agenda.get("preset_title", "📋 DAILY RUN AGENDA")
|
||||||
|
|
||||||
print("=" * 60)
|
print("=" * 60)
|
||||||
print("📋 DAILY RUN AGENDA")
|
print(title)
|
||||||
print("=" * 60)
|
print("=" * 60)
|
||||||
print(f"Generated: {agenda['generated_at']}")
|
print(f"Generated: {agenda['generated_at']}")
|
||||||
print(f"Time budget: {agenda['time_budget_minutes']} minutes")
|
print(f"Time budget: {agenda['time_budget_minutes']} minutes")
|
||||||
print(f"Candidates considered: {agenda['candidates_considered']}")
|
print(f"Candidates considered: {agenda['candidates_considered']}")
|
||||||
|
if agenda.get("preset"):
|
||||||
|
print(f"Focus preset: {agenda['preset']}")
|
||||||
print()
|
print()
|
||||||
|
|
||||||
if not agenda["items"]:
|
if not agenda["items"]:
|
||||||
@@ -487,12 +560,64 @@ def parse_args() -> argparse.Namespace:
|
|||||||
default=None,
|
default=None,
|
||||||
help="Override max agenda items",
|
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()
|
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:
|
def main() -> int:
|
||||||
args = parse_args()
|
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:
|
if args.max_items:
|
||||||
config["max_agenda_items"] = args.max_items
|
config["max_agenda_items"] = args.max_items
|
||||||
|
|||||||
Reference in New Issue
Block a user