task-agent 1.3.7__tar.gz → 1.3.9__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 (157) hide show
  1. {task_agent-1.3.7 → task_agent-1.3.9}/PKG-INFO +2 -2
  2. {task_agent-1.3.7 → task_agent-1.3.9}/pyproject.toml +2 -2
  3. {task_agent-1.3.7 → task_agent-1.3.9}/src/taskagent/cli.py +12 -0
  4. {task_agent-1.3.7 → task_agent-1.3.9}/src/taskagent/manager.py +27 -4
  5. {task_agent-1.3.7 → task_agent-1.3.9}/src/taskagent/mcp.py +7 -0
  6. {task_agent-1.3.7 → task_agent-1.3.9}/tests/test_cli.py +20 -0
  7. task_agent-1.3.9/tests/test_manager_git.py +253 -0
  8. {task_agent-1.3.7 → task_agent-1.3.9}/tests/test_mcp.py +16 -0
  9. {task_agent-1.3.7 → task_agent-1.3.9}/uv.lock +5 -5
  10. task_agent-1.3.7/tests/test_manager_git.py +0 -131
  11. {task_agent-1.3.7 → task_agent-1.3.9}/.agents/skills/next/SKILL.md +0 -0
  12. {task_agent-1.3.7 → task_agent-1.3.9}/.gemini/settings.json +0 -0
  13. {task_agent-1.3.7 → task_agent-1.3.9}/.github/workflows/ci.yml +0 -0
  14. {task_agent-1.3.7 → task_agent-1.3.9}/.github/workflows/publish.yml +0 -0
  15. {task_agent-1.3.7 → task_agent-1.3.9}/.gitignore +0 -0
  16. {task_agent-1.3.7 → task_agent-1.3.9}/.mise.toml +0 -0
  17. {task_agent-1.3.7 → task_agent-1.3.9}/.pre-commit-config.yaml +0 -0
  18. {task_agent-1.3.7 → task_agent-1.3.9}/.python-version +0 -0
  19. {task_agent-1.3.7 → task_agent-1.3.9}/.ta/agents/agy-cli/meta.toml +0 -0
  20. {task_agent-1.3.7 → task_agent-1.3.9}/.ta/agents/antigravity/meta.toml +0 -0
  21. {task_agent-1.3.7 → task_agent-1.3.9}/.ta/agents/gh/meta.toml +0 -0
  22. {task_agent-1.3.7 → task_agent-1.3.9}/.ta/agents/minimal/meta.toml +0 -0
  23. {task_agent-1.3.7 → task_agent-1.3.9}/.ta/agents/uat-aws/meta.toml +0 -0
  24. {task_agent-1.3.7 → task_agent-1.3.9}/.ta/worker +0 -0
  25. {task_agent-1.3.7 → task_agent-1.3.9}/.ta-config.json +0 -0
  26. {task_agent-1.3.7 → task_agent-1.3.9}/CLAUDE.md +0 -0
  27. {task_agent-1.3.7 → task_agent-1.3.9}/GEMINI.md +0 -0
  28. {task_agent-1.3.7 → task_agent-1.3.9}/Makefile +0 -0
  29. {task_agent-1.3.7 → task_agent-1.3.9}/README.md +0 -0
  30. {task_agent-1.3.7 → task_agent-1.3.9}/agent.env +0 -0
  31. {task_agent-1.3.7 → task_agent-1.3.9}/docs/README.md +0 -0
  32. {task_agent-1.3.7 → task_agent-1.3.9}/docs/STATION-MAP.md +0 -0
  33. {task_agent-1.3.7 → task_agent-1.3.9}/docs/architecture/README.md +0 -0
  34. {task_agent-1.3.7 → task_agent-1.3.9}/docs/chats/gemini-conversation-1773283034591.json +0 -0
  35. {task_agent-1.3.7 → task_agent-1.3.9}/docs/chats/project-init-to-v0.1.46.md +0 -0
  36. {task_agent-1.3.7 → task_agent-1.3.9}/docs/cli/README.md +0 -0
  37. {task_agent-1.3.7 → task_agent-1.3.9}/docs/development/README.md +0 -0
  38. {task_agent-1.3.7 → task_agent-1.3.9}/docs/task-agents.md +0 -0
  39. {task_agent-1.3.7 → task_agent-1.3.9}/docs/workflow/README.md +0 -0
  40. {task_agent-1.3.7 → task_agent-1.3.9}/docs/workflow/trivial_worker_policy.md +0 -0
  41. {task_agent-1.3.7 → task_agent-1.3.9}/opencode.json +0 -0
  42. {task_agent-1.3.7 → task_agent-1.3.9}/plugins/antigravity/.gemini-plugin/plugin.json +0 -0
  43. {task_agent-1.3.7 → task_agent-1.3.9}/plugins/antigravity/.mcp.json +0 -0
  44. {task_agent-1.3.7 → task_agent-1.3.9}/plugins/antigravity/README.md +0 -0
  45. {task_agent-1.3.7 → task_agent-1.3.9}/plugins/antigravity/hooks/hooks.json +0 -0
  46. {task_agent-1.3.7 → task_agent-1.3.9}/plugins/antigravity/hooks/session_start.sh +0 -0
  47. {task_agent-1.3.7 → task_agent-1.3.9}/plugins/antigravity/rules/task_agent.md +0 -0
  48. {task_agent-1.3.7 → task_agent-1.3.9}/plugins/antigravity/scripts/statusline.sh +0 -0
  49. {task_agent-1.3.7 → task_agent-1.3.9}/plugins/antigravity/skills/README.md +0 -0
  50. {task_agent-1.3.7 → task_agent-1.3.9}/plugins/antigravity/skills/agent-import/SKILL.md +0 -0
  51. {task_agent-1.3.7 → task_agent-1.3.9}/plugins/antigravity/skills/complete-task/SKILL.md +0 -0
  52. {task_agent-1.3.7 → task_agent-1.3.9}/plugins/antigravity/skills/mission-workflow/SKILL.md +0 -0
  53. {task_agent-1.3.7 → task_agent-1.3.9}/plugins/antigravity/skills/next/SKILL.md +0 -0
  54. {task_agent-1.3.7 → task_agent-1.3.9}/plugins/antigravity/skills/next-task/SKILL.md +0 -0
  55. {task_agent-1.3.7 → task_agent-1.3.9}/plugins/claude-code/.claude-plugin/plugin.json +0 -0
  56. {task_agent-1.3.7 → task_agent-1.3.9}/plugins/claude-code/.mcp.json +0 -0
  57. {task_agent-1.3.7 → task_agent-1.3.9}/plugins/claude-code/README.md +0 -0
  58. {task_agent-1.3.7 → task_agent-1.3.9}/plugins/claude-code/hooks/hooks.json +0 -0
  59. {task_agent-1.3.7 → task_agent-1.3.9}/plugins/claude-code/hooks/session_start.sh +0 -0
  60. {task_agent-1.3.7 → task_agent-1.3.9}/plugins/claude-code/scripts/statusline.sh +0 -0
  61. {task_agent-1.3.7 → task_agent-1.3.9}/plugins/claude-code/skills/agent-import/SKILL.md +0 -0
  62. {task_agent-1.3.7 → task_agent-1.3.9}/plugins/claude-code/skills/complete-task/SKILL.md +0 -0
  63. {task_agent-1.3.7 → task_agent-1.3.9}/plugins/claude-code/skills/mission-workflow/SKILL.md +0 -0
  64. {task_agent-1.3.7 → task_agent-1.3.9}/plugins/claude-code/skills/next-task/SKILL.md +0 -0
  65. {task_agent-1.3.7 → task_agent-1.3.9}/plugins/codex/.mcp.json +0 -0
  66. {task_agent-1.3.7 → task_agent-1.3.9}/plugins/codex/README.md +0 -0
  67. {task_agent-1.3.7 → task_agent-1.3.9}/plugins/copilot/.mcp.json +0 -0
  68. {task_agent-1.3.7 → task_agent-1.3.9}/plugins/copilot/README.md +0 -0
  69. {task_agent-1.3.7 → task_agent-1.3.9}/plugins/grok/.mcp.json +0 -0
  70. {task_agent-1.3.7 → task_agent-1.3.9}/plugins/grok/README.md +0 -0
  71. {task_agent-1.3.7 → task_agent-1.3.9}/scripts/clean_duplicates.py +0 -0
  72. {task_agent-1.3.7 → task_agent-1.3.9}/scripts/migrate_tasks.py +0 -0
  73. {task_agent-1.3.7 → task_agent-1.3.9}/scripts/recover_dates.py +0 -0
  74. {task_agent-1.3.7 → task_agent-1.3.9}/sidecars/adk-worker/.env.example +0 -0
  75. {task_agent-1.3.7 → task_agent-1.3.9}/sidecars/adk-worker/README.md +0 -0
  76. {task_agent-1.3.7 → task_agent-1.3.9}/sidecars/adk-worker/instructions/manager.md +0 -0
  77. {task_agent-1.3.7 → task_agent-1.3.9}/sidecars/adk-worker/instructions/validator.md +0 -0
  78. {task_agent-1.3.7 → task_agent-1.3.9}/sidecars/adk-worker/instructions/worker.md +0 -0
  79. {task_agent-1.3.7 → task_agent-1.3.9}/sidecars/adk-worker/profiles/default.json +0 -0
  80. {task_agent-1.3.7 → task_agent-1.3.9}/sidecars/adk-worker/pyproject.toml +0 -0
  81. {task_agent-1.3.7 → task_agent-1.3.9}/sidecars/adk-worker/uv.lock +0 -0
  82. {task_agent-1.3.7 → task_agent-1.3.9}/sidecars/adk-worker/worker.py +0 -0
  83. {task_agent-1.3.7 → task_agent-1.3.9}/sidecars/keet-bridge/README.md +0 -0
  84. {task_agent-1.3.7 → task_agent-1.3.9}/sidecars/keet-bridge/bridge.js +0 -0
  85. {task_agent-1.3.7 → task_agent-1.3.9}/sidecars/keet-bridge/package-lock.json +0 -0
  86. {task_agent-1.3.7 → task_agent-1.3.9}/sidecars/keet-bridge/package.json +0 -0
  87. {task_agent-1.3.7 → task_agent-1.3.9}/sidecars/keet-bridge/run.sh +0 -0
  88. {task_agent-1.3.7 → task_agent-1.3.9}/sidecars/keet-bridge/test-e2e.js +0 -0
  89. {task_agent-1.3.7 → task_agent-1.3.9}/sidecars/matrix-bridge/README.md +0 -0
  90. {task_agent-1.3.7 → task_agent-1.3.9}/sidecars/matrix-bridge/matrix_bridge.py +0 -0
  91. {task_agent-1.3.7 → task_agent-1.3.9}/skills/README.md +0 -0
  92. {task_agent-1.3.7 → task_agent-1.3.9}/skills/agent-import/SKILL.md +0 -0
  93. {task_agent-1.3.7 → task_agent-1.3.9}/skills/complete-task/SKILL.md +0 -0
  94. {task_agent-1.3.7 → task_agent-1.3.9}/skills/mission-workflow/SKILL.md +0 -0
  95. {task_agent-1.3.7 → task_agent-1.3.9}/skills/next/SKILL.md +0 -0
  96. {task_agent-1.3.7 → task_agent-1.3.9}/skills/next-task/SKILL.md +0 -0
  97. {task_agent-1.3.7 → task_agent-1.3.9}/src/taskagent/__init__.py +0 -0
  98. {task_agent-1.3.7 → task_agent-1.3.9}/src/taskagent/__main__.py +0 -0
  99. {task_agent-1.3.7 → task_agent-1.3.9}/src/taskagent/agent.py +0 -0
  100. {task_agent-1.3.7 → task_agent-1.3.9}/src/taskagent/agent_registry.py +0 -0
  101. {task_agent-1.3.7 → task_agent-1.3.9}/src/taskagent/audit.py +0 -0
  102. {task_agent-1.3.7 → task_agent-1.3.9}/src/taskagent/chat/__init__.py +0 -0
  103. {task_agent-1.3.7 → task_agent-1.3.9}/src/taskagent/chat/config.py +0 -0
  104. {task_agent-1.3.7 → task_agent-1.3.9}/src/taskagent/chat/discovery.py +0 -0
  105. {task_agent-1.3.7 → task_agent-1.3.9}/src/taskagent/chat/last_used.py +0 -0
  106. {task_agent-1.3.7 → task_agent-1.3.9}/src/taskagent/config.py +0 -0
  107. {task_agent-1.3.7 → task_agent-1.3.9}/src/taskagent/discovery.py +0 -0
  108. {task_agent-1.3.7 → task_agent-1.3.9}/src/taskagent/importers/__init__.py +0 -0
  109. {task_agent-1.3.7 → task_agent-1.3.9}/src/taskagent/importers/antigravity.py +0 -0
  110. {task_agent-1.3.7 → task_agent-1.3.9}/src/taskagent/importers/base.py +0 -0
  111. {task_agent-1.3.7 → task_agent-1.3.9}/src/taskagent/importers/claude_code.py +0 -0
  112. {task_agent-1.3.7 → task_agent-1.3.9}/src/taskagent/importers/generic.py +0 -0
  113. {task_agent-1.3.7 → task_agent-1.3.9}/src/taskagent/inbox.py +0 -0
  114. {task_agent-1.3.7 → task_agent-1.3.9}/src/taskagent/models/__init__.py +0 -0
  115. {task_agent-1.3.7 → task_agent-1.3.9}/src/taskagent/models/issue.py +0 -0
  116. {task_agent-1.3.7 → task_agent-1.3.9}/src/taskagent/models/metric.py +0 -0
  117. {task_agent-1.3.7 → task_agent-1.3.9}/src/taskagent/perf.py +0 -0
  118. {task_agent-1.3.7 → task_agent-1.3.9}/src/taskagent/plugins/__init__.py +0 -0
  119. {task_agent-1.3.7 → task_agent-1.3.9}/src/taskagent/plugins/github.py +0 -0
  120. {task_agent-1.3.7 → task_agent-1.3.9}/src/taskagent/runner.py +0 -0
  121. {task_agent-1.3.7 → task_agent-1.3.9}/src/taskagent/store_registry.py +0 -0
  122. {task_agent-1.3.7 → task_agent-1.3.9}/src/taskagent/templates.py +0 -0
  123. {task_agent-1.3.7 → task_agent-1.3.9}/src/taskagent/theme.py +0 -0
  124. {task_agent-1.3.7 → task_agent-1.3.9}/test-dir/opencode.json +0 -0
  125. {task_agent-1.3.7 → task_agent-1.3.9}/tests/conftest.py +0 -0
  126. {task_agent-1.3.7 → task_agent-1.3.9}/tests/e2e/test_agent_sandboxing.sh +0 -0
  127. {task_agent-1.3.7 → task_agent-1.3.9}/tests/test_agent.py +0 -0
  128. {task_agent-1.3.7 → task_agent-1.3.9}/tests/test_agent_registry.py +0 -0
  129. {task_agent-1.3.7 → task_agent-1.3.9}/tests/test_agy_cli_worker.py +0 -0
  130. {task_agent-1.3.7 → task_agent-1.3.9}/tests/test_atomic_mutations.py +0 -0
  131. {task_agent-1.3.7 → task_agent-1.3.9}/tests/test_auto_commit.py +0 -0
  132. {task_agent-1.3.7 → task_agent-1.3.9}/tests/test_chat_config.py +0 -0
  133. {task_agent-1.3.7 → task_agent-1.3.9}/tests/test_chat_discovery.py +0 -0
  134. {task_agent-1.3.7 → task_agent-1.3.9}/tests/test_chat_last_used.py +0 -0
  135. {task_agent-1.3.7 → task_agent-1.3.9}/tests/test_cli_windows.py +0 -0
  136. {task_agent-1.3.7 → task_agent-1.3.9}/tests/test_discovery.py +0 -0
  137. {task_agent-1.3.7 → task_agent-1.3.9}/tests/test_github_token.py +0 -0
  138. {task_agent-1.3.7 → task_agent-1.3.9}/tests/test_importers.py +0 -0
  139. {task_agent-1.3.7 → task_agent-1.3.9}/tests/test_inbox.py +0 -0
  140. {task_agent-1.3.7 → task_agent-1.3.9}/tests/test_inbox_watch.py +0 -0
  141. {task_agent-1.3.7 → task_agent-1.3.9}/tests/test_lease.py +0 -0
  142. {task_agent-1.3.7 → task_agent-1.3.9}/tests/test_manager.py +0 -0
  143. {task_agent-1.3.7 → task_agent-1.3.9}/tests/test_metrics.py +0 -0
  144. {task_agent-1.3.7 → task_agent-1.3.9}/tests/test_migration.py +0 -0
  145. {task_agent-1.3.7 → task_agent-1.3.9}/tests/test_perf.py +0 -0
  146. {task_agent-1.3.7 → task_agent-1.3.9}/tests/test_plugin_antigravity.py +0 -0
  147. {task_agent-1.3.7 → task_agent-1.3.9}/tests/test_plugin_claude.py +0 -0
  148. {task_agent-1.3.7 → task_agent-1.3.9}/tests/test_project_host_root.py +0 -0
  149. {task_agent-1.3.7 → task_agent-1.3.9}/tests/test_rebind.py +0 -0
  150. {task_agent-1.3.7 → task_agent-1.3.9}/tests/test_remote_attach.py +0 -0
  151. {task_agent-1.3.7 → task_agent-1.3.9}/tests/test_remote_create.py +0 -0
  152. {task_agent-1.3.7 → task_agent-1.3.9}/tests/test_repo_resolve.py +0 -0
  153. {task_agent-1.3.7 → task_agent-1.3.9}/tests/test_runner.py +0 -0
  154. {task_agent-1.3.7 → task_agent-1.3.9}/tests/test_store_registry.py +0 -0
  155. {task_agent-1.3.7 → task_agent-1.3.9}/tests/test_store_symlink.py +0 -0
  156. {task_agent-1.3.7 → task_agent-1.3.9}/tests/test_templates.py +0 -0
  157. {task_agent-1.3.7 → task_agent-1.3.9}/tests/test_yazi.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: task-agent
