C0: retire Todoist blumeops-tasks; point task discovery at heph
Replace the Todoist-backed blumeops-tasks mise task with `heph list --project Blumeops --json` (hephaestus, now at v1 prototype on gilbert). Update task-discovery, rotation-reminder, and zk references across docs; note the zk zettelkasten is migrating into heph docs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
308c8e3dad
commit
2148714584
10 changed files with 16 additions and 226 deletions
12
AGENTS.md
12
AGENTS.md
|
|
@ -65,7 +65,7 @@ See [[agent-change-process]] for the full methodology.
|
|||
./pulumi/ # Pulumi IaC (tailnet ACLs, dns, cloud)
|
||||
~/.config/{nvim,fish} # user's shell config, managed by chezmoi
|
||||
~/code/personal/ # user's projects
|
||||
~/code/personal/zk # user's Obsidian-sync managed zettelkasten. Potential source for reference data.
|
||||
~/code/personal/zk # user's zettelkasten (Obsidian-sync). Reference-data source; migrating into heph docs (hephaestus).
|
||||
~/code/3rd/ # mirrored external projects
|
||||
~/code/work # FORBIDDEN
|
||||
```
|
||||
|
|
@ -147,10 +147,16 @@ Create a new spork: `mise run spork-create <mirror-name>`
|
|||
|
||||
## Task Discovery
|
||||
|
||||
BlumeOps tasks live in [hephaestus](https://github.com/eblume/hephaestus) (`heph`),
|
||||
the user's self-hosted context/task system. Fetch them with the CLI:
|
||||
|
||||
```fish
|
||||
mise run blumeops-tasks # fetch from Todoist, sorted by priority
|
||||
heph list --project Blumeops --json # outstanding Blumeops tasks as JSON
|
||||
```
|
||||
Most tasks are stored in `./mise-tasks/`. For scripts with any logic or
|
||||
|
||||
(This replaced the retired `blumeops-tasks` mise task, which read from Todoist.)
|
||||
|
||||
Most operational scripts are stored in `./mise-tasks/`. For scripts with any logic or
|
||||
complexity, use uv run --script 's with explicit dependencies. Complex
|
||||
workflows with artifacts should become dagger pipelines. Mise tasks are for
|
||||
development processes and operations - tools for the user or the agent.
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
`blumeops-tasks` now annotates each task with a human-readable due offset (`5d overdue` / `due in 2d` / `due today`) and a `↻ <recurrence>` marker for recurring tasks, and sorts by overdue-ness (most overdue first, no-due-date last) with priority as tiebreaker.
|
||||
1
docs/changelog.d/+retire-todoist-for-heph.infra.md
Normal file
1
docs/changelog.d/+retire-todoist-for-heph.infra.md
Normal file
|
|
@ -0,0 +1 @@
|
|||
Retired the `blumeops-tasks` mise task (Todoist API) in favor of `heph list --project Blumeops --json` from the self-hosted [hephaestus](https://github.com/eblume/hephaestus) system. Updated docs to point task discovery and rotation reminders at heph, and noted that the `~/code/personal/zk` zettelkasten is migrating into heph docs.
|
||||
|
|
@ -14,7 +14,7 @@ How to rotate the Fly.io API token used to deploy [[flyio-proxy]]. The token liv
|
|||
|
||||
## When to rotate
|
||||
|
||||
- Every 75 days (Todoist recurring task)
|
||||
- Every 75 days (heph recurring task)
|
||||
- After any compromise / accidental disclosure
|
||||
- If `fly deploy` starts returning auth errors
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ How to rotate the Gandi Personal Access Token. **One PAT** is shared by [[caddy]
|
|||
|
||||
## When to rotate
|
||||
|
||||
- Every 60 days (Todoist recurring task)
|
||||
- Every 60 days (heph recurring task)
|
||||
- After any compromise / accidental disclosure
|
||||
- Whenever Gandi starts rejecting the PAT (see [Debugging](#debugging))
|
||||
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ Daily backup system using Borg backup, running on indri.
|
|||
## What Gets Backed Up
|
||||
|
||||
**Directories:**
|
||||
- `~/code/personal/zk` - Zettelkasten
|
||||
- `~/code/personal/zk` - Zettelkasten (migrating into heph docs; see [hephaestus](https://github.com/eblume/hephaestus))
|
||||
- `/opt/homebrew/var/forgejo` - Git forge data
|
||||
- `~/.config/borgmatic` - Borgmatic config
|
||||
- `~/Documents` - Personal documents
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ Daily automated backups from [[indri]] to [[sifaka|Sifaka]] NAS.
|
|||
|
||||
| Path | Description | Priority |
|
||||
|------|-------------|----------|
|
||||
| `~/code/personal/zk` | Zettelkasten notes | Critical |
|
||||
| `~/code/personal/zk` | Zettelkasten notes (migrating into heph docs) | Critical |
|
||||
| `/opt/homebrew/var/forgejo` | Git repositories | Critical |
|
||||
| `~/.config/borgmatic` | Backup config | High |
|
||||
| `~/Documents` | Personal documents (includes [[1password]] encrypted export) | High |
|
||||
|
|
|
|||
|
|
@ -69,7 +69,6 @@ Run `mise tasks --sort name` for the live list with descriptions.
|
|||
|------|-------------|
|
||||
| `services-check` | Check all services are online and responding |
|
||||
| `service-review` | Review the most stale service for version freshness |
|
||||
| `blumeops-tasks` | List tasks from Todoist sorted by priority |
|
||||
| `op-backup` | Encrypt 1Password export and send to indri for borgmatic |
|
||||
|
||||
## Infrastructure Setup
|
||||
|
|
|
|||
|
|
@ -98,7 +98,6 @@ BlumeOps operations are driven by mise tasks. Run `mise tasks` to list all avail
|
|||
| `provision-indri` | Deploy changes to [[indri]]-hosted services via Ansible |
|
||||
| `services-check` | After deployments - verify all services are healthy |
|
||||
| `pr-comments` | Check unresolved PR comments during review |
|
||||
| `blumeops-tasks` | Find pending tasks from Todoist |
|
||||
| `container-list` | View available container images and tags |
|
||||
| `container-build-and-release` | Trigger container build workflows |
|
||||
| `dns-preview` | Preview DNS changes before applying |
|
||||
|
|
@ -111,6 +110,8 @@ BlumeOps operations are driven by mise tasks. Run `mise tasks` to list all avail
|
|||
| `docs-review` | Review the most stale doc by last-reviewed date |
|
||||
| `runner-logs` | View Forgejo workflow logs (indri or ringtail runner) |
|
||||
|
||||
For task discovery, BlumeOps tasks live in [hephaestus](https://github.com/eblume/hephaestus) (`heph`), not Todoist. List outstanding work with `heph list --project Blumeops --json`.
|
||||
|
||||
For ArgoCD operations, use the `argocd` CLI directly:
|
||||
- `argocd app diff <service>` - Preview changes
|
||||
- `argocd app sync <service>` - Deploy changes
|
||||
|
|
|
|||
|
|
@ -1,216 +0,0 @@
|
|||
#!/usr/bin/env -S uv run --script
|
||||
# /// script
|
||||
# requires-python = ">=3.12"
|
||||
# dependencies = ["httpx==0.28.1", "rich==15.0.0"]
|
||||
# ///
|
||||
#MISE description="List Blumeops tasks from Todoist sorted by priority"
|
||||
"""Fetch and display Blumeops tasks from Todoist, sorted by priority.
|
||||
|
||||
This script is specific to Erich Blume's personal development workflow and
|
||||
is not intended for general use. It requires:
|
||||
|
||||
- A 1Password CLI (`op`) configured with access to the author's vault
|
||||
- A Todoist account with a project named "Blumeops"
|
||||
|
||||
The script fetches tasks and displays them sorted by a custom priority order:
|
||||
p1 (urgent), p2 (high), p4 (normal/default), p3 (backlog). The p3-last ordering
|
||||
reflects a deliberate choice to treat p3 as "backlog" rather than moderate
|
||||
priority.
|
||||
|
||||
Usage: mise run blumeops-tasks
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
from datetime import date
|
||||
|
||||
import httpx
|
||||
from rich.console import Console
|
||||
from rich.markup import escape
|
||||
from rich.text import Text
|
||||
|
||||
TODOIST_API_BASE = "https://api.todoist.com/api/v1"
|
||||
PROJECT_NAME = "Blumeops"
|
||||
|
||||
# Priority mapping: Todoist API uses 1=normal(p4), 2=moderate(p3), 3=high(p2), 4=urgent(p1)
|
||||
# User wants order: p1, p2, p4, p3 (p3 is backlog, goes last)
|
||||
PRIORITY_LABELS = {4: "p1", 3: "p2", 1: "p4", 2: "p3"}
|
||||
PRIORITY_SORT_ORDER = {4: 1, 3: 2, 1: 3, 2: 4} # Lower = earlier
|
||||
|
||||
|
||||
def get_todoist_token() -> str:
|
||||
"""Retrieve Todoist API token from 1Password."""
|
||||
result = subprocess.run(
|
||||
["op", "read", "op://vg6xf6vvfmoh5hqjjhlhbeoaie/c53h3xnmswhvexa5mntoyvhgpm/credential"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError(f"Failed to get Todoist token from 1Password: {result.stderr}")
|
||||
return result.stdout.strip()
|
||||
|
||||
|
||||
def get_project_id(client: httpx.Client, project_name: str) -> str:
|
||||
"""Find project ID by name."""
|
||||
cursor = None
|
||||
while True:
|
||||
params = {}
|
||||
if cursor:
|
||||
params["cursor"] = cursor
|
||||
response = client.get(f"{TODOIST_API_BASE}/projects", params=params)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
for project in data.get("results", data if isinstance(data, list) else []):
|
||||
if project["name"] == project_name:
|
||||
return project["id"]
|
||||
cursor = data.get("next_cursor") if isinstance(data, dict) else None
|
||||
if not cursor:
|
||||
break
|
||||
|
||||
raise RuntimeError(f"Project '{project_name}' not found in Todoist")
|
||||
|
||||
|
||||
def get_tasks(client: httpx.Client, project_id: str) -> list[dict]:
|
||||
"""Get all tasks for a project."""
|
||||
tasks = []
|
||||
cursor = None
|
||||
while True:
|
||||
params = {"project_id": project_id}
|
||||
if cursor:
|
||||
params["cursor"] = cursor
|
||||
response = client.get(f"{TODOIST_API_BASE}/tasks", params=params)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
tasks.extend(data.get("results", data if isinstance(data, list) else []))
|
||||
cursor = data.get("next_cursor") if isinstance(data, dict) else None
|
||||
if not cursor:
|
||||
break
|
||||
return tasks
|
||||
|
||||
|
||||
def is_due(task: dict) -> bool:
|
||||
"""Check if a task should be displayed based on its due date.
|
||||
|
||||
Tasks without a due date are always shown. Tasks with a due date
|
||||
are only shown when the date is today or in the past.
|
||||
"""
|
||||
due = task.get("due")
|
||||
if due is None:
|
||||
return True
|
||||
due_date = date.fromisoformat(due["date"][:10])
|
||||
return due_date <= date.today()
|
||||
|
||||
|
||||
def days_until_due(task: dict) -> int | None:
|
||||
"""Return signed days offset from today, or None if no due date.
|
||||
|
||||
Negative = days remaining before due (e.g. -2 = due in 2 days).
|
||||
Positive = days past due (overdue). Zero = due today.
|
||||
"""
|
||||
due = task.get("due")
|
||||
if due is None:
|
||||
return None
|
||||
due_date = date.fromisoformat(due["date"][:10])
|
||||
return (date.today() - due_date).days
|
||||
|
||||
|
||||
def recurrence_string(task: dict) -> str | None:
|
||||
"""Return the Todoist natural-language recurrence string, or None.
|
||||
|
||||
Todoist's REST API doesn't expose RFC 5545 RRULE; the natural-language
|
||||
`due.string` (e.g. "every monday", "every 2 weeks") is the terse form.
|
||||
"""
|
||||
due = task.get("due")
|
||||
if due is None or not due.get("is_recurring"):
|
||||
return None
|
||||
return due.get("string")
|
||||
|
||||
|
||||
def sort_tasks(tasks: list[dict]) -> list[dict]:
|
||||
"""Sort by overdue-ness, then priority.
|
||||
|
||||
Most overdue first (largest +N); tasks with no due date come last.
|
||||
Within a given day, tiebreaker is the custom priority order p1, p2, p4, p3.
|
||||
"""
|
||||
|
||||
def key(task: dict) -> tuple[int, int, int]:
|
||||
days = days_until_due(task)
|
||||
no_due = 1 if days is None else 0
|
||||
days_key = -(days if days is not None else 0) # descending
|
||||
return (no_due, days_key, PRIORITY_SORT_ORDER.get(task["priority"], 5))
|
||||
|
||||
return sorted(tasks, key=key)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
console = Console()
|
||||
|
||||
# Get API token
|
||||
try:
|
||||
token = get_todoist_token()
|
||||
except RuntimeError as e:
|
||||
console.print(f"[red]Error:[/red] {e}")
|
||||
return 1
|
||||
|
||||
# Create HTTP client with auth header
|
||||
with httpx.Client(headers={"Authorization": f"Bearer {token}"}) as client:
|
||||
# Find project
|
||||
try:
|
||||
project_id = get_project_id(client, PROJECT_NAME)
|
||||
except RuntimeError as e:
|
||||
console.print(f"[red]Error:[/red] {e}")
|
||||
return 1
|
||||
|
||||
# Get, filter, and sort tasks
|
||||
tasks = get_tasks(client, project_id)
|
||||
tasks = [t for t in tasks if is_due(t)]
|
||||
sorted_tasks = sort_tasks(tasks)
|
||||
|
||||
if not sorted_tasks:
|
||||
console.print("No tasks found in Blumeops project")
|
||||
return 0
|
||||
|
||||
# Display tasks
|
||||
console.print(f"[bold]Blumeops Tasks[/bold] ({len(sorted_tasks)} tasks)")
|
||||
console.print("=" * 40)
|
||||
console.print()
|
||||
|
||||
for task in sorted_tasks:
|
||||
priority = task["priority"]
|
||||
label = PRIORITY_LABELS.get(priority, "p?")
|
||||
content = task["content"]
|
||||
description = task.get("description", "")
|
||||
|
||||
# Header line with priority and content
|
||||
header = Text()
|
||||
header.append(f"[{label}]", style="bold")
|
||||
header.append(f" {content}")
|
||||
|
||||
meta = []
|
||||
days = days_until_due(task)
|
||||
if days is not None:
|
||||
if days == 0:
|
||||
meta.append("due today")
|
||||
elif days > 0:
|
||||
meta.append(f"{days}d overdue")
|
||||
else:
|
||||
meta.append(f"due in {-days}d")
|
||||
recurrence = recurrence_string(task)
|
||||
if recurrence:
|
||||
meta.append(f"↻ {recurrence}")
|
||||
if meta:
|
||||
header.append(f" ({', '.join(meta)})", style="dim")
|
||||
console.print(header)
|
||||
|
||||
# Description indented (escape rich markup to preserve brackets)
|
||||
if description:
|
||||
for line in description.split("\n"):
|
||||
console.print(f" {escape(line)}", style="dim")
|
||||
|
||||
console.print()
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Loading…
Add table
Add a link
Reference in a new issue