task-agent 0.1.321__tar.gz → 0.1.342__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 (65) hide show
  1. {task_agent-0.1.321 → task_agent-0.1.342}/PKG-INFO +1 -1
  2. {task_agent-0.1.321 → task_agent-0.1.342}/pyproject.toml +2 -2
  3. {task_agent-0.1.321 → task_agent-0.1.342}/src/taskagent/cli.py +153 -0
  4. {task_agent-0.1.321 → task_agent-0.1.342}/src/taskagent/manager.py +81 -0
  5. {task_agent-0.1.321 → task_agent-0.1.342}/src/taskagent/mcp.py +81 -14
  6. task_agent-0.1.342/tests/conftest.py +10 -0
  7. {task_agent-0.1.321 → task_agent-0.1.342}/tests/test_cli.py +63 -0
  8. {task_agent-0.1.321 → task_agent-0.1.342}/tests/test_manager.py +59 -0
  9. {task_agent-0.1.321 → task_agent-0.1.342}/tests/test_mcp.py +39 -0
  10. {task_agent-0.1.321 → task_agent-0.1.342}/uv.lock +1 -1
  11. {task_agent-0.1.321 → task_agent-0.1.342}/.env +0 -0
  12. {task_agent-0.1.321 → task_agent-0.1.342}/.gemini/settings.json +0 -0
  13. {task_agent-0.1.321 → task_agent-0.1.342}/.github/workflows/ci.yml +0 -0
  14. {task_agent-0.1.321 → task_agent-0.1.342}/.github/workflows/publish.yml +0 -0
  15. {task_agent-0.1.321 → task_agent-0.1.342}/.gitignore +0 -0
  16. {task_agent-0.1.321 → task_agent-0.1.342}/.mise.toml +0 -0
  17. {task_agent-0.1.321 → task_agent-0.1.342}/.pre-commit-config.yaml +0 -0
  18. {task_agent-0.1.321 → task_agent-0.1.342}/.python-version +0 -0
  19. {task_agent-0.1.321 → task_agent-0.1.342}/.ta/agents/gh/meta.toml +0 -0
  20. {task_agent-0.1.321 → task_agent-0.1.342}/.ta/agents/minimal/meta.toml +0 -0
  21. {task_agent-0.1.321 → task_agent-0.1.342}/.ta/agents/uat-aws/meta.toml +0 -0
  22. {task_agent-0.1.321 → task_agent-0.1.342}/.ta/worker +0 -0
  23. {task_agent-0.1.321 → task_agent-0.1.342}/.task-agent/worktree-config.json +0 -0
  24. {task_agent-0.1.321 → task_agent-0.1.342}/CLAUDE.md +0 -0
  25. {task_agent-0.1.321 → task_agent-0.1.342}/GEMINI.md +0 -0
  26. {task_agent-0.1.321 → task_agent-0.1.342}/Makefile +0 -0
  27. {task_agent-0.1.321 → task_agent-0.1.342}/README.md +0 -0
  28. {task_agent-0.1.321 → task_agent-0.1.342}/agent.env +0 -0
  29. {task_agent-0.1.321 → task_agent-0.1.342}/docs/README.md +0 -0
  30. {task_agent-0.1.321 → task_agent-0.1.342}/docs/architecture/README.md +0 -0
  31. {task_agent-0.1.321 → task_agent-0.1.342}/docs/chats/gemini-conversation-1773283034591.json +0 -0
  32. {task_agent-0.1.321 → task_agent-0.1.342}/docs/chats/project-init-to-v0.1.46.md +0 -0
  33. {task_agent-0.1.321 → task_agent-0.1.342}/docs/cli/README.md +0 -0
  34. {task_agent-0.1.321 → task_agent-0.1.342}/docs/development/README.md +0 -0
  35. {task_agent-0.1.321 → task_agent-0.1.342}/docs/task-agents.md +0 -0
  36. {task_agent-0.1.321 → task_agent-0.1.342}/docs/workflow/README.md +0 -0
  37. {task_agent-0.1.321 → task_agent-0.1.342}/sidecars/adk-worker/.env.example +0 -0
  38. {task_agent-0.1.321 → task_agent-0.1.342}/sidecars/adk-worker/README.md +0 -0
  39. {task_agent-0.1.321 → task_agent-0.1.342}/sidecars/adk-worker/instructions/manager.md +0 -0
  40. {task_agent-0.1.321 → task_agent-0.1.342}/sidecars/adk-worker/instructions/validator.md +0 -0
  41. {task_agent-0.1.321 → task_agent-0.1.342}/sidecars/adk-worker/instructions/worker.md +0 -0
  42. {task_agent-0.1.321 → task_agent-0.1.342}/sidecars/adk-worker/profiles/default.json +0 -0
  43. {task_agent-0.1.321 → task_agent-0.1.342}/sidecars/adk-worker/pyproject.toml +0 -0
  44. {task_agent-0.1.321 → task_agent-0.1.342}/sidecars/adk-worker/worker.py +0 -0
  45. {task_agent-0.1.321 → task_agent-0.1.342}/src/taskagent/__init__.py +0 -0
  46. {task_agent-0.1.321 → task_agent-0.1.342}/src/taskagent/__main__.py +0 -0
  47. {task_agent-0.1.321 → task_agent-0.1.342}/src/taskagent/agent.py +0 -0
  48. {task_agent-0.1.321 → task_agent-0.1.342}/src/taskagent/audit.py +0 -0
  49. {task_agent-0.1.321 → task_agent-0.1.342}/src/taskagent/config.py +0 -0
  50. {task_agent-0.1.321 → task_agent-0.1.342}/src/taskagent/discovery.py +0 -0
  51. {task_agent-0.1.321 → task_agent-0.1.342}/src/taskagent/models/__init__.py +0 -0
  52. {task_agent-0.1.321 → task_agent-0.1.342}/src/taskagent/models/issue.py +0 -0
  53. {task_agent-0.1.321 → task_agent-0.1.342}/src/taskagent/models/metric.py +0 -0
  54. {task_agent-0.1.321 → task_agent-0.1.342}/src/taskagent/plugins/__init__.py +0 -0
  55. {task_agent-0.1.321 → task_agent-0.1.342}/src/taskagent/plugins/github.py +0 -0
  56. {task_agent-0.1.321 → task_agent-0.1.342}/src/taskagent/templates.py +0 -0
  57. {task_agent-0.1.321 → task_agent-0.1.342}/test-dir/opencode.json +0 -0
  58. {task_agent-0.1.321 → task_agent-0.1.342}/tests/e2e/test_agent_sandboxing.sh +0 -0
  59. {task_agent-0.1.321 → task_agent-0.1.342}/tests/test_agent.py +0 -0
  60. {task_agent-0.1.321 → task_agent-0.1.342}/tests/test_cli_windows.py +0 -0
  61. {task_agent-0.1.321 → task_agent-0.1.342}/tests/test_discovery.py +0 -0
  62. {task_agent-0.1.321 → task_agent-0.1.342}/tests/test_manager_git.py +0 -0
  63. {task_agent-0.1.321 → task_agent-0.1.342}/tests/test_metrics.py +0 -0
  64. {task_agent-0.1.321 → task_agent-0.1.342}/tests/test_migration.py +0 -0
  65. {task_agent-0.1.321 → task_agent-0.1.342}/tests/test_templates.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: task-agent