3
- Version: 1.3.7
3
+ Version: 1.3.9
4
4
  Summary: A prioritized, file-based task queue for autonomous agentic workers
5
5
  Project-URL: Homepage, https://github.com/InTEGr8or/task-agent
6
6
  Project-URL: Repository, https://github.com/InTEGr8or/task-agent.git
@@ -28,7 +28,7 @@ Requires-Dist: pyyaml>=6.0
28
28
  Requires-Dist: questionary>=2.1.1
29
29
  Requires-Dist: rich>=13.9.4
30
30
  Requires-Dist: transitions>=0.9.2
31
- Requires-Dist: verkit>=0.1.4
31
+ Requires-Dist: verkit>=0.2.0
32
32
  Description-Content-Type: text/markdown
33
33
 
34
34
  # Task Agent 🤖
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "task-agent"
3
- version = "1.3.7"
3
+ version = "1.3.9"
4
4
  description = "A prioritized, file-based task queue for autonomous agentic workers"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -38,7 +38,7 @@ dependencies = [
38
38
  "questionary>=2.1.1",
39
39
  "rich>=13.9.4",
40
40
  "githubkit>=0.15.5",
41
- "verkit>=0.1.4",
41
+ "verkit>=0.2.0",
42
42
  "multi-agent-registry>=0.1.0",
43
43
  "transitions>=0.9.2",
44
44
  "boto3>=1.34.0",
@@ -1669,6 +1669,7 @@ def cmd_done(
1669
1669
  solution: Optional[str] = None,
1670
1670
  no_verify: bool = True,
1671
1671
  metrics: Optional[SubtaskMetric] = None,
1672
+ cached_only: bool = False,
1672
1673
  ):
1673
1674
  """Mark an issue as done."""
1674
1675
  if not slug:
@@ -1699,6 +1700,7 @@ def cmd_done(
1699
1700
  solution_explanation=solution,
1700
1701
  no_verify=no_verify,
1701
1702
  metrics=metrics,
1703
+ cached_only=cached_only,
1702
1704
  )
1703
1705
  console.print(
1704
1706
  f"[bold green]Issue '{issue.slug}' marked as done and "
@@ -7050,6 +7052,15 @@ TA_STRATEGY_COOLDOWN_HOURS environment variable.
7050
7052
  default=True,
7051
7053
  help="Skip running git pre-commit hooks (default)",
7052
7054
  )
7055
+ done_parser.add_argument(
7056
+ "--cached-only",
7057
+ action="store_true",
7058
+ help=(
7059
+ "Commit only what's already staged (git add) instead of `git add .` — "
7060
+ "for concurrent agents in the same working tree/branch on unrelated "
7061
+ "files, so one agent's `done` doesn't sweep up the other's changes"
7062
+ ),
7063
+ )
7053
7064
  done_parser.add_argument(
7054
7065
  "--hooks",
7055
7066
  dest="no_verify",
@@ -7456,6 +7467,7 @@ TA_STRATEGY_COOLDOWN_HOURS environment variable.
7456
7467
  args.solution,
7457
7468
  args.no_verify,
7458
7469
  metrics=done_metrics,
7470
+ cached_only=getattr(args, "cached_only", False),
7459
7471
  )
7460
7472
  elif args.command == "delete":
7461
7473
  cmd_soft_delete(console, manager, args.slug)
@@ -1305,8 +1305,19 @@ class TaskAgent:
1305
1305
  amend: bool = False,
1306
1306
  files: Optional[List[str]] = None,
1307
1307
  no_verify: bool = True,
1308
+ cached_only: bool = False,
1308
1309
  ) -> str:
