task-agent 1.2.1__tar.gz → 1.3.1__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 (148) hide show
  1. {task_agent-1.2.1 → task_agent-1.3.1}/PKG-INFO +1 -1
  2. {task_agent-1.2.1 → task_agent-1.3.1}/pyproject.toml +1 -1
  3. {task_agent-1.2.1 → task_agent-1.3.1}/src/taskagent/chat/__init__.py +6 -0
  4. task_agent-1.3.1/src/taskagent/chat/last_used.py +200 -0
  5. {task_agent-1.2.1 → task_agent-1.3.1}/src/taskagent/cli.py +86 -1
  6. {task_agent-1.2.1 → task_agent-1.3.1}/src/taskagent/mcp.py +41 -0
  7. task_agent-1.3.1/tests/test_chat_last_used.py +86 -0
  8. {task_agent-1.2.1 → task_agent-1.3.1}/tests/test_mcp.py +1 -0
  9. {task_agent-1.2.1 → task_agent-1.3.1}/uv.lock +1 -1
  10. {task_agent-1.2.1 → task_agent-1.3.1}/.agents/skills/next/SKILL.md +0 -0
  11. {task_agent-1.2.1 → task_agent-1.3.1}/.env +0 -0
  12. {task_agent-1.2.1 → task_agent-1.3.1}/.gemini/settings.json +0 -0
  13. {task_agent-1.2.1 → task_agent-1.3.1}/.github/workflows/ci.yml +0 -0
  14. {task_agent-1.2.1 → task_agent-1.3.1}/.github/workflows/publish.yml +0 -0
  15. {task_agent-1.2.1 → task_agent-1.3.1}/.gitignore +0 -0
  16. {task_agent-1.2.1 → task_agent-1.3.1}/.mise.toml +0 -0
  17. {task_agent-1.2.1 → task_agent-1.3.1}/.pre-commit-config.yaml +0 -0
  18. {task_agent-1.2.1 → task_agent-1.3.1}/.python-version +0 -0
  19. {task_agent-1.2.1 → task_agent-1.3.1}/.ta/agents/agy-cli/meta.toml +0 -0
  20. {task_agent-1.2.1 → task_agent-1.3.1}/.ta/agents/antigravity/meta.toml +0 -0
  21. {task_agent-1.2.1 → task_agent-1.3.1}/.ta/agents/gh/meta.toml +0 -0
  22. {task_agent-1.2.1 → task_agent-1.3.1}/.ta/agents/minimal/meta.toml +0 -0
  23. {task_agent-1.2.1 → task_agent-1.3.1}/.ta/agents/uat-aws/meta.toml +0 -0
  24. {task_agent-1.2.1 → task_agent-1.3.1}/.ta/worker +0 -0
  25. {task_agent-1.2.1 → task_agent-1.3.1}/.ta-config.json +0 -0
  26. {task_agent-1.2.1 → task_agent-1.3.1}/CLAUDE.md +0 -0
  27. {task_agent-1.2.1 → task_agent-1.3.1}/GEMINI.md +0 -0
  28. {task_agent-1.2.1 → task_agent-1.3.1}/Makefile +0 -0
  29. {task_agent-1.2.1 → task_agent-1.3.1}/README.md +0 -0
  30. {task_agent-1.2.1 → task_agent-1.3.1}/agent.env +0 -0
  31. {task_agent-1.2.1 → task_agent-1.3.1}/docs/README.md +0 -0
  32. {task_agent-1.2.1 → task_agent-1.3.1}/docs/STATION-MAP.md +0 -0
  33. {task_agent-1.2.1 → task_agent-1.3.1}/docs/architecture/README.md +0 -0
  34. {task_agent-1.2.1 → task_agent-1.3.1}/docs/chats/gemini-conversation-1773283034591.json +0 -0
  35. {task_agent-1.2.1 → task_agent-1.3.1}/docs/chats/project-init-to-v0.1.46.md +0 -0
  36. {task_agent-1.2.1 → task_agent-1.3.1}/docs/cli/README.md +0 -0
  37. {task_agent-1.2.1 → task_agent-1.3.1}/docs/development/README.md +0 -0
  38. {task_agent-1.2.1 → task_agent-1.3.1}/docs/task-agents.md +0 -0
  39. {task_agent-1.2.1 → task_agent-1.3.1}/docs/workflow/README.md +0 -0
  40. {task_agent-1.2.1 → task_agent-1.3.1}/docs/workflow/trivial_worker_policy.md +0 -0
  41. {task_agent-1.2.1 → task_agent-1.3.1}/opencode.json +0 -0
  42. {task_agent-1.2.1 → task_agent-1.3.1}/plugins/antigravity/.gemini-plugin/plugin.json +0 -0
  43. {task_agent-1.2.1 → task_agent-1.3.1}/plugins/antigravity/.mcp.json +0 -0
  44. {task_agent-1.2.1 → task_agent-1.3.1}/plugins/antigravity/README.md +0 -0
  45. {task_agent-1.2.1 → task_agent-1.3.1}/plugins/antigravity/hooks/hooks.json +0 -0
  46. {task_agent-1.2.1 → task_agent-1.3.1}/plugins/antigravity/hooks/session_start.sh +0 -0
  47. {task_agent-1.2.1 → task_agent-1.3.1}/plugins/antigravity/rules/task_agent.md +0 -0
  48. {task_agent-1.2.1 → task_agent-1.3.1}/plugins/antigravity/scripts/statusline.sh +0 -0
  49. {task_agent-1.2.1 → task_agent-1.3.1}/plugins/antigravity/skills/README.md +0 -0
  50. {task_agent-1.2.1 → task_agent-1.3.1}/plugins/antigravity/skills/agent-import/SKILL.md +0 -0
  51. {task_agent-1.2.1 → task_agent-1.3.1}/plugins/antigravity/skills/complete-task/SKILL.md +0 -0
  52. {task_agent-1.2.1 → task_agent-1.3.1}/plugins/antigravity/skills/mission-workflow/SKILL.md +0 -0
  53. {task_agent-1.2.1 → task_agent-1.3.1}/plugins/antigravity/skills/next/SKILL.md +0 -0
  54. {task_agent-1.2.1 → task_agent-1.3.1}/plugins/antigravity/skills/next-task/SKILL.md +0 -0
  55. {task_agent-1.2.1 → task_agent-1.3.1}/plugins/claude-code/.claude-plugin/plugin.json +0 -0
  56. {task_agent-1.2.1 → task_agent-1.3.1}/plugins/claude-code/.mcp.json +0 -0
  57. {task_agent-1.2.1 → task_agent-1.3.1}/plugins/claude-code/README.md +0 -0
  58. {task_agent-1.2.1 → task_agent-1.3.1}/plugins/claude-code/hooks/hooks.json +0 -0
  59. {task_agent-1.2.1 → task_agent-1.3.1}/plugins/claude-code/hooks/session_start.sh +0 -0
  60. {task_agent-1.2.1 → task_agent-1.3.1}/plugins/claude-code/scripts/statusline.sh +0 -0
  61. {task_agent-1.2.1 → task_agent-1.3.1}/plugins/claude-code/skills/agent-import/SKILL.md +0 -0
  62. {task_agent-1.2.1 → task_agent-1.3.1}/plugins/claude-code/skills/complete-task/SKILL.md +0 -0
  63. {task_agent-1.2.1 → task_agent-1.3.1}/plugins/claude-code/skills/mission-workflow/SKILL.md +0 -0
  64. {task_agent-1.2.1 → task_agent-1.3.1}/plugins/claude-code/skills/next-task/SKILL.md +0 -0
  65. {task_agent-1.2.1 → task_agent-1.3.1}/plugins/codex/.mcp.json +0 -0
  66. {task_agent-1.2.1 → task_agent-1.3.1}/plugins/codex/README.md +0 -0
  67. {task_agent-1.2.1 → task_agent-1.3.1}/plugins/copilot/.mcp.json +0 -0
  68. {task_agent-1.2.1 → task_agent-1.3.1}/plugins/copilot/README.md +0 -0
  69. {task_agent-1.2.1 → task_agent-1.3.1}/plugins/grok/.mcp.json +0 -0
  70. {task_agent-1.2.1 → task_agent-1.3.1}/plugins/grok/README.md +0 -0
  71. {task_agent-1.2.1 → task_agent-1.3.1}/scripts/clean_duplicates.py +0 -0
  72. {task_agent-1.2.1 → task_agent-1.3.1}/scripts/migrate_tasks.py +0 -0
  73. {task_agent-1.2.1 → task_agent-1.3.1}/scripts/recover_dates.py +0 -0
  74. {task_agent-1.2.1 → task_agent-1.3.1}/sidecars/adk-worker/.env.example +0 -0
  75. {task_agent-1.2.1 → task_agent-1.3.1}/sidecars/adk-worker/README.md +0 -0
  76. {task_agent-1.2.1 → task_agent-1.3.1}/sidecars/adk-worker/instructions/manager.md +0 -0
  77. {task_agent-1.2.1 → task_agent-1.3.1}/sidecars/adk-worker/instructions/validator.md +0 -0
  78. {task_agent-1.2.1 → task_agent-1.3.1}/sidecars/adk-worker/instructions/worker.md +0 -0
  79. {task_agent-1.2.1 → task_agent-1.3.1}/sidecars/adk-worker/profiles/default.json +0 -0
  80. {task_agent-1.2.1 → task_agent-1.3.1}/sidecars/adk-worker/pyproject.toml +0 -0
  81. {task_agent-1.2.1 → task_agent-1.3.1}/sidecars/adk-worker/uv.lock +0 -0
  82. {task_agent-1.2.1 → task_agent-1.3.1}/sidecars/adk-worker/worker.py +0 -0
  83. {task_agent-1.2.1 → task_agent-1.3.1}/skills/README.md +0 -0
  84. {task_agent-1.2.1 → task_agent-1.3.1}/skills/agent-import/SKILL.md +0 -0
  85. {task_agent-1.2.1 → task_agent-1.3.1}/skills/complete-task/SKILL.md +0 -0
  86. {task_agent-1.2.1 → task_agent-1.3.1}/skills/mission-workflow/SKILL.md +0 -0
  87. {task_agent-1.2.1 → task_agent-1.3.1}/skills/next/SKILL.md +0 -0
  88. {task_agent-1.2.1 → task_agent-1.3.1}/skills/next-task/SKILL.md +0 -0
  89. {task_agent-1.2.1 → task_agent-1.3.1}/src/taskagent/__init__.py +0 -0
  90. {task_agent-1.2.1 → task_agent-1.3.1}/src/taskagent/__main__.py +0 -0
  91. {task_agent-1.2.1 → task_agent-1.3.1}/src/taskagent/agent.py +0 -0
  92. {task_agent-1.2.1 → task_agent-1.3.1}/src/taskagent/agent_registry.py +0 -0
  93. {task_agent-1.2.1 → task_agent-1.3.1}/src/taskagent/audit.py +0 -0
  94. {task_agent-1.2.1 → task_agent-1.3.1}/src/taskagent/chat/config.py +0 -0
  95. {task_agent-1.2.1 → task_agent-1.3.1}/src/taskagent/chat/discovery.py +0 -0
  96. {task_agent-1.2.1 → task_agent-1.3.1}/src/taskagent/config.py +0 -0
  97. {task_agent-1.2.1 → task_agent-1.3.1}/src/taskagent/discovery.py +0 -0
  98. {task_agent-1.2.1 → task_agent-1.3.1}/src/taskagent/importers/__init__.py +0 -0
  99. {task_agent-1.2.1 → task_agent-1.3.1}/src/taskagent/importers/antigravity.py +0 -0
  100. {task_agent-1.2.1 → task_agent-1.3.1}/src/taskagent/importers/base.py +0 -0
  101. {task_agent-1.2.1 → task_agent-1.3.1}/src/taskagent/importers/claude_code.py +0 -0
  102. {task_agent-1.2.1 → task_agent-1.3.1}/src/taskagent/importers/generic.py +0 -0
  103. {task_agent-1.2.1 → task_agent-1.3.1}/src/taskagent/inbox.py +0 -0
  104. {task_agent-1.2.1 → task_agent-1.3.1}/src/taskagent/manager.py +0 -0
  105. {task_agent-1.2.1 → task_agent-1.3.1}/src/taskagent/models/__init__.py +0 -0
  106. {task_agent-1.2.1 → task_agent-1.3.1}/src/taskagent/models/issue.py +0 -0
  107. {task_agent-1.2.1 → task_agent-1.3.1}/src/taskagent/models/metric.py +0 -0
  108. {task_agent-1.2.1 → task_agent-1.3.1}/src/taskagent/perf.py +0 -0
  109. {task_agent-1.2.1 → task_agent-1.3.1}/src/taskagent/plugins/__init__.py +0 -0
  110. {task_agent-1.2.1 → task_agent-1.3.1}/src/taskagent/plugins/github.py +0 -0
  111. {task_agent-1.2.1 → task_agent-1.3.1}/src/taskagent/runner.py +0 -0
  112. {task_agent-1.2.1 → task_agent-1.3.1}/src/taskagent/store_registry.py +0 -0
  113. {task_agent-1.2.1 → task_agent-1.3.1}/src/taskagent/templates.py +0 -0
  114. {task_agent-1.2.1 → task_agent-1.3.1}/src/taskagent/theme.py +0 -0
  115. {task_agent-1.2.1 → task_agent-1.3.1}/test-dir/opencode.json +0 -0
  116. {task_agent-1.2.1 → task_agent-1.3.1}/tests/conftest.py +0 -0
  117. {task_agent-1.2.1 → task_agent-1.3.1}/tests/e2e/test_agent_sandboxing.sh +0 -0
  118. {task_agent-1.2.1 → task_agent-1.3.1}/tests/test_agent.py +0 -0
  119. {task_agent-1.2.1 → task_agent-1.3.1}/tests/test_agent_registry.py +0 -0
  120. {task_agent-1.2.1 → task_agent-1.3.1}/tests/test_agy_cli_worker.py +0 -0
  121. {task_agent-1.2.1 → task_agent-1.3.1}/tests/test_atomic_mutations.py +0 -0
  122. {task_agent-1.2.1 → task_agent-1.3.1}/tests/test_auto_commit.py +0 -0
  123. {task_agent-1.2.1 → task_agent-1.3.1}/tests/test_chat_config.py +0 -0
  124. {task_agent-1.2.1 → task_agent-1.3.1}/tests/test_chat_discovery.py +0 -0
  125. {task_agent-1.2.1 → task_agent-1.3.1}/tests/test_cli.py +0 -0
  126. {task_agent-1.2.1 → task_agent-1.3.1}/tests/test_cli_windows.py +0 -0
  127. {task_agent-1.2.1 → task_agent-1.3.1}/tests/test_discovery.py +0 -0
  128. {task_agent-1.2.1 → task_agent-1.3.1}/tests/test_github_token.py +0 -0
  129. {task_agent-1.2.1 → task_agent-1.3.1}/tests/test_importers.py +0 -0
  130. {task_agent-1.2.1 → task_agent-1.3.1}/tests/test_inbox.py +0 -0
  131. {task_agent-1.2.1 → task_agent-1.3.1}/tests/test_inbox_watch.py +0 -0
  132. {task_agent-1.2.1 → task_agent-1.3.1}/tests/test_lease.py +0 -0
  133. {task_agent-1.2.1 → task_agent-1.3.1}/tests/test_manager.py +0 -0
  134. {task_agent-1.2.1 → task_agent-1.3.1}/tests/test_manager_git.py +0 -0
  135. {task_agent-1.2.1 → task_agent-1.3.1}/tests/test_metrics.py +0 -0
  136. {task_agent-1.2.1 → task_agent-1.3.1}/tests/test_migration.py +0 -0
  137. {task_agent-1.2.1 → task_agent-1.3.1}/tests/test_perf.py +0 -0
  138. {task_agent-1.2.1 → task_agent-1.3.1}/tests/test_plugin_antigravity.py +0 -0
  139. {task_agent-1.2.1 → task_agent-1.3.1}/tests/test_plugin_claude.py +0 -0
  140. {task_agent-1.2.1 → task_agent-1.3.1}/tests/test_project_host_root.py +0 -0
  141. {task_agent-1.2.1 → task_agent-1.3.1}/tests/test_rebind.py +0 -0
  142. {task_agent-1.2.1 → task_agent-1.3.1}/tests/test_remote_attach.py +0 -0
  143. {task_agent-1.2.1 → task_agent-1.3.1}/tests/test_remote_create.py +0 -0
  144. {task_agent-1.2.1 → task_agent-1.3.1}/tests/test_repo_resolve.py +0 -0
  145. {task_agent-1.2.1 → task_agent-1.3.1}/tests/test_runner.py +0 -0
  146. {task_agent-1.2.1 → task_agent-1.3.1}/tests/test_store_registry.py +0 -0
  147. {task_agent-1.2.1 → task_agent-1.3.1}/tests/test_store_symlink.py +0 -0
  148. {task_agent-1.2.1 → task_agent-1.3.1}/tests/test_templates.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: task-agent
