task-agent 0.1.321__tar.gz → 0.1.351__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.351}/PKG-INFO +1 -1
  2. {task_agent-0.1.321 → task_agent-0.1.351}/pyproject.toml +2 -2
  3. {task_agent-0.1.321 → task_agent-0.1.351}/src/taskagent/cli.py +181 -0
  4. {task_agent-0.1.321 → task_agent-0.1.351}/src/taskagent/manager.py +81 -0
  5. {task_agent-0.1.321 → task_agent-0.1.351}/src/taskagent/mcp.py +78 -14
  6. task_agent-0.1.351/tests/conftest.py +10 -0
  7. {task_agent-0.1.321 → task_agent-0.1.351}/tests/test_cli.py +63 -0
  8. {task_agent-0.1.321 → task_agent-0.1.351}/tests/test_manager.py +59 -0
  9. {task_agent-0.1.321 → task_agent-0.1.351}/tests/test_mcp.py +39 -0
  10. {task_agent-0.1.321 → task_agent-0.1.351}/uv.lock +1 -1
  11. {task_agent-0.1.321 → task_agent-0.1.351}/.env +0 -0
  12. {task_agent-0.1.321 → task_agent-0.1.351}/.gemini/settings.json +0 -0
  13. {task_agent-0.1.321 → task_agent-0.1.351}/.github/workflows/ci.yml +0 -0
  14. {task_agent-0.1.321 → task_agent-0.1.351}/.github/workflows/publish.yml +0 -0
  15. {task_agent-0.1.321 → task_agent-0.1.351}/.gitignore +0 -0
  16. {task_agent-0.1.321 → task_agent-0.1.351}/.mise.toml +0 -0
  17. {task_agent-0.1.321 → task_agent-0.1.351}/.pre-commit-config.yaml +0 -0
  18. {task_agent-0.1.321 → task_agent-0.1.351}/.python-version +0 -0
  19. {task_agent-0.1.321 → task_agent-0.1.351}/.ta/agents/gh/meta.toml +0 -0
  20. {task_agent-0.1.321 → task_agent-0.1.351}/.ta/agents/minimal/meta.toml +0 -0
  21. {task_agent-0.1.321 → task_agent-0.1.351}/.ta/agents/uat-aws/meta.toml +0 -0
  22. {task_agent-0.1.321 → task_agent-0.1.351}/.ta/worker +0 -0
  23. {task_agent-0.1.321 → task_agent-0.1.351}/.task-agent/worktree-config.json +0 -0
  24. {task_agent-0.1.321 → task_agent-0.1.351}/CLAUDE.md +0 -0
  25. {task_agent-0.1.321 → task_agent-0.1.351}/GEMINI.md +0 -0
  26. {task_agent-0.1.321 → task_agent-0.1.351}/Makefile +0 -0
  27. {task_agent-0.1.321 → task_agent-0.1.351}/README.md +0 -0
  28. {task_agent-0.1.321 → task_agent-0.1.351}/agent.env +0 -0
  29. {task_agent-0.1.321 → task_agent-0.1.351}/docs/README.md +0 -0
  30. {task_agent-0.1.321 → task_agent-0.1.351}/docs/architecture/README.md +0 -0
  31. {task_agent-0.1.321 → task_agent-0.1.351}/docs/chats/gemini-conversation-1773283034591.json +0 -0
  32. {task_agent-0.1.321 → task_agent-0.1.351}/docs/chats/project-init-to-v0.1.46.md +0 -0
  33. {task_agent-0.1.321 → task_agent-0.1.351}/docs/cli/README.md +0 -0
  34. {task_agent-0.1.321 → task_agent-0.1.351}/docs/development/README.md +0 -0
  35. {task_agent-0.1.321 → task_agent-0.1.351}/docs/task-agents.md +0 -0
  36. {task_agent-0.1.321 → task_agent-0.1.351}/docs/workflow/README.md +0 -0
  37. {task_agent-0.1.321 → task_agent-0.1.351}/sidecars/adk-worker/.env.example +0 -0
  38. {task_agent-0.1.321 → task_agent-0.1.351}/sidecars/adk-worker/README.md +0 -0
  39. {task_agent-0.1.321 → task_agent-0.1.351}/sidecars/adk-worker/instructions/manager.md +0 -0
  40. {task_agent-0.1.321 → task_agent-0.1.351}/sidecars/adk-worker/instructions/validator.md +0 -0
  41. {task_agent-0.1.321 → task_agent-0.1.351}/sidecars/adk-worker/instructions/worker.md +0 -0
  42. {task_agent-0.1.321 → task_agent-0.1.351}/sidecars/adk-worker/profiles/default.json +0 -0
  43. {task_agent-0.1.321 → task_agent-0.1.351}/sidecars/adk-worker/pyproject.toml +0 -0
  44. {task_agent-0.1.321 → task_agent-0.1.351}/sidecars/adk-worker/worker.py +0 -0
  45. {task_agent-0.1.321 → task_agent-0.1.351}/src/taskagent/__init__.py +0 -0
  46. {task_agent-0.1.321 → task_agent-0.1.351}/src/taskagent/__main__.py +0 -0
  47. {task_agent-0.1.321 → task_agent-0.1.351}/src/taskagent/agent.py +0 -0
  48. {task_agent-0.1.321 → task_agent-0.1.351}/src/taskagent/audit.py +0 -0
  49. {task_agent-0.1.321 → task_agent-0.1.351}/src/taskagent/config.py +0 -0
  50. {task_agent-0.1.321 → task_agent-0.1.351}/src/taskagent/discovery.py +0 -0
  51. {task_agent-0.1.321 → task_agent-0.1.351}/src/taskagent/models/__init__.py +0 -0
  52. {task_agent-0.1.321 → task_agent-0.1.351}/src/taskagent/models/issue.py +0 -0
  53. {task_agent-0.1.321 → task_agent-0.1.351}/src/taskagent/models/metric.py +0 -0
  54. {task_agent-0.1.321 → task_agent-0.1.351}/src/taskagent/plugins/__init__.py +0 -0
  55. {task_agent-0.1.321 → task_agent-0.1.351}/src/taskagent/plugins/github.py +0 -0
  56. {task_agent-0.1.321 → task_agent-0.1.351}/src/taskagent/templates.py +0 -0
  57. {task_agent-0.1.321 → task_agent-0.1.351}/test-dir/opencode.json +0 -0
  58. {task_agent-0.1.321 → task_agent-0.1.351}/tests/e2e/test_agent_sandboxing.sh +0 -0
  59. {task_agent-0.1.321 → task_agent-0.1.351}/tests/test_agent.py +0 -0
  60. {task_agent-0.1.321 → task_agent-0.1.351}/tests/test_cli_windows.py +0 -0
  61. {task_agent-0.1.321 → task_agent-0.1.351}/tests/test_discovery.py +0 -0
  62. {task_agent-0.1.321 → task_agent-0.1.351}/tests/test_manager_git.py +0 -0
  63. {task_agent-0.1.321 → task_agent-0.1.351}/tests/test_metrics.py +0 -0
  64. {task_agent-0.1.321 → task_agent-0.1.351}/tests/test_migration.py +0 -0
  65. {task_agent-0.1.321 → task_agent-0.1.351}/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.351
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.351"
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.351"
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,158 @@ 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
+ # Print the title, body (with custom theme), and subtitle
386
+ console.print(f"[bold blue]📐 {title}[/bold blue]")
387
+
388
+ from rich.theme import Theme
389
+
390
+ strategy_theme = Theme(
391
+ {
392
+ "markdown.paragraph": "green",
393
+ "markdown.item": "green",
394
+ "markdown.h1": "bold blue",
395
+ "markdown.h2": "bold blue",
396
+ "markdown.h3": "bold blue",
397
+ "markdown.h4": "bold blue",
398
+ "markdown.h5": "bold blue",
399
+ "markdown.h6": "bold blue",
400
+ }
401
+ )
402
+
403
+ with console.use_theme(strategy_theme):
404
+ console.print(Markdown(body))
405
+
406
+ console.print(f"[dim]{subtitle}[/dim]")
407
+ console.print()
408
+ manager.update_strategy_last_shown()
409
+ return True
410
+
411
+
412
+ def cmd_strategy(
413
+ console: Console,
414
+ manager: TaskAgent,
415
+ action: Optional[str] = None,
416
+ ):
417
+ """View, edit, or initialize the project strategy."""
418
+ if action == "init":
419
+ path = manager.init_strategy()
420
+ console.print(f"[bold green]Strategy initialized:[/bold green] {path}")
421
+ console.print("[dim]Edit it with: ta strategy edit[/dim]")
422
+ return
423
+
424
+ if action == "edit":
425
+ path = manager.init_strategy()
426
+ editor = get_editor()
427
+ subprocess.run([editor, str(path)])
428
+ console.print("[bold green]Strategy updated.[/bold green]")
429
+ return
430
+
431
+ # Default: view
432
+ content = manager.get_strategy()
433
+ if not content:
434
+ console.print(
435
+ "[yellow]No strategy defined yet.[/yellow]\n"
436
+ "[dim]Run [bold]ta strategy init[/bold] to create one.[/dim]"
437
+ )
438
+ return
439
+
440
+ lines = content.split("\n")
441
+ title = "Strategy"
442
+ body_lines = []
443
+ for line in lines:
444
+ stripped = line.strip()
445
+ if stripped.startswith("# ") and title == "Strategy":
446
+ title = stripped.lstrip("# ").strip()
447
+ continue
448
+ if stripped.startswith("<!--") and stripped.endswith("-->"):
449
+ continue
450
+ body_lines.append(line)
451
+
452
+ body = "\n".join(body_lines).strip()
453
+
454
+ meta = manager.get_strategy_meta()
455
+ last_shown = meta.get("last_shown_at", "never")
456
+
457
+ # Print the title, body (with custom theme), and subtitle
458
+ console.print(f"[bold blue]📐 {title}[/bold blue]")
459
+
460
+ from rich.theme import Theme
461
+
462
+ strategy_theme = Theme(
463
+ {
464
+ "markdown.paragraph": "green",
465
+ "markdown.item": "green",
466
+ "markdown.h1": "bold blue",
467
+ "markdown.h2": "bold blue",
468
+ "markdown.h3": "bold blue",
469
+ "markdown.h4": "bold blue",
470
+ "markdown.h5": "bold blue",
471
+ "markdown.h6": "bold blue",
472
+ }
473
+ )
474
+
475
+ if body:
476
+ with console.use_theme(strategy_theme):
477
+ console.print(Markdown(body))
478
+ else:
479
+ console.print("[dim]Empty strategy — edit it with: ta strategy edit[/dim]")
480
+
481
+ console.print(f"[dim]last shown: {last_shown} · ta strategy edit[/dim]")
482
+
483
+
335
484
  def cmd_next(console: Console, manager: TaskAgent):
