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.
Files changed (31) hide show
  1. taskflow_git-0.3.3/.taskflow/backlog/0-now.md +5 -0
  2. taskflow_git-0.3.3/.taskflow/backlog/1-blocked.md +23 -0
  3. taskflow_git-0.3.3/.taskflow/backlog/2-paused.md +23 -0
  4. taskflow_git-0.3.3/.taskflow/backlog/3-next.md +5 -0
  5. taskflow_git-0.3.3/.taskflow/backlog/4-later.md +47 -0
  6. taskflow_git-0.3.3/.taskflow/backlog/archive/.gitkeep +0 -0
  7. taskflow_git-0.3.3/.taskflow/backlog/done.md +11 -0
  8. taskflow_git-0.3.3/.taskflow/changelog/weekly/.gitkeep +0 -0
  9. taskflow_git-0.3.3/.taskflow.yml +70 -0
  10. {taskflow_git-0.3.0 → taskflow_git-0.3.3}/PKG-INFO +1 -1
  11. {taskflow_git-0.3.0 → taskflow_git-0.3.3}/pyproject.toml +2 -2
  12. {taskflow_git-0.3.0 → taskflow_git-0.3.3}/src/taskflow/cli.py +81 -7
  13. {taskflow_git-0.3.0 → taskflow_git-0.3.3}/src/taskflow/config.py +9 -9
  14. {taskflow_git-0.3.0 → taskflow_git-0.3.3}/src/taskflow/setup_cmd.py +27 -66
  15. {taskflow_git-0.3.0 → taskflow_git-0.3.3}/src/taskflow/tasklib.py +1 -1
  16. {taskflow_git-0.3.0 → taskflow_git-0.3.3}/taskflow +6 -6
  17. {taskflow_git-0.3.0 → taskflow_git-0.3.3}/tests/conftest.py +7 -28
  18. {taskflow_git-0.3.0 → taskflow_git-0.3.3}/tests/test_cli.py +34 -13
  19. {taskflow_git-0.3.0 → taskflow_git-0.3.3}/tests/test_config.py +2 -3
  20. {taskflow_git-0.3.0 → taskflow_git-0.3.3}/tests/test_reports.py +5 -5
  21. {taskflow_git-0.3.0 → taskflow_git-0.3.3}/uv.lock +1 -1
  22. {taskflow_git-0.3.0 → taskflow_git-0.3.3}/.github/workflows/ci.yml +0 -0
  23. {taskflow_git-0.3.0 → taskflow_git-0.3.3}/.github/workflows/publish.yml +0 -0
  24. {taskflow_git-0.3.0 → taskflow_git-0.3.3}/.gitignore +0 -0
  25. {taskflow_git-0.3.0 → taskflow_git-0.3.3}/README.md +0 -0
  26. {taskflow_git-0.3.0 → taskflow_git-0.3.3}/coverage.xml +0 -0
  27. {taskflow_git-0.3.0 → taskflow_git-0.3.3}/src/taskflow/__init__.py +0 -0
  28. {taskflow_git-0.3.0 → taskflow_git-0.3.3}/src/taskflow/archive.py +0 -0
  29. {taskflow_git-0.3.0 → taskflow_git-0.3.3}/src/taskflow/reports.py +0 -0
  30. {taskflow_git-0.3.0 → taskflow_git-0.3.3}/tests/test_archive.py +0 -0
  31. {taskflow_git-0.3.0 → taskflow_git-0.3.3}/tests/test_tasklib.py +0 -0
@@ -0,0 +1,5 @@
1
+ # Now
2
+
3
+ Tasks actively being executed this week.
4
+
5
+ ---
@@ -0,0 +1,23 @@
1
+ # Blocked
2
+
3
+ Tasks waiting on something external.
4
+
5
+ Note what's blocking each one.
6
+
7
+ ---
8
+
9
+ ### 🔵 Engineering
10
+
11
+ ---
12
+
13
+ ### 🔴 Operations
14
+
15
+ ---
16
+
17
+ ### 🟡 Product
18
+
19
+ ---
20
+
21
+ ### ⚫ Business
22
+
23
+ ---
@@ -0,0 +1,23 @@
1
+ # Paused
2
+
3
+ Tasks deliberately on hold.
4
+
5
+ Not blocked, not forgotten — just not now.
6
+
7
+ ---
8
+
9
+ ### 🔵 Engineering
10
+
11
+ ---
12
+
13
+ ### 🔴 Operations
14
+
15
+ ---
16
+
17
+ ### 🟡 Product
18
+
19
+ ---
20
+
21
+ ### ⚫ Business
22
+
23
+ ---
@@ -0,0 +1,5 @@
1
+ # Next
2
+
3
+ Planned work queued for the next execution window.
4
+
5
+ ---
@@ -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.0
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.0"
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 next to the done file."""
141
- default_archive = str(self.state_path("done").parent / "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 a new .taskflow.yml in the current directory.
4
+ init creates .taskflow.yml in the current directory.
5
5
  setup regenerates backlog file skeletons and installs git aliases.
6
6
 
7
- Both write files relative to the project root setup uses the discovered
8
- root, init uses cwd since there's no root yet.
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 4-later.md into planning horizons.
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
- "Blocked",
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
- ["git", "config", "user.email", "test@test.com"],
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
- "3-next.md",
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 TestStatus:
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
- # default should be next to done.md in backlog/archive/
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.0"
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