task-agent 1.3.10__tar.gz → 1.3.12__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 (164) hide show
  1. {task_agent-1.3.10 → task_agent-1.3.12}/.gitignore +2 -0
  2. {task_agent-1.3.10 → task_agent-1.3.12}/PKG-INFO +1 -1
  3. {task_agent-1.3.10 → task_agent-1.3.12}/docs/README.md +2 -0
  4. task_agent-1.3.12/docs/adr/0001-sync-task-stores-with-an-operation-log.md +121 -0
  5. {task_agent-1.3.10 → task_agent-1.3.12}/docs/architecture/README.md +2 -0
  6. {task_agent-1.3.10 → task_agent-1.3.12}/pyproject.toml +1 -1
  7. task_agent-1.3.12/scripts/check_task_worktree_cleanup.py +240 -0
  8. task_agent-1.3.12/src/taskagent/chat/last_used.py +531 -0
  9. {task_agent-1.3.10 → task_agent-1.3.12}/src/taskagent/cli.py +442 -41
  10. {task_agent-1.3.10 → task_agent-1.3.12}/src/taskagent/manager.py +150 -2
  11. {task_agent-1.3.10 → task_agent-1.3.12}/src/taskagent/mcp.py +32 -4
  12. task_agent-1.3.12/src/taskagent/oplog.py +289 -0
  13. {task_agent-1.3.10 → task_agent-1.3.12}/src/taskagent/store_registry.py +52 -2
  14. task_agent-1.3.12/src/taskagent/store_sync.py +441 -0
  15. {task_agent-1.3.10 → task_agent-1.3.12}/tests/test_chat_last_used.py +87 -0
  16. {task_agent-1.3.10 → task_agent-1.3.12}/tests/test_cli.py +103 -1
  17. task_agent-1.3.12/tests/test_oplog.py +220 -0
  18. task_agent-1.3.12/tests/test_store_sync.py +306 -0
  19. task_agent-1.3.12/tests/test_wsl_paths.py +31 -0
  20. {task_agent-1.3.10 → task_agent-1.3.12}/uv.lock +5 -5
  21. task_agent-1.3.10/src/taskagent/chat/last_used.py +0 -255
  22. task_agent-1.3.10/src/taskagent/theme.py +0 -17
  23. {task_agent-1.3.10 → task_agent-1.3.12}/.agents/skills/next/SKILL.md +0 -0
  24. {task_agent-1.3.10 → task_agent-1.3.12}/.gemini/settings.json +0 -0
  25. {task_agent-1.3.10 → task_agent-1.3.12}/.github/workflows/ci.yml +0 -0
  26. {task_agent-1.3.10 → task_agent-1.3.12}/.github/workflows/publish.yml +0 -0
  27. {task_agent-1.3.10 → task_agent-1.3.12}/.mise.toml +0 -0
  28. {task_agent-1.3.10 → task_agent-1.3.12}/.pre-commit-config.yaml +0 -0
  29. {task_agent-1.3.10 → task_agent-1.3.12}/.python-version +0 -0
  30. {task_agent-1.3.10 → task_agent-1.3.12}/.ta/agents/agy-cli/meta.toml +0 -0
  31. {task_agent-1.3.10 → task_agent-1.3.12}/.ta/agents/antigravity/meta.toml +0 -0
  32. {task_agent-1.3.10 → task_agent-1.3.12}/.ta/agents/gh/meta.toml +0 -0
  33. {task_agent-1.3.10 → task_agent-1.3.12}/.ta/agents/minimal/meta.toml +0 -0
  34. {task_agent-1.3.10 → task_agent-1.3.12}/.ta/agents/uat-aws/meta.toml +0 -0
  35. {task_agent-1.3.10 → task_agent-1.3.12}/.ta/worker +0 -0
  36. {task_agent-1.3.10 → task_agent-1.3.12}/.ta-config.json +0 -0
  37. {task_agent-1.3.10 → task_agent-1.3.12}/CLAUDE.md +0 -0
  38. {task_agent-1.3.10 → task_agent-1.3.12}/GEMINI.md +0 -0
  39. {task_agent-1.3.10 → task_agent-1.3.12}/Makefile +0 -0
  40. {task_agent-1.3.10 → task_agent-1.3.12}/README.md +0 -0
  41. {task_agent-1.3.10 → task_agent-1.3.12}/agent.env +0 -0
  42. {task_agent-1.3.10 → task_agent-1.3.12}/docs/STATION-MAP.md +0 -0
  43. {task_agent-1.3.10 → task_agent-1.3.12}/docs/chats/gemini-conversation-1773283034591.json +0 -0
  44. {task_agent-1.3.10 → task_agent-1.3.12}/docs/chats/project-init-to-v0.1.46.md +0 -0
  45. {task_agent-1.3.10 → task_agent-1.3.12}/docs/cli/README.md +0 -0
  46. {task_agent-1.3.10 → task_agent-1.3.12}/docs/development/README.md +0 -0
  47. {task_agent-1.3.10 → task_agent-1.3.12}/docs/task-agents.md +0 -0
  48. {task_agent-1.3.10 → task_agent-1.3.12}/docs/workflow/README.md +0 -0
  49. {task_agent-1.3.10 → task_agent-1.3.12}/docs/workflow/trivial_worker_policy.md +0 -0
  50. {task_agent-1.3.10 → task_agent-1.3.12}/opencode.json +0 -0
  51. {task_agent-1.3.10 → task_agent-1.3.12}/plugins/antigravity/.gemini-plugin/plugin.json +0 -0
  52. {task_agent-1.3.10 → task_agent-1.3.12}/plugins/antigravity/.mcp.json +0 -0
  53. {task_agent-1.3.10 → task_agent-1.3.12}/plugins/antigravity/README.md +0 -0
  54. {task_agent-1.3.10 → task_agent-1.3.12}/plugins/antigravity/hooks/hooks.json +0 -0
  55. {task_agent-1.3.10 → task_agent-1.3.12}/plugins/antigravity/hooks/session_start.sh +0 -0
  56. {task_agent-1.3.10 → task_agent-1.3.12}/plugins/antigravity/rules/task_agent.md +0 -0
  57. {task_agent-1.3.10 → task_agent-1.3.12}/plugins/antigravity/scripts/statusline.sh +0 -0
  58. {task_agent-1.3.10 → task_agent-1.3.12}/plugins/antigravity/skills/README.md +0 -0
  59. {task_agent-1.3.10 → task_agent-1.3.12}/plugins/antigravity/skills/agent-import/SKILL.md +0 -0
  60. {task_agent-1.3.10 → task_agent-1.3.12}/plugins/antigravity/skills/complete-task/SKILL.md +0 -0
  61. {task_agent-1.3.10 → task_agent-1.3.12}/plugins/antigravity/skills/mission-workflow/SKILL.md +0 -0
  62. {task_agent-1.3.10 → task_agent-1.3.12}/plugins/antigravity/skills/next/SKILL.md +0 -0
  63. {task_agent-1.3.10 → task_agent-1.3.12}/plugins/antigravity/skills/next-task/SKILL.md +0 -0
  64. {task_agent-1.3.10 → task_agent-1.3.12}/plugins/claude-code/.claude-plugin/plugin.json +0 -0
  65. {task_agent-1.3.10 → task_agent-1.3.12}/plugins/claude-code/.mcp.json +0 -0
  66. {task_agent-1.3.10 → task_agent-1.3.12}/plugins/claude-code/README.md +0 -0
  67. {task_agent-1.3.10 → task_agent-1.3.12}/plugins/claude-code/hooks/hooks.json +0 -0
  68. {task_agent-1.3.10 → task_agent-1.3.12}/plugins/claude-code/hooks/session_start.sh +0 -0
  69. {task_agent-1.3.10 → task_agent-1.3.12}/plugins/claude-code/scripts/statusline.sh +0 -0
  70. {task_agent-1.3.10 → task_agent-1.3.12}/plugins/claude-code/skills/agent-import/SKILL.md +0 -0
  71. {task_agent-1.3.10 → task_agent-1.3.12}/plugins/claude-code/skills/complete-task/SKILL.md +0 -0
  72. {task_agent-1.3.10 → task_agent-1.3.12}/plugins/claude-code/skills/mission-workflow/SKILL.md +0 -0
  73. {task_agent-1.3.10 → task_agent-1.3.12}/plugins/claude-code/skills/next-task/SKILL.md +0 -0
  74. {task_agent-1.3.10 → task_agent-1.3.12}/plugins/codex/.mcp.json +0 -0
  75. {task_agent-1.3.10 → task_agent-1.3.12}/plugins/codex/README.md +0 -0
  76. {task_agent-1.3.10 → task_agent-1.3.12}/plugins/copilot/.mcp.json +0 -0
  77. {task_agent-1.3.10 → task_agent-1.3.12}/plugins/copilot/README.md +0 -0
  78. {task_agent-1.3.10 → task_agent-1.3.12}/plugins/grok/.mcp.json +0 -0
  79. {task_agent-1.3.10 → task_agent-1.3.12}/plugins/grok/README.md +0 -0
  80. {task_agent-1.3.10 → task_agent-1.3.12}/scripts/clean_duplicates.py +0 -0
  81. {task_agent-1.3.10 → task_agent-1.3.12}/scripts/migrate_tasks.py +0 -0
  82. {task_agent-1.3.10 → task_agent-1.3.12}/scripts/recover_dates.py +0 -0
  83. {task_agent-1.3.10 → task_agent-1.3.12}/sidecars/adk-worker/.env.example +0 -0
  84. {task_agent-1.3.10 → task_agent-1.3.12}/sidecars/adk-worker/README.md +0 -0
  85. {task_agent-1.3.10 → task_agent-1.3.12}/sidecars/adk-worker/instructions/manager.md +0 -0
  86. {task_agent-1.3.10 → task_agent-1.3.12}/sidecars/adk-worker/instructions/validator.md +0 -0
  87. {task_agent-1.3.10 → task_agent-1.3.12}/sidecars/adk-worker/instructions/worker.md +0 -0
  88. {task_agent-1.3.10 → task_agent-1.3.12}/sidecars/adk-worker/profiles/default.json +0 -0
  89. {task_agent-1.3.10 → task_agent-1.3.12}/sidecars/adk-worker/pyproject.toml +0 -0
  90. {task_agent-1.3.10 → task_agent-1.3.12}/sidecars/adk-worker/uv.lock +0 -0
  91. {task_agent-1.3.10 → task_agent-1.3.12}/sidecars/adk-worker/worker.py +0 -0
  92. {task_agent-1.3.10 → task_agent-1.3.12}/sidecars/keet-bridge/README.md +0 -0
  93. {task_agent-1.3.10 → task_agent-1.3.12}/sidecars/keet-bridge/bridge.js +0 -0
  94. {task_agent-1.3.10 → task_agent-1.3.12}/sidecars/keet-bridge/package-lock.json +0 -0
  95. {task_agent-1.3.10 → task_agent-1.3.12}/sidecars/keet-bridge/package.json +0 -0
  96. {task_agent-1.3.10 → task_agent-1.3.12}/sidecars/keet-bridge/run.sh +0 -0
  97. {task_agent-1.3.10 → task_agent-1.3.12}/sidecars/keet-bridge/test-e2e.js +0 -0
  98. {task_agent-1.3.10 → task_agent-1.3.12}/sidecars/matrix-bridge/README.md +0 -0
  99. {task_agent-1.3.10 → task_agent-1.3.12}/sidecars/matrix-bridge/matrix_bridge.py +0 -0
  100. {task_agent-1.3.10 → task_agent-1.3.12}/skills/README.md +0 -0
  101. {task_agent-1.3.10 → task_agent-1.3.12}/skills/agent-import/SKILL.md +0 -0
  102. {task_agent-1.3.10 → task_agent-1.3.12}/skills/complete-task/SKILL.md +0 -0
  103. {task_agent-1.3.10 → task_agent-1.3.12}/skills/mission-workflow/SKILL.md +0 -0
  104. {task_agent-1.3.10 → task_agent-1.3.12}/skills/next/SKILL.md +0 -0
  105. {task_agent-1.3.10 → task_agent-1.3.12}/skills/next-task/SKILL.md +0 -0
  106. {task_agent-1.3.10 → task_agent-1.3.12}/src/taskagent/__init__.py +0 -0
  107. {task_agent-1.3.10 → task_agent-1.3.12}/src/taskagent/__main__.py +0 -0
  108. {task_agent-1.3.10 → task_agent-1.3.12}/src/taskagent/agent.py +0 -0
  109. {task_agent-1.3.10 → task_agent-1.3.12}/src/taskagent/agent_registry.py +0 -0
  110. {task_agent-1.3.10 → task_agent-1.3.12}/src/taskagent/audit.py +0 -0
  111. {task_agent-1.3.10 → task_agent-1.3.12}/src/taskagent/chat/__init__.py +0 -0
  112. {task_agent-1.3.10 → task_agent-1.3.12}/src/taskagent/chat/config.py +0 -0
  113. {task_agent-1.3.10 → task_agent-1.3.12}/src/taskagent/chat/discovery.py +0 -0
  114. {task_agent-1.3.10 → task_agent-1.3.12}/src/taskagent/config.py +0 -0
  115. {task_agent-1.3.10 → task_agent-1.3.12}/src/taskagent/discovery.py +0 -0
  116. {task_agent-1.3.10 → task_agent-1.3.12}/src/taskagent/importers/__init__.py +0 -0
  117. {task_agent-1.3.10 → task_agent-1.3.12}/src/taskagent/importers/antigravity.py +0 -0
  118. {task_agent-1.3.10 → task_agent-1.3.12}/src/taskagent/importers/base.py +0 -0
  119. {task_agent-1.3.10 → task_agent-1.3.12}/src/taskagent/importers/claude_code.py +0 -0
  120. {task_agent-1.3.10 → task_agent-1.3.12}/src/taskagent/importers/generic.py +0 -0
  121. {task_agent-1.3.10 → task_agent-1.3.12}/src/taskagent/inbox.py +0 -0
  122. {task_agent-1.3.10 → task_agent-1.3.12}/src/taskagent/models/__init__.py +0 -0
  123. {task_agent-1.3.10 → task_agent-1.3.12}/src/taskagent/models/issue.py +0 -0
  124. {task_agent-1.3.10 → task_agent-1.3.12}/src/taskagent/models/metric.py +0 -0
  125. {task_agent-1.3.10 → task_agent-1.3.12}/src/taskagent/perf.py +0 -0
  126. {task_agent-1.3.10 → task_agent-1.3.12}/src/taskagent/plugins/__init__.py +0 -0
  127. {task_agent-1.3.10 → task_agent-1.3.12}/src/taskagent/plugins/github.py +0 -0
  128. {task_agent-1.3.10 → task_agent-1.3.12}/src/taskagent/runner.py +0 -0
  129. {task_agent-1.3.10 → task_agent-1.3.12}/src/taskagent/templates.py +0 -0
  130. {task_agent-1.3.10 → task_agent-1.3.12}/test-dir/opencode.json +0 -0
  131. {task_agent-1.3.10 → task_agent-1.3.12}/tests/conftest.py +0 -0
  132. {task_agent-1.3.10 → task_agent-1.3.12}/tests/e2e/test_agent_sandboxing.sh +0 -0
  133. {task_agent-1.3.10 → task_agent-1.3.12}/tests/test_agent.py +0 -0
  134. {task_agent-1.3.10 → task_agent-1.3.12}/tests/test_agent_registry.py +0 -0
  135. {task_agent-1.3.10 → task_agent-1.3.12}/tests/test_agy_cli_worker.py +0 -0
  136. {task_agent-1.3.10 → task_agent-1.3.12}/tests/test_atomic_mutations.py +0 -0
  137. {task_agent-1.3.10 → task_agent-1.3.12}/tests/test_auto_commit.py +0 -0
  138. {task_agent-1.3.10 → task_agent-1.3.12}/tests/test_chat_config.py +0 -0
  139. {task_agent-1.3.10 → task_agent-1.3.12}/tests/test_chat_discovery.py +0 -0
  140. {task_agent-1.3.10 → task_agent-1.3.12}/tests/test_cli_windows.py +0 -0
  141. {task_agent-1.3.10 → task_agent-1.3.12}/tests/test_discovery.py +0 -0
  142. {task_agent-1.3.10 → task_agent-1.3.12}/tests/test_github_token.py +0 -0
  143. {task_agent-1.3.10 → task_agent-1.3.12}/tests/test_importers.py +0 -0
  144. {task_agent-1.3.10 → task_agent-1.3.12}/tests/test_inbox.py +0 -0
  145. {task_agent-1.3.10 → task_agent-1.3.12}/tests/test_inbox_watch.py +0 -0
  146. {task_agent-1.3.10 → task_agent-1.3.12}/tests/test_lease.py +0 -0
  147. {task_agent-1.3.10 → task_agent-1.3.12}/tests/test_manager.py +0 -0
  148. {task_agent-1.3.10 → task_agent-1.3.12}/tests/test_manager_git.py +0 -0
  149. {task_agent-1.3.10 → task_agent-1.3.12}/tests/test_mcp.py +0 -0
  150. {task_agent-1.3.10 → task_agent-1.3.12}/tests/test_metrics.py +0 -0
  151. {task_agent-1.3.10 → task_agent-1.3.12}/tests/test_migration.py +0 -0
  152. {task_agent-1.3.10 → task_agent-1.3.12}/tests/test_perf.py +0 -0
  153. {task_agent-1.3.10 → task_agent-1.3.12}/tests/test_plugin_antigravity.py +0 -0
  154. {task_agent-1.3.10 → task_agent-1.3.12}/tests/test_plugin_claude.py +0 -0
  155. {task_agent-1.3.10 → task_agent-1.3.12}/tests/test_project_host_root.py +0 -0
  156. {task_agent-1.3.10 → task_agent-1.3.12}/tests/test_rebind.py +0 -0
  157. {task_agent-1.3.10 → task_agent-1.3.12}/tests/test_remote_attach.py +0 -0
  158. {task_agent-1.3.10 → task_agent-1.3.12}/tests/test_remote_create.py +0 -0
  159. {task_agent-1.3.10 → task_agent-1.3.12}/tests/test_repo_resolve.py +0 -0
  160. {task_agent-1.3.10 → task_agent-1.3.12}/tests/test_runner.py +0 -0
  161. {task_agent-1.3.10 → task_agent-1.3.12}/tests/test_store_registry.py +0 -0
  162. {task_agent-1.3.10 → task_agent-1.3.12}/tests/test_store_symlink.py +0 -0
  163. {task_agent-1.3.10 → task_agent-1.3.12}/tests/test_templates.py +0 -0
  164. {task_agent-1.3.10 → task_agent-1.3.12}/tests/test_yazi.py +0 -0