3
- Version: 0.1.321
3
+ Version: 0.1.342
4
4
  Summary: A prioritized, file-based task queue for autonomous agentic workers
5
5
  Author-email: Mark Stouffer <1802850+InTEGr8or@users.noreply.github.com>
6
6
  License: MIT
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "task-agent"
3
- version = "0.1.321"
3
+ version = "0.1.342"
4
4
  description = "A prioritized, file-based task queue for autonomous agentic workers"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -39,7 +39,7 @@ dev = [
39
39
  ]
40
40
 
41
41
  [tool.bumpversion]
42
- current_version = "0.1.321"
42
+ current_version = "0.1.342"
43
43
  parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)"
44
44
  serialize = ["{major}.{minor}.{patch}"]
45
45
  search = "version = \"{current_version}\""
@@ -332,8 +332,130 @@ def render_issue(console: Console, issue: Issue, issue_file: Path):
332
332
  console.print(md)
333
333
 
334
334
 
335
+ def maybe_show_strategy(console: Console, manager: TaskAgent) -> bool:
336
+ """Show the project strategy panel if the cooldown has elapsed.
337
+
338
+ Returns True if the strategy was displayed.
339
+ """
340
+ if not manager.should_show_strategy():
341
+ return False
342
+
343
+ content = manager.get_strategy()
344
+ if not content:
345
+ return False
346
+
347
+ # Strip the H1 header if present — we use it as the panel title instead
348
+ lines = content.split("\n")
349
+ title = "Strategy"
350
+ body_lines = []
351
+ for line in lines:
352
+ stripped = line.strip()
353
+ if stripped.startswith("# ") and title == "Strategy":
354
+ title = stripped.lstrip("# ").strip()
355
+ continue
356
+ # Skip HTML comments (the hint comment)
357
+ if stripped.startswith("<!--") and stripped.endswith("-->"):
358
+ continue
359
+ body_lines.append(line)
360
+
361
+ body = "\n".join(body_lines).strip()
362
+ if not body or body == "_Define the current strategic direction for this project._":
363
+ return False
364
+
365
+ meta = manager.get_strategy_meta()
366
+ last_shown = meta.get("last_shown_at", "never")
367
+ if last_shown != "never":
368
+ try:
369
+ from datetime import datetime as dt
370
+
371
+ last_dt = dt.fromisoformat(last_shown)
372
+ elapsed = (dt.now() - last_dt).total_seconds()
373
+ if elapsed < 3600:
374
+ age = f"{int(elapsed / 60)}m ago"
375
+ elif elapsed < 86400:
376
+ age = f"{int(elapsed / 3600)}h ago"
377
+ else:
378
+ age = f"{int(elapsed / 86400)}d ago"
379
+ subtitle = f"last shown {age} · ta strategy"
380
+ except Exception:
381
+ subtitle = "ta strategy"
382
+ else:
383
+ subtitle = "ta strategy"
384
+
385
+ # Construct a clean Markdown blockquote
386
+ lines = [f"> 📐 **{title}**", ">"]
387
+ for line in body.split("\n"):
388
+ lines.append(f"> {line}")
389
+ lines.append(">")
390
+ lines.append(f"> *{subtitle}*")
391
+
392
+ console.print(Markdown("\n".join(lines)))
393
+ console.print()
394
+ manager.update_strategy_last_shown()
395
+ return True
396
+
397
+
398
+ def cmd_strategy(
399
+ console: Console,
400
+ manager: TaskAgent,
401
+ action: Optional[str] = None,
402
+ ):
403
+ """View, edit, or initialize the project strategy."""
404
+ if action == "init":
405
+ path = manager.init_strategy()
406
+ console.print(f"[bold green]Strategy initialized:[/bold green] {path}")
407
+ console.print("[dim]Edit it with: ta strategy edit[/dim]")
408
+ return
409
+
410
+ if action == "edit":
411
+ path = manager.init_strategy()
412
+ editor = get_editor()
413
+ subprocess.run([editor, str(path)])
414
+ console.print("[bold green]Strategy updated.[/bold green]")
415
+ return
416
+
417
+ # Default: view
418
+ content = manager.get_strategy()
419
+ if not content:
420
+ console.print(
421
+ "[yellow]No strategy defined yet.[/yellow]\n"
422
+ "[dim]Run [bold]ta strategy init[/bold] to create one.[/dim]"
423
+ )
424
+ return
425
+
426
+ lines = content.split("\n")
427
+ title = "Strategy"
428
+ body_lines = []
429
+ for line in lines:
430
+ stripped = line.strip()
431
+ if stripped.startswith("# ") and title == "Strategy":
432
+ title = stripped.lstrip("# ").strip()
433
+ continue
434
+ if stripped.startswith("<!--") and stripped.endswith("-->"):
435
+ continue
436
+ body_lines.append(line)
437
+
438
+ body = "\n".join(body_lines).strip()
439
+
440
+ meta = manager.get_strategy_meta()
441
+ last_shown = meta.get("last_shown_at", "never")
442
+
443
+ # Construct a clean Markdown blockquote
444
+ lines = [f"> 📐 **{title}**", ">"]
445
+ if body:
446
+ for line in body.split("\n"):
447
+ lines.append(f"> {line}")
448
+ else:
449
+ lines.append("> *Empty strategy — edit it with: ta strategy edit*")
450
+ lines.append(">")
451
+ lines.append(f"> *last shown: {last_shown} · ta strategy edit*")
452
+
453
+ console.print(Markdown("\n".join(lines)))
454
+
455
+
335
456
  def cmd_next(console: Console, manager: TaskAgent):
