taskflow-git 0.3.0__tar.gz → 0.3.3__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.
- taskflow_git-0.3.3/.taskflow/backlog/0-now.md +5 -0
- taskflow_git-0.3.3/.taskflow/backlog/1-blocked.md +23 -0
- taskflow_git-0.3.3/.taskflow/backlog/2-paused.md +23 -0
- taskflow_git-0.3.3/.taskflow/backlog/3-next.md +5 -0
- taskflow_git-0.3.3/.taskflow/backlog/4-later.md +47 -0
- taskflow_git-0.3.3/.taskflow/backlog/archive/.gitkeep +0 -0
- taskflow_git-0.3.3/.taskflow/backlog/done.md +11 -0
- taskflow_git-0.3.3/.taskflow/changelog/weekly/.gitkeep +0 -0
- taskflow_git-0.3.3/.taskflow.yml +70 -0
- {taskflow_git-0.3.0 → taskflow_git-0.3.3}/PKG-INFO +1 -1
- {taskflow_git-0.3.0 → taskflow_git-0.3.3}/pyproject.toml +2 -2
- {taskflow_git-0.3.0 → taskflow_git-0.3.3}/src/taskflow/cli.py +81 -7
- {taskflow_git-0.3.0 → taskflow_git-0.3.3}/src/taskflow/config.py +9 -9
- {taskflow_git-0.3.0 → taskflow_git-0.3.3}/src/taskflow/setup_cmd.py +27 -66
- {taskflow_git-0.3.0 → taskflow_git-0.3.3}/src/taskflow/tasklib.py +1 -1
- {taskflow_git-0.3.0 → taskflow_git-0.3.3}/taskflow +6 -6
- {taskflow_git-0.3.0 → taskflow_git-0.3.3}/tests/conftest.py +7 -28
- {taskflow_git-0.3.0 → taskflow_git-0.3.3}/tests/test_cli.py +34 -13
- {taskflow_git-0.3.0 → taskflow_git-0.3.3}/tests/test_config.py +2 -3
- {taskflow_git-0.3.0 → taskflow_git-0.3.3}/tests/test_reports.py +5 -5
- {taskflow_git-0.3.0 → taskflow_git-0.3.3}/uv.lock +1 -1
- {taskflow_git-0.3.0 → taskflow_git-0.3.3}/.github/workflows/ci.yml +0 -0
- {taskflow_git-0.3.0 → taskflow_git-0.3.3}/.github/workflows/publish.yml +0 -0
- {taskflow_git-0.3.0 → taskflow_git-0.3.3}/.gitignore +0 -0
- {taskflow_git-0.3.0 → taskflow_git-0.3.3}/README.md +0 -0
- {taskflow_git-0.3.0 → taskflow_git-0.3.3}/coverage.xml +0 -0
- {taskflow_git-0.3.0 → taskflow_git-0.3.3}/src/taskflow/__init__.py +0 -0
- {taskflow_git-0.3.0 → taskflow_git-0.3.3}/src/taskflow/archive.py +0 -0
- {taskflow_git-0.3.0 → taskflow_git-0.3.3}/src/taskflow/reports.py +0 -0
- {taskflow_git-0.3.0 → taskflow_git-0.3.3}/tests/test_archive.py +0 -0
- {taskflow_git-0.3.0 → taskflow_git-0.3.3}/tests/test_tasklib.py +0 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Later
|
|
2
|
+
|
|
3
|
+
Longer-horizon work organised by phase.
|
|
4
|
+
|
|
5
|
+
Promote to next when the time is right:
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
taskflow promote "task text"
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Phase 1 — Foundation
|
|
14
|
+
|
|
15
|
+
> Core work needed to get started.
|
|
16
|
+
|
|
17
|
+
### 🔵 Engineering
|
|
18
|
+
|
|
19
|
+
### 🔴 Operations
|
|
20
|
+
|
|
21
|
+
### 🟡 Product
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Phase 2 — Build
|
|
26
|
+
|
|
27
|
+
> Main execution phase.
|
|
28
|
+
|
|
29
|
+
### 🔵 Engineering
|
|
30
|
+
|
|
31
|
+
### 🔴 Operations
|
|
32
|
+
|
|
33
|
+
### 🟡 Product
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Phase 3 — Growth
|
|
38
|
+
|
|
39
|
+
> Expansion and refinement.
|
|
40
|
+
|
|
41
|
+
### 🔵 Engineering
|
|
42
|
+
|
|
43
|
+
### 🔴 Operations
|
|
44
|
+
|
|
45
|
+
### 🟡 Product
|
|
46
|
+
|
|
47
|
+
---
|
|
File without changes
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Done
|
|
2
|
+
|
|
3
|
+
Completed tasks, appended by `taskflow done`.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Week of 2026-03-30
|
|
8
|
+
|
|
9
|
+
[2026-03-30 18:58:41] done: (Engineering) - fix init worlflow to handle situations when a git repo doesn't exist yet
|
|
10
|
+
[2026-03-30 19:26:09] done: (Engineering) - Fix multi word quoting issues where there are tickes and other quote-like chars
|
|
11
|
+
[2026-03-30 19:26:17] done: (Engineering) - move init defaults to `.taskflow` dir
|
|
File without changes
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# .taskflow.yml
|
|
2
|
+
# Run `taskflow setup` after editing to regenerate your backlog.
|
|
3
|
+
# All sections except categories and phases are optional.
|
|
4
|
+
|
|
5
|
+
# ---------------------------------------------------------------------------
|
|
6
|
+
# States — file paths and terminal icons for each task state.
|
|
7
|
+
# Paths are relative to this file unless absolute.
|
|
8
|
+
# Remove this section entirely to use the defaults shown here.
|
|
9
|
+
# ---------------------------------------------------------------------------
|
|
10
|
+
|
|
11
|
+
states:
|
|
12
|
+
now:
|
|
13
|
+
file: ".taskflow/backlog/0-now.md"
|
|
14
|
+
icon: "▶"
|
|
15
|
+
blocked:
|
|
16
|
+
file: ".taskflow/backlog/1-blocked.md"
|
|
17
|
+
icon: "⊘"
|
|
18
|
+
paused:
|
|
19
|
+
file: ".taskflow/backlog/2-paused.md"
|
|
20
|
+
icon: "⏸"
|
|
21
|
+
next:
|
|
22
|
+
file: ".taskflow/backlog/3-next.md"
|
|
23
|
+
icon: "◈"
|
|
24
|
+
later:
|
|
25
|
+
file: ".taskflow/backlog/4-later.md"
|
|
26
|
+
icon: "◇"
|
|
27
|
+
done:
|
|
28
|
+
file: ".taskflow/backlog/done.md"
|
|
29
|
+
icon: "✓"
|
|
30
|
+
|
|
31
|
+
# ---------------------------------------------------------------------------
|
|
32
|
+
# Categories — define whatever makes sense for your project.
|
|
33
|
+
# Icons are optional. Colored circles work well: 🔴 🟠 🟡 🟢 🔵 🟣 ⚫ ⚪ 🟤
|
|
34
|
+
# ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
categories:
|
|
37
|
+
- name: Engineering
|
|
38
|
+
icon: "🔵"
|
|
39
|
+
aliases: []
|
|
40
|
+
|
|
41
|
+
- name: Operations
|
|
42
|
+
icon: "🔴"
|
|
43
|
+
aliases: []
|
|
44
|
+
|
|
45
|
+
- name: Product
|
|
46
|
+
icon: "🟡"
|
|
47
|
+
aliases: []
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# ---------------------------------------------------------------------------
|
|
51
|
+
# Phases — organise 4-later.md into planning horizons.
|
|
52
|
+
# No effect on any other backlog file.
|
|
53
|
+
# ---------------------------------------------------------------------------
|
|
54
|
+
|
|
55
|
+
phases:
|
|
56
|
+
- name: "Next Up"
|
|
57
|
+
description: "Tasks that are important and should be done soon. These are the things you want to do after finishing your current work."
|
|
58
|
+
- name: "Ongoing Tinkering"
|
|
59
|
+
description: "Tasks that are interesting but not a priority. These are the things you want to do if you have free time, but they don't need to be done soon."
|
|
60
|
+
|
|
61
|
+
# ---------------------------------------------------------------------------
|
|
62
|
+
# Settings
|
|
63
|
+
# ---------------------------------------------------------------------------
|
|
64
|
+
|
|
65
|
+
settings:
|
|
66
|
+
repo_name: "taskflow"
|
|
67
|
+
done_weeks: 4
|
|
68
|
+
weekly_plan_dir: ".taskflow/changelog/weekly"
|
|
69
|
+
# archive_path defaults to backlog/archive/ — uncomment to override
|
|
70
|
+
archive_path: ".taskflow/backlog/archive"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: taskflow-git
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.3
|
|
4
4
|
Summary: Git-native task management for people who live in the terminal.
|
|
5
5
|
Project-URL: Homepage, https://github.com/quorum-systems/taskflow
|
|
6
6
|
Project-URL: Repository, https://github.com/quorum-systems/taskflow
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "taskflow-git"
|
|
7
|
-
version = "0.3.
|
|
7
|
+
version = "0.3.3"
|
|
8
8
|
description = "Git-native task management for people who live in the terminal."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
@@ -66,4 +66,4 @@ target-version = "py311"
|
|
|
66
66
|
|
|
67
67
|
[tool.ruff.lint]
|
|
68
68
|
select = ["E", "F", "I"]
|
|
69
|
-
ignore = ["E741"]
|
|
69
|
+
ignore = ["E741"]
|
|
@@ -110,13 +110,7 @@ def main() -> None:
|
|
|
110
110
|
|
|
111
111
|
|
|
112
112
|
@main.command()
|
|
113
|
-
@click.option(
|
|
114
|
-
"--from",
|
|
115
|
-
"from_url",
|
|
116
|
-
default=None,
|
|
117
|
-
metavar="URL",
|
|
118
|
-
help="Fetch starter config from a URL instead of using the built-in template.",
|
|
119
|
-
)
|
|
113
|
+
@click.option("--from", "from_url", default=None, metavar="URL", help="Fetch starter config from a URL instead of using the built-in template.")
|
|
120
114
|
@click.option("--name", default=None, help="Project name (defaults to current directory name).")
|
|
121
115
|
def init(from_url: Optional[str], name: Optional[str]) -> None:
|
|
122
116
|
"""Set up a new taskflow project in the current directory."""
|
|
@@ -145,6 +139,27 @@ def init(from_url: Optional[str], name: Optional[str]) -> None:
|
|
|
145
139
|
config_path.write_text(content, encoding="utf-8")
|
|
146
140
|
click.echo(f" created {config_path.name}")
|
|
147
141
|
|
|
142
|
+
# initialize git if we're not already in a repo — taskflow without git
|
|
143
|
+
# is half the point, so just handle it
|
|
144
|
+
git_root = None
|
|
145
|
+
try:
|
|
146
|
+
result = subprocess.check_output(
|
|
147
|
+
["git", "rev-parse", "--show-toplevel"],
|
|
148
|
+
cwd=str(cwd),
|
|
149
|
+
text=True,
|
|
150
|
+
stderr=subprocess.DEVNULL,
|
|
151
|
+
).strip()
|
|
152
|
+
git_root = Path(result)
|
|
153
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
154
|
+
pass
|
|
155
|
+
|
|
156
|
+
if git_root is None:
|
|
157
|
+
try:
|
|
158
|
+
subprocess.run(["git", "init"], cwd=str(cwd), check=True, capture_output=True)
|
|
159
|
+
click.echo(" git init")
|
|
160
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
161
|
+
click.echo(" note: git not found — skipping git init and alias installation", err=True)
|
|
162
|
+
|
|
148
163
|
# load and run setup immediately
|
|
149
164
|
cfg = load_config(cwd)
|
|
150
165
|
run_setup(cfg, force=False, dry_run=False)
|
|
@@ -354,6 +369,65 @@ def add(state: str, category: str, task: tuple) -> None:
|
|
|
354
369
|
git_commit([target_rel], f"add ({state}): {query}", cfg.root)
|
|
355
370
|
|
|
356
371
|
|
|
372
|
+
# ---------------------------------------------------------------------------
|
|
373
|
+
# list
|
|
374
|
+
# ---------------------------------------------------------------------------
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
@main.command("list")
|
|
378
|
+
@click.argument("state", default="now", type=click.Choice(["now", "next", "later", "blocked", "paused"]))
|
|
379
|
+
def list_tasks(state: str) -> None:
|
|
380
|
+
"""
|
|
381
|
+
List tasks in a state. Defaults to 'now'.
|
|
382
|
+
|
|
383
|
+
\b
|
|
384
|
+
taskflow list # tasks in now
|
|
385
|
+
taskflow list next # tasks queued up
|
|
386
|
+
taskflow list blocked # what's stuck
|
|
387
|
+
"""
|
|
388
|
+
|
|
389
|
+
from taskflow.tasklib import CATEGORY_RE, PHASE_RE, TASK_RE
|
|
390
|
+
|
|
391
|
+
cfg = load_config()
|
|
392
|
+
path = cfg.state_path(state)
|
|
393
|
+
icon = cfg.state_icon(state)
|
|
394
|
+
|
|
395
|
+
if not path.exists():
|
|
396
|
+
click.echo(f"\n {icon} {state} (empty)\n")
|
|
397
|
+
return
|
|
398
|
+
|
|
399
|
+
lines = path.read_text(encoding="utf-8").splitlines()
|
|
400
|
+
current = None
|
|
401
|
+
printed = set() # track which categories we've printed headings for
|
|
402
|
+
any_task = False
|
|
403
|
+
|
|
404
|
+
click.echo(f"\n {icon} {state}\n")
|
|
405
|
+
for line in lines:
|
|
406
|
+
if PHASE_RE.match(line):
|
|
407
|
+
phase = line.lstrip("#").strip()
|
|
408
|
+
click.echo(f" ── {phase}")
|
|
409
|
+
continue
|
|
410
|
+
m = CATEGORY_RE.match(line)
|
|
411
|
+
if m:
|
|
412
|
+
current = m.group(1).strip() # canonical name (emoji stripped)
|
|
413
|
+
continue
|
|
414
|
+
t = TASK_RE.match(line)
|
|
415
|
+
if t:
|
|
416
|
+
if current and current not in printed and not line.startswith(" "):
|
|
417
|
+
cat_icon = cfg.category_icon(current)
|
|
418
|
+
label = f"{cat_icon} {current}".strip() if cat_icon else current
|
|
419
|
+
click.echo(f" {label}")
|
|
420
|
+
printed.add(current)
|
|
421
|
+
indent = " " if line.startswith(" ") else " "
|
|
422
|
+
click.echo(f"{indent}· {t.group(3)}")
|
|
423
|
+
if not line.startswith(" "):
|
|
424
|
+
any_task = True
|
|
425
|
+
|
|
426
|
+
if not any_task:
|
|
427
|
+
click.echo(" (empty)")
|
|
428
|
+
click.echo()
|
|
429
|
+
|
|
430
|
+
|
|
357
431
|
# ---------------------------------------------------------------------------
|
|
358
432
|
# status
|
|
359
433
|
# ---------------------------------------------------------------------------
|
|
@@ -18,12 +18,12 @@ CONFIG_FILE = ".taskflow.yml"
|
|
|
18
18
|
|
|
19
19
|
# defaults used when states section is missing or partially specified
|
|
20
20
|
STATE_DEFAULTS: dict[str, dict] = {
|
|
21
|
-
"now": {"file": "backlog/0-now.md", "icon": "▶"},
|
|
22
|
-
"blocked": {"file": "backlog/1-blocked.md", "icon": "⊘"},
|
|
23
|
-
"paused": {"file": "backlog/2-paused.md", "icon": "⏸"},
|
|
24
|
-
"next": {"file": "backlog/3-next.md", "icon": "◈"},
|
|
25
|
-
"later": {"file": "backlog/4-later.md", "icon": "◇"},
|
|
26
|
-
"done": {"file": "backlog/done.md", "icon": "✓"},
|
|
21
|
+
"now": {"file": ".taskflow/backlog/0-now.md", "icon": "▶"},
|
|
22
|
+
"blocked": {"file": ".taskflow/backlog/1-blocked.md", "icon": "⊘"},
|
|
23
|
+
"paused": {"file": ".taskflow/backlog/2-paused.md", "icon": "⏸"},
|
|
24
|
+
"next": {"file": ".taskflow/backlog/3-next.md", "icon": "◈"},
|
|
25
|
+
"later": {"file": ".taskflow/backlog/4-later.md", "icon": "◇"},
|
|
26
|
+
"done": {"file": ".taskflow/backlog/done.md", "icon": "✓"},
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
# the order transitions are defined matters for help text and validation
|
|
@@ -126,7 +126,7 @@ class TaskflowConfig:
|
|
|
126
126
|
|
|
127
127
|
@property
|
|
128
128
|
def weekly_plan_dir(self) -> Path:
|
|
129
|
-
d = self._data.get("settings", {}).get("weekly_plan_dir", "changelog/weekly")
|
|
129
|
+
d = self._data.get("settings", {}).get("weekly_plan_dir", ".taskflow/changelog/weekly")
|
|
130
130
|
p = Path(d)
|
|
131
131
|
return p if p.is_absolute() else self.root / p
|
|
132
132
|
|
|
@@ -137,8 +137,8 @@ class TaskflowConfig:
|
|
|
137
137
|
|
|
138
138
|
@property
|
|
139
139
|
def archive_path(self) -> Path:
|
|
140
|
-
"""Where archived week files live. Defaults
|
|
141
|
-
default_archive =
|
|
140
|
+
"""Where archived week files live. Defaults inside .taskflow/backlog/archive/."""
|
|
141
|
+
default_archive = ".taskflow/backlog/archive"
|
|
142
142
|
d = self._data.get("settings", {}).get("archive_path", default_archive)
|
|
143
143
|
p = Path(d)
|
|
144
144
|
return p if p.is_absolute() else self.root / p
|
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
"""
|
|
2
2
|
setup_cmd.py — taskflow init and setup.
|
|
3
3
|
|
|
4
|
-
init creates
|
|
4
|
+
init creates .taskflow.yml in the current directory.
|
|
5
5
|
setup regenerates backlog file skeletons and installs git aliases.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
The week-plan patching logic that existed in the old standalone script
|
|
8
|
+
version is gone — reports.py reads directly from config now.
|
|
9
9
|
"""
|
|
10
10
|
|
|
11
11
|
from __future__ import annotations
|
|
12
12
|
|
|
13
|
-
import re
|
|
14
13
|
import subprocess
|
|
15
14
|
from pathlib import Path
|
|
16
15
|
|
|
@@ -32,22 +31,22 @@ STARTER_CONFIG = """\
|
|
|
32
31
|
|
|
33
32
|
states:
|
|
34
33
|
now:
|
|
35
|
-
file: "backlog/0-now.md"
|
|
34
|
+
file: ".taskflow/backlog/0-now.md"
|
|
36
35
|
icon: "▶"
|
|
37
36
|
blocked:
|
|
38
|
-
file: "backlog/1-blocked.md"
|
|
37
|
+
file: ".taskflow/backlog/1-blocked.md"
|
|
39
38
|
icon: "⊘"
|
|
40
39
|
paused:
|
|
41
|
-
file: "backlog/2-paused.md"
|
|
40
|
+
file: ".taskflow/backlog/2-paused.md"
|
|
42
41
|
icon: "⏸"
|
|
43
42
|
next:
|
|
44
|
-
file: "backlog/3-next.md"
|
|
43
|
+
file: ".taskflow/backlog/3-next.md"
|
|
45
44
|
icon: "◈"
|
|
46
45
|
later:
|
|
47
|
-
file: "backlog/4-later.md"
|
|
46
|
+
file: ".taskflow/backlog/4-later.md"
|
|
48
47
|
icon: "◇"
|
|
49
48
|
done:
|
|
50
|
-
file: "backlog/done.md"
|
|
49
|
+
file: ".taskflow/backlog/done.md"
|
|
51
50
|
icon: "✓"
|
|
52
51
|
|
|
53
52
|
# ---------------------------------------------------------------------------
|
|
@@ -73,7 +72,7 @@ categories:
|
|
|
73
72
|
aliases: []
|
|
74
73
|
|
|
75
74
|
# ---------------------------------------------------------------------------
|
|
76
|
-
# Phases — organise
|
|
75
|
+
# Phases — organise the later file into planning horizons.
|
|
77
76
|
# No effect on any other backlog file.
|
|
78
77
|
# ---------------------------------------------------------------------------
|
|
79
78
|
|
|
@@ -94,24 +93,15 @@ phases:
|
|
|
94
93
|
settings:
|
|
95
94
|
repo_name: "{repo_name}"
|
|
96
95
|
done_weeks: 4
|
|
97
|
-
weekly_plan_dir: "changelog/weekly"
|
|
98
|
-
# archive_path defaults to backlog/archive/ — uncomment to override
|
|
99
|
-
# archive_path: "backlog/archive"
|
|
96
|
+
weekly_plan_dir: ".taskflow/changelog/weekly"
|
|
97
|
+
# archive_path defaults to .taskflow/backlog/archive/ — uncomment to override
|
|
98
|
+
# archive_path: ".taskflow/backlog/archive"
|
|
100
99
|
"""
|
|
101
100
|
|
|
102
|
-
WEEK_PLAN_BLOCK_START = "# === TASKFLOW GENERATED — do not edit below this line ==="
|
|
103
|
-
WEEK_PLAN_BLOCK_END = "# === END TASKFLOW GENERATED ==="
|
|
104
|
-
|
|
105
101
|
SIMPLE_STATE_TITLES = {
|
|
106
102
|
"now": ("Now", "Tasks actively being executed this week."),
|
|
107
|
-
"blocked": (
|
|
108
|
-
|
|
109
|
-
"Tasks waiting on something external.\n\nNote what's blocking each one.",
|
|
110
|
-
),
|
|
111
|
-
"paused": (
|
|
112
|
-
"Paused",
|
|
113
|
-
"Tasks deliberately on hold.\n\nNot blocked, not forgotten — just not now.",
|
|
114
|
-
),
|
|
103
|
+
"blocked": ("Blocked", "Tasks waiting on something external.\n\nNote what's blocking each one."),
|
|
104
|
+
"paused": ("Paused", "Tasks deliberately on hold.\n\nNot blocked, not forgotten — just not now."),
|
|
115
105
|
"next": ("Next", "Planned work queued for the next execution window."),
|
|
116
106
|
}
|
|
117
107
|
|
|
@@ -158,51 +148,23 @@ def _build_later_file(categories: list[dict], phases: list[dict]) -> str:
|
|
|
158
148
|
return "\n".join(lines) + "\n"
|
|
159
149
|
|
|
160
150
|
|
|
161
|
-
def _build_week_plan_block(categories: list[dict]) -> str:
|
|
162
|
-
alias_lines = ["CATEGORY_ALIASES = {"]
|
|
163
|
-
for cat in categories:
|
|
164
|
-
name = cat["name"]
|
|
165
|
-
for alias in cat.get("aliases", []):
|
|
166
|
-
alias_lines.append(f" {alias!r}: {name!r},")
|
|
167
|
-
alias_lines.append(f" {name!r}: {name!r},")
|
|
168
|
-
alias_lines.append("}")
|
|
169
|
-
|
|
170
|
-
order = "DEFAULT_CATEGORY_ORDER = [\n"
|
|
171
|
-
for cat in categories:
|
|
172
|
-
order += f" {cat['name']!r},\n"
|
|
173
|
-
order += "]"
|
|
174
|
-
|
|
175
|
-
return f"{WEEK_PLAN_BLOCK_START}\n" + "\n".join(alias_lines) + f"\n\n{order}\n" + f"{WEEK_PLAN_BLOCK_END}\n"
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
def patch_week_plan(week_plan_path: Path, categories: list[dict], dry_run: bool) -> bool:
|
|
179
|
-
"""Regenerate the TASKFLOW GENERATED block in week-plan. Returns True if changed."""
|
|
180
|
-
if not week_plan_path.exists():
|
|
181
|
-
return False
|
|
182
|
-
content = week_plan_path.read_text(encoding="utf-8")
|
|
183
|
-
new_block = _build_week_plan_block(categories)
|
|
184
|
-
|
|
185
|
-
if WEEK_PLAN_BLOCK_START in content:
|
|
186
|
-
pattern = re.compile(
|
|
187
|
-
re.escape(WEEK_PLAN_BLOCK_START) + r".*?" + re.escape(WEEK_PLAN_BLOCK_END) + r"\n?",
|
|
188
|
-
re.DOTALL,
|
|
189
|
-
)
|
|
190
|
-
new_content = pattern.sub(new_block, content)
|
|
191
|
-
else:
|
|
192
|
-
new_content = content.rstrip() + "\n\n" + new_block
|
|
193
|
-
|
|
194
|
-
if new_content == content:
|
|
195
|
-
return False
|
|
196
|
-
if not dry_run:
|
|
197
|
-
week_plan_path.write_text(new_content, encoding="utf-8")
|
|
198
|
-
return True
|
|
199
|
-
|
|
200
|
-
|
|
201
151
|
def install_git_aliases(root: Path) -> None:
|
|
202
152
|
"""
|
|
203
153
|
Install git aliases that just call `taskflow <subcommand>`.
|
|
204
154
|
Once taskflow is on PATH the aliases are trivial — no path resolution needed.
|
|
155
|
+
Skips silently if not in a git repo — init handles that case.
|
|
205
156
|
"""
|
|
157
|
+
# check we're actually in a git repo before attempting
|
|
158
|
+
try:
|
|
159
|
+
subprocess.run(
|
|
160
|
+
["git", "rev-parse", "--git-dir"],
|
|
161
|
+
cwd=str(root),
|
|
162
|
+
check=True,
|
|
163
|
+
capture_output=True,
|
|
164
|
+
)
|
|
165
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
166
|
+
return
|
|
167
|
+
|
|
206
168
|
transitions = [
|
|
207
169
|
("promote", "promote"),
|
|
208
170
|
("start", "start"),
|
|
@@ -285,7 +247,6 @@ def run_setup(config: TaskflowConfig, force: bool = False, dry_run: bool = False
|
|
|
285
247
|
(weekly_dir / ".gitkeep").touch()
|
|
286
248
|
changed.append(str(weekly_dir.relative_to(root)) + "/")
|
|
287
249
|
|
|
288
|
-
# archive dir — just make sure it exists
|
|
289
250
|
archive_dir = config.archive_path
|
|
290
251
|
if not archive_dir.exists() and not dry_run:
|
|
291
252
|
archive_dir.mkdir(parents=True, exist_ok=True)
|
|
@@ -171,7 +171,7 @@ def find_task(sections: list[dict], query: str, src_path: Path) -> tuple[dict, i
|
|
|
171
171
|
matches.append((section, line_idx, txt, raw_line))
|
|
172
172
|
|
|
173
173
|
if not matches:
|
|
174
|
-
raise click.UsageError(f"No task matching '{query}' in {src_path.name}")
|
|
174
|
+
raise click.UsageError(f"No task matching '{query}' in {src_path.name}\n" f" Run `taskflow list` to see what's there.")
|
|
175
175
|
|
|
176
176
|
if len(matches) > 1:
|
|
177
177
|
lines = "\n".join(f" line {idx + 1} [{sec['heading']}] {txt}" for sec, idx, txt, _ in matches)
|
|
@@ -39,12 +39,12 @@ SCRIPTS_DIR = REPO_ROOT / "scripts" / "git"
|
|
|
39
39
|
# ---------------------------------------------------------------------------
|
|
40
40
|
|
|
41
41
|
STATE_DEFAULTS = {
|
|
42
|
-
"now": {"file": "backlog/0-now.md", "icon": "▶"},
|
|
43
|
-
"blocked": {"file": "backlog/1-blocked.md", "icon": "⊘"},
|
|
44
|
-
"paused": {"file": "backlog/2-paused.md", "icon": "⏸"},
|
|
45
|
-
"next": {"file": "backlog/3-next.md", "icon": "◈"},
|
|
46
|
-
"later": {"file": "backlog/4-later.md", "icon": "◇"},
|
|
47
|
-
"done": {"file": "backlog/done.md", "icon": "✓"},
|
|
42
|
+
"now": {"file": ".taskflow/backlog/0-now.md", "icon": "▶"},
|
|
43
|
+
"blocked": {"file": ".taskflow/backlog/1-blocked.md", "icon": "⊘"},
|
|
44
|
+
"paused": {"file": ".taskflow/backlog/2-paused.md", "icon": "⏸"},
|
|
45
|
+
"next": {"file": ".taskflow/backlog/3-next.md", "icon": "◈"},
|
|
46
|
+
"later": {"file": ".taskflow/backlog/4-later.md", "icon": "◇"},
|
|
47
|
+
"done": {"file": ".taskflow/backlog/done.md", "icon": "✓"},
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
# Workflow transitions: command → (src_state, dst_state, commit_prefix)
|
|
@@ -18,18 +18,8 @@ import yaml
|
|
|
18
18
|
def tmp_git_repo(tmp_path: Path) -> Path:
|
|
19
19
|
"""A temp directory with git initialized and a basic user config."""
|
|
20
20
|
subprocess.run(["git", "init"], cwd=tmp_path, check=True, capture_output=True)
|
|
21
|
-
subprocess.run(
|
|
22
|
-
|
|
23
|
-
cwd=tmp_path,
|
|
24
|
-
check=True,
|
|
25
|
-
capture_output=True,
|
|
26
|
-
)
|
|
27
|
-
subprocess.run(
|
|
28
|
-
["git", "config", "user.name", "test"],
|
|
29
|
-
cwd=tmp_path,
|
|
30
|
-
check=True,
|
|
31
|
-
capture_output=True,
|
|
32
|
-
)
|
|
21
|
+
subprocess.run(["git", "config", "user.email", "test@test.com"], cwd=tmp_path, check=True, capture_output=True)
|
|
22
|
+
subprocess.run(["git", "config", "user.name", "test"], cwd=tmp_path, check=True, capture_output=True)
|
|
33
23
|
return tmp_path
|
|
34
24
|
|
|
35
25
|
|
|
@@ -64,32 +54,21 @@ def project_root(tmp_git_repo: Path, basic_config_data: dict) -> Path:
|
|
|
64
54
|
yaml.dump(basic_config_data, f)
|
|
65
55
|
|
|
66
56
|
# create minimal backlog structure
|
|
67
|
-
backlog = tmp_git_repo / "backlog"
|
|
68
|
-
backlog.mkdir()
|
|
57
|
+
backlog = tmp_git_repo / ".taskflow" / "backlog"
|
|
58
|
+
backlog.mkdir(parents=True)
|
|
69
59
|
|
|
70
60
|
for name, content in [
|
|
71
61
|
("0-now.md", "# Now\n\n---\n\n### 🔵 Engineering\n\n---\n"),
|
|
72
62
|
("1-blocked.md", "# Blocked\n\n---\n"),
|
|
73
63
|
("2-paused.md", "# Paused\n\n---\n"),
|
|
74
|
-
(
|
|
75
|
-
|
|
76
|
-
"# Next\n\n---\n\n### 🔵 Engineering\n* write deployment docs\n* set up monitoring\n\n---\n\n### 🔴 Operations\n* configure alerting\n\n---\n",
|
|
77
|
-
),
|
|
78
|
-
(
|
|
79
|
-
"4-later.md",
|
|
80
|
-
"# Later\n\n---\n\n## Phase 1 — Foundation\n\n### 🔵 Engineering\n* evaluate caching layer\n\n---\n",
|
|
81
|
-
),
|
|
64
|
+
("3-next.md", "# Next\n\n---\n\n### 🔵 Engineering\n* write deployment docs\n* set up monitoring\n\n---\n\n### 🔴 Operations\n* configure alerting\n\n---\n"),
|
|
65
|
+
("4-later.md", "# Later\n\n---\n\n## Phase 1 — Foundation\n\n### 🔵 Engineering\n* evaluate caching layer\n\n---\n"),
|
|
82
66
|
("done.md", "# Done\n\nCompleted tasks.\n\n---\n"),
|
|
83
67
|
]:
|
|
84
68
|
(backlog / name).write_text(content, encoding="utf-8")
|
|
85
69
|
|
|
86
70
|
# initial commit so git operations work
|
|
87
71
|
subprocess.run(["git", "add", "."], cwd=tmp_git_repo, check=True, capture_output=True)
|
|
88
|
-
subprocess.run(
|
|
89
|
-
["git", "commit", "-m", "init"],
|
|
90
|
-
cwd=tmp_git_repo,
|
|
91
|
-
check=True,
|
|
92
|
-
capture_output=True,
|
|
93
|
-
)
|
|
72
|
+
subprocess.run(["git", "commit", "-m", "init"], cwd=tmp_git_repo, check=True, capture_output=True)
|
|
94
73
|
|
|
95
74
|
return tmp_git_repo
|
|
@@ -57,8 +57,8 @@ class TestInit:
|
|
|
57
57
|
with runner.isolated_filesystem(temp_dir=tmp_git_repo.parent):
|
|
58
58
|
os.chdir(tmp_git_repo)
|
|
59
59
|
runner.invoke(main, ["init"], catch_exceptions=False)
|
|
60
|
-
assert (tmp_git_repo / "backlog" / "0-now.md").exists()
|
|
61
|
-
assert (tmp_git_repo / "backlog" / "done.md").exists()
|
|
60
|
+
assert (tmp_git_repo / ".taskflow" / "backlog" / "0-now.md").exists()
|
|
61
|
+
assert (tmp_git_repo / ".taskflow" / "backlog" / "done.md").exists()
|
|
62
62
|
|
|
63
63
|
def test_error_if_already_exists(self, runner: CliRunner, project_root: Path) -> None:
|
|
64
64
|
os.chdir(project_root)
|
|
@@ -94,8 +94,8 @@ class TestStart:
|
|
|
94
94
|
def test_moves_task_to_now(self, run_in_project, project_root: Path) -> None:
|
|
95
95
|
result = run_in_project("start", "deployment docs")
|
|
96
96
|
assert result.exit_code == 0
|
|
97
|
-
assert "write deployment docs" in (project_root / "backlog/0-now.md").read_text()
|
|
98
|
-
assert "write deployment docs" not in (project_root / "backlog/3-next.md").read_text()
|
|
97
|
+
assert "write deployment docs" in (project_root / ".taskflow/backlog/0-now.md").read_text()
|
|
98
|
+
assert "write deployment docs" not in (project_root / ".taskflow/backlog/3-next.md").read_text()
|
|
99
99
|
|
|
100
100
|
def test_commit_uses_full_task_text(self, run_in_project, project_root: Path) -> None:
|
|
101
101
|
import subprocess
|
|
@@ -112,8 +112,8 @@ class TestDone:
|
|
|
112
112
|
# then mark it done
|
|
113
113
|
result = run_in_project("done", "deployment")
|
|
114
114
|
assert result.exit_code == 0
|
|
115
|
-
assert "write deployment docs" not in (project_root / "backlog/0-now.md").read_text()
|
|
116
|
-
assert "write deployment docs" in (project_root / "backlog/done.md").read_text()
|
|
115
|
+
assert "write deployment docs" not in (project_root / ".taskflow/backlog/0-now.md").read_text()
|
|
116
|
+
assert "write deployment docs" in (project_root / ".taskflow/backlog/done.md").read_text()
|
|
117
117
|
|
|
118
118
|
|
|
119
119
|
class TestBlock:
|
|
@@ -121,8 +121,8 @@ class TestBlock:
|
|
|
121
121
|
run_in_project("start", "deployment docs")
|
|
122
122
|
result = run_in_project("block", "deployment")
|
|
123
123
|
assert result.exit_code == 0
|
|
124
|
-
assert "write deployment docs" in (project_root / "backlog/1-blocked.md").read_text()
|
|
125
|
-
assert "write deployment docs" not in (project_root / "backlog/0-now.md").read_text()
|
|
124
|
+
assert "write deployment docs" in (project_root / ".taskflow/backlog/1-blocked.md").read_text()
|
|
125
|
+
assert "write deployment docs" not in (project_root / ".taskflow/backlog/0-now.md").read_text()
|
|
126
126
|
|
|
127
127
|
|
|
128
128
|
class TestUnblock:
|
|
@@ -131,31 +131,52 @@ class TestUnblock:
|
|
|
131
131
|
run_in_project("block", "deployment")
|
|
132
132
|
result = run_in_project("unblock", "deployment")
|
|
133
133
|
assert result.exit_code == 0
|
|
134
|
-
assert "write deployment docs" in (project_root / "backlog/0-now.md").read_text()
|
|
134
|
+
assert "write deployment docs" in (project_root / ".taskflow/backlog/0-now.md").read_text()
|
|
135
135
|
|
|
136
136
|
|
|
137
137
|
class TestAdd:
|
|
138
138
|
def test_adds_to_next(self, run_in_project, project_root: Path) -> None:
|
|
139
139
|
result = run_in_project("add", "next", "Eng", "evaluate caching")
|
|
140
140
|
assert result.exit_code == 0
|
|
141
|
-
assert "evaluate caching" in (project_root / "backlog/3-next.md").read_text()
|
|
141
|
+
assert "evaluate caching" in (project_root / ".taskflow/backlog/3-next.md").read_text()
|
|
142
142
|
|
|
143
143
|
def test_fuzzy_category(self, run_in_project, project_root: Path) -> None:
|
|
144
144
|
result = run_in_project("add", "next", "ops", "update firewall rules")
|
|
145
145
|
assert result.exit_code == 0
|
|
146
|
-
assert "update firewall rules" in (project_root / "backlog/3-next.md").read_text()
|
|
146
|
+
assert "update firewall rules" in (project_root / ".taskflow/backlog/3-next.md").read_text()
|
|
147
147
|
|
|
148
148
|
def test_add_done_directly(self, run_in_project, project_root: Path) -> None:
|
|
149
149
|
result = run_in_project("add", "done", "Eng", "emergency hotfix deployed")
|
|
150
150
|
assert result.exit_code == 0
|
|
151
|
-
assert "emergency hotfix deployed" in (project_root / "backlog/done.md").read_text()
|
|
151
|
+
assert "emergency hotfix deployed" in (project_root / ".taskflow/backlog/done.md").read_text()
|
|
152
152
|
|
|
153
153
|
def test_unknown_state_errors(self, run_in_project) -> None:
|
|
154
154
|
result = run_in_project("add", "banana", "Eng", "some task")
|
|
155
155
|
assert result.exit_code != 0
|
|
156
156
|
|
|
157
157
|
|
|
158
|
-
class
|
|
158
|
+
class TestList:
|
|
159
|
+
def test_list_now_default(self, run_in_project) -> None:
|
|
160
|
+
result = run_in_project("list")
|
|
161
|
+
assert result.exit_code == 0
|
|
162
|
+
assert "now" in result.output
|
|
163
|
+
|
|
164
|
+
def test_list_next_shows_tasks(self, run_in_project) -> None:
|
|
165
|
+
result = run_in_project("list", "next")
|
|
166
|
+
assert result.exit_code == 0
|
|
167
|
+
assert "write deployment docs" in result.output
|
|
168
|
+
assert "Engineering" in result.output
|
|
169
|
+
|
|
170
|
+
def test_list_empty_state(self, run_in_project) -> None:
|
|
171
|
+
result = run_in_project("list", "blocked")
|
|
172
|
+
assert result.exit_code == 0
|
|
173
|
+
assert "empty" in result.output
|
|
174
|
+
|
|
175
|
+
def test_no_match_error_includes_hint(self, run_in_project) -> None:
|
|
176
|
+
result = run_in_project("start", "this does not exist")
|
|
177
|
+
assert result.exit_code != 0
|
|
178
|
+
assert "taskflow list" in result.output
|
|
179
|
+
|
|
159
180
|
def test_shows_now_tasks(self, run_in_project, project_root: Path) -> None:
|
|
160
181
|
run_in_project("start", "deployment docs")
|
|
161
182
|
result = run_in_project("status")
|
|
@@ -40,7 +40,7 @@ class TestTaskflowConfig:
|
|
|
40
40
|
return TaskflowConfig(tmp_path, basic_config_data)
|
|
41
41
|
|
|
42
42
|
def test_state_path_relative(self, cfg: TaskflowConfig, tmp_path: Path) -> None:
|
|
43
|
-
assert cfg.state_path("now") == tmp_path / "backlog" / "0-now.md"
|
|
43
|
+
assert cfg.state_path("now") == tmp_path / ".taskflow" / "backlog" / "0-now.md"
|
|
44
44
|
|
|
45
45
|
def test_state_path_absolute_override(self, tmp_path: Path, basic_config_data: dict) -> None:
|
|
46
46
|
basic_config_data["states"] = {"now": {"file": "/absolute/path/now.md"}}
|
|
@@ -102,8 +102,7 @@ class TestTaskflowConfig:
|
|
|
102
102
|
assert cfg.done_weeks == 2
|
|
103
103
|
|
|
104
104
|
def test_archive_path_default(self, cfg: TaskflowConfig, tmp_path: Path) -> None:
|
|
105
|
-
|
|
106
|
-
assert cfg.archive_path == tmp_path / "backlog" / "archive"
|
|
105
|
+
assert cfg.archive_path == tmp_path / ".taskflow" / "backlog" / "archive"
|
|
107
106
|
|
|
108
107
|
def test_archive_path_override(self, tmp_path: Path, basic_config_data: dict) -> None:
|
|
109
108
|
basic_config_data["settings"]["archive_path"] = "custom/archive"
|
|
@@ -121,9 +121,9 @@ class TestOrderedCategories:
|
|
|
121
121
|
class TestReportProgress:
|
|
122
122
|
@pytest.fixture
|
|
123
123
|
def cfg(self, tmp_path: Path) -> TaskflowConfig:
|
|
124
|
-
(tmp_path / "backlog").mkdir()
|
|
125
|
-
(tmp_path / "backlog/0-now.md").write_text(SAMPLE_NOW)
|
|
126
|
-
(tmp_path / "backlog/done.md").write_text(SAMPLE_DONE)
|
|
124
|
+
(tmp_path / ".taskflow" / "backlog").mkdir(parents=True)
|
|
125
|
+
(tmp_path / ".taskflow/backlog/0-now.md").write_text(SAMPLE_NOW)
|
|
126
|
+
(tmp_path / ".taskflow/backlog/done.md").write_text(SAMPLE_DONE)
|
|
127
127
|
data = {
|
|
128
128
|
"categories": [
|
|
129
129
|
{"name": "Engineering", "icon": "🔵"},
|
|
@@ -154,8 +154,8 @@ class TestReportProgress:
|
|
|
154
154
|
class TestReportPipeline:
|
|
155
155
|
@pytest.fixture
|
|
156
156
|
def cfg(self, tmp_path: Path) -> TaskflowConfig:
|
|
157
|
-
backlog = tmp_path / "backlog"
|
|
158
|
-
backlog.mkdir()
|
|
157
|
+
backlog = tmp_path / ".taskflow" / "backlog"
|
|
158
|
+
backlog.mkdir(parents=True)
|
|
159
159
|
for name, text in [
|
|
160
160
|
("0-now.md", SAMPLE_NOW),
|
|
161
161
|
("1-blocked.md", "### Engineering\n* blocked task\n---\n"),
|
|
@@ -765,7 +765,7 @@ wheels = [
|
|
|
765
765
|
|
|
766
766
|
[[package]]
|
|
767
767
|
name = "taskflow-git"
|
|
768
|
-
version = "0.3.
|
|
768
|
+
version = "0.3.3"
|
|
769
769
|
source = { editable = "." }
|
|
770
770
|
dependencies = [
|
|
771
771
|
{ name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
|
|
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
|