@@ -20,3 +20,5 @@ docks/tasks
20
20
 
21
21
  # task-agent: human-facing store symlink
22
22
  docs/tasks
23
+ .map/
24
+ .ma/prompt/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: task-agent
3
- Version: 1.3.10
3
+ Version: 1.3.12
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
@@ -14,6 +14,8 @@ Welcome to the comprehensive documentation for **Task Agent**, a prioritized, fi
14
14
  - Information on project setup, testing, and release processes.
15
15
  - [**Station Map**](./STATION-MAP.md)
16
16
  - Formal declaration of task-agent as a typed file-path station system: stations, transitions, edge roles, and conformance gaps.
17
+ - [**ADR 0001: store sync**](./adr/0001-sync-task-stores-with-an-operation-log.md)
18
+ - Git carries an append-only per-writer operation log. The station tree is a projection.
17
19
 
18
20
  ## 🚀 Quick Start
19
21
 
@@ -0,0 +1,121 @@
1
+ # ADR 0001: Sync task stores with an append-only operation log
2
+
3
+ - Status: Accepted
4
+ - Date: 2026-10-29
5
+ - Scope: task-agent station stores, their `*-tasks` git remotes, native Windows clients, and handterm `ta`
6
+
7
+ ## Context
8
+
9
+ A task store is a typed path queue. Stations (`pending/`, `draft/`, `active/`, `completed/`, `deleted/`) hold one directory per slug. `mission.usv` is the priority index. Leases live in `lease.json`. The inbox is same-machine only.
10
+
11
+ Each durable store is already its own git repo under the machine data root (`TA_DATA_ROOT`, else `$XDG_DATA_HOME/task-agent`, else `~/.local/share/task-agent`). The subject repo stays separate. GitHub sibling repos are the remotes, for example `InTEGr8or/task-agent-tasks` and `bizkite-co/handterm-tasks`. `ta store remote {suggest,create,set,attach}` configures that remote. Mutations commit locally through `_commit_task_store`. They do not push. There is no `ta store pull` and no `ta store clone`.
12
+
13
+ That layout is the right archive. It is a weak multi-writer database.
14
+
15
+ - WSL and native Windows use different home directories. `ta store migrate` is refused on native Windows because of that split and because git often materializes symlinks as text files. A PowerShell client cannot replay the WSL migration.
16
+ - Two clones of one store on one PC diverge even when a remote exists. This machine's `task-agent-tasks` history had already moved past the GitHub `main` tip.
17
+ - The hot files are `mission.usv` and directory renames. Overlapping edits do not merge. `git pull --rebase` before every push stops the worker on the first conflict.
18
+ - handterm `ta` already targets `bizkite-co/handterm-tasks` through the GitHub Contents API. It writes a single markdown file, does not update `mission.usv`, and leaves the pending copy in place when it marks a task active. A phone cannot join a rebase.
19
+
20
+ Workers that must keep working are: WSL `ta`, a Windows client of the same PC, other machines, and handterm on a device that only has HTTPS.
21
+
22
+ ## Decision
23
+
24
+ Git remains the transport and the history log. The replicated object is an append-only operation log. The station tree and `mission.usv` are a projection of that log.
25
+
26
+ ### Log shape
27
+
28
+ Ops live inside the store:
29
+
30
+ ```text
31
+ .task-agent/ops/<writer-id>/<op-id>.op.json
32
+ ```
33
+
34
+ `<writer-id>` is one id per machine, stored at `<data-root>/writer-id` (override with `TA_WRITER_ID`). A writer appends only under its own directory. Sync unions every writer's files. Apply order is `(at, id)` with UTC timestamps. A hybrid logical clock can replace that sort later without moving the files.
35
+
36
+ Each file is one JSON object:
37
+
38
+ | Field | Meaning |
39
+ | :--- | :--- |
40
+ | `v` | Schema version. `1` for this ADR. |
41
+ | `id` | Unique, time-prefixed id. |
42
+ | `writer` | Machine writer id. |
43
+ | `at` | UTC timestamp. |
44
+ | `verb` | `create`, `move`, `edges`, `rename`, `lease`, `release`. |
45
+ | `slug` | Task slug the verb applies to. |
46
+ | `payload` | Verb fields. `create` carries `name` and `station`. `move` carries `station` and `from_station`. `edges` carries `blocked_by` and `subtask_of`. `rename` carries `new_slug`. |
47
+
48
+ Rules for version 1:
49
+
50
+ - The first `create` for a slug wins. A second `create` for the same slug is a no-op.
51
+ - A later `move` for the same slug wins.
52
+ - `edges` replaces the fields it names.
53
+ - Task markdown still rides in the same commit as the op while `ta` is the writer. A client with no working tree (handterm) puts the new task body in the `create` payload so a materializer can write the file.
54
+ - `ingest` is not an op. Adoption of loose markdown stays a local repair, not a sync verb.
55
+
56
+ ### Same machine
57
+
58
+ The data root stays on the WSL Linux filesystem (`~/.local/share/task-agent`). Do not point `TA_DATA_ROOT` at `/mnt/c` or `%LOCALAPPDATA%`. That creates a second store on NTFS and splits the registry.
59
+
60
+ Windows PowerShell sets one variable, from `ta store data-root --windows`:
61
+
62
+ ```text
63
+ TA_DATA_ROOT=\\wsl$\Debian\home\<user>\.local\share\task-agent
64
+ ```
65
+
66
+ That UNC is the same directory, not a copy. Code repos may stay on `C:\`. `ta store migrate` run from WSL moves an in-repo `.task-agent/tasks` tree into the Linux data root and records both `/mnt/c/...` and `C:\...` as host paths. There is no gate that blocks `ta` until every repo is migrated. Migrate the checkout you are using. Windows does not run `ta store migrate`.
67
+
68
+ ### Other machines and the phone
69
+
70
+ `ta sync` (not built yet) pulls the `*-tasks` repo, unions the log, rebuilds `mission.usv` and station moves from the projection, and pushes. Ordinary mutations keep appending ops and committing the projection so current agents still read directories.
71
+
72
+ handterm `ta`, defaulting to the subject repo's sibling `*-tasks` repo (`handterm` unless `--repo` is set), appends one op with the Git Data API. The branch update compare-and-swaps the expected SHA and retries once. It does not edit `mission.usv` itself.
73
+
74
+ A sequencer HTTP service may later assign order and fan the same log out. The file format does not change if that service appears. GitHub stays a valid relay until then.
75
+
76
+ ## Consequences
77
+
78
+ - Concurrent creates of different slugs merge by taking both files.
79
+ - Two moves of one slug have a defined winner (later op id) instead of a rebase conflict in `mission.usv`.
80
+ - The projection can be rebuilt from the log. Until `ta sync` exists, the committed tree is still what `ta next` reads, and the log is written beside it.
81
+ - Clock skew can reorder ops across machines. Acceptable for v1 because ids are the only cross-writer order. A logical clock is a follow-on, not a new store.
82
+ - Writers must not edit another writer's op directory. Git should not present those files as modify/modify conflicts.
83
+ - Existing stores gain ops on the next mutation. Old history is not backfilled.
84
+
85
+ ## Rejected strategies
86
+
87
+ ### Push the materialized tree after every change
88
+
89
+ Commits already happen. Adding `git push` after `_commit_task_store`, with or without `git pull --rebase`, publishes the projection. The second writer to touch `mission.usv` or rename the same slug loses or stops. handterm's one-file Contents API cannot rebase. Useful only as a one-time backup of a store that has no log yet.
90
+
91
+ ### Two clones on one PC, synced only through GitHub
92
+
93
+ This is the failure already seen when a centralized store and an ejected copy both stayed writable. Shared `TA_DATA_ROOT` removes the need to sync that pair.
94
+
95
+ ### GitHub Issues as the queue
96
+
97
+ `ta github sync` is a separate, unfinished, one-way import. Issues have no station paths, no `mission.usv` order, and no leases. Mirroring there would be a third queue.
98
+
99
+ ### GitHub Wiki, Keet, or Matrix as the store
100
+
101
+ The wiki was rejected as a product path when sibling `*-tasks` repos were chosen. Keet and Matrix rooms configured on a store are notification channels. They are not a file log and not a branch with compare-and-swap.
102
+
103
+ ### CRDT database or hosted SQL sync instead of the file checkout
104
+
105
+ Automerge, Yjs, ElectricSQL, Turso, and similar systems solve multi-writer sync by replacing the working copy. Agents and `ta` read station directories. The station map is a typed path queue. Putting a list CRDT under the op log is possible later for reorder. Replacing the tree with a hosted database drops the checkout those workers use.
106
+
107
+ ### A bespoke sequencer as the only writer, now
108
+
109
+ One online API that sequences every transition is a good relay once the phone and several agents are always connected. Offline `ta` would become a client of that service. The op log gets the same ordering rules and still moves through git. The sequencer can be added in front of the log. It is not a reason to pick a different replicated object.
110
+
111
+ ## Implementation status
112
+
113
+ Accepted and in progress on the task "Sync task stores with an append-only operation log".
114
+
115
+ | Slice | State |
116
+ | :--- | :--- |
117
+ | This ADR | Accepted |
118
+ | Append ops from store mutations, plus an in-memory reduce | In `src/taskagent/oplog.py` |
119
+ | `ta store sync` | Fast-forward or merge, then replay ops onto the tree and push. A successful store commit runs this when the store has `origin` and that git root is the store itself. A store inside a subject checkout is committed locally and not pushed. `TA_NO_AUTO_SYNC=1` skips the sync. A fetch, merge, or push failure keeps the local commit and aborts a half-finished merge. |
120
+ | handterm `ta new` | One `create` op plus the task file, via `commit-repo-files` (Git Data API, fast-forward ref update, one retry). The Lambda route has to be deployed before a phone can call it. |
121
+ | One-time publish | `ta store sync` pushes when the local store is a fast-forward of GitHub. |
@@ -2,6 +2,8 @@
2
2
 
3
3
  Task Agent is designed with a "filesystem-as-a-database" philosophy. This ensures that the task queue is highly portable, version-controlled by git, and easily readable by both humans and machines.
4
4
 
5
+ Multi-writer sync of a store is an append-only operation log under `.task-agent/ops/<writer-id>/`. Git transports that log. `mission.usv` and the station directories are a projection. See [ADR 0001](../adr/0001-sync-task-stores-with-an-operation-log.md).
6
+
5
7
  ## 📂 Data Storage
6
8
 
7
9
  All task data is stored within the `docs/tasks/` directory.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "task-agent"
3
- version = "1.3.10"
3
+ version = "1.3.12"
4
4
  description = "A prioritized, file-based task queue for autonomous agentic workers"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -0,0 +1,240 @@
1
+ """Classify task-agent worktrees whose branch is merged and whose task is closed.
2
+
3
+ A worktree is safe to remove when three local signals all agree:
4
+
5
+ 1. Branch merged: ``issue/<slug>`` is an ancestor of the integration branch
6
+ (--base, default uat). ``git cherry`` additionally detects content that
7
+ landed via cherry-pick/rebase (patch-merged); those branches need ``-D``
8
+ because ``git branch -d`` only trusts ancestry.
9
+ 2. Task closed: the task-agent store encodes status as directory location, so
10
+ the slug under ``completed/YYYY/MM/`` (or ``deleted/``) means closed, while
11
+ ``active/``, ``pending/``, ``draft/`` mean the worktree may still be a
12
+ runner staging area.
13
+ 3. Nothing to lose: ``git status --porcelain`` in the worktree is empty. A
14
+ worktree whose directory is gone (prunable registration) counts as clean;
15
+ removal uses ``git worktree prune`` for those.
16
+
17
+ Run from any project repo registered in the task-agent registry.
18
+
19
+ Usage:
20
+ python scripts/check_task_worktree_cleanup.py # report only
21
+ python scripts/check_task_worktree_cleanup.py --remove # remove safe ones
22
+ python scripts/check_task_worktree_cleanup.py --remove --allow-patch-merged
23
+
24
+ --base defaults to the current branch of the repo it runs in.
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ import argparse
30
+ import json
31
+ import subprocess
32
+ import sys
33
+ from dataclasses import dataclass
34
+ from pathlib import Path
35
+ from typing import Optional
36
+
37
+ REGISTRY_PATH = Path.home() / ".local/share/task-agent/registry.json"
38
+ OPEN_STATUSES = ("active", "pending", "draft")
39
+ CLOSED_STATUSES = ("completed", "deleted")
40
+
41
+
42
+ def run_git(
43
+ args: list[str], cwd: Optional[Path] = None
44
+ ) -> subprocess.CompletedProcess[str]:
45
+ return subprocess.run(
46
+ ["git", *args], capture_output=True, text=True, cwd=cwd, check=False
47
+ )
48
+
49
+
50
+ def moniker_from_remote(url: str) -> Optional[str]:
51
+ tail = url.removesuffix(".git")
52
+ tail = tail.rsplit(":", 1)[-1] if ":" in tail else tail
53
+ parts = [p for p in tail.split("/") if p]
54
+ if len(parts) >= 2:
55
+ return f"{parts[-2]}/{parts[-1]}"
56
+ return None
57
+
58
+
59
+ def resolve_store(repo: Path, override: Optional[str]) -> Optional[Path]:
60
+ if override:
61
+ return Path(override).expanduser()
62
+ if not REGISTRY_PATH.is_file():
63
+ return None
64
+ try:
65
+ registry = json.loads(REGISTRY_PATH.read_text(encoding="utf-8"))
66
+ except (OSError, ValueError):
67
+ return None
68
+ entries = registry.get("stores", {})
69
+ for entry in entries.values():
70
+ if str(repo) in entry.get("host_paths", []):
71
+ return Path(entry["store_path"])
72
+ remote = run_git(["remote", "get-url", "origin"], cwd=repo).stdout.strip()
73
+ moniker = moniker_from_remote(remote)
74
+ if moniker and moniker in entries:
75
+ return Path(entries[moniker]["store_path"])
76
+ return None
77
+
78
+
79
+ def task_status(store: Path, slug: str) -> str:
80
+ for status in OPEN_STATUSES:
81
+ if (store / status / slug).is_dir():
82
+ return status
83
+ if (store / "deleted" / slug).is_dir():
84
+ return "deleted"
85
+ for match in (store / "completed").glob(f"*/*/{slug}"):
86
+ if match.is_dir():
87
+ return "completed"
88
+ return "not-found"
89
+
90
+
91
+ def merged_state(repo: Path, branch: str, base: str) -> str:
92
+ if run_git(["merge-base", "--is-ancestor", branch, base], cwd=repo).returncode == 0:
93
+ return "ancestor"
94
+ cherry = run_git(["cherry", base, branch], cwd=repo)
95
+ if cherry.returncode == 0:
96
+ lines = [line for line in cherry.stdout.splitlines() if line.strip()]
97
+ if lines and all(line.startswith("-") for line in lines):
98
+ return "patch"
99
+ return "no"
100
+
101
+
102
+ def dirty_count(worktree: Path) -> Optional[int]:
103
+ if not worktree.is_dir():
104
+ return None
105
+ proc = run_git(["status", "--porcelain"], cwd=worktree)
106
+ return len([line for line in proc.stdout.splitlines() if line.strip()])
107
+
108
+
109
+ def issue_worktrees(repo: Path) -> list[tuple[Path, str]]:
110
+ proc = run_git(["worktree", "list", "--porcelain"], cwd=repo)
111
+ pairs: list[tuple[Path, str]] = []
112
+ worktree: Optional[Path] = None
113
+ for line in proc.stdout.splitlines():
114
+ if line.startswith("worktree "):
115
+ worktree = Path(line.split(" ", 1)[1])
116
+ elif line.startswith("branch refs/heads/issue/"):
117
+ branch = line.split(" ", 1)[1][len("refs/heads/") :]
118
+ if worktree is not None:
119
+ pairs.append((worktree, branch))
120
+ return pairs
121
+
122
+
123
+ @dataclass
124
+ class Row:
125
+ slug: str
126
+ worktree: Path
127
+ branch: str
128
+ merged: str
129
+ status: str
130
+ dirty: Optional[int]
131
+ verdict: str
132
+
133
+
134
+ def classify(repo: Path, store: Optional[Path], base: str) -> list[Row]:
135
+ rows: list[Row] = []
136
+ for worktree, branch in issue_worktrees(repo):
137
+ slug = branch[len("issue/") :]
138
+ merged = merged_state(repo, branch, base)
139
+ status = task_status(store, slug) if store else "unknown-store"
140
+ dirty = dirty_count(worktree)
141
+ if status not in CLOSED_STATUSES:
142
+ verdict = f"KEEP (task {status})"
143
+ elif merged == "no":
144
+ verdict = "REVIEW (unmerged)"
145
+ elif dirty:
146
+ verdict = "REVIEW (dirty)"
147
+ elif merged == "patch":
148
+ verdict = "REMOVE (patch-merged, -D)"
149
+ else:
150
+ verdict = "REMOVE"
151
+ rows.append(Row(slug, worktree, branch, merged, status, dirty, verdict))
152
+ return rows
153
+
154
+
155
+ def remove_row(repo: Path, row: Row, force_branch: bool) -> bool:
156
+ if row.worktree.is_dir():
157
+ proc = run_git(["worktree", "remove", str(row.worktree)], cwd=repo)
158
+ else:
159
+ proc = run_git(["worktree", "prune"], cwd=repo)
160
+ if proc.returncode != 0:
161
+ print(f" worktree remove FAILED: {row.worktree}")
162
+ return False
163
+ delete_flag = "-D" if force_branch else "-d"
164
+ proc = run_git(["branch", delete_flag, row.branch], cwd=repo)
165
+ if proc.returncode != 0:
166
+ print(f" branch {delete_flag} FAILED: {row.branch}: {proc.stderr.strip()}")
167
+ return False
168
+ print(f" removed {row.branch} ({row.worktree.name})")
169
+ return True
170
+
171
+
172
+ def main(argv: Optional[list[str]] = None) -> int:
173
+ parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
174
+ parser.add_argument("--base", help="integration branch (default: current branch)")
175
+ parser.add_argument("--store", help="task-agent store path override")
176
+ parser.add_argument(
177
+ "--remove",
178
+ action="store_true",
179
+ help="remove worktrees/branches verdicted REMOVE",
180
+ )
181
+ parser.add_argument(
182
+ "--allow-patch-merged",
183
+ action="store_true",
184
+ help="also remove patch-merged branches (deleted with -D)",
185
+ )
186
+ args = parser.parse_args(argv)
187
+
188
+ toplevel = run_git(["rev-parse", "--show-toplevel"]).stdout.strip()
189
+ if not toplevel:
190
+ print("not a git repository")
191
+ return 1
192
+ repo = Path(toplevel)
193
+ store = resolve_store(repo, args.store)
194
+ base = (
195
+ args.base
196
+ or run_git(["rev-parse", "--abbrev-ref", "HEAD"], cwd=repo).stdout.strip()
197
+ )
198
+ if not base:
199
+ print("could not determine integration branch; pass --base")
200
+ return 1
201
+ if store is None:
202
+ print("warning: task-agent store not found; task status unavailable")
203
+ else:
204
+ print(f"store: {store}")
205
+
206
+ rows = sorted(classify(repo, store, base), key=lambda r: (r.verdict, r.slug))
207
+ for row in rows:
208
+ if row.dirty is None:
209
+ dirty = "missing"
210
+ elif row.dirty:
211
+ dirty = f"{row.dirty} changed"
212
+ else:
213
+ dirty = "clean"
214
+ print(
215
+ f"{row.verdict:<26} merged={row.merged:<8} "
216
+ f"task={row.status:<10} {dirty:<12} {row.branch}"
217
+ )
218
+
219
+ removable = [r for r in rows if r.verdict == "REMOVE"]
220
+ patchable = [r for r in rows if r.verdict == "REMOVE (patch-merged, -D)"]
221
+ print(
222
+ f"\n{len(removable)} removable, {len(patchable)} patch-merged, "
223
+ f"{len(rows) - len(removable) - len(patchable)} to keep/review"
224
+ )
225
+ if patchable and not args.allow_patch_merged:
226
+ print("patch-merged branches need --allow-patch-merged (or manual -D)")
227
+
228
+ if args.remove:
229
+ ok = True
230
+ for row in removable:
231
+ ok = remove_row(repo, row, force_branch=False) and ok
232
+ if args.allow_patch_merged:
233
+ for row in patchable:
234
+ ok = remove_row(repo, row, force_branch=True) and ok
235
+ return 0 if ok else 1
236
+ return 0
237
+
238
+
239
+ if __name__ == "__main__":
240
+ sys.exit(main())