task-agent 1.2.1__tar.gz → 1.3.0__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.0}/PKG-INFO +1 -1
  2. {task_agent-1.2.1 → task_agent-1.3.0}/pyproject.toml +1 -1
  3. {task_agent-1.2.1 → task_agent-1.3.0}/src/taskagent/chat/__init__.py +6 -0
  4. task_agent-1.3.0/src/taskagent/chat/last_used.py +200 -0
  5. {task_agent-1.2.1 → task_agent-1.3.0}/src/taskagent/cli.py +88 -1
  6. {task_agent-1.2.1 → task_agent-1.3.0}/src/taskagent/mcp.py +41 -0
  7. task_agent-1.3.0/tests/test_chat_last_used.py +86 -0
  8. {task_agent-1.2.1 → task_agent-1.3.0}/tests/test_mcp.py +1 -0
  9. {task_agent-1.2.1 → task_agent-1.3.0}/uv.lock +1 -1
  10. {task_agent-1.2.1 → task_agent-1.3.0}/.agents/skills/next/SKILL.md +0 -0
  11. {task_agent-1.2.1 → task_agent-1.3.0}/.env +0 -0
  12. {task_agent-1.2.1 → task_agent-1.3.0}/.gemini/settings.json +0 -0
  13. {task_agent-1.2.1 → task_agent-1.3.0}/.github/workflows/ci.yml +0 -0
  14. {task_agent-1.2.1 → task_agent-1.3.0}/.github/workflows/publish.yml +0 -0
  15. {task_agent-1.2.1 → task_agent-1.3.0}/.gitignore +0 -0
  16. {task_agent-1.2.1 → task_agent-1.3.0}/.mise.toml +0 -0
  17. {task_agent-1.2.1 → task_agent-1.3.0}/.pre-commit-config.yaml +0 -0
  18. {task_agent-1.2.1 → task_agent-1.3.0}/.python-version +0 -0
  19. {task_agent-1.2.1 → task_agent-1.3.0}/.ta/agents/agy-cli/meta.toml +0 -0
  20. {task_agent-1.2.1 → task_agent-1.3.0}/.ta/agents/antigravity/meta.toml +0 -0
  21. {task_agent-1.2.1 → task_agent-1.3.0}/.ta/agents/gh/meta.toml +0 -0
  22. {task_agent-1.2.1 → task_agent-1.3.0}/.ta/agents/minimal/meta.toml +0 -0
  23. {task_agent-1.2.1 → task_agent-1.3.0}/.ta/agents/uat-aws/meta.toml +0 -0
  24. {task_agent-1.2.1 → task_agent-1.3.0}/.ta/worker +0 -0
  25. {task_agent-1.2.1 → task_agent-1.3.0}/.ta-config.json +0 -0
  26. {task_agent-1.2.1 → task_agent-1.3.0}/CLAUDE.md +0 -0
  27. {task_agent-1.2.1 → task_agent-1.3.0}/GEMINI.md +0 -0
  28. {task_agent-1.2.1 → task_agent-1.3.0}/Makefile +0 -0
  29. {task_agent-1.2.1 → task_agent-1.3.0}/README.md +0 -0
  30. {task_agent-1.2.1 → task_agent-1.3.0}/agent.env +0 -0
  31. {task_agent-1.2.1 → task_agent-1.3.0}/docs/README.md +0 -0
  32. {task_agent-1.2.1 → task_agent-1.3.0}/docs/STATION-MAP.md +0 -0
  33. {task_agent-1.2.1 → task_agent-1.3.0}/docs/architecture/README.md +0 -0
  34. {task_agent-1.2.1 → task_agent-1.3.0}/docs/chats/gemini-conversation-1773283034591.json +0 -0
  35. {task_agent-1.2.1 → task_agent-1.3.0}/docs/chats/project-init-to-v0.1.46.md +0 -0
  36. {task_agent-1.2.1 → task_agent-1.3.0}/docs/cli/README.md +0 -0
  37. {task_agent-1.2.1 → task_agent-1.3.0}/docs/development/README.md +0 -0
  38. {task_agent-1.2.1 → task_agent-1.3.0}/docs/task-agents.md +0 -0
  39. {task_agent-1.2.1 → task_agent-1.3.0}/docs/workflow/README.md +0 -0
  40. {task_agent-1.2.1 → task_agent-1.3.0}/docs/workflow/trivial_worker_policy.md +0 -0
  41. {task_agent-1.2.1 → task_agent-1.3.0}/opencode.json +0 -0
  42. {task_agent-1.2.1 → task_agent-1.3.0}/plugins/antigravity/.gemini-plugin/plugin.json +0 -0
  43. {task_agent-1.2.1 → task_agent-1.3.0}/plugins/antigravity/.mcp.json +0 -0
  44. {task_agent-1.2.1 → task_agent-1.3.0}/plugins/antigravity/README.md +0 -0
  45. {task_agent-1.2.1 → task_agent-1.3.0}/plugins/antigravity/hooks/hooks.json +0 -0
  46. {task_agent-1.2.1 → task_agent-1.3.0}/plugins/antigravity/hooks/session_start.sh +0 -0
  47. {task_agent-1.2.1 → task_agent-1.3.0}/plugins/antigravity/rules/task_agent.md +0 -0
  48. {task_agent-1.2.1 → task_agent-1.3.0}/plugins/antigravity/scripts/statusline.sh +0 -0
  49. {task_agent-1.2.1 → task_agent-1.3.0}/plugins/antigravity/skills/README.md +0 -0
  50. {task_agent-1.2.1 → task_agent-1.3.0}/plugins/antigravity/skills/agent-import/SKILL.md +0 -0
  51. {task_agent-1.2.1 → task_agent-1.3.0}/plugins/antigravity/skills/complete-task/SKILL.md +0 -0
  52. {task_agent-1.2.1 → task_agent-1.3.0}/plugins/antigravity/skills/mission-workflow/SKILL.md +0 -0
  53. {task_agent-1.2.1 → task_agent-1.3.0}/plugins/antigravity/skills/next/SKILL.md +0 -0
  54. {task_agent-1.2.1 → task_agent-1.3.0}/plugins/antigravity/skills/next-task/SKILL.md +0 -0
  55. {task_agent-1.2.1 → task_agent-1.3.0}/plugins/claude-code/.claude-plugin/plugin.json +0 -0
  56. {task_agent-1.2.1 → task_agent-1.3.0}/plugins/claude-code/.mcp.json +0 -0
  57. {task_agent-1.2.1 → task_agent-1.3.0}/plugins/claude-code/README.md +0 -0
  58. {task_agent-1.2.1 → task_agent-1.3.0}/plugins/claude-code/hooks/hooks.json +0 -0
  59. {task_agent-1.2.1 → task_agent-1.3.0}/plugins/claude-code/hooks/session_start.sh +0 -0
  60. {task_agent-1.2.1 → task_agent-1.3.0}/plugins/claude-code/scripts/statusline.sh +0 -0
  61. {task_agent-1.2.1 → task_agent-1.3.0}/plugins/claude-code/skills/agent-import/SKILL.md +0 -0
  62. {task_agent-1.2.1 → task_agent-1.3.0}/plugins/claude-code/skills/complete-task/SKILL.md +0 -0
  63. {task_agent-1.2.1 → task_agent-1.3.0}/plugins/claude-code/skills/mission-workflow/SKILL.md +0 -0
  64. {task_agent-1.2.1 → task_agent-1.3.0}/plugins/claude-code/skills/next-task/SKILL.md +0 -0
  65. {task_agent-1.2.1 → task_agent-1.3.0}/plugins/codex/.mcp.json +0 -0
  66. {task_agent-1.2.1 → task_agent-1.3.0}/plugins/codex/README.md +0 -0
  67. {task_agent-1.2.1 → task_agent-1.3.0}/plugins/copilot/.mcp.json +0 -0
  68. {task_agent-1.2.1 → task_agent-1.3.0}/plugins/copilot/README.md +0 -0
  69. {task_agent-1.2.1 → task_agent-1.3.0}/plugins/grok/.mcp.json +0 -0
  70. {task_agent-1.2.1 → task_agent-1.3.0}/plugins/grok/README.md +0 -0
  71. {task_agent-1.2.1 → task_agent-1.3.0}/scripts/clean_duplicates.py +0 -0
  72. {task_agent-1.2.1 → task_agent-1.3.0}/scripts/migrate_tasks.py +0 -0
  73. {task_agent-1.2.1 → task_agent-1.3.0}/scripts/recover_dates.py +0 -0
  74. {task_agent-1.2.1 → task_agent-1.3.0}/sidecars/adk-worker/.env.example +0 -0
  75. {task_agent-1.2.1 → task_agent-1.3.0}/sidecars/adk-worker/README.md +0 -0
  76. {task_agent-1.2.1 → task_agent-1.3.0}/sidecars/adk-worker/instructions/manager.md +0 -0
  77. {task_agent-1.2.1 → task_agent-1.3.0}/sidecars/adk-worker/instructions/validator.md +0 -0
  78. {task_agent-1.2.1 → task_agent-1.3.0}/sidecars/adk-worker/instructions/worker.md +0 -0
  79. {task_agent-1.2.1 → task_agent-1.3.0}/sidecars/adk-worker/profiles/default.json +0 -0
  80. {task_agent-1.2.1 → task_agent-1.3.0}/sidecars/adk-worker/pyproject.toml +0 -0
  81. {task_agent-1.2.1 → task_agent-1.3.0}/sidecars/adk-worker/uv.lock +0 -0
  82. {task_agent-1.2.1 → task_agent-1.3.0}/sidecars/adk-worker/worker.py +0 -0
  83. {task_agent-1.2.1 → task_agent-1.3.0}/skills/README.md +0 -0
  84. {task_agent-1.2.1 → task_agent-1.3.0}/skills/agent-import/SKILL.md +0 -0
  85. {task_agent-1.2.1 → task_agent-1.3.0}/skills/complete-task/SKILL.md +0 -0
  86. {task_agent-1.2.1 → task_agent-1.3.0}/skills/mission-workflow/SKILL.md +0 -0
  87. {task_agent-1.2.1 → task_agent-1.3.0}/skills/next/SKILL.md +0 -0
  88. {task_agent-1.2.1 → task_agent-1.3.0}/skills/next-task/SKILL.md +0 -0
  89. {task_agent-1.2.1 → task_agent-1.3.0}/src/taskagent/__init__.py +0 -0
  90. {task_agent-1.2.1 → task_agent-1.3.0}/src/taskagent/__main__.py +0 -0
  91. {task_agent-1.2.1 → task_agent-1.3.0}/src/taskagent/agent.py +0 -0
  92. {task_agent-1.2.1 → task_agent-1.3.0}/src/taskagent/agent_registry.py +0 -0
  93. {task_agent-1.2.1 → task_agent-1.3.0}/src/taskagent/audit.py +0 -0
  94. {task_agent-1.2.1 → task_agent-1.3.0}/src/taskagent/chat/config.py +0 -0
  95. {task_agent-1.2.1 → task_agent-1.3.0}/src/taskagent/chat/discovery.py +0 -0
  96. {task_agent-1.2.1 → task_agent-1.3.0}/src/taskagent/config.py +0 -0
  97. {task_agent-1.2.1 → task_agent-1.3.0}/src/taskagent/discovery.py +0 -0
  98. {task_agent-1.2.1 → task_agent-1.3.0}/src/taskagent/importers/__init__.py +0 -0
  99. {task_agent-1.2.1 → task_agent-1.3.0}/src/taskagent/importers/antigravity.py +0 -0
  100. {task_agent-1.2.1 → task_agent-1.3.0}/src/taskagent/importers/base.py +0 -0
  101. {task_agent-1.2.1 → task_agent-1.3.0}/src/taskagent/importers/claude_code.py +0 -0
  102. {task_agent-1.2.1 → task_agent-1.3.0}/src/taskagent/importers/generic.py +0 -0
  103. {task_agent-1.2.1 → task_agent-1.3.0}/src/taskagent/inbox.py +0 -0
  104. {task_agent-1.2.1 → task_agent-1.3.0}/src/taskagent/manager.py +0 -0
  105. {task_agent-1.2.1 → task_agent-1.3.0}/src/taskagent/models/__init__.py +0 -0
  106. {task_agent-1.2.1 → task_agent-1.3.0}/src/taskagent/models/issue.py +0 -0
  107. {task_agent-1.2.1 → task_agent-1.3.0}/src/taskagent/models/metric.py +0 -0
  108. {task_agent-1.2.1 → task_agent-1.3.0}/src/taskagent/perf.py +0 -0
  109. {task_agent-1.2.1 → task_agent-1.3.0}/src/taskagent/plugins/__init__.py +0 -0
  110. {task_agent-1.2.1 → task_agent-1.3.0}/src/taskagent/plugins/github.py +0 -0
  111. {task_agent-1.2.1 → task_agent-1.3.0}/src/taskagent/runner.py +0 -0
  112. {task_agent-1.2.1 → task_agent-1.3.0}/src/taskagent/store_registry.py +0 -0
  113. {task_agent-1.2.1 → task_agent-1.3.0}/src/taskagent/templates.py +0 -0
  114. {task_agent-1.2.1 → task_agent-1.3.0}/src/taskagent/theme.py +0 -0
  115. {task_agent-1.2.1 → task_agent-1.3.0}/test-dir/opencode.json +0 -0
  116. {task_agent-1.2.1 → task_agent-1.3.0}/tests/conftest.py +0 -0
  117. {task_agent-1.2.1 → task_agent-1.3.0}/tests/e2e/test_agent_sandboxing.sh +0 -0
  118. {task_agent-1.2.1 → task_agent-1.3.0}/tests/test_agent.py +0 -0
  119. {task_agent-1.2.1 → task_agent-1.3.0}/tests/test_agent_registry.py +0 -0
  120. {task_agent-1.2.1 → task_agent-1.3.0}/tests/test_agy_cli_worker.py +0 -0
  121. {task_agent-1.2.1 → task_agent-1.3.0}/tests/test_atomic_mutations.py +0 -0
  122. {task_agent-1.2.1 → task_agent-1.3.0}/tests/test_auto_commit.py +0 -0
  123. {task_agent-1.2.1 → task_agent-1.3.0}/tests/test_chat_config.py +0 -0
  124. {task_agent-1.2.1 → task_agent-1.3.0}/tests/test_chat_discovery.py +0 -0
  125. {task_agent-1.2.1 → task_agent-1.3.0}/tests/test_cli.py +0 -0
  126. {task_agent-1.2.1 → task_agent-1.3.0}/tests/test_cli_windows.py +0 -0
  127. {task_agent-1.2.1 → task_agent-1.3.0}/tests/test_discovery.py +0 -0
  128. {task_agent-1.2.1 → task_agent-1.3.0}/tests/test_github_token.py +0 -0
  129. {task_agent-1.2.1 → task_agent-1.3.0}/tests/test_importers.py +0 -0
  130. {task_agent-1.2.1 → task_agent-1.3.0}/tests/test_inbox.py +0 -0
  131. {task_agent-1.2.1 → task_agent-1.3.0}/tests/test_inbox_watch.py +0 -0
  132. {task_agent-1.2.1 → task_agent-1.3.0}/tests/test_lease.py +0 -0
  133. {task_agent-1.2.1 → task_agent-1.3.0}/tests/test_manager.py +0 -0
  134. {task_agent-1.2.1 → task_agent-1.3.0}/tests/test_manager_git.py +0 -0
  135. {task_agent-1.2.1 → task_agent-1.3.0}/tests/test_metrics.py +0 -0
  136. {task_agent-1.2.1 → task_agent-1.3.0}/tests/test_migration.py +0 -0
  137. {task_agent-1.2.1 → task_agent-1.3.0}/tests/test_perf.py +0 -0
  138. {task_agent-1.2.1 → task_agent-1.3.0}/tests/test_plugin_antigravity.py +0 -0
  139. {task_agent-1.2.1 → task_agent-1.3.0}/tests/test_plugin_claude.py +0 -0
  140. {task_agent-1.2.1 → task_agent-1.3.0}/tests/test_project_host_root.py +0 -0
  141. {task_agent-1.2.1 → task_agent-1.3.0}/tests/test_rebind.py +0 -0
  142. {task_agent-1.2.1 → task_agent-1.3.0}/tests/test_remote_attach.py +0 -0
  143. {task_agent-1.2.1 → task_agent-1.3.0}/tests/test_remote_create.py +0 -0
  144. {task_agent-1.2.1 → task_agent-1.3.0}/tests/test_repo_resolve.py +0 -0
  145. {task_agent-1.2.1 → task_agent-1.3.0}/tests/test_runner.py +0 -0
  146. {task_agent-1.2.1 → task_agent-1.3.0}/tests/test_store_registry.py +0 -0
  147. {task_agent-1.2.1 → task_agent-1.3.0}/tests/test_store_symlink.py +0 -0
  148. {task_agent-1.2.1 → task_agent-1.3.0}/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.0
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.0"
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 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 _parse_last_user_comment_and_timestamp(
31
+ discovered: DiscoveredChat,
32
+ ) -> Tuple[datetime, str]:
33
+ """Extract timestamp and first 200 chars of last user comment from chat log file."""
34
+ path = discovered.path
35
+ file_mtime = datetime.fromtimestamp(path.stat().st_mtime, tz=timezone.utc)
36
+ user_comment = ""
37
+
38
+ if not path.is_file():
39
+ return file_mtime, user_comment
40
+
41
+ try:
42
+ if discovered.parser_type == "jsonl":
43
+ text_lines = path.read_text(encoding="utf-8", errors="replace").splitlines()
44
+ for line in reversed(text_lines):
45
+ line = line.strip()
46
+ if not line:
47
+ continue
48
+ try:
49
+ data = json.loads(line)
50
+ if isinstance(data, dict):
51
+ role = (
52
+ data.get("type")
53
+ or data.get("role")
54
+ or data.get("source")
55
+ )
56
+ if role in (
57
+ "user",
58
+ "USER_INPUT",
59
+ "USER_EXPLICIT",
60
+ "human",
61
+ ):
62
+ content = (
63
+ data.get("content")
64
+ or data.get("text")
65
+ or data.get("message")
66
+ )
67
+ if isinstance(content, list):
68
+ text_parts = []
69
+ for part in content:
70
+ if (
71
+ isinstance(part, dict)
72
+ and part.get("type") == "text"
73
+ ):
74
+ text_parts.append(part.get("text", ""))
75
+ elif isinstance(part, str):
76
+ text_parts.append(part)
77
+ content = " ".join(text_parts)
78
+ if isinstance(content, str) and content.strip():
79
+ user_comment = content.strip()
80
+ break
81
+ except Exception:
82
+ continue
83
+
84
+ elif discovered.parser_type == "json":
85
+ raw_text = path.read_text(encoding="utf-8", errors="replace")
86
+ data = json.loads(raw_text)
87
+ messages = []
88
+ if isinstance(data, list):
89
+ messages = data
90
+ elif isinstance(data, dict):
91
+ messages = (
92
+ data.get("messages")
93
+ or data.get("steps")
94
+ or data.get("history")
95
+ or []
96
+ )
97
+
98
+ if isinstance(messages, list):
99
+ for msg in reversed(messages):
100
+ if isinstance(msg, dict):
101
+ role = (
102
+ msg.get("say")
103
+ or msg.get("role")
104
+ or msg.get("type")
105
+ or msg.get("source")
106
+ )
107
+ if role in (
108
+ "user",
109
+ "say",
110
+ "user_feedback",
111
+ "USER_INPUT",
112
+ "human",
113
+ ):
114
+ content = (
115
+ msg.get("text")
116
+ or msg.get("content")
117
+ or msg.get("message")
118
+ )
119
+ if isinstance(content, str) and content.strip():
120
+ user_comment = content.strip()
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,65 @@ 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("Description", style="white")
4277
+ table.add_column("Last Active", style="green")
4278
+ table.add_column("Last Comment Snippet", style="dim white")
4279
+
4280
+ for idx, item in enumerate(results, start=1):
4281
+ rel_time = item.last_active.strftime("%Y-%m-%d %H:%M:%S UTC")
4282
+ comment_snippet = item.last_user_comment if item.last_user_comment else "[dim](no prompt comment logged)[/dim]"
4283
+ table.add_row(
4284
+ str(idx),
4285
+ item.agent_name,
4286
+ item.description,
4287
+ rel_time,
4288
+ comment_snippet,
4289
+ )
4290
+
4291
+ console.print(table)
4292
+
4293
+
4235
4294
  def cmd_version(
4236
4295
  console: Console,
4237
4296
  promote: Optional[str] = None,
@@ -6055,6 +6114,27 @@ Start working on a task. This command automates the following workflow:
6055
6114
  action="store_true",
6056
6115
  help="Output result as JSON",
6057
6116
  )
