taskcheck 1.6.1__tar.gz → 1.7.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {taskcheck-1.6.1 → taskcheck-1.7.0}/PKG-INFO +11 -2
- taskcheck-1.6.1/taskcheck.egg-info/PKG-INFO → taskcheck-1.7.0/README.md +9 -15
- {taskcheck-1.6.1 → taskcheck-1.7.0}/pyproject.toml +2 -2
- {taskcheck-1.6.1 → taskcheck-1.7.0}/taskcheck/__main__.py +2 -2
- {taskcheck-1.6.1 → taskcheck-1.7.0}/taskcheck/dry_run.py +16 -1
- {taskcheck-1.6.1 → taskcheck-1.7.0}/taskcheck/install.py +2 -2
- {taskcheck-1.6.1 → taskcheck-1.7.0}/taskcheck/parallel.py +7 -9
- {taskcheck-1.6.1 → taskcheck-1.7.0}/taskcheck/report.py +50 -56
- taskcheck-1.6.1/README.md → taskcheck-1.7.0/taskcheck.egg-info/PKG-INFO +24 -0
- {taskcheck-1.6.1 → taskcheck-1.7.0}/taskcheck.egg-info/SOURCES.txt +2 -0
- taskcheck-1.7.0/tests/test_dry_run.py +21 -0
- {taskcheck-1.6.1 → taskcheck-1.7.0}/tests/test_parallel.py +19 -0
- {taskcheck-1.6.1 → taskcheck-1.7.0}/tests/test_report.py +2 -3
- taskcheck-1.7.0/tests/test_report_columns.py +29 -0
- {taskcheck-1.6.1 → taskcheck-1.7.0}/LICENSE +0 -0
- {taskcheck-1.6.1 → taskcheck-1.7.0}/setup.cfg +0 -0
- {taskcheck-1.6.1 → taskcheck-1.7.0}/taskcheck/agent_blocks.py +0 -0
- {taskcheck-1.6.1 → taskcheck-1.7.0}/taskcheck/common.py +0 -0
- {taskcheck-1.6.1 → taskcheck-1.7.0}/taskcheck/google_calendar.py +0 -0
- {taskcheck-1.6.1 → taskcheck-1.7.0}/taskcheck/ical.py +0 -0
- {taskcheck-1.6.1 → taskcheck-1.7.0}/taskcheck/theme.py +0 -0
- {taskcheck-1.6.1 → taskcheck-1.7.0}/taskcheck/timeline.py +0 -0
- {taskcheck-1.6.1 → taskcheck-1.7.0}/taskcheck/undo.py +0 -0
- {taskcheck-1.6.1 → taskcheck-1.7.0}/taskcheck.egg-info/dependency_links.txt +0 -0
- {taskcheck-1.6.1 → taskcheck-1.7.0}/taskcheck.egg-info/entry_points.txt +0 -0
- {taskcheck-1.6.1 → taskcheck-1.7.0}/taskcheck.egg-info/requires.txt +0 -0
- {taskcheck-1.6.1 → taskcheck-1.7.0}/taskcheck.egg-info/top_level.txt +0 -0
- {taskcheck-1.6.1 → taskcheck-1.7.0}/tests/test_agent_blocks.py +0 -0
- {taskcheck-1.6.1 → taskcheck-1.7.0}/tests/test_agent_scheduler_integration.py +0 -0
- {taskcheck-1.6.1 → taskcheck-1.7.0}/tests/test_common.py +0 -0
- {taskcheck-1.6.1 → taskcheck-1.7.0}/tests/test_google_calendar.py +0 -0
- {taskcheck-1.6.1 → taskcheck-1.7.0}/tests/test_ical.py +0 -0
- {taskcheck-1.6.1 → taskcheck-1.7.0}/tests/test_install.py +0 -0
- {taskcheck-1.6.1 → taskcheck-1.7.0}/tests/test_main.py +0 -0
- {taskcheck-1.6.1 → taskcheck-1.7.0}/tests/test_timeline.py +0 -0
- {taskcheck-1.6.1 → taskcheck-1.7.0}/tests/test_undo.py +0 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: taskcheck
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.7.0
|
|
4
4
|
Summary: Automatic scheduler for taskwarrior
|
|
5
|
-
Requires-Python: >=3.
|
|
5
|
+
Requires-Python: >=3.12
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
7
7
|
Requires-Dist: appdirs>=1.4.4
|
|
8
8
|
Requires-Dist: google-api-python-client>=2.198.0
|
|
@@ -114,6 +114,11 @@ Create a preview with `taskcheck --schedule --dry-run`. It does not update Taskw
|
|
|
114
114
|
saves the generated schedule to the Taskwarrior data directory. Reuse it with `taskcheck --report
|
|
115
115
|
today --dry-run` or `taskcheck --timeline 1w --dry-run`.
|
|
116
116
|
|
|
117
|
+
Report columns use the labels from `uda.<attribute>.label` in the active Taskwarrior configuration.
|
|
118
|
+
Reports show a robot icon beside the task ID for tasks tagged `agent`.
|
|
119
|
+
Dry-run schedules retain custom task attributes, including `agent_weight`.
|
|
120
|
+
When you load an older preview, Taskcheck fills missing task attributes from Taskwarrior and keeps the saved schedule.
|
|
121
|
+
|
|
117
122
|
Timeline rows follow dotted project levels such as `work.client`. Without `--zoom`, the most
|
|
118
123
|
detailed view that fits the terminal width is selected: day, then week, then month. The track uses
|
|
119
124
|
`▶` for a start, `━` for work, `◀` for an end, and `◆` for a deadline. Thin, heavy, and double rules
|
|
@@ -148,6 +153,10 @@ Before writing a schedule, taskcheck backs up the affected tasks' `scheduled`, `
|
|
|
148
153
|
|
|
149
154
|
If a task would finish after its due date, taskcheck retries with a total-urgency override. Each retry moves the task immediately above the task with the smallest greater urgency by adding `urgency_epsilon` (default: `0.1`). At rank 1, it adds epsilon for up to `max_top_urgency_rounds` retries (default: `10`), then tries once at urgency `1000`. If the task is still late, taskcheck stops raising it and optimizes the other overdue tasks.
|
|
150
155
|
|
|
156
|
+
Each task gets at most `N - 1 + max_top_urgency_rounds` priority increases before the final retry at urgency `1000`.
|
|
157
|
+
Here, `N` is the number of tasks in the schedule.
|
|
158
|
+
This limit stops repeated retries when tasks with competing deadlines keep overtaking each other.
|
|
159
|
+
|
|
151
160
|
## Tips and Tricks
|
|
152
161
|
|
|
153
162
|
- You can exclude a task from being scheduled by removing the `time_map` or `estimated` attributes.
|
|
@@ -1,18 +1,3 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: taskcheck
|
|
3
|
-
Version: 1.6.1
|
|
4
|
-
Summary: Automatic scheduler for taskwarrior
|
|
5
|
-
Requires-Python: >=3.11
|
|
6
|
-
Description-Content-Type: text/markdown
|
|
7
|
-
Requires-Dist: appdirs>=1.4.4
|
|
8
|
-
Requires-Dist: google-api-python-client>=2.198.0
|
|
9
|
-
Requires-Dist: google-auth>=2.56.2
|
|
10
|
-
Requires-Dist: google-auth-oauthlib>=1.2.2
|
|
11
|
-
Requires-Dist: icalendar>=6.0.1
|
|
12
|
-
Requires-Dist: random-unicode-emoji>=2.8
|
|
13
|
-
Requires-Dist: requests>=2.32.3
|
|
14
|
-
Requires-Dist: rich>=13.9.4
|
|
15
|
-
|
|
16
1
|
A Taskwarrior scheduler for people who want a realistic plan, not a manual to-do list.
|
|
17
2
|
It turns tasks, working hours, and calendar blocks into an actionable schedule — then keeps due dates visible.
|
|
18
3
|
|
|
@@ -114,6 +99,11 @@ Create a preview with `taskcheck --schedule --dry-run`. It does not update Taskw
|
|
|
114
99
|
saves the generated schedule to the Taskwarrior data directory. Reuse it with `taskcheck --report
|
|
115
100
|
today --dry-run` or `taskcheck --timeline 1w --dry-run`.
|
|
116
101
|
|
|
102
|
+
Report columns use the labels from `uda.<attribute>.label` in the active Taskwarrior configuration.
|
|
103
|
+
Reports show a robot icon beside the task ID for tasks tagged `agent`.
|
|
104
|
+
Dry-run schedules retain custom task attributes, including `agent_weight`.
|
|
105
|
+
When you load an older preview, Taskcheck fills missing task attributes from Taskwarrior and keeps the saved schedule.
|
|
106
|
+
|
|
117
107
|
Timeline rows follow dotted project levels such as `work.client`. Without `--zoom`, the most
|
|
118
108
|
detailed view that fits the terminal width is selected: day, then week, then month. The track uses
|
|
119
109
|
`▶` for a start, `━` for work, `◀` for an end, and `◆` for a deadline. Thin, heavy, and double rules
|
|
@@ -148,6 +138,10 @@ Before writing a schedule, taskcheck backs up the affected tasks' `scheduled`, `
|
|
|
148
138
|
|
|
149
139
|
If a task would finish after its due date, taskcheck retries with a total-urgency override. Each retry moves the task immediately above the task with the smallest greater urgency by adding `urgency_epsilon` (default: `0.1`). At rank 1, it adds epsilon for up to `max_top_urgency_rounds` retries (default: `10`), then tries once at urgency `1000`. If the task is still late, taskcheck stops raising it and optimizes the other overdue tasks.
|
|
150
140
|
|
|
141
|
+
Each task gets at most `N - 1 + max_top_urgency_rounds` priority increases before the final retry at urgency `1000`.
|
|
142
|
+
Here, `N` is the number of tasks in the schedule.
|
|
143
|
+
This limit stops repeated retries when tasks with competing deadlines keep overtaking each other.
|
|
144
|
+
|
|
151
145
|
## Tips and Tricks
|
|
152
146
|
|
|
153
147
|
- You can exclude a task from being scheduled by removing the `time_map` or `estimated` attributes.
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "taskcheck"
|
|
3
|
-
version = "1.
|
|
3
|
+
version = "1.7.0"
|
|
4
4
|
description = "Automatic scheduler for taskwarrior"
|
|
5
5
|
readme = "README.md"
|
|
6
|
-
requires-python = ">=3.
|
|
6
|
+
requires-python = ">=3.12"
|
|
7
7
|
dependencies = [
|
|
8
8
|
"appdirs>=1.4.4",
|
|
9
9
|
"google-api-python-client>=2.198.0",
|
|
@@ -102,8 +102,8 @@ def load_config():
|
|
|
102
102
|
},
|
|
103
103
|
"report": {
|
|
104
104
|
"include_unplanned": true,
|
|
105
|
-
"
|
|
106
|
-
"
|
|
105
|
+
"attributes": ["id", "project", "description", "scheduling_hours"],
|
|
106
|
+
"attributes_unplanned": ["id", "project", "description"],
|
|
107
107
|
"emoji_keywords": {},
|
|
108
108
|
...
|
|
109
109
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import json
|
|
2
|
+
import subprocess
|
|
2
3
|
from pathlib import Path
|
|
3
4
|
|
|
4
5
|
from taskcheck.undo import get_task_data_dir
|
|
6
|
+
from taskcheck.common import get_task_env
|
|
5
7
|
|
|
6
8
|
|
|
7
9
|
def get_dry_run_path(taskrc=None):
|
|
@@ -19,4 +21,17 @@ def load_dry_run_results(taskrc=None):
|
|
|
19
21
|
path = get_dry_run_path(taskrc)
|
|
20
22
|
if not path.exists():
|
|
21
23
|
return None
|
|
22
|
-
|
|
24
|
+
results = json.loads(path.read_text())
|
|
25
|
+
if not results:
|
|
26
|
+
return results
|
|
27
|
+
response = subprocess.run(
|
|
28
|
+
["task", "export"], capture_output=True, text=True,
|
|
29
|
+
env=get_task_env(taskrc),
|
|
30
|
+
)
|
|
31
|
+
current_tasks = {
|
|
32
|
+
task["uuid"]: task for task in json.loads(response.stdout)
|
|
33
|
+
}
|
|
34
|
+
return [
|
|
35
|
+
{**current_tasks.get(task.get("uuid"), {}), **task}
|
|
36
|
+
for task in results
|
|
37
|
+
]
|
|
@@ -20,8 +20,8 @@ n_undos = 1
|
|
|
20
20
|
|
|
21
21
|
[report]
|
|
22
22
|
include_unplanned = true
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
attributes = ["id", "project", "description", "scheduling_hours"]
|
|
24
|
+
attributes_unplanned = ["id", "project", "description"]
|
|
25
25
|
# unplanned_max_due = "30d"
|
|
26
26
|
emoji_keywords = {}
|
|
27
27
|
|
|
@@ -58,7 +58,7 @@ def advance_urgency_override(
|
|
|
58
58
|
for task_uuid in overdue_uuids:
|
|
59
59
|
state = adjustment_states.setdefault(
|
|
60
60
|
task_uuid,
|
|
61
|
-
{"top_rounds": 0, "fallback_used": False, "exhausted": False},
|
|
61
|
+
{"top_rounds": 0, "rounds": 0, "fallback_used": False, "exhausted": False},
|
|
62
62
|
)
|
|
63
63
|
if state["exhausted"]:
|
|
64
64
|
continue
|
|
@@ -72,7 +72,10 @@ def advance_urgency_override(
|
|
|
72
72
|
for uuid in task_info
|
|
73
73
|
if uuid != task_uuid and effective_urgency(uuid) > current_urgency
|
|
74
74
|
]
|
|
75
|
-
if
|
|
75
|
+
if state["rounds"] >= len(task_info) - 1 + max_top_rounds:
|
|
76
|
+
urgency_overrides[task_uuid] = 1000.0
|
|
77
|
+
state["fallback_used"] = True
|
|
78
|
+
elif greater_urgencies:
|
|
76
79
|
urgency_overrides[task_uuid] = min(greater_urgencies) + epsilon
|
|
77
80
|
elif state["top_rounds"] < max_top_rounds:
|
|
78
81
|
urgency_overrides[task_uuid] = current_urgency + epsilon
|
|
@@ -80,6 +83,7 @@ def advance_urgency_override(
|
|
|
80
83
|
else:
|
|
81
84
|
urgency_overrides[task_uuid] = 1000.0
|
|
82
85
|
state["fallback_used"] = True
|
|
86
|
+
state["rounds"] += 1
|
|
83
87
|
return task_uuid
|
|
84
88
|
return None
|
|
85
89
|
|
|
@@ -289,13 +293,7 @@ def check_tasks_parallel(
|
|
|
289
293
|
scheduling_note += f"{date_str} - {hours_to_pdth(hours)}\n"
|
|
290
294
|
|
|
291
295
|
task_result = {
|
|
292
|
-
|
|
293
|
-
"uuid": task["uuid"],
|
|
294
|
-
"description": task["description"],
|
|
295
|
-
"project": task.get("project", ""),
|
|
296
|
-
"urgency": task.get("urgency", 0),
|
|
297
|
-
"estimated": task["estimated"],
|
|
298
|
-
"due": task.get("due", ""),
|
|
296
|
+
**task,
|
|
299
297
|
"scheduled": start_date,
|
|
300
298
|
"completion_date": end_date,
|
|
301
299
|
"scheduling": scheduling_note.strip(),
|
|
@@ -198,16 +198,10 @@ def get_tasks(config, tasks, year, month, day):
|
|
|
198
198
|
due = due - datetime.today()
|
|
199
199
|
valid_tasks.append(
|
|
200
200
|
{
|
|
201
|
-
|
|
202
|
-
"project": task.get("project", ""),
|
|
203
|
-
"description": task["description"],
|
|
204
|
-
"urgency": task["urgency"],
|
|
201
|
+
**task,
|
|
205
202
|
"scheduling_day": f"{year}-{month}-{day}",
|
|
206
203
|
"scheduling_hours": m.group(1),
|
|
207
|
-
|
|
208
|
-
attr: task.get(attr, "")
|
|
209
|
-
for attr in config.get("additional_attributes", [])
|
|
210
|
-
},
|
|
204
|
+
"tags": task.get("tags", []),
|
|
211
205
|
}
|
|
212
206
|
)
|
|
213
207
|
valid_tasks = sorted(valid_tasks, key=lambda x: x["urgency"], reverse=True)
|
|
@@ -287,7 +281,22 @@ def get_unplanned_tasks(config, tasks, taskrc=None):
|
|
|
287
281
|
|
|
288
282
|
|
|
289
283
|
def generate_report(config, constraint, verbose=False, force_update=False, taskrc=None, scheduling_results=None):
|
|
290
|
-
|
|
284
|
+
from taskcheck.common import get_task_env
|
|
285
|
+
|
|
286
|
+
report_config = dict(config["report"])
|
|
287
|
+
result = subprocess.run(
|
|
288
|
+
["task", "_show"],
|
|
289
|
+
capture_output=True,
|
|
290
|
+
text=True,
|
|
291
|
+
env=get_task_env(taskrc),
|
|
292
|
+
)
|
|
293
|
+
report_config["attribute_labels"] = {
|
|
294
|
+
key[4:-6]: value
|
|
295
|
+
for line in result.stdout.splitlines()
|
|
296
|
+
if "=" in line
|
|
297
|
+
for key, value in [line.split("=", 1)]
|
|
298
|
+
if key.startswith("uda.") and key.endswith(".label") and value
|
|
299
|
+
}
|
|
291
300
|
theme = detect_theme(config=config.get("timeline", {}))
|
|
292
301
|
console = Console()
|
|
293
302
|
|
|
@@ -347,32 +356,38 @@ def display_tasks_table(console, config, tasks, theme="dark"):
|
|
|
347
356
|
|
|
348
357
|
def build_tasks_table(config, tasks, theme="dark"):
|
|
349
358
|
"""Build a Rich table for displaying tasks."""
|
|
359
|
+
return _build_report_table(
|
|
360
|
+
config, tasks, config.get("attributes", ["id", "project", "description", "scheduling_hours"]), theme
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def _build_report_table(config, tasks, attributes, theme):
|
|
350
365
|
header_style = "bold cyan" if theme == "dark" else "bold blue"
|
|
351
366
|
table = Table(show_header=True, header_style=header_style)
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
for attr in config.get("additional_attributes", []):
|
|
357
|
-
table.add_column(attr.capitalize(), justify="right")
|
|
358
|
-
|
|
359
|
-
for task in tasks:
|
|
360
|
-
task_id = f"[bold green]#{task['id']}[/bold green]"
|
|
361
|
-
project = task.get("project", "")
|
|
362
|
-
description = Text(task["description"], style="italic")
|
|
363
|
-
hours = f"[yellow]{task['scheduling_hours']}[/yellow]"
|
|
364
|
-
emoji = get_task_emoji(config, task)
|
|
365
|
-
|
|
366
|
-
table.add_row(
|
|
367
|
-
f"{emoji} {task_id}",
|
|
368
|
-
project,
|
|
369
|
-
description,
|
|
370
|
-
hours,
|
|
371
|
-
*[
|
|
372
|
-
tostring(task.get(attr, ""))
|
|
373
|
-
for attr in config.get("additional_attributes", [])
|
|
374
|
-
],
|
|
367
|
+
labels = {"id": "Task", "scheduling_hours": "Time"}
|
|
368
|
+
for attr in attributes:
|
|
369
|
+
label = config.get("attribute_labels", {}).get(
|
|
370
|
+
attr, labels.get(attr, attr.capitalize())
|
|
375
371
|
)
|
|
372
|
+
if attr in ("id", "project"):
|
|
373
|
+
table.add_column(label, style="dim", width=12)
|
|
374
|
+
else:
|
|
375
|
+
table.add_column(label, justify="left" if attr == "description" else "right")
|
|
376
|
+
if not attributes:
|
|
377
|
+
return table
|
|
378
|
+
for task in tasks:
|
|
379
|
+
cells = []
|
|
380
|
+
for attr in attributes:
|
|
381
|
+
if attr == "id":
|
|
382
|
+
icon = " 🤖" if "agent" in task.get("tags", []) else ""
|
|
383
|
+
cells.append(f"{get_task_emoji(config, task)} [bold green]#{task['id']}[/bold green]{icon}")
|
|
384
|
+
elif attr == "description":
|
|
385
|
+
cells.append(Text(task.get(attr, ""), style="italic"))
|
|
386
|
+
elif attr == "scheduling_hours":
|
|
387
|
+
cells.append(f"[yellow]{task.get(attr, '')}[/yellow]")
|
|
388
|
+
else:
|
|
389
|
+
cells.append(tostring(task.get(attr, "")))
|
|
390
|
+
table.add_row(*cells)
|
|
376
391
|
return table
|
|
377
392
|
|
|
378
393
|
|
|
@@ -417,27 +432,6 @@ def display_unplanned_tasks(console, config, tasks, theme="dark"):
|
|
|
417
432
|
|
|
418
433
|
def build_unplanned_tasks_table(config, tasks, theme="dark"):
|
|
419
434
|
"""Build a Rich table for displaying unplanned tasks."""
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
table.add_column("Project", style="dim", width=12)
|
|
424
|
-
table.add_column("Description")
|
|
425
|
-
for attr in config.get("additional_attributes_unplanned", []):
|
|
426
|
-
table.add_column(attr.capitalize(), justify="right")
|
|
427
|
-
|
|
428
|
-
for task in tasks:
|
|
429
|
-
task_id = f"[bold green]#{task['id']}[/bold green]"
|
|
430
|
-
project = task.get("project", "")
|
|
431
|
-
description = Text(task["description"], style="italic")
|
|
432
|
-
emoji = get_task_emoji(config, task)
|
|
433
|
-
|
|
434
|
-
table.add_row(
|
|
435
|
-
f"{emoji} {task_id}",
|
|
436
|
-
project,
|
|
437
|
-
description,
|
|
438
|
-
*[
|
|
439
|
-
tostring(task.get(attr, ""))
|
|
440
|
-
for attr in config.get("additional_attributes_unplanned", [])
|
|
441
|
-
],
|
|
442
|
-
)
|
|
443
|
-
return table
|
|
435
|
+
return _build_report_table(
|
|
436
|
+
config, tasks, config.get("attributes_unplanned", ["id", "project", "description"]), theme
|
|
437
|
+
)
|
|
@@ -1,3 +1,18 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: taskcheck
|
|
3
|
+
Version: 1.7.0
|
|
4
|
+
Summary: Automatic scheduler for taskwarrior
|
|
5
|
+
Requires-Python: >=3.12
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: appdirs>=1.4.4
|
|
8
|
+
Requires-Dist: google-api-python-client>=2.198.0
|
|
9
|
+
Requires-Dist: google-auth>=2.56.2
|
|
10
|
+
Requires-Dist: google-auth-oauthlib>=1.2.2
|
|
11
|
+
Requires-Dist: icalendar>=6.0.1
|
|
12
|
+
Requires-Dist: random-unicode-emoji>=2.8
|
|
13
|
+
Requires-Dist: requests>=2.32.3
|
|
14
|
+
Requires-Dist: rich>=13.9.4
|
|
15
|
+
|
|
1
16
|
A Taskwarrior scheduler for people who want a realistic plan, not a manual to-do list.
|
|
2
17
|
It turns tasks, working hours, and calendar blocks into an actionable schedule — then keeps due dates visible.
|
|
3
18
|
|
|
@@ -99,6 +114,11 @@ Create a preview with `taskcheck --schedule --dry-run`. It does not update Taskw
|
|
|
99
114
|
saves the generated schedule to the Taskwarrior data directory. Reuse it with `taskcheck --report
|
|
100
115
|
today --dry-run` or `taskcheck --timeline 1w --dry-run`.
|
|
101
116
|
|
|
117
|
+
Report columns use the labels from `uda.<attribute>.label` in the active Taskwarrior configuration.
|
|
118
|
+
Reports show a robot icon beside the task ID for tasks tagged `agent`.
|
|
119
|
+
Dry-run schedules retain custom task attributes, including `agent_weight`.
|
|
120
|
+
When you load an older preview, Taskcheck fills missing task attributes from Taskwarrior and keeps the saved schedule.
|
|
121
|
+
|
|
102
122
|
Timeline rows follow dotted project levels such as `work.client`. Without `--zoom`, the most
|
|
103
123
|
detailed view that fits the terminal width is selected: day, then week, then month. The track uses
|
|
104
124
|
`▶` for a start, `━` for work, `◀` for an end, and `◆` for a deadline. Thin, heavy, and double rules
|
|
@@ -133,6 +153,10 @@ Before writing a schedule, taskcheck backs up the affected tasks' `scheduled`, `
|
|
|
133
153
|
|
|
134
154
|
If a task would finish after its due date, taskcheck retries with a total-urgency override. Each retry moves the task immediately above the task with the smallest greater urgency by adding `urgency_epsilon` (default: `0.1`). At rank 1, it adds epsilon for up to `max_top_urgency_rounds` retries (default: `10`), then tries once at urgency `1000`. If the task is still late, taskcheck stops raising it and optimizes the other overdue tasks.
|
|
135
155
|
|
|
156
|
+
Each task gets at most `N - 1 + max_top_urgency_rounds` priority increases before the final retry at urgency `1000`.
|
|
157
|
+
Here, `N` is the number of tasks in the schedule.
|
|
158
|
+
This limit stops repeated retries when tasks with competing deadlines keep overtaking each other.
|
|
159
|
+
|
|
136
160
|
## Tips and Tricks
|
|
137
161
|
|
|
138
162
|
- You can exclude a task from being scheduled by removing the `time_map` or `estimated` attributes.
|
|
@@ -22,11 +22,13 @@ taskcheck.egg-info/top_level.txt
|
|
|
22
22
|
tests/test_agent_blocks.py
|
|
23
23
|
tests/test_agent_scheduler_integration.py
|
|
24
24
|
tests/test_common.py
|
|
25
|
+
tests/test_dry_run.py
|
|
25
26
|
tests/test_google_calendar.py
|
|
26
27
|
tests/test_ical.py
|
|
27
28
|
tests/test_install.py
|
|
28
29
|
tests/test_main.py
|
|
29
30
|
tests/test_parallel.py
|
|
30
31
|
tests/test_report.py
|
|
32
|
+
tests/test_report_columns.py
|
|
31
33
|
tests/test_timeline.py
|
|
32
34
|
tests/test_undo.py
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from unittest.mock import patch
|
|
3
|
+
|
|
4
|
+
from taskcheck.dry_run import load_dry_run_results
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def test_old_preview_recovers_missing_attributes_without_changing_schedule(tmp_path):
|
|
8
|
+
path = tmp_path / "dry-run.json"
|
|
9
|
+
cached = [{"uuid": "agent", "scheduling": "2026-11-10 - PT2H"}]
|
|
10
|
+
path.write_text(json.dumps(cached))
|
|
11
|
+
current = [{"uuid": "agent", "agent_weight": 25,
|
|
12
|
+
"scheduling": "2026-11-11 - PT1H"}]
|
|
13
|
+
with patch("taskcheck.dry_run.get_dry_run_path", return_value=path), patch(
|
|
14
|
+
"taskcheck.dry_run.subprocess.run"
|
|
15
|
+
) as export:
|
|
16
|
+
export.return_value.stdout = json.dumps(current)
|
|
17
|
+
result = load_dry_run_results()
|
|
18
|
+
assert result is not None
|
|
19
|
+
assert result[0]["agent_weight"] == 25
|
|
20
|
+
assert result[0]["scheduling"] == "2026-11-10 - PT2H"
|
|
21
|
+
assert json.loads(path.read_text()) == cached
|
|
@@ -388,6 +388,25 @@ class TestMainSchedulingFunction:
|
|
|
388
388
|
|
|
389
389
|
|
|
390
390
|
class TestAutoAdjustUrgency:
|
|
391
|
+
def test_competing_overdue_tasks_exhaust_retries(self):
|
|
392
|
+
task_info = {
|
|
393
|
+
uuid: {"task": {"uuid": uuid, "urgency": urgency}, "urgency": urgency}
|
|
394
|
+
for uuid, urgency in (("a", 1.0), ("b", 2.0))
|
|
395
|
+
}
|
|
396
|
+
overrides = {}
|
|
397
|
+
states = {}
|
|
398
|
+
# Resolving one deadline can make the other task late on the next pass.
|
|
399
|
+
for index in range(12):
|
|
400
|
+
overdue = [task_info["a" if index % 2 == 0 else "b"]["task"]]
|
|
401
|
+
advance_urgency_override(
|
|
402
|
+
overdue, task_info, overrides, states, epsilon=0.1, max_top_rounds=2
|
|
403
|
+
)
|
|
404
|
+
assert advance_urgency_override(
|
|
405
|
+
[info["task"] for info in task_info.values()],
|
|
406
|
+
task_info, overrides, states, epsilon=0.1, max_top_rounds=2,
|
|
407
|
+
) is None
|
|
408
|
+
assert overrides == {"a": 1000.0, "b": 1000.0}
|
|
409
|
+
|
|
391
410
|
def test_urgency_override_moves_up_one_rank_then_uses_bounded_top_rounds(self):
|
|
392
411
|
task_info = {
|
|
393
412
|
"t1": {"task": {"uuid": "t1"}, "urgency": 1.0},
|
|
@@ -246,7 +246,7 @@ class TestReportGeneration:
|
|
|
246
246
|
@patch('taskcheck.report.fetch_tasks')
|
|
247
247
|
@patch('taskcheck.report.get_days_in_constraint')
|
|
248
248
|
@patch('taskcheck.report.get_unplanned_tasks')
|
|
249
|
-
def test_generate_report_with_unplanned(self, mock_unplanned, mock_days, mock_fetch, sample_config, test_taskrc):
|
|
249
|
+
def test_generate_report_with_unplanned(self, mock_unplanned, mock_days, mock_fetch, sample_config, test_taskrc, capsys):
|
|
250
250
|
mock_fetch.return_value = []
|
|
251
251
|
mock_days.return_value = [(2023, 12, 5)]
|
|
252
252
|
mock_unplanned.return_value = [
|
|
@@ -260,7 +260,7 @@ class TestReportGeneration:
|
|
|
260
260
|
|
|
261
261
|
generate_report(sample_config, "today", verbose=True, taskrc=test_taskrc)
|
|
262
262
|
|
|
263
|
-
|
|
263
|
+
assert "Unplanned task" in capsys.readouterr().out
|
|
264
264
|
|
|
265
265
|
@patch('taskcheck.report.display_unplanned_tasks')
|
|
266
266
|
@patch('taskcheck.report.get_unplanned_tasks')
|
|
@@ -278,5 +278,4 @@ class TestReportGeneration:
|
|
|
278
278
|
generate_report(sample_config, "today", verbose=False, taskrc=test_taskrc, scheduling_results=scheduling_results)
|
|
279
279
|
|
|
280
280
|
mock_fetch.assert_called_once_with(test_taskrc)
|
|
281
|
-
mock_unplanned.assert_called_once_with(sample_config["report"], mock_fetch.return_value, taskrc=test_taskrc)
|
|
282
281
|
mock_display.assert_called_once()
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from io import StringIO
|
|
2
|
+
|
|
3
|
+
from rich.console import Console
|
|
4
|
+
|
|
5
|
+
from taskcheck.report import build_tasks_table, build_unplanned_tasks_table, get_tasks
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_explicit_columns_replace_defaults_in_order():
|
|
9
|
+
task = {"id": 2, "description": "Agent work", "urgency": 1,
|
|
10
|
+
"tags": ["agent"], "agent_weight": 0.34,
|
|
11
|
+
"scheduling": "2026-11-10 - PT2H"}
|
|
12
|
+
config = {"attributes": ["agent_weight", "description"],
|
|
13
|
+
"attribute_labels": {"agent_weight": "Attention"}}
|
|
14
|
+
rows = get_tasks(config, [task], 2026, 11, 10)
|
|
15
|
+
table = build_tasks_table(config, rows)
|
|
16
|
+
assert [column.header for column in table.columns] == ["Attention", "Description"]
|
|
17
|
+
stream = StringIO()
|
|
18
|
+
Console(file=stream, width=100).print(table)
|
|
19
|
+
assert "0.34" in stream.getvalue()
|
|
20
|
+
assert "Agent work" in stream.getvalue()
|
|
21
|
+
assert "#2" not in stream.getvalue()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_unplanned_columns_are_independent_and_empty_means_none():
|
|
25
|
+
config = {"attributes": [], "attributes_unplanned": ["description"]}
|
|
26
|
+
tasks = [{"id": 1, "description": "Unplanned work"}]
|
|
27
|
+
assert build_tasks_table(config, tasks).columns == []
|
|
28
|
+
table = build_unplanned_tasks_table(config, tasks)
|
|
29
|
+
assert [column.header for column in table.columns] == ["Description"]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|