3
- Version: 1.2.1
3
+ Version: 1.3.1
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 = "1.2.1"
3
+ version = "1.3.1"
4
4
  description = "A prioritized, file-based task queue for autonomous agentic workers"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -11,13 +11,19 @@ from taskagent.chat.discovery import (
11
11
  DiscoveredChat,
12
12
  discover_agent_chats,
13
13
  )
14
+ from taskagent.chat.last_used import (
15
+ AgentLastUsedInfo,
16
+ get_last_active_agents,
17
+ )
14
18
 
15
19
  __all__ = [
20
+ "AgentLastUsedInfo",
16
21
  "ChatConfig",
17
22
  "DiscoveredChat",
18
23
  "RetentionConfig",
19
24
  "S3Config",
20
25
  "SummarizationConfig",
21
26
  "discover_agent_chats",
27
+ "get_last_active_agents",
22
28
  "load_chat_config",
23
29
  ]
@@ -0,0 +1,200 @@
1
+ """Module for discovering and extracting last-active agent session info."""
2
+
3
+ import json
4
+ from dataclasses import dataclass
5
+ from datetime import datetime, timezone
6
+ from pathlib import Path
7
+ from typing import Any, List, Optional, Tuple
8
+
9
+ from agent_registry import (
10
+ AgentCLIInfo,
11
+ DiscoveredChat,
12
+ discover_agent_chats,
13
+ get_agent_cli_registry,
14
+ )
15
+ from taskagent.store_registry import project_host_root
16
+
17
+
18
+ @dataclass
19
+ class AgentLastUsedInfo:
20
+ """Information on the last active session for an agent CLI in a repository."""
21
+
22
+ agent_id: str
23
+ agent_name: str
24
+ description: str
25
+ last_active: datetime
26
+ last_user_comment: str
27
+ log_path: Path
28
+
29
+
30
+ def _extract_text(node: Any) -> str:
31
+ """Recursively extract string content from dicts, lists, or primitive strings."""
32
+ if isinstance(node, str):
33
+ return node
34
+ elif isinstance(node, dict):
35
+ if "text" in node and isinstance(node["text"], str):
36
+ return node["text"]
37
+ if "content" in node:
38
+ return _extract_text(node["content"])
39
+ if "message" in node:
40
+ return _extract_text(node["message"])
41
+ elif isinstance(node, list):
42
+ parts = [_extract_text(item) for item in node]
43
+ return " ".join(p for p in parts if p)
44
+ return ""
45
+
46
+
47
+ def _is_user_role(role_val: Any) -> bool:
48
+ if isinstance(role_val, str):
49
+ r = role_val.lower()
50
+ return r in ("user", "say", "user_input", "user_explicit", "human", "user_feedback")
51
+ return False
52
+
53
+
54
+ def _parse_last_user_comment_and_timestamp(
55
+ discovered: DiscoveredChat,
56
+ ) -> Tuple[datetime, str]:
57
+ """Extract timestamp and first 200 chars of last user comment from chat log file."""
58
+ path = discovered.path
59
+ file_mtime = datetime.fromtimestamp(path.stat().st_mtime, tz=timezone.utc)
60
+ user_comment = ""
61
+
62
+ if not path.is_file():
63
+ return file_mtime, user_comment
64
+
65
+ try:
66
+ if discovered.parser_type == "jsonl":
67
+ text_lines = path.read_text(encoding="utf-8", errors="replace").splitlines()
68
+ for line in reversed(text_lines):
69
+ line = line.strip()
70
+ if not line:
71
+ continue
72
+ try:
73
+ data = json.loads(line)
74
+ if isinstance(data, dict):
75
+ msg = data.get("message")
76
+ role = (
77
+ data.get("type")
78
+ or data.get("role")
79
+ or data.get("source")
80
+ or (msg.get("role") if isinstance(msg, dict) else None)
81
+ )
82
+ if _is_user_role(role):
83
+ raw_comment = _extract_text(msg if msg else data).strip()
84
+ if (
85
+ raw_comment
86
+ and not raw_comment.startswith("<local-command-")
87
+ and not raw_comment.startswith("<command-name>")
88
+ ):
89
+ user_comment = raw_comment
90
+ break
91
+ except Exception:
92
+ continue
93
+
94
+ elif discovered.parser_type == "json":
95
+ raw_text = path.read_text(encoding="utf-8", errors="replace")
96
+ data = json.loads(raw_text)
97
+ messages = []
98
+ if isinstance(data, list):
99
+ messages = data
100
+ elif isinstance(data, dict):
101
+ messages = (
102
+ data.get("messages")
103
+ or data.get("steps")
104
+ or data.get("history")
105
+ or []
106
+ )
107
+
108
+ if isinstance(messages, list):
109
+ for msg in reversed(messages):
110
+ if isinstance(msg, dict):
111
+ role = (
112
+ msg.get("say")
113
+ or msg.get("role")
114
+ or msg.get("type")
115
+ or msg.get("source")
116
+ )
117
+ if _is_user_role(role):
118
+ raw_comment = _extract_text(msg).strip()
119
+ if raw_comment:
120
+ user_comment = raw_comment
121
+ break
122
+
123
+ elif discovered.parser_type == "markdown":
124
+ raw_text = path.read_text(encoding="utf-8", errors="replace")
125
+ text_lines = raw_text.splitlines()
126
+ user_blocks = []
127
+ current_block = []
128
+ in_user = False
129
+ for line in text_lines:
130
+ if line.startswith("#") or line.startswith("> "):
131
+ if in_user and current_block:
132
+ user_blocks.append("\n".join(current_block).strip())
133
+ current_block = []
134
+ in_user = ("user" in line.lower() or "human" in line.lower())
135
+ elif in_user:
136
+ current_block.append(line)
137
+ if in_user and current_block:
138
+ user_blocks.append("\n".join(current_block).strip())
139
+ if user_blocks:
140
+ user_comment = user_blocks[-1]
141
+
142
+ except Exception:
143
+ pass
144
+
145
+ user_comment = " ".join(user_comment.split())
146
+
147
+ if len(user_comment) > 200:
148
+ user_comment = user_comment[:197] + "..."
149
+
150
+ return file_mtime, user_comment
151
+
152
+
153
+ def get_last_active_agents(
154
+ project_dir: Optional[Path] = None,
155
+ limit: int = 5,
156
+ ) -> List[AgentLastUsedInfo]:
157
+ """Find and rank most recently active AI agent CLIs in a project repository."""
158
+ if project_dir is None:
159
+ project_dir = Path.cwd()
160
+
161
+ host_root = project_host_root(project_dir)
162
+ discovered_chats = discover_agent_chats(project_dir=host_root)
163
+ registry = get_agent_cli_registry()
164
+
165
+ agent_chats_map: dict[str, List[DiscoveredChat]] = {}
166
+ for chat in discovered_chats:
167
+ agent_chats_map.setdefault(chat.agent_id, []).append(chat)
168
+
169
+ results: List[AgentLastUsedInfo] = []
170
+
171
+ for agent_id, chats in agent_chats_map.items():
172
+ agent_info: Optional[AgentCLIInfo] = registry.get(agent_id)
173
+ name = agent_info.name if agent_info else agent_id.capitalize()
174
+ description = agent_info.description if agent_info else ""
175
+
176
+ newest_mtime: Optional[datetime] = None
177
+ newest_comment: str = ""
178
+ newest_path: Optional[Path] = None
179
+
180
+ for chat in chats:
181
+ mtime, comment = _parse_last_user_comment_and_timestamp(chat)
182
+ if newest_mtime is None or mtime > newest_mtime:
183
+ newest_mtime = mtime
184
+ newest_comment = comment
185
+ newest_path = chat.path
186
+
187
+ if newest_mtime is not None and newest_path is not None:
188
+ results.append(
189
+ AgentLastUsedInfo(
190
+ agent_id=agent_id,
191
+ agent_name=name,
192
+ description=description,
193
+ last_active=newest_mtime,
194
+ last_user_comment=newest_comment,
195
+ log_path=newest_path,
196
+ )
197
+ )
198
+
199
+ results.sort(key=lambda item: item.last_active, reverse=True)
200
+ return results[:limit]
@@ -4232,6 +4232,63 @@ def cmd_agent_import(
4232
4232
  sys.exit(1)
4233
4233
 
4234
4234
 
4235
+ def cmd_agent_last_used(
4236
+ console: Console,
4237
+ path_arg: Optional[str] = None,
4238
+ limit: int = 5,
4239
+ json_format: bool = False,
4240
+ ) -> None:
4241
+ """Handler for 'ta agent last-used' subcommand."""
4242
+ from rich import box
4243
+ from taskagent.chat.last_used import get_last_active_agents
4244
+
4245
+ target_path = Path(path_arg).resolve() if path_arg else Path.cwd()
4246
+ results = get_last_active_agents(project_dir=target_path, limit=limit)
4247
+
4248
+ if json_format:
4249
+ output = [
4250
+ {
4251
+ "agent_id": item.agent_id,
4252
+ "agent_name": item.agent_name,
4253
+ "description": item.description,
4254
+ "last_active": item.last_active.isoformat(),
4255
+ "last_user_comment": item.last_user_comment,
4256
+ "log_path": str(item.log_path),
4257
+ }
4258
+ for item in results
4259
+ ]
4260
+ print(json.dumps(output, indent=2))
4261
+ return
4262
+
4263
+ if not results:
4264
+ console.print(
4265
+ f"[yellow]No agent chat sessions found for repository at [bold]{target_path}[/bold].[/yellow]"
4266
+ )
4267
+ return
4268
+
4269
+ console.print(
4270
+ f"\n[bold green]🤖 Recently Active AI Agents[/bold green] [dim]({target_path})[/dim]\n"
4271
+ )
4272
+
4273
+ table = Table(box=box.ROUNDED, show_header=True, header_style="bold cyan")
4274
+ table.add_column("#", style="dim", width=3)
4275
+ table.add_column("Agent CLI", style="bold yellow")
4276
+ table.add_column("Last Active", style="green")
4277
+ table.add_column("Last Comment Snippet", style="dim white")
4278
+
4279
+ for idx, item in enumerate(results, start=1):
4280
+ rel_time = item.last_active.strftime("%Y-%m-%d %H:%M:%S UTC")
4281
+ comment_snippet = item.last_user_comment if item.last_user_comment else "[dim](no prompt comment logged)[/dim]"
4282
+ table.add_row(
4283
+ str(idx),
4284
+ item.agent_name,
4285
+ rel_time,
4286
+ comment_snippet,
4287
+ )
4288
+
4289
+ console.print(table)
4290
+
4291
+
4235
4292
  def cmd_version(
4236
4293
  console: Console,
4237
4294
  promote: Optional[str] = None,
@@ -6055,6 +6112,27 @@ Start working on a task. This command automates the following workflow:
6055
6112
  action="store_true",
6056
6113
  help="Output result as JSON",
6057
6114
  )
6115
+ agent_last_used_p = agent_sub.add_parser(
6116
+ "last-used",
6117
+ help="List AI coding agents recently active in this repository",
6118
+ )
6119
+ agent_last_used_p.add_argument(
6120
+ "path",
6121
+ nargs="?",
6122
+ default=None,
6123
+ help="Target repository directory (defaults to cwd)",
6124
+ )
6125
+ agent_last_used_p.add_argument(
6126
+ "--limit",
6127
+ type=int,
6128
+ default=5,
6129
+ help="Maximum number of active agents to return (default: 5)",
6130
+ )
6131
+ agent_last_used_p.add_argument(
6132
+ "--json",
6133
+ action="store_true",
6134
+ help="Output result as JSON",
6135
+ )
6058
6136
 
6059
6137
  # plan
6060
6138
  subparsers.add_parser("plan", help="View or edit the project plan")
@@ -6738,9 +6816,16 @@ TA_STRATEGY_COOLDOWN_HOURS environment variable.
6738
6816
  file_path=args.file,
6739
6817
  json_format=args.json,
6740
6818
  )