1309
- """Helper to perform a git commit with retry logic for hooks."""
1310
+ """Helper to perform a git commit with retry logic for hooks.
1311
+
1312
+ ``cached_only`` skips every ``git add`` this method would otherwise
1313
+ run (both ``files`` and the ``git add .`` fallback) and commits
1314
+ exactly what's already staged. For two agents editing the same
1315
+ working tree/branch concurrently on unrelated files, this is the
1316
+ difference between "commit my changes" and "commit whatever's lying
1317
+ around" — the latter silently sweeps up the other agent's in-progress
1318
+ work. The caller is expected to have already run their own
1319
+ ``git add`` for exactly what they want committed.
1320
+ """
1310
1321
 
1311
1322
  def _get_git_add_path(f: str) -> str:
1312
1323
  resolved_f = Path(f).resolve()
@@ -1316,7 +1327,9 @@ class TaskAgent:
1316
1327
  except ValueError:
1317
1328
  return f
1318
1329
 
1319
- if files:
1330
+ if cached_only:
1331
+ pass
1332
+ elif files:
1320
1333
  for f in files:
1321
1334
  git_add_path = _get_git_add_path(f)
1322
1335
  # Force-add so gitignored task paths are included when needed
@@ -1353,7 +1366,7 @@ class TaskAgent:
1353
1366
  ):