336
457
  """Show the top issue."""
458
+ maybe_show_strategy(console, manager)
337
459
  next_issue = manager.get_next_issue()
338
460
  if not next_issue:
339
461
  console.print(f"[yellow]No issues found in {manager.mission_path}[/yellow]")
@@ -1405,6 +1527,8 @@ def cmd_list(
1405
1527
  output_format: str = "table",
1406
1528
  ):
1407
1529
  """List all issues in mission.usv."""
1530
+ if output_format == "table":
1531
+ maybe_show_strategy(console, manager)
1408
1532
  issues = manager.sync_mission()
1409
1533
  if not issues:
1410
1534
  if output_format == "json":
@@ -1555,6 +1679,8 @@ def cmd_active(
1555
1679
  ) -> Optional[Issue]:
1556
1680
  """Move an issue to active status, or list active tasks."""
1557
1681
  issues = manager.load_mission()
1682
+ if not slug_part and list_if_none and not silent:
1683
+ maybe_show_strategy(console, manager)
1558
1684
  if not slug_part and list_if_none:
1559
1685
  active_issues = [i for i in issues if i.status == "active"]
1560
1686
  if not silent:
@@ -3207,6 +3333,31 @@ Start working on a task. This command automates the following workflow:
3207
3333
  # plan
3208
3334
  subparsers.add_parser("plan", help="View or edit the project plan")