6819
+ elif args.agent_subcommand == "last-used":
6820
+ cmd_agent_last_used(
6821
+ console,
6822
+ path_arg=args.path,
6823
+ limit=args.limit,
6824
+ json_format=args.json,
6825
+ )
6741
6826
  else:
6742
6827
  console.print(
6743
- "[yellow]Unknown agent subcommand. Use 'ta agent import'.[/yellow]"
6828
+ "[yellow]Unknown agent subcommand. Use 'ta agent import' or 'ta agent last-used'.[/yellow]"
6744
6829
  )
6745
6830
  elif args.command == "push":
6746
6831
  cmd_push(console, manager)
@@ -1431,6 +1431,47 @@ def import_agent_tasks(
1431
1431
  )
1432
1432
 
1433
1433
 
1434
+ @mcp.tool()
1435
+ def get_last_active_agent(
1436
+ path: str = "",
1437
+ limit: int = 5,
1438
+ ) -> str:
1439
+ """Find and rank most recently active AI coding agent CLIs in a project repository.
1440
+
1441
+ Args:
1442
+ path: Target repository directory path (defaults to working directory if empty).
1443
+ limit: Maximum number of active agents to return (default: 5).
1444
+
1445
+ Returns:
1446
+ Formatted summary list of recently active AI agent CLIs, sorted descending by last active date,
1447
+ including description and first 200 characters of the last user comment.
1448
+ """
1449
+ from pathlib import Path
1450
+ from taskagent.chat.last_used import get_last_active_agents
1451
+
1452
+ target_path = Path(path).resolve() if path and path.strip() else Path.cwd()
1453
+ results = get_last_active_agents(project_dir=target_path, limit=limit)
1454
+
1455
+ if not results:
1456
+ return f"No agent chat sessions found for repository at {target_path}."
1457
+
1458
+ lines = [f"### 🤖 Recently Active AI Agents ({target_path})\n"]
1459
+ for idx, item in enumerate(results, start=1):
1460
+ rel_time = item.last_active.strftime("%Y-%m-%d %H:%M:%S UTC")
1461
+ comment_snippet = (
1462
+ item.last_user_comment
1463
+ if item.last_user_comment
1464
+ else "(no prompt comment logged)"
1465
+ )
1466
+ lines.append(f"{idx}. **{item.agent_name}** (`{item.agent_id}`)")
1467
+ lines.append(f" - **Description**: {item.description}")
1468
+ lines.append(f" - **Last Active**: {rel_time}")
1469
+ lines.append(f" - **Last Comment Snippet**: {comment_snippet}")
1470
+ lines.append("")
1471
+
1472
+ return "\n".join(lines).strip()
1473
+
1474
+
1434
1475
  def run_mcp_server():