1354
1367
  return "no_changes"
1355
1368
 
1356
- if not amend:
1369
+ if not amend and not cached_only:
1357
1370
  # Retry once for pre-commit hooks
1358
1371
  if files:
1359
1372
  for f in files:
@@ -1418,12 +1431,19 @@ class TaskAgent:
1418
1431
  solution_explanation: Optional[str] = None,
1419
1432
  no_verify: bool = True,
1420
1433
  metrics: Optional[SubtaskMetric] = None,
1434
+ cached_only: bool = False,
1421
1435
  ) -> Tuple[Issue, str]:
1422
1436
  """Mark an issue as done. Returns (issue, commit_hash).
1423
1437
 
1424
1438
  Optional ``metrics`` captures agent self-reported cost context
1425
1439
  (model, harness, tokens, duration) written into the task README and
1426
1440
  ``meta.json`` for later cost analysis via ``ta report``.
1441
+
1442
+ ``cached_only`` commits only what's already staged in the code repo
1443
+ (no ``git add .``) — for two agents completing tasks in the same
1444
+ working tree/branch at once on unrelated files, so one agent's
1445
+ ``ta done`` never sweeps up the other's untracked or modified-but-
1446
+ unstaged work. The caller must ``git add`` their own files first.
1427
1447
  """
