task-agent 1.3.7__tar.gz → 1.3.8__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {task_agent-1.3.7 → task_agent-1.3.8}/PKG-INFO +1 -1
- {task_agent-1.3.7 → task_agent-1.3.8}/pyproject.toml +1 -1
- {task_agent-1.3.7 → task_agent-1.3.8}/src/taskagent/cli.py +12 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/src/taskagent/manager.py +27 -4
- {task_agent-1.3.7 → task_agent-1.3.8}/src/taskagent/mcp.py +7 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/tests/test_cli.py +20 -0
- task_agent-1.3.8/tests/test_manager_git.py +253 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/tests/test_mcp.py +16 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/uv.lock +1 -1
- task_agent-1.3.7/tests/test_manager_git.py +0 -131
- {task_agent-1.3.7 → task_agent-1.3.8}/.agents/skills/next/SKILL.md +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/.gemini/settings.json +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/.github/workflows/ci.yml +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/.github/workflows/publish.yml +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/.gitignore +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/.mise.toml +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/.pre-commit-config.yaml +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/.python-version +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/.ta/agents/agy-cli/meta.toml +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/.ta/agents/antigravity/meta.toml +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/.ta/agents/gh/meta.toml +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/.ta/agents/minimal/meta.toml +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/.ta/agents/uat-aws/meta.toml +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/.ta/worker +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/.ta-config.json +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/CLAUDE.md +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/GEMINI.md +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/Makefile +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/README.md +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/agent.env +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/docs/README.md +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/docs/STATION-MAP.md +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/docs/architecture/README.md +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/docs/chats/gemini-conversation-1773283034591.json +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/docs/chats/project-init-to-v0.1.46.md +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/docs/cli/README.md +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/docs/development/README.md +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/docs/task-agents.md +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/docs/workflow/README.md +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/docs/workflow/trivial_worker_policy.md +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/opencode.json +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/plugins/antigravity/.gemini-plugin/plugin.json +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/plugins/antigravity/.mcp.json +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/plugins/antigravity/README.md +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/plugins/antigravity/hooks/hooks.json +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/plugins/antigravity/hooks/session_start.sh +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/plugins/antigravity/rules/task_agent.md +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/plugins/antigravity/scripts/statusline.sh +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/plugins/antigravity/skills/README.md +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/plugins/antigravity/skills/agent-import/SKILL.md +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/plugins/antigravity/skills/complete-task/SKILL.md +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/plugins/antigravity/skills/mission-workflow/SKILL.md +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/plugins/antigravity/skills/next/SKILL.md +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/plugins/antigravity/skills/next-task/SKILL.md +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/plugins/claude-code/.claude-plugin/plugin.json +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/plugins/claude-code/.mcp.json +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/plugins/claude-code/README.md +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/plugins/claude-code/hooks/hooks.json +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/plugins/claude-code/hooks/session_start.sh +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/plugins/claude-code/scripts/statusline.sh +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/plugins/claude-code/skills/agent-import/SKILL.md +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/plugins/claude-code/skills/complete-task/SKILL.md +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/plugins/claude-code/skills/mission-workflow/SKILL.md +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/plugins/claude-code/skills/next-task/SKILL.md +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/plugins/codex/.mcp.json +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/plugins/codex/README.md +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/plugins/copilot/.mcp.json +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/plugins/copilot/README.md +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/plugins/grok/.mcp.json +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/plugins/grok/README.md +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/scripts/clean_duplicates.py +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/scripts/migrate_tasks.py +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/scripts/recover_dates.py +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/sidecars/adk-worker/.env.example +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/sidecars/adk-worker/README.md +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/sidecars/adk-worker/instructions/manager.md +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/sidecars/adk-worker/instructions/validator.md +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/sidecars/adk-worker/instructions/worker.md +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/sidecars/adk-worker/profiles/default.json +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/sidecars/adk-worker/pyproject.toml +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/sidecars/adk-worker/uv.lock +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/sidecars/adk-worker/worker.py +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/sidecars/keet-bridge/README.md +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/sidecars/keet-bridge/bridge.js +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/sidecars/keet-bridge/package-lock.json +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/sidecars/keet-bridge/package.json +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/sidecars/keet-bridge/run.sh +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/sidecars/keet-bridge/test-e2e.js +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/sidecars/matrix-bridge/README.md +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/sidecars/matrix-bridge/matrix_bridge.py +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/skills/README.md +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/skills/agent-import/SKILL.md +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/skills/complete-task/SKILL.md +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/skills/mission-workflow/SKILL.md +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/skills/next/SKILL.md +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/skills/next-task/SKILL.md +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/src/taskagent/__init__.py +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/src/taskagent/__main__.py +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/src/taskagent/agent.py +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/src/taskagent/agent_registry.py +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/src/taskagent/audit.py +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/src/taskagent/chat/__init__.py +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/src/taskagent/chat/config.py +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/src/taskagent/chat/discovery.py +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/src/taskagent/chat/last_used.py +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/src/taskagent/config.py +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/src/taskagent/discovery.py +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/src/taskagent/importers/__init__.py +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/src/taskagent/importers/antigravity.py +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/src/taskagent/importers/base.py +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/src/taskagent/importers/claude_code.py +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/src/taskagent/importers/generic.py +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/src/taskagent/inbox.py +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/src/taskagent/models/__init__.py +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/src/taskagent/models/issue.py +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/src/taskagent/models/metric.py +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/src/taskagent/perf.py +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/src/taskagent/plugins/__init__.py +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/src/taskagent/plugins/github.py +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/src/taskagent/runner.py +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/src/taskagent/store_registry.py +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/src/taskagent/templates.py +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/src/taskagent/theme.py +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/test-dir/opencode.json +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/tests/conftest.py +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/tests/e2e/test_agent_sandboxing.sh +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/tests/test_agent.py +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/tests/test_agent_registry.py +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/tests/test_agy_cli_worker.py +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/tests/test_atomic_mutations.py +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/tests/test_auto_commit.py +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/tests/test_chat_config.py +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/tests/test_chat_discovery.py +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/tests/test_chat_last_used.py +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/tests/test_cli_windows.py +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/tests/test_discovery.py +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/tests/test_github_token.py +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/tests/test_importers.py +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/tests/test_inbox.py +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/tests/test_inbox_watch.py +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/tests/test_lease.py +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/tests/test_manager.py +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/tests/test_metrics.py +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/tests/test_migration.py +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/tests/test_perf.py +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/tests/test_plugin_antigravity.py +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/tests/test_plugin_claude.py +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/tests/test_project_host_root.py +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/tests/test_rebind.py +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/tests/test_remote_attach.py +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/tests/test_remote_create.py +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/tests/test_repo_resolve.py +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/tests/test_runner.py +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/tests/test_store_registry.py +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/tests/test_store_symlink.py +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/tests/test_templates.py +0 -0
- {task_agent-1.3.7 → task_agent-1.3.8}/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.
|
|
3
|
+
Version: 1.3.8
|
|
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
|
|
@@ -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
|
|
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,
|
|
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
|
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|