Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions bin/tasker
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,16 @@ class TaskerCLI < Thor
puts "Error: #{e.message}"
end

desc 'week LIST_ID', 'Generate Obsidian week plan with category/project grouping via Claude'
def week(list_id)
require_relative '../lib/week_planner'
require_relative '../lib/settings_manager'
planner = WeekPlanner.new(client, SettingsManager.new)
planner.generate_week(list_id)
rescue => e
puts "Error: #{e.message}"
end

private

def client
Expand Down
331 changes: 331 additions & 0 deletions docs/superpowers/specs/2026-03-24-week-command-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,331 @@
# Week Command: Obsidian Weekly Task Organization

## Problem

Weekly task planning requires manually grouping ~80 groomed Google Tasks into categories and projects inside Obsidian Week files. This is tedious and time-consuming. The data (categories, priorities) already exists in Google Tasks notes but the organizational structure (projects) must be inferred.

## Solution

New `week` command that fetches the current week's tasks, groups by category, uses Claude CLI to suggest project groupings (with vault context), and writes a structured Obsidian Week file.

## Output Format

```markdown
→23 to 29 March 2026 ← preserved from existing file
[[existing wikilinks]] ← preserved from existing file

# 🧩 Product

## [[Fix the billing]]
- [ ] 2026-03-24 🔥 User facing UI billing %%encoded_id%%
- [ ] 2026-03-24 🟢 Fix problem with visibility %%encoded_id%%

## [[Play with Shells POC]]
- [ ] 2026-03-24 🟢 prepare self-demo button %%encoded_id%%

# 📈 Business

## [[Capacity planning]]
- [ ] 2026-03-24 🔥 review RI purchases %%encoded_id%%
- [ ] 2026-03-24 🟠 Generate a doc with RI purchases %%encoded_id%%

# No Category

## [[Misc Admin]]
- [ ] 2026-03-24 some uncategorized task %%encoded_id%%
```

## Data Flow

```
Google Tasks API (fetch week's uncompleted tasks)
Group by category (emoji extraction — existing method)
Scan Obsidian vault (all .md files → extract note titles)
Parallel: spawn `claude -p` per category (Ruby threads)
│ Context: task titles + all vault note names
│ Returns JSON: {"Project Name": ["task title", ...]}
Present suggestions per category
Interactive: accept / move individual tasks / create new projects
Generate markdown (sorted by priority within each project)
Write Week N.md (preserve header, replace groomed section)
Create missing TaskNotes (backlink to [[Week N]])
```

## Claude CLI Integration