1435
1476
  """Main entry point to run the MCP server."""
1436
1477
  mcp.run()
@@ -0,0 +1,86 @@
1
+ import json
2
+
3
+ from agent_registry.models import DiscoveredChat
4
+ from taskagent.chat.last_used import (
5
+ AgentLastUsedInfo,
6
+ _parse_last_user_comment_and_timestamp,
7
+ get_last_active_agents,
8
+ )
9
+
10
+
11
+ def test_parse_last_user_comment_jsonl(tmp_path):
12
+ log_file = tmp_path / "claude_session.jsonl"
13
+ lines = [
14
+ json.dumps({"type": "system", "content": "Init"}),
15
+ json.dumps({"type": "user", "content": "First prompt from user"}),
16
+ json.dumps({"type": "assistant", "content": "AI response"}),
17
+ json.dumps({"type": "user", "content": "Please refactor the database connector for production"}),
18
+ ]
19
+ log_file.write_text("\n".join(lines))
20
+
21
+ chat = DiscoveredChat(agent_id="claude", path=log_file, parser_type="jsonl")
22
+ mtime, comment = _parse_last_user_comment_and_timestamp(chat)
23
+
24
+ assert comment == "Please refactor the database connector for production"
25
+ assert mtime is not None
26
+
27
+
28
+ def test_parse_last_user_comment_json(tmp_path):
29
+ log_file = tmp_path / "session.json"
30
+ data = {
31
+ "messages": [
32
+ {"say": "user", "text": "Initial task message"},
33
+ {"say": "assistant", "text": "Working on it"},
34
+ {"say": "user", "text": "Add unit tests for the chat discovery module"},
35
+ ]
36
+ }
37
+ log_file.write_text(json.dumps(data))
38
+
39
+ chat = DiscoveredChat(agent_id="agy", path=log_file, parser_type="json")
40
+ mtime, comment = _parse_last_user_comment_and_timestamp(chat)
41
+
42
+ assert comment == "Add unit tests for the chat discovery module"
43
+
44
+
45
+ def test_parse_last_user_comment_markdown(tmp_path):
46
+ log_file = tmp_path / ".aider.chat.history.md"
47
+ content = """# session log
48
+
49
+ #### user
50
+ Fix memory leak in background worker queue
51
+
52
+ #### assistant
53
+ Analyzing queue implementation...
54
+ """
55
+ log_file.write_text(content)
56
+
57
+ chat = DiscoveredChat(agent_id="aider", path=log_file, parser_type="markdown")
58
+ mtime, comment = _parse_last_user_comment_and_timestamp(chat)
59
+
60
+ assert "Fix memory leak in background worker queue" in comment
61
+
62
+
63
+ def test_get_last_active_agents(monkeypatch, tmp_path):
64
+ f1 = tmp_path / "claude.jsonl"
65
+ f1.write_text(json.dumps({"type": "user", "content": "Claude task"}))
66
+
67
+ f2 = tmp_path / "agy.json"
68
+ f2.write_text(json.dumps([{"say": "user", "text": "Antigravity task"}]))
69
+
70
+ discovered = [
71
+ DiscoveredChat(agent_id="claude", path=f1, parser_type="jsonl"),
72
+ DiscoveredChat(agent_id="agy", path=f2, parser_type="json"),
73
+ ]
74
+
75
+ monkeypatch.setattr(
76
+ "taskagent.chat.last_used.discover_agent_chats",
77
+ lambda project_dir=None: discovered,
78
+ )
79
+
80
+ results = get_last_active_agents(project_dir=tmp_path, limit=5)
81
+ assert len(results) == 2
82
+ agent_ids = {r.agent_id for r in results}
83
+ assert agent_ids == {"claude", "agy"}
84
+ for r in results:
85
+ assert isinstance(r, AgentLastUsedInfo)
86
+ assert r.description != ""
@@ -638,6 +638,7 @@ EXPECTED_TOOLS = {
638
638
  "ack_inbox",
639
639
  "watch_inbox_mcp",
640
640
  "import_agent_tasks",
641
+ "get_last_active_agent",
641
642
  "start_task",
642
643
  "run_task",
643
644
  "get_store_log",
@@ -1441,7 +1441,7 @@ wheels = [
1441
1441
 
1442
1442
  [[package]]
1443
1443
  name = "task-agent"
1444
- version = "1.2.1"
1444
+ version = "1.3.1"
1445
1445
  source = { editable = "." }
1446
1446
  dependencies = [
1447
1447
  { name = "agent-cli-registry" },
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