3209
3335
 
3336
+ # strategy
3337
+ strategy_parser = subparsers.add_parser(
3338
+ "strategy",
3339
+ help="View, edit, or initialize the project strategy",
3340
+ description="""
3341
+ Manage the project's strategic direction document.
3342
+
3343
+ The strategy is a concise statement of the project's current direction, goals,
3344
+ and priorities. It is displayed periodically at the top of 'list', 'next', and
3345
+ 'active' commands to keep all workers aligned.
3346
+
3347
+ Usage:
3348
+ ta strategy View the current strategy
3349
+ ta strategy edit Open the strategy in your $EDITOR
3350
+ ta strategy init Create a starter strategy file
3351
+ """,
3352
+ formatter_class=argparse.RawDescriptionHelpFormatter,
3353
+ )
3354
+ strategy_parser.add_argument(
3355
+ "action",
3356
+ nargs="?",
3357
+ choices=["edit", "init"],
3358
+ help="Action to perform (default: view)",
3359
+ )
3360
+
3210
3361
  # push
3211
3362
  subparsers.add_parser("push", help="Push the mission repository to origin")
3212
3363
 
@@ -3389,6 +3540,8 @@ Start working on a task. This command automates the following workflow:
3389
3540
  cmd_push(console, manager)
3390
3541
  elif args.command == "plan":
3391
3542
  cmd_plan(console, manager)
3543
+ elif args.command == "strategy":
3544
+ cmd_strategy(console, manager, action=args.action)
3392
3545
  elif args.command == "commit":
3393
3546
  if args.target == "repo":
3394
3547
  cmd_commit(console, manager, message=args.message, should_push=args.push)
@@ -1210,3 +1210,84 @@ class TaskAgent:
1210
1210
  self.sync_mission()
1211
1211
 
1212
1212
  return len(new_issues), len(existing_issues) - len(present_issues)