6117
+ agent_last_used_p = agent_sub.add_parser(
6118
+ "last-used",
6119
+ help="List AI coding agents recently active in this repository",
6120
+ )
6121
+ agent_last_used_p.add_argument(
6122
+ "path",
6123
+ nargs="?",
6124
+ default=None,
6125
+ help="Target repository directory (defaults to cwd)",
6126
+ )
6127
+ agent_last_used_p.add_argument(
6128
+ "--limit",
6129
+ type=int,
6130
+ default=5,
6131
+ help="Maximum number of active agents to return (default: 5)",
6132
+ )
6133
+ agent_last_used_p.add_argument(
6134
+ "--json",
6135
+ action="store_true",
6136
+ help="Output result as JSON",
6137
+ )
6058
6138
 
6059
6139
  # plan
6060
6140
  subparsers.add_parser("plan", help="View or edit the project plan")
@@ -6738,9 +6818,16 @@ TA_STRATEGY_COOLDOWN_HOURS environment variable.
6738
6818
  file_path=args.file,
6739
6819
  json_format=args.json,
6740
6820
  )
6821
+ elif args.agent_subcommand == "last-used":
6822
+ cmd_agent_last_used(
6823
+ console,
6824
+ path_arg=args.path,
6825
+ limit=args.limit,
6826
+ json_format=args.json,
6827
+ )
6741
6828
  else:
6742
6829
  console.print(
6743
- "[yellow]Unknown agent subcommand. Use 'ta agent import'.[/yellow]"
6830
+ "[yellow]Unknown agent subcommand. Use 'ta agent import' or 'ta agent last-used'.[/yellow]"
6744
6831
  )
6745
6832
  elif args.command == "push":
6746
6833
  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.0"
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