336
485
  """Show the top issue."""
486
+ maybe_show_strategy(console, manager)
337
487
  next_issue = manager.get_next_issue()
338
488
  if not next_issue:
339
489
  console.print(f"[yellow]No issues found in {manager.mission_path}[/yellow]")
@@ -1405,6 +1555,8 @@ def cmd_list(
1405
1555
  output_format: str = "table",
1406
1556
  ):
1407
1557
  """List all issues in mission.usv."""
1558
+ if output_format == "table":
1559
+ maybe_show_strategy(console, manager)
1408
1560
  issues = manager.sync_mission()
1409
1561
  if not issues:
1410
1562
  if output_format == "json":
@@ -1555,6 +1707,8 @@ def cmd_active(
1555
1707
  ) -> Optional[Issue]:
1556
1708
  """Move an issue to active status, or list active tasks."""
1557
1709
  issues = manager.load_mission()
1710
+ if not slug_part and list_if_none and not silent:
1711
+ maybe_show_strategy(console, manager)
1558
1712
  if not slug_part and list_if_none:
1559
1713
  active_issues = [i for i in issues if i.status == "active"]
1560
1714
  if not silent:
@@ -3207,6 +3361,31 @@ Start working on a task. This command automates the following workflow:
3207
3361
  # plan
3208
3362
  subparsers.add_parser("plan", help="View or edit the project plan")