Shell out to `claude -p` (user's existing OAuth auth). One call per category, all calls run in parallel via Ruby threads.

### Prompt Template

```
You are organizing tasks for weekly planning. Given these tasks in the "{category}"
category, group them into logical projects.

Existing notes in the Obsidian vault (prefer reusing these names when tasks match):
{newline-separated list of all vault note titles}

Tasks to organize:
1. {task title}
2. {task title}
...

Return ONLY valid JSON, no markdown fences:
{"Project Name": ["task title exactly as given", ...]}

Rules:
- Project names: concise (2-5 words), suitable as Obsidian note titles
- Group related tasks (same initiative, system, or goal)
- Single-task projects are fine if the task doesn't belong elsewhere
- Reuse existing vault note names when tasks clearly relate to them
```

### Response Parsing

1. Run `claude -p "..." --output-format json`
2. Parse outer JSON — Claude CLI returns `{"type":"result", "result":"<text>", ...}`
3. Extract `result` field (string containing the JSON project groupings)
4. Parse inner JSON (the `{"Project Name": ["task", ...]}` structure)
5. Fallback on parse failure: one project per task using task title

### Error Handling for Parallel Calls

- Each thread rescues exceptions independently; failed categories fall back to one-project-per-task
- 30-second timeout per `claude -p` call via `Timeout.timeout(30)`
- If all calls fail, print warning and proceed with manual grouping mode

### Thread Implementation

```ruby
threads = categories.map do |category, tasks|
Thread.new { [category, claude_suggest(category, tasks, vault_notes)] }
end
results = threads.map(&:value).to_h
```

## Interactive Confirmation

Per category, show suggested groupings:

```
============================================================
🧩 Product (8 tasks → 3 projects)
============================================================

1. [[Fix the billing]] (3 tasks)
a. 🔥 User facing UI billing
b. 🟢 Fix problem with visibility
c. 🟢 Add the analysis suite to backoffice

2. [[Play with Shells POC]] (2 tasks)
d. 🟢 prepare self-demo button
e. 🟠 mimic the login page from cloudzero

3. [[Infrastructure Maintenance]] (3 tasks)
f. 🔥 Set storageclaim default
g. 🟢 Onboarding verification check s3
h. 🟠 review how onboarding uses KEDA

Accept? (Y/move/skip)
Y = accept all projects in this category
move = move individual tasks between projects
skip = exclude this entire category from the Week file output
```

### Move Flow

```
Which task? (letter): f
Current project: Infrastructure Maintenance

Available projects:
1. Fix the billing
2. Play with Shells POC
3. Infrastructure Maintenance
4. (new project)

Move to (1-4): 4
New project name: AWS Operations
✅ Moved "Set storageclaim default" → [[AWS Operations]]

Move another? (y/N):
```

## Priority Handling

### Sorting

Tasks sorted within each project by priority weight:
1. 🔥 Hot (weight 4)
2. 🟢 Must (weight 3)
3. 🟠 Nice (weight 2)
4. 🔴 NotNow (weight 1)
5. No priority (weight 0)

### Display

Priority emoji shown on each task line in the output:
```
- [ ] 2026-03-24 🔥 Task title %%id%%
```

### TaskNotes Frontmatter Mapping

| Emoji | Priority | Frontmatter value |
|-------|----------|-------------------|
| 🔥 | Hot | high |
| 🟢 | Must | normal |
| 🟠 | Nice | low |
| 🔴 | NotNow | none |

## File Writing

### Week File

- Path: `{vault_path}/Week {N}.md`
- Week number: `Date#cweek` (ISO week numbering)
- Week boundaries: Monday 00:00 to Sunday 23:59, local timezone
- **Header preservation**: scan file line by line; everything before a line matching `^# ` (H1 heading) is the header. Preserve header, replace everything from first H1 onward.
- **Sentinel**: if file contains `# All tasks groomed`, that line and everything below is replaced.
- If no file: create fresh with date range header + grouped structure.
- **Idempotency**: re-running `week` in the same week regenerates the grouped section while preserving the header. User is warned before overwrite.

### TaskNotes

Created only for projects that don't exist as notes anywhere in the vault.

Path: `{vault_path}/{ProjectName}.md`

```yaml
---
title: {ProjectName}
status: open
priority: normal
scheduled: {monday of the week, YYYY-MM-DD}
dateCreated: {ISO8601 with timezone}
dateModified: {ISO8601 with timezone}
tags:
- task
---

[[Week {N}]]
```

For existing notes: append `[[Week N]]` to the end of the file body (after frontmatter) if the string `[[Week N]]` is not already present anywhere in the file. No other modifications to existing files.

## Vault Context Scanning

Glob `{vault_path}/**/*.md` for all markdown files. Extract titles from filenames (strip path and `.md`). Pass full list to Claude prompt so it can match tasks to existing notes.

## Settings

Add `vault_path` to `~/.tasker/settings.json`:

```json
{
"vault_path": "~/home",
"categories": [...],
"preferred_days": {...}
}
```

Default: `~/home/`. Configurable via `settings` command.

## Files to Create/Modify

| File | Action | Purpose |
|------|--------|---------|
| `lib/week_planner.rb` | CREATE | Core logic class |
| `lib/task_classification.rb` | CREATE | Shared module extracting `extract_category_from_notes`, `extract_priority_from_notes`, `get_priority_weight` from `interactive_mode.rb` |
| `lib/google_tasks_client.rb` | MODIFY | Add `due_min:`, `due_max:` params to `list_tasks` (Google Tasks API supports these) |
| `lib/settings_manager.rb` | MODIFY | Add `vault_path` config + settings menu option |
| `lib/interactive_mode.rb` | MODIFY | Add `week` command routing; include `TaskClassification` module |
| `bin/tasker` | MODIFY | Add Thor `week LIST_ID` command |

### WeekPlanner Class API

```ruby
class WeekPlanner
def initialize(client, settings_manager)
def generate_week(list_id)
def fetch_week_tasks(list_id)
def group_by_category(tasks)
def scan_vault_notes
def suggest_projects_parallel(categories, vault_notes)
def claude_suggest(category, tasks, vault_notes)
def confirm_assignments(category, suggestions)
def generate_markdown(week_num, all_groups)
def write_week_file(week_num, markdown)
def ensure_project_notes(projects, week_num)
end
```

### TaskClassification Module

Extract from `interactive_mode.rb` into `lib/task_classification.rb`:
- `extract_category_from_notes(notes)` (line 1054) — returns `'🧩Product'` (no space)
- `extract_priority_from_notes(notes)` (line 1037) — returns `'🔥Hot'`
- `get_priority_weight` — priority sorting weights

**Category display format**: `extract_category_from_notes` returns `'🧩Product'`. For display in headings, split on first non-emoji char: `emoji = '🧩'`, `name = 'Product'` → `# 🧩 Product`.

Both `WeekPlanner` and `InteractiveMode` include this module.

## Verification

1. `ruby bin/tasker interactive` → select list → `week`
2. Verify Claude CLI spawns per category (check terminal output)
3. Confirm interactive flow: accept, move tasks, create new project
4. Check `~/home/Week {N}.md`: header preserved, grouped structure correct
5. Check new TaskNotes created with correct frontmatter
6. Check existing notes get `[[Week N]]` backlink
7. `ruby bin/tasker week LIST_ID` for console mode
8. Test with `DEBUG=1` to see API calls and Claude prompts

## Task ID Format

Task IDs in `%%...%%` markers are the raw Google Tasks API IDs (already base64-encoded by Google). Example: `%%LXRrQ1Nqc2pHMTZ5MXpzdw%%`. These are passed through as-is from the API response `task.id`.

## Google Tasks API Changes

Add `due_min:` and `due_max:` keyword args to `GoogleTasksClient#list_tasks`:

```ruby
def list_tasks(list_id, show_completed: false, max_results: nil, due_min: nil, due_max: nil)
# ... existing code ...
response = @service.list_tasks(
list_id,
show_completed: show_completed,
max_results: max_results || 100,
page_token: next_page_token,
due_min: due_min&.rfc3339, # Google Tasks API supports dueMin
due_max: due_max&.rfc3339 # Google Tasks API supports dueMax
)
```

This enables `fetch_week_tasks` to query only the relevant date range rather than fetching all tasks and filtering client-side.

## Edge Cases

- **No tasks for the week:** Print message, exit gracefully
- **Claude CLI not installed/no auth:** Print error with setup instructions
- **Claude returns invalid JSON:** Fallback to one project per task, print warning
- **Partial thread failures:** Failed categories fall back individually; successful ones proceed
- **Empty category (all tasks uncategorized):** Group under "# No Category" heading
- **Existing Week file with different format:** Detect header boundary (first `# ` line), preserve above
- **Very large vault (hundreds of notes):** Truncate vault note list in prompt to 200 entries
- **Re-running same week:** Warn user, overwrite grouped section, preserve header
9 changes: 6 additions & 3 deletions lib/google_tasks_client.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
require 'time'
require 'google/apis/tasks_v1'
require 'googleauth'
require 'googleauth/stores/file_token_store'
Expand Down Expand Up @@ -448,18 +449,20 @@ def delete_task_list(list_id)
end
end

def list_tasks(list_id, show_completed: false, max_results: nil)
def list_tasks(list_id, show_completed: false, max_results: nil, due_min: nil, due_max: nil)
ensure_authenticated
handle_api_error do
all_tasks = []
next_page_token = nil

loop do
response = @service.list_tasks(
list_id,
show_completed: show_completed,
max_results: max_results || 100, # Default page size
page_token: next_page_token
page_token: next_page_token,
due_min: due_min&.xmlschema,
due_max: due_max&.xmlschema
)

# Add tasks from this page
Expand Down
Loading