1213
+
1214
+ # ── Strategy ─────────────────────────────────────────────────────
1215
+
1216
+ @property
1217
+ def strategy_dir(self) -> Path:
1218
+ """Path to the strategy directory."""
1219
+ return self.issues_root / "strategy"
1220
+
1221
+ @property
1222
+ def strategy_file(self) -> Path:
1223
+ """Path to the strategy README."""
1224
+ return self.strategy_dir / "README.md"
1225
+
1226
+ @property
1227
+ def strategy_meta_file(self) -> Path:
1228
+ """Path to the strategy metadata file."""
1229
+ return self.strategy_dir / ".meta.json"
1230
+
1231
+ def get_strategy(self) -> Optional[str]:
1232
+ """Read the strategy content. Returns None if no strategy file exists."""
1233
+ if not self.strategy_file.exists():
1234
+ return None
1235
+ try:
1236
+ content = self.strategy_file.read_text(encoding="utf-8").strip()
1237
+ return content if content else None
1238
+ except Exception:
1239
+ return None
1240
+
1241
+ def get_strategy_meta(self) -> dict:
1242
+ """Read strategy metadata (last_shown_at, cooldown_hours)."""
1243
+ if not self.strategy_meta_file.exists():
1244
+ return {}
1245
+ try:
1246
+ with self.strategy_meta_file.open("r", encoding="utf-8") as f:
1247
+ return json.load(f)
1248
+ except Exception:
1249
+ return {}
1250
+
1251
+ def update_strategy_last_shown(self) -> None:
1252
+ """Update the timestamp of when the strategy was last displayed."""
1253
+ self.strategy_dir.mkdir(parents=True, exist_ok=True)
1254
+ meta = self.get_strategy_meta()
1255
+ meta["last_shown_at"] = datetime.now().isoformat()
1256
+ with self.strategy_meta_file.open("w", encoding="utf-8") as f:
1257
+ json.dump(meta, f, indent=2)
1258
+
1259
+ def should_show_strategy(self, cooldown_hours: float = 2.0) -> bool:
1260
+ """Check if enough time has elapsed to display the strategy again.
1261
+
1262
+ Returns True if:
1263
+ - A strategy file exists with content, AND
1264
+ - The strategy has never been shown, OR
1265
+ - At least `cooldown_hours` have passed since last shown.
1266
+ """
1267
+ content = self.get_strategy()
1268
+ if not content:
1269
+ return False
1270
+
1271
+ meta = self.get_strategy_meta()
1272
+ last_shown = meta.get("last_shown_at")
1273
+ if not last_shown:
1274
+ return True
1275
+
1276
+ try:
1277
+ last_dt = datetime.fromisoformat(last_shown)
1278
+ elapsed = (datetime.now() - last_dt).total_seconds()
1279
+ return elapsed >= cooldown_hours * 3600
1280
+ except (ValueError, TypeError):
1281
+ return True
1282
+
1283
+ def init_strategy(self) -> Path:
1284
+ """Create the strategy directory and a starter README if it doesn't exist."""
1285
+ self.strategy_dir.mkdir(parents=True, exist_ok=True)
1286
+ if not self.strategy_file.exists():
1287
+ self.strategy_file.write_text(
1288
+ "# Strategy\n\n"
1289
+ "_Define the current strategic direction for this project._\n\n"
1290
+ "<!-- Keep this concise — it will be displayed periodically in CLI output. -->\n",
1291
+ encoding="utf-8",
1292
+ )
1293
+ return self.strategy_file
@@ -17,6 +17,67 @@ def get_manager() -> TaskAgent:
17
17
  return discover()
18
18
 
19
19
 