3209
3363
 
3364
+ # strategy
3365
+ strategy_parser = subparsers.add_parser(
3366
+ "strategy",
3367
+ help="View, edit, or initialize the project strategy",
3368
+ description="""
3369
+ Manage the project's strategic direction document.
3370
+
3371
+ The strategy is a concise statement of the project's current direction, goals,
3372
+ and priorities. It is displayed periodically at the top of 'list', 'next', and
3373
+ 'active' commands to keep all workers aligned.
3374
+
3375
+ Usage:
3376
+ ta strategy View the current strategy
3377
+ ta strategy edit Open the strategy in your $EDITOR
3378
+ ta strategy init Create a starter strategy file
3379
+ """,
3380
+ formatter_class=argparse.RawDescriptionHelpFormatter,
3381
+ )
3382
+ strategy_parser.add_argument(
3383
+ "action",
3384
+ nargs="?",
3385
+ choices=["edit", "init"],
3386
+ help="Action to perform (default: view)",
3387
+ )
3388
+
3210
3389
  # push
3211
3390
  subparsers.add_parser("push", help="Push the mission repository to origin")
3212
3391
 
@@ -3389,6 +3568,8 @@ Start working on a task. This command automates the following workflow:
3389
3568
  cmd_push(console, manager)
3390
3569
  elif args.command == "plan":