1428
1448
  issues = self.load_mission()
1429
1449
  target_issue = next((i for i in issues if i.slug == slug), None)
@@ -1454,7 +1474,10 @@ class TaskAgent:
1454
1474
  msg = commit_message or f"feat: complete {target_issue.slug}"
1455
1475
  if self.code_root:
1456
1476
  code_hash = self._git_commit(
1457
- self.code_root, msg, no_verify=no_verify
1477
+ self.code_root,
1478
+ msg,
1479
+ no_verify=no_verify,
1480
+ cached_only=cached_only,
1458
1481
  )
1459
1482
  if code_hash == "failed":
1460
1483
  raise RuntimeError(
@@ -720,6 +720,7 @@ def complete_task(
720
720
  started_at: Optional[str] = None,
721
721
  ended_at: Optional[str] = None,
722
722
  metrics_notes: Optional[str] = None,
723
+ cached_only: bool = False,
723
724
  ) -> str:
724
725
  """Mark a task as completed and commit the changes.
725
726
 
@@ -743,6 +744,11 @@ def complete_task(
743
744
  started_at: ISO-8601 start time (optional; derived from duration when omitted).
744
745
  ended_at: ISO-8601 end time (defaults to now when metrics are provided).
745
746
  metrics_notes: Free-form cost notes (retries, cache hits, tool-loop count, …).
747
+ cached_only: Commit only what's already staged (git add) instead of
748
+ `git add .`. Use this when another agent is working in the same
749
+ repo/branch/worktree on unrelated files at the same time — stage
750
+ exactly your own files first, then complete with this on, so you
751
+ never sweep up their in-progress, unstaged work into your commit.
746
752
  """
747
753
  from taskagent.models.metric import SubtaskMetric
748
754
 
@@ -768,6 +774,7 @@ def complete_task(
768
774
  commit_message=message,
769
775
  solution_explanation=solution,
770
776
  metrics=metrics,
777
+ cached_only=cached_only,
771
778
  )
772
779
  lines = [
773
780
  "### Task Completed Successfully",
@@ -1551,6 +1551,26 @@ def test_cli_log(manager, monkeypatch):
1551
1551
  assert "2" in extra_args
1552
1552
 
1553
1553
 
1554
+ def test_cli_done_cached_only_flag(manager, monkeypatch):
1555
+ """ta done --cached-only must reach cmd_done as cached_only=True, and
1556
+ plain `ta done` (the default) must not."""
1557
+ from taskagent.cli import main
1558
+ from unittest.mock import patch
1559
+
1560
+ monkeypatch.setattr("sys.argv", ["ta", "done", "some-task", "--cached-only"])
1561
+ with patch("taskagent.cli.cmd_done") as mock_cmd_done:
1562
+ with patch("taskagent.cli.TaskAgent", return_value=manager):
1563
+ main()
1564
+ mock_cmd_done.assert_called_once()
1565
+ assert mock_cmd_done.call_args[1].get("cached_only") is True
1566
+
1567
+ monkeypatch.setattr("sys.argv", ["ta", "done", "some-task"])
1568
+ with patch("taskagent.cli.cmd_done") as mock_cmd_done:
1569
+ with patch("taskagent.cli.TaskAgent", return_value=manager):
1570
+ main()
1571
+ assert mock_cmd_done.call_args[1].get("cached_only") is False
1572
+
1573
+
1554
1574
  def test_format_git_log_rich():
1555
1575
  from taskagent.cli import format_git_log_rich
1556
1576
 
@@ -0,0 +1,253 @@
1
+ import subprocess
2
+
3
+ import pytest
4
+ from unittest.mock import MagicMock, patch
5
+ from pathlib import Path
6
+ from taskagent.manager import TaskAgent
7
+
8
+
9
+ @pytest.fixture
10
+ def manager(tmp_path):
11
+ issues_root = tmp_path / "docs" / "tasks"
12
+ return TaskAgent(config_dir=str(issues_root))
13
+
14
+
15
+ def test_git_root_detection(tmp_path, manager):
16
+ with patch("subprocess.run") as mock_run:
17
+ mock_run.return_value.stdout = "/path/to/repo\n"
18
+ mock_run.return_value.returncode = 0
19
+
20
+ root = manager._get_git_root(tmp_path)
21
+ assert root == Path("/path/to/repo")
22
+ # We now use shell=(os.name == "nt")
23
+ import os
24
+
25
+ mock_run.assert_called_with(
26
+ ["git", "-C", str(tmp_path), "rev-parse", "--show-toplevel"],
27
+ capture_output=True,
28
+ text=True,
29
+ check=True,
30
+ shell=(os.name == "nt"),
31
+ )
32
+
33
+
34
+ def test_git_commit_retry_on_hook_failure(manager):
35
+ with (
36
+ patch("subprocess.run") as mock_run,
37
+ patch("subprocess.check_output") as mock_check,
38
+ ):
39
+ # First call fails (hook), second succeeds
40
+ mock_run.side_effect = [
41
+ MagicMock(returncode=0), # git add
42
+ MagicMock(returncode=1), # git commit fail
43
+ MagicMock(returncode=0), # git add retry
44
+ MagicMock(returncode=0), # git commit retry
45
+ ]
46
+ mock_check.return_value = "abc1234\n"
47
+
48
+ result = manager._git_commit(Path("/repo"), "feat: test")
49
+ assert result == "abc1234"
50
+ assert mock_run.call_count == 4
51
+
52
+
53
+ def test_dual_repo_detection(tmp_path, manager):
54
+ # Mock code_root and mission_root to be different
55
+ manager.code_root = Path("/projects/app")
56
+ manager.mission_root = Path("/projects/tasks")
57
+ assert manager.is_dual_repo is True
58
+
59
+ manager.mission_root = Path("/projects/app")
60
+ assert manager.is_dual_repo is False
61
+
62
+
63
+ @pytest.mark.skip(reason="Test failing due to environment issue")
64
+ def test_complete_issue_dual_repo_flow(tmp_path, manager):
65
+ # Setup dual repo state
66
+ code_root = tmp_path / "code"
67
+ mission_root = tmp_path / "mission"
68
+ code_root.mkdir()
69
+ mission_root.mkdir()
70
+
71
+ # Set them explicitly
72
+ manager.code_root = code_root
73
+ manager.mission_root = mission_root
74
+ manager.issues_root = mission_root / "tasks"
75
+ manager.create_issue("Test Task")
76
+
77
+ print(f"DEBUG: mission_root={manager.mission_root}")
78
+ print(f"DEBUG: code_root={manager.code_root}")
79
+ print(f"DEBUG: is_dual_repo={manager.is_dual_repo}")
80
+
81
+ with patch.object(TaskAgent, "_git_commit") as mock_commit:
82
+ mock_commit.return_value = "hash123"
83
+
84
+ manager.complete_issue("test-task", should_commit=True)
85
+
86
+ # Should call commit twice (once for code, once for mission)
87
+ # Plus the amend call
88
+ assert mock_commit.call_count >= 2
89
+ # Verify first call is for code repo
90
+ assert mock_commit.call_args_list[0].args[0] == code_root
91
+ # Verify second call is for mission repo
92
+ assert mock_commit.call_args_list[1].args[0] == mission_root
93
+
94
+
95
+ def test_git_commit_no_verify(manager):
96
+
97
+ with (
98
+ patch("subprocess.run") as mock_run,
99
+ patch("subprocess.check_output") as mock_check,
100
+ ):
101
+ mock_run.return_value = MagicMock(returncode=0)
102
+ mock_check.return_value = "abc1234\n"
103
+
104
+ # Call with no_verify=True (default)
105
+ result = manager._git_commit(Path("/repo"), "feat: test", no_verify=True)
106
+ assert result == "abc1234"
107
+
108
+ # Verify the commit call included --no-verify
109
+ commit_call = mock_run.call_args_list[1]
110
+ assert "--no-verify" in commit_call.args[0]
111
+
112
+ # Call with no_verify=False
113
+ result = manager._git_commit(Path("/repo"), "feat: test", no_verify=False)
114
+ assert result == "abc1234"
115
+
116
+ # Verify the commit call did NOT include --no-verify
117
+ commit_call_2 = mock_run.call_args_list[3]
118
+ assert "--no-verify" not in commit_call_2.args[0]
119
+
120
+
121
+ def test_git_commit_cached_only_skips_git_add(manager):
122
+ """cached_only must never run `git add` — only the commit itself — since
123
+ the whole point is committing exactly what the caller already staged,
124
+ not sweeping in whatever else is sitting in the working tree (e.g. a
125
+ second agent's in-progress, unstaged edits in the same repo)."""
126
+ with (
127
+ patch("subprocess.run") as mock_run,
128
+ patch("subprocess.check_output") as mock_check,
129
+ ):
130
+ mock_run.return_value = MagicMock(returncode=0)
131
+ mock_check.return_value = "abc1234\n"
132
+
133
+ result = manager._git_commit(Path("/repo"), "feat: test", cached_only=True)
134
+
135
+ assert result == "abc1234"
136
+ assert mock_run.call_count == 1 # commit only, no add
137
+ commit_call = mock_run.call_args_list[0]
138
+ assert commit_call.args[0][3] == "commit"
139
+
140
+
141
+ def test_git_commit_cached_only_with_files_still_skips_add(manager):
142
+ """files= is meaningless once cached_only=True — still no add call."""
143
+ with (
144
+ patch("subprocess.run") as mock_run,
145
+ patch("subprocess.check_output") as mock_check,
146
+ ):
147
+ mock_run.return_value = MagicMock(returncode=0)
148
+ mock_check.return_value = "abc1234\n"
149
+
150
+ manager._git_commit(
151
+ Path("/repo"),
152
+ "feat: test",
153
+ files=["some/file.py"],
154
+ cached_only=True,
155
+ )
156
+
157
+ assert mock_run.call_count == 1
158
+
159
+
160
+ def test_git_commit_cached_only_does_not_retry_on_failure(manager):
161
+ """The retry path re-adds (`git add .` or specific files) to pick up
162
+ hook-modified files — for cached_only that would defeat the purpose by
163
+ staging whatever else is in the tree, so it must not retry at all."""
164
+ with patch("subprocess.run") as mock_run:
165
+ mock_run.return_value = MagicMock(
166
+ returncode=1, stdout="", stderr="hook rejected"
167
+ )
168
+
169
+ result = manager._git_commit(Path("/repo"), "feat: test", cached_only=True)
170
+
171
+ assert result == "failed"
172
+ assert mock_run.call_count == 1 # the one failed commit attempt, no retry
173
+
174
+
175
+ def test_git_commit_cached_only_reports_no_changes_when_index_empty(manager):
176
+ with patch("subprocess.run") as mock_run:
177
+ mock_run.return_value = MagicMock(
178
+ returncode=1, stdout="nothing to commit, working tree clean", stderr=""
179
+ )
180
+
181
+ result = manager._git_commit(Path("/repo"), "feat: test", cached_only=True)
182
+
183
+ assert result == "no_changes"
184
+ assert mock_run.call_count == 1
185
+
186
+
187
+ def test_git_commit_cached_only_leaves_other_agents_unstaged_work_alone(
188
+ tmp_path, manager
189
+ ):
190
+ """The actual scenario this exists for: two agents editing the same
191
+ working tree/branch on unrelated files. Real git repo, no mocking —
192
+ stage only "my" file, leave "their" file modified-but-unstaged, and
193
+ confirm cached_only=True commits exactly the former and never touches
194
+ the latter."""
195
+ repo = tmp_path / "repo"
196
+ repo.mkdir()
197
+
198
+ def git(*args):
199
+ return subprocess.run(
200
+ ["git", "-C", str(repo), *args],
201
+ check=True,
202
+ capture_output=True,
203
+ text=True,
204
+ )
205
+
206
+ git("init", "-q")
207
+ git("config", "user.email", "t@example.com")
208
+ git("config", "user.name", "t")
209
+
210
+ mine = repo / "backend.py"
211
+ theirs = repo / "tui.py"
212
+ mine.write_text("original backend\n")
213
+ theirs.write_text("original tui\n")
214
+ git("add", "-A")
215
+ git("commit", "-q", "-m", "seed")
216
+
217
+ # Both agents edit their own file.
218
+ mine.write_text("backend: my finished change\n")
219
+ theirs.write_text("tui: their in-progress, unrelated change\n")
220
+ # Only I stage mine — theirs stays a plain unstaged working-tree edit,
221
+ # exactly like a second agent's uncommitted work sitting in the same
222
+ # checkout.
223
+ git("add", "backend.py")
224
+
225
+ result = manager._git_commit(repo, "feat: complete backend work", cached_only=True)
226
+
227
+ assert result != "failed"
228
+ assert result != "no_changes"
229
+
230
+ committed_files = git("show", "--name-only", "--format=", "HEAD").stdout.split()
231
+ assert committed_files == ["backend.py"]
232
+
233
+ # Their edit must still be sitting there, unstaged, exactly as they left
234
+ # it: " M" (leading space, not staged) — not "M " or "A ", which would
235
+ # mean it got swept into the index.
236
+ status = git("status", "--porcelain").stdout.rstrip("\n")
237
+ assert status == " M tui.py"
238
+ assert theirs.read_text() == "tui: their in-progress, unrelated change\n"
239
+
240
+
241
+ def test_git_commit_clean_tree(manager):
242
+ with patch("subprocess.run") as mock_run:
243
+ # First call: git add (succeeds)
244
+ # Second call: git commit (fails with returncode=1, but stdout says nothing to commit)
245
+ mock_run.side_effect = [
246
+ MagicMock(returncode=0),
247
+ MagicMock(
248
+ returncode=1, stdout="nothing to commit, working tree clean", stderr=""
249
+ ),
250
+ ]
251
+
252
+ result = manager._git_commit(Path("/repo"), "feat: test")
253
+ assert result == "no_changes"
@@ -142,9 +142,25 @@ def test_mcp_complete_task(mock_manager):
142
142
  commit_message="Done",
143
143
  solution_explanation="Implemented feature X",
144
144
  metrics=None,
145
+ cached_only=False,
145
146
  )
146
147
 
147
148
 
149
+ def test_mcp_complete_task_cached_only(mock_manager):
150
+ """Concurrent-agent case: stage your own files first, then complete
151
+ with cached_only so the commit can't sweep up another agent's unstaged,
152
+ in-progress work in the same repo/branch/worktree."""
153
+ mock_manager.slugify.return_value = "task-1"
154
+ mock_manager.complete_issue.return_value = (
155
+ Issue(name="Task 1", slug="task-1", status="completed"),
156
+ "abc1234",
157
+ )
158
+
159
+ mcp.complete_task("Task 1", solution="Backend work", cached_only=True)
160
+
161
+ assert mock_manager.complete_issue.call_args.kwargs["cached_only"] is True
162
+
163
+
148
164
  def test_mcp_complete_task_with_metrics(mock_manager):
149
165
  from taskagent.models.metric import SubtaskMetric
150
166
 
@@ -1442,7 +1442,7 @@ wheels = [
1442
1442
 
1443
1443
  [[package]]
1444
1444
  name = "task-agent"
1445
- version = "1.3.7"
1445
+ version = "1.3.9"
1446
1446
  source = { editable = "." }
1447
1447
  dependencies = [
1448
1448
  { name = "boto3" },
@@ -1483,7 +1483,7 @@ requires-dist = [
1483
1483
  { name = "questionary", specifier = ">=2.1.1" },
1484
1484
  { name = "rich", specifier = ">=13.9.4" },
1485
1485
  { name = "transitions", specifier = ">=0.9.2" },
1486
- { name = "verkit", specifier = ">=0.1.4" },
1486
+ { name = "verkit", specifier = ">=0.2.0" },
1487
1487
  ]
1488
1488
 
1489
1489
  [package.metadata.requires-dev]
@@ -1570,14 +1570,14 @@ wheels = [
1570
1570
 
1571
1571
  [[package]]
1572
1572
  name = "verkit"
1573
- version = "0.1.5"
1573
+ version = "0.2.1"
1574
1574
  source = { registry = "https://pypi.org/simple" }
1575
1575
  dependencies = [
1576
1576
  { name = "rich" },
1577
1577
  ]
1578
- sdist = { url = "https://files.pythonhosted.org/packages/37/c4/f179bda37f2c6608d8b456233f849526821efbf4dd0c3d7ff46f458a19e7/verkit-0.1.5.tar.gz", hash = "sha256:70d7b0fa6f18c1fa4e0c0b2f2ca318ff0282a2d006c34d83bd8569034c79ef20", size = 9463, upload-time = "2026-08-25T18:59:25.03Z" }
1578
+ sdist = { url = "https://files.pythonhosted.org/packages/b8/42/8074f60fdf5b0026cf414dc76f4a12fe7581fbe41a115127e1f89590ea6e/verkit-0.2.1.tar.gz", hash = "sha256:9f11e341a0f22cf961b3ec75f82b3a2bc4b9883ddd000f3dd0198bb46eb6102a", size = 11328, upload-time = "2026-09-15T06:06:49.716Z" }
1579
1579
  wheels = [
1580
- { url = "https://files.pythonhosted.org/packages/8b/99/2ad1b39a694762d1f4bdb22691ac12ba5f5793fe3f84beff336c351c7b92/verkit-0.1.5-py3-none-any.whl", hash = "sha256:3176908e71301d63e544fd6411d2dacba8d11f301b45b187af47513c141bf1be", size = 9792, upload-time = "2026-08-25T18:59:23.792Z" },
1580
+ { url = "https://files.pythonhosted.org/packages/3a/26/1042329f9a9933447906a288527b640bfaab7369059b27a037c6786a8325/verkit-0.2.1-py3-none-any.whl", hash = "sha256:54482e6b20c8646aba4a4271696dff5a9a39eb883db8719ddd75643b34754c3c", size = 12091, upload-time = "2026-09-15T06:06:48.402Z" },
1581
1581
  ]
1582
1582
 
1583
1583
  [[package]]
@@ -1,131 +0,0 @@
1
- import pytest
2
- from unittest.mock import MagicMock, patch
3
- from pathlib import Path
4
- from taskagent.manager import TaskAgent
5
-
6
-
7
- @pytest.fixture
8
- def manager(tmp_path):
9
- issues_root = tmp_path / "docs" / "tasks"
10
- return TaskAgent(config_dir=str(issues_root))
11
-
12
-
13
- def test_git_root_detection(tmp_path, manager):
14
- with patch("subprocess.run") as mock_run:
15
- mock_run.return_value.stdout = "/path/to/repo\n"
16
- mock_run.return_value.returncode = 0
17
-
18
- root = manager._get_git_root(tmp_path)
19
- assert root == Path("/path/to/repo")
20
- # We now use shell=(os.name == "nt")
21
- import os
22
-
23
- mock_run.assert_called_with(
24
- ["git", "-C", str(tmp_path), "rev-parse", "--show-toplevel"],
25
- capture_output=True,
26
- text=True,
27
- check=True,
28
- shell=(os.name == "nt"),
29
- )
30
-
31
-
32
- def test_git_commit_retry_on_hook_failure(manager):
33
- with (
34
- patch("subprocess.run") as mock_run,
35
- patch("subprocess.check_output") as mock_check,
36
- ):
37
- # First call fails (hook), second succeeds
38
- mock_run.side_effect = [
39
- MagicMock(returncode=0), # git add
40
- MagicMock(returncode=1), # git commit fail
41
- MagicMock(returncode=0), # git add retry
42
- MagicMock(returncode=0), # git commit retry
43
- ]
44
- mock_check.return_value = "abc1234\n"
45
-
46
- result = manager._git_commit(Path("/repo"), "feat: test")
47
- assert result == "abc1234"
48
- assert mock_run.call_count == 4
49
-
50
-
51
- def test_dual_repo_detection(tmp_path, manager):
52
- # Mock code_root and mission_root to be different
53
- manager.code_root = Path("/projects/app")
54
- manager.mission_root = Path("/projects/tasks")
55
- assert manager.is_dual_repo is True
56
-
57
- manager.mission_root = Path("/projects/app")
58
- assert manager.is_dual_repo is False
59
-
60
-
61
- @pytest.mark.skip(reason="Test failing due to environment issue")
62
- def test_complete_issue_dual_repo_flow(tmp_path, manager):
63
- # Setup dual repo state
64
- code_root = tmp_path / "code"
65
- mission_root = tmp_path / "mission"
66
- code_root.mkdir()
67
- mission_root.mkdir()
68
-
69
- # Set them explicitly
70
- manager.code_root = code_root
71
- manager.mission_root = mission_root
72
- manager.issues_root = mission_root / "tasks"
73
- manager.create_issue("Test Task")
74
-
75
- print(f"DEBUG: mission_root={manager.mission_root}")
76
- print(f"DEBUG: code_root={manager.code_root}")
77
- print(f"DEBUG: is_dual_repo={manager.is_dual_repo}")
78
-
79
- with patch.object(TaskAgent, "_git_commit") as mock_commit:
80
- mock_commit.return_value = "hash123"
81
-
82
- manager.complete_issue("test-task", should_commit=True)
83
-
84
- # Should call commit twice (once for code, once for mission)
85
- # Plus the amend call
86
- assert mock_commit.call_count >= 2
87
- # Verify first call is for code repo
88
- assert mock_commit.call_args_list[0].args[0] == code_root
89
- # Verify second call is for mission repo
90
- assert mock_commit.call_args_list[1].args[0] == mission_root
91
-
92
-
93
- def test_git_commit_no_verify(manager):
94
-
95
- with (
96
- patch("subprocess.run") as mock_run,
97
- patch("subprocess.check_output") as mock_check,
98
- ):
99
- mock_run.return_value = MagicMock(returncode=0)
100
- mock_check.return_value = "abc1234\n"
101
-
102
- # Call with no_verify=True (default)
103
- result = manager._git_commit(Path("/repo"), "feat: test", no_verify=True)
104
- assert result == "abc1234"
105
-
106
- # Verify the commit call included --no-verify
107
- commit_call = mock_run.call_args_list[1]
108
- assert "--no-verify" in commit_call.args[0]
109
-
110
- # Call with no_verify=False
111
- result = manager._git_commit(Path("/repo"), "feat: test", no_verify=False)
112
- assert result == "abc1234"
113
-
114
- # Verify the commit call did NOT include --no-verify
115
- commit_call_2 = mock_run.call_args_list[3]
116
- assert "--no-verify" not in commit_call_2.args[0]
117
-
118
-
119
- def test_git_commit_clean_tree(manager):
120
- with patch("subprocess.run") as mock_run:
121
- # First call: git add (succeeds)
122
- # Second call: git commit (fails with returncode=1, but stdout says nothing to commit)
123
- mock_run.side_effect = [
124
- MagicMock(returncode=0),
125
- MagicMock(
126
- returncode=1, stdout="nothing to commit, working tree clean", stderr=""
127
- ),
128
- ]
129
-
130
- result = manager._git_commit(Path("/repo"), "feat: test")
131
- assert result == "no_changes"
File without changes
File without changes
File without changes