20
+ def _maybe_prepend_strategy(manager: TaskAgent, response: str) -> str:
21
+ """If the strategy cooldown has elapsed, prepend the strategy to the response."""
22
+ if hasattr(manager, "should_show_strategy") and manager.should_show_strategy():
23
+ if hasattr(manager, "get_strategy"):
24
+ strategy = manager.get_strategy()
25
+ if strategy:
26
+ # Strip the H1 header if present — we want to keep it clean and concise
27
+ lines = strategy.split("\n")
28
+ title = "Strategy"
29
+ body_lines = []
30
+ for line in lines:
31
+ stripped = line.strip()
32
+ if stripped.startswith("# ") and title == "Strategy":
33
+ title = stripped.lstrip("# ").strip()
34
+ continue
35
+ # Skip HTML comments (the hint comment)
36
+ if stripped.startswith("<!--") and stripped.endswith("-->"):
37
+ continue
38
+ body_lines.append(line)
39
+
40
+ body = "\n".join(body_lines).strip()
41
+ if (
42
+ body
43
+ and body
44
+ != "_Define the current strategic direction for this project._"
45
+ ):
46
+ if hasattr(manager, "update_strategy_last_shown"):
47
+ manager.update_strategy_last_shown()
48
+ # Construct clean Markdown blockquote for LLM/agent
49
+ strategy_lines = [f"> 📐 **{title}**", ">"]
50
+ for line in body.split("\n"):
51
+ strategy_lines.append(f"> {line}")
52
+ prefix = "\n".join(strategy_lines) + "\n\n"
53
+ return prefix + response
54
+ return response
55
+
56
+
57
+ @mcp.tool()
58
+ def get_strategy() -> str:
59
+ """Retrieve the current project strategy statement.
60
+
61
+ This provides high-level principles, constraints, or goals that should guide development.
62
+ """
63
+ manager = get_manager()
64
+ if not hasattr(manager, "get_strategy"):
65
+ return "No strategy defined yet."
66
+ strategy = manager.get_strategy()
67
+ if not strategy:
68
+ return "No strategy defined yet."
69
+
70
+ # Strip HTML comments for the tool output to keep it clean
71
+ lines = strategy.split("\n")
72
+ body_lines = []
73
+ for line in lines:
74
+ stripped = line.strip()
75
+ if stripped.startswith("<!--") and stripped.endswith("-->"):
76
+ continue
77
+ body_lines.append(line)
78
+ return "\n".join(body_lines).strip()
79
+
80
+
20
81
  @mcp.tool()
21
82
  def list_tasks() -> str:
22
83
  """List all tasks in the current project's mission queue.
@@ -28,13 +89,16 @@ def list_tasks() -> str:
28
89
  manager = get_manager()
29
90
  issues = manager.sync_mission()
30
91
  if not issues:
31
- return "No tasks found in the queue."
32
-
33
- lines = []
34
- for i in issues:
35
- deps = f" (depends on: {', '.join(i.dependencies)})" if i.dependencies else ""
36
- lines.append(f"[{i.priority}] {i.status.upper()}: {i.name}{deps}")
37
- return "\n".join(lines)
92
+ res = "No tasks found in the queue."
93
+ else:
94
+ lines = []
95
+ for i in issues:
96
+ deps = (
97
+ f" (depends on: {', '.join(i.dependencies)})" if i.dependencies else ""
98
+ )
99
+ lines.append(f"[{i.priority}] {i.status.upper()}: {i.name}{deps}")
100
+ res = "\n".join(lines)
101
+ return _maybe_prepend_strategy(manager, res)
38
102
 
39
103
 
40
104
  @mcp.tool()
@@ -44,13 +108,16 @@ def list_active_tasks() -> str:
44
108
  issues = manager.sync_mission()
45
109
  active_issues = [i for i in issues if i.status == "active"]
46
110
  if not active_issues:
47
- return "No active tasks found."
48
-
49
- lines = []
50
- for i in active_issues:
51
- deps = f" (depends on: {', '.join(i.dependencies)})" if i.dependencies else ""
52
- lines.append(f"[{i.priority}] ACTIVE: {i.name}{deps}")
53
- return "\n".join(lines)
111
+ res = "No active tasks found."
112
+ else:
113
+ lines = []
114
+ for i in active_issues:
115
+ deps = (
116
+ f" (depends on: {', '.join(i.dependencies)})" if i.dependencies else ""
117
+ )
118
+ lines.append(f"[{i.priority}] ACTIVE: {i.name}{deps}")
119
+ res = "\n".join(lines)
120
+ return _maybe_prepend_strategy(manager, res)
54
121
 
55
122
 
56
123
  @mcp.tool()
@@ -0,0 +1,10 @@
1
+ import os
2
+ import pytest
3
+
4
+
5
+ @pytest.fixture(autouse=True)
6
+ def clean_test_env(monkeypatch):
7
+ """Remove git and task-agent environment variables from the test process to prevent leakage."""
8
+ for key in list(os.environ.keys()):
9
+ if key.startswith("GIT_") or key.startswith("TA_"):
10
+ monkeypatch.delenv(key, raising=False)
@@ -363,3 +363,66 @@ def test_cmd_done_cleanup(manager, temp_issues_dir, tmp_path):
363
363
  with patch("subprocess.run", side_effect=mock_run):
364
364
  cmd_done(console, manager, "done-clean-task", should_commit=False)
365
365
  assert not worktree_mock_path.exists()
366
+
367
+
368
+ def test_maybe_show_strategy(manager):
369
+ from taskagent.cli import maybe_show_strategy
370
+
371
+ console = Console()
372
+
373
+ # 1. No strategy defined -> should return False
374
+ assert not maybe_show_strategy(console, manager)
375
+
376
+ # 2. Strategy initialized but has default starter content -> should return False
377
+ manager.init_strategy()
378
+ assert not maybe_show_strategy(console, manager)
379
+
380
+ # 3. Strategy has real content -> should return True
381
+ manager.strategy_file.write_text(
382
+ "# Real Strategy\nSome real strategic goals.", encoding="utf-8"
383
+ )
384
+ assert maybe_show_strategy(console, manager)
385
+
386
+ # 4. Strategy within cooldown -> should return False
387
+ assert not maybe_show_strategy(console, manager)
388
+
389
+
390
+ def test_cmd_strategy(manager, monkeypatch):
391
+ from taskagent.cli import cmd_strategy
392
+
393
+ console = Console()
394
+
395
+ # Test action=None (view) when not initialized
396
+ # Should print message "No strategy defined yet"
397
+ with console.capture() as capture:
398
+ cmd_strategy(console, manager, action=None)
399
+ assert "No strategy defined yet" in capture.get()
400
+
401
+ # Test action="init"
402
+ with console.capture() as capture:
403
+ cmd_strategy(console, manager, action="init")
404
+ assert "Strategy initialized:" in capture.get()
405
+ assert manager.strategy_file.exists()
406
+
407
+ # Test action="edit"
408
+ import subprocess
409
+ import taskagent.cli as cli_module
410
+
411
+ called_run = []
412
+
413
+ def mock_run(args, **kwargs):
414
+ called_run.append(args)
415
+
416
+ monkeypatch.setattr(subprocess, "run", mock_run)
417
+ monkeypatch.setattr(cli_module, "get_editor", lambda: "nano")
418
+
419
+ with console.capture() as capture:
420
+ cmd_strategy(console, manager, action="edit")
421
+ assert "Strategy updated." in capture.get()
422
+ assert len(called_run) == 1
423
+ assert called_run[0] == ["nano", str(manager.strategy_file)]
424
+
425
+ # Test action=None (view) after initialization
426
+ with console.capture() as capture:
427
+ cmd_strategy(console, manager, action=None)
428
+ assert "Strategy" in capture.get()
@@ -384,3 +384,62 @@ def test_api_update_dependencies(manager):
384
384
  # Clear dependencies
385
385
  manager.update_dependencies("task-b", "")
386
386
  assert manager.extract_deps(issue_file) == []
387
+
388
+
389
+ def test_get_strategy_returns_none_when_no_file(manager):
390
+ assert manager.get_strategy() is None
391
+
392
+
393
+ def test_get_strategy_reads_content(manager):
394
+ manager.strategy_dir.mkdir(parents=True, exist_ok=True)
395
+ manager.strategy_file.write_text("# Test Strategy\nContent here", encoding="utf-8")
396
+ assert manager.get_strategy() == "# Test Strategy\nContent here"
397
+
398
+
399
+ def test_strategy_meta_roundtrip(manager):
400
+ assert manager.get_strategy_meta() == {}
401
+ manager.update_strategy_last_shown()
402
+ meta = manager.get_strategy_meta()
403
+ assert "last_shown_at" in meta
404
+ # Try parsing it
405
+ datetime.fromisoformat(meta["last_shown_at"])
406
+
407
+
408
+ def test_should_show_strategy_no_file_returns_false(manager):
409
+ assert not manager.should_show_strategy()
410
+
411
+
412
+ def test_should_show_strategy_first_time_returns_true(manager):
413
+ manager.init_strategy()
414
+ assert manager.should_show_strategy()
415
+
416
+
417
+ def test_should_show_strategy_within_cooldown_returns_false(manager):
418
+ manager.init_strategy()
419
+ manager.update_strategy_last_shown()
420
+ assert not manager.should_show_strategy()
421
+
422
+
423
+ def test_should_show_strategy_after_cooldown_returns_true(manager):
424
+ from datetime import datetime as dt, timedelta
425
+ import json
426
+
427
+ manager.init_strategy()
428
+ # Mock last shown far in the past
429
+ past_time = dt.now() - timedelta(hours=3)
430
+ manager.strategy_dir.mkdir(parents=True, exist_ok=True)
431
+ with manager.strategy_meta_file.open("w", encoding="utf-8") as f:
432
+ json.dump({"last_shown_at": past_time.isoformat()}, f)
433
+ assert manager.should_show_strategy(cooldown_hours=2.0)
434
+ assert not manager.should_show_strategy(cooldown_hours=4.0)
435
+
436
+
437
+ def test_init_strategy_creates_files(manager):
438
+ assert not manager.strategy_dir.exists()
439
+ assert not manager.strategy_file.exists()
440
+ path = manager.init_strategy()
441
+ assert path == manager.strategy_file
442
+ assert manager.strategy_dir.exists()
443
+ assert manager.strategy_file.exists()
444
+ content = manager.strategy_file.read_text(encoding="utf-8")
445
+ assert "# Strategy" in content
@@ -8,6 +8,7 @@ from taskagent.models.issue import Issue
8
8
  def mock_manager():
9
9
  with patch("taskagent.mcp.get_manager") as mock:
10
10
  manager = MagicMock()
11
+ manager.should_show_strategy.return_value = False
11
12
  mock.return_value = manager
12
13
  yield manager
13
14
 
@@ -198,6 +199,43 @@ def test_mcp_update_task_dependencies(monkeypatch):
198
199
  assert called == [("task-one", "task-two,task-three")]
199
200
 
200
201
 
202
+ def test_mcp_get_strategy(mock_manager):
203
+ mock_manager.get_strategy.return_value = None
204
+ assert mcp.get_strategy() == "No strategy defined yet."
205
+
206
+ mock_manager.get_strategy.return_value = (
207
+ "# Core Strategy\n<!-- comment -->\nFocus on quality."
208
+ )
209
+ assert mcp.get_strategy() == "# Core Strategy\nFocus on quality."
210
+
211
+
212
+ def test_mcp_list_tasks_with_strategy(mock_manager):
213
+ mock_manager.sync_mission.return_value = [
214
+ Issue(name="Task 1", slug="task-1", status="pending", priority=1)
215
+ ]
216
+ mock_manager.should_show_strategy.return_value = True
217
+ mock_manager.get_strategy.return_value = (
218
+ "# Core Strategy\n<!-- comment -->\nFocus on quality."
219
+ )
220
+
221
+ result = mcp.list_tasks()
222
+ assert "> 📐 **Core Strategy**" in result
223
+ assert "> Focus on quality." in result
224
+ assert "[1] PENDING: Task 1" in result
225
+ mock_manager.update_strategy_last_shown.assert_called_once()
226
+
227
+
228
+ def test_mcp_list_tasks_without_strategy(mock_manager):
229
+ mock_manager.sync_mission.return_value = [
230
+ Issue(name="Task 1", slug="task-1", status="pending", priority=1)
231
+ ]
232
+ mock_manager.should_show_strategy.return_value = False
233
+
234
+ result = mcp.list_tasks()
235
+ assert "> 📐" not in result
236
+ assert "[1] PENDING: Task 1" in result
237
+
238
+
201
239
  EXPECTED_TOOLS = {
202
240
  "list_tasks",
203
241
  "list_active_tasks",
@@ -213,6 +251,7 @@ EXPECTED_TOOLS = {
213
251
  "update_task_dependencies",
214
252
  "commit_repo",
215
253
  "commit_tasks",
254
+ "get_strategy",
216
255
  }
217
256
 
218
257
 
@@ -1080,7 +1080,7 @@ wheels = [
1080
1080
 
1081
1081
  [[package]]
1082
1082
  name = "task-agent"
1083
- version = "0.1.321"
1083
+ version = "0.1.342"
1084
1084
  source = { editable = "." }
1085
1085
  dependencies = [
1086
1086
  { name = "githubkit" },
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