3391
3570
  cmd_plan(console, manager)
3571
+ elif args.command == "strategy":
3572
+ cmd_strategy(console, manager, action=args.action)
3392
3573
  elif args.command == "commit":
3393
3574
  if args.target == "repo":
3394
3575
  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,64 @@ 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
+ # Prepend without a blockquote: just standard Markdown headers/body
49
+ prefix = f"## 📐 {title}\n\n{body}\n\n"
50
+ return prefix + response
51
+ return response
52
+
53
+
54
+ @mcp.tool()
55
+ def get_strategy() -> str:
56
+ """Retrieve the current project strategy statement.
57
+
58
+ This provides high-level principles, constraints, or goals that should guide development.
59
+ """
60
+ manager = get_manager()
61
+ if not hasattr(manager, "get_strategy"):
62
+ return "No strategy defined yet."
63
+ strategy = manager.get_strategy()
64
+ if not strategy:
65
+ return "No strategy defined yet."
66
+
67
+ # Strip HTML comments for the tool output to keep it clean
68
+ lines = strategy.split("\n")
69
+ body_lines = []
70
+ for line in lines:
71
+ stripped = line.strip()
72
+ if stripped.startswith("<!--") and stripped.endswith("-->"):
73
+ continue
74
+ body_lines.append(line)
75
+ return "\n".join(body_lines).strip()
76
+
77
+
20
78
  @mcp.tool()
21
79
  def list_tasks() -> str:
22
80
  """List all tasks in the current project's mission queue.
@@ -28,13 +86,16 @@ def list_tasks() -> str:
28
86
  manager = get_manager()
29
87
  issues = manager.sync_mission()
30
88
  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)
89
+ res = "No tasks found in the queue."
90
+ else:
91
+ lines = []
92
+ for i in issues:
93
+ deps = (
94
+ f" (depends on: {', '.join(i.dependencies)})" if i.dependencies else ""
95
+ )
96
+ lines.append(f"[{i.priority}] {i.status.upper()}: {i.name}{deps}")
97
+ res = "\n".join(lines)
98
+ return _maybe_prepend_strategy(manager, res)
38
99
 
39
100
 
40
101
  @mcp.tool()
@@ -44,13 +105,16 @@ def list_active_tasks() -> str:
44
105
  issues = manager.sync_mission()
45
106
  active_issues = [i for i in issues if i.status == "active"]
46
107
  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)
108
+ res = "No active tasks found."
109
+ else:
110
+ lines = []
111
+ for i in active_issues:
112
+ deps = (
113
+ f" (depends on: {', '.join(i.dependencies)})" if i.dependencies else ""
114
+ )
115
+ lines.append(f"[{i.priority}] ACTIVE: {i.name}{deps}")
116
+ res = "\n".join(lines)
117
+ return _maybe_prepend_strategy(manager, res)
54
118
 
55
119
 
56
120
  @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.351"
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