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.
- {task_agent-0.1.321 → task_agent-0.1.351}/PKG-INFO +1 -1
- {task_agent-0.1.321 → task_agent-0.1.351}/pyproject.toml +2 -2
- {task_agent-0.1.321 → task_agent-0.1.351}/src/taskagent/cli.py +181 -0
- {task_agent-0.1.321 → task_agent-0.1.351}/src/taskagent/manager.py +81 -0
- {task_agent-0.1.321 → task_agent-0.1.351}/src/taskagent/mcp.py +78 -14
- task_agent-0.1.351/tests/conftest.py +10 -0
- {task_agent-0.1.321 → task_agent-0.1.351}/tests/test_cli.py +63 -0
- {task_agent-0.1.321 → task_agent-0.1.351}/tests/test_manager.py +59 -0
- {task_agent-0.1.321 → task_agent-0.1.351}/tests/test_mcp.py +39 -0
- {task_agent-0.1.321 → task_agent-0.1.351}/uv.lock +1 -1
- {task_agent-0.1.321 → task_agent-0.1.351}/.env +0 -0
- {task_agent-0.1.321 → task_agent-0.1.351}/.gemini/settings.json +0 -0
- {task_agent-0.1.321 → task_agent-0.1.351}/.github/workflows/ci.yml +0 -0
- {task_agent-0.1.321 → task_agent-0.1.351}/.github/workflows/publish.yml +0 -0
- {task_agent-0.1.321 → task_agent-0.1.351}/.gitignore +0 -0
- {task_agent-0.1.321 → task_agent-0.1.351}/.mise.toml +0 -0
- {task_agent-0.1.321 → task_agent-0.1.351}/.pre-commit-config.yaml +0 -0
- {task_agent-0.1.321 → task_agent-0.1.351}/.python-version +0 -0
- {task_agent-0.1.321 → task_agent-0.1.351}/.ta/agents/gh/meta.toml +0 -0
- {task_agent-0.1.321 → task_agent-0.1.351}/.ta/agents/minimal/meta.toml +0 -0
- {task_agent-0.1.321 → task_agent-0.1.351}/.ta/agents/uat-aws/meta.toml +0 -0
- {task_agent-0.1.321 → task_agent-0.1.351}/.ta/worker +0 -0
- {task_agent-0.1.321 → task_agent-0.1.351}/.task-agent/worktree-config.json +0 -0
- {task_agent-0.1.321 → task_agent-0.1.351}/CLAUDE.md +0 -0
- {task_agent-0.1.321 → task_agent-0.1.351}/GEMINI.md +0 -0
- {task_agent-0.1.321 → task_agent-0.1.351}/Makefile +0 -0
- {task_agent-0.1.321 → task_agent-0.1.351}/README.md +0 -0
- {task_agent-0.1.321 → task_agent-0.1.351}/agent.env +0 -0
- {task_agent-0.1.321 → task_agent-0.1.351}/docs/README.md +0 -0
- {task_agent-0.1.321 → task_agent-0.1.351}/docs/architecture/README.md +0 -0
- {task_agent-0.1.321 → task_agent-0.1.351}/docs/chats/gemini-conversation-1773283034591.json +0 -0
- {task_agent-0.1.321 → task_agent-0.1.351}/docs/chats/project-init-to-v0.1.46.md +0 -0
- {task_agent-0.1.321 → task_agent-0.1.351}/docs/cli/README.md +0 -0
- {task_agent-0.1.321 → task_agent-0.1.351}/docs/development/README.md +0 -0
- {task_agent-0.1.321 → task_agent-0.1.351}/docs/task-agents.md +0 -0
- {task_agent-0.1.321 → task_agent-0.1.351}/docs/workflow/README.md +0 -0
- {task_agent-0.1.321 → task_agent-0.1.351}/sidecars/adk-worker/.env.example +0 -0
- {task_agent-0.1.321 → task_agent-0.1.351}/sidecars/adk-worker/README.md +0 -0
- {task_agent-0.1.321 → task_agent-0.1.351}/sidecars/adk-worker/instructions/manager.md +0 -0
- {task_agent-0.1.321 → task_agent-0.1.351}/sidecars/adk-worker/instructions/validator.md +0 -0
- {task_agent-0.1.321 → task_agent-0.1.351}/sidecars/adk-worker/instructions/worker.md +0 -0
- {task_agent-0.1.321 → task_agent-0.1.351}/sidecars/adk-worker/profiles/default.json +0 -0
- {task_agent-0.1.321 → task_agent-0.1.351}/sidecars/adk-worker/pyproject.toml +0 -0
- {task_agent-0.1.321 → task_agent-0.1.351}/sidecars/adk-worker/worker.py +0 -0
- {task_agent-0.1.321 → task_agent-0.1.351}/src/taskagent/__init__.py +0 -0
- {task_agent-0.1.321 → task_agent-0.1.351}/src/taskagent/__main__.py +0 -0
- {task_agent-0.1.321 → task_agent-0.1.351}/src/taskagent/agent.py +0 -0
- {task_agent-0.1.321 → task_agent-0.1.351}/src/taskagent/audit.py +0 -0
- {task_agent-0.1.321 → task_agent-0.1.351}/src/taskagent/config.py +0 -0
- {task_agent-0.1.321 → task_agent-0.1.351}/src/taskagent/discovery.py +0 -0
- {task_agent-0.1.321 → task_agent-0.1.351}/src/taskagent/models/__init__.py +0 -0
- {task_agent-0.1.321 → task_agent-0.1.351}/src/taskagent/models/issue.py +0 -0
- {task_agent-0.1.321 → task_agent-0.1.351}/src/taskagent/models/metric.py +0 -0
- {task_agent-0.1.321 → task_agent-0.1.351}/src/taskagent/plugins/__init__.py +0 -0
- {task_agent-0.1.321 → task_agent-0.1.351}/src/taskagent/plugins/github.py +0 -0
- {task_agent-0.1.321 → task_agent-0.1.351}/src/taskagent/templates.py +0 -0
- {task_agent-0.1.321 → task_agent-0.1.351}/test-dir/opencode.json +0 -0
- {task_agent-0.1.321 → task_agent-0.1.351}/tests/e2e/test_agent_sandboxing.sh +0 -0
- {task_agent-0.1.321 → task_agent-0.1.351}/tests/test_agent.py +0 -0
- {task_agent-0.1.321 → task_agent-0.1.351}/tests/test_cli_windows.py +0 -0
- {task_agent-0.1.321 → task_agent-0.1.351}/tests/test_discovery.py +0 -0
- {task_agent-0.1.321 → task_agent-0.1.351}/tests/test_manager_git.py +0 -0
- {task_agent-0.1.321 → task_agent-0.1.351}/tests/test_metrics.py +0 -0
- {task_agent-0.1.321 → task_agent-0.1.351}/tests/test_migration.py +0 -0
- {task_agent-0.1.321 → task_agent-0.1.351}/tests/test_templates.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "task-agent"
|
|
3
|
-
version = "0.1.
|
|
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.
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|