task-agent 1.0.2__tar.gz → 1.0.5__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.0.2 → task_agent-1.0.5}/PKG-INFO +1 -1
- {task_agent-1.0.2 → task_agent-1.0.5}/docs/README.md +2 -0
- task_agent-1.0.5/docs/STATION-MAP.md +155 -0
- {task_agent-1.0.2 → task_agent-1.0.5}/pyproject.toml +2 -2
- {task_agent-1.0.2 → task_agent-1.0.5}/src/taskagent/cli.py +59 -0
- {task_agent-1.0.2 → task_agent-1.0.5}/src/taskagent/manager.py +7 -4
- {task_agent-1.0.2 → task_agent-1.0.5}/src/taskagent/mcp.py +69 -2
- {task_agent-1.0.2 → task_agent-1.0.5}/tests/test_cli.py +29 -0
- {task_agent-1.0.2 → task_agent-1.0.5}/tests/test_manager.py +19 -0
- {task_agent-1.0.2 → task_agent-1.0.5}/tests/test_mcp.py +17 -0
- {task_agent-1.0.2 → task_agent-1.0.5}/uv.lock +1 -1
- {task_agent-1.0.2 → task_agent-1.0.5}/.env +0 -0
- {task_agent-1.0.2 → task_agent-1.0.5}/.gemini/settings.json +0 -0
- {task_agent-1.0.2 → task_agent-1.0.5}/.github/workflows/ci.yml +0 -0
- {task_agent-1.0.2 → task_agent-1.0.5}/.github/workflows/publish.yml +0 -0
- {task_agent-1.0.2 → task_agent-1.0.5}/.gitignore +0 -0
- {task_agent-1.0.2 → task_agent-1.0.5}/.mise.toml +0 -0
- {task_agent-1.0.2 → task_agent-1.0.5}/.pre-commit-config.yaml +0 -0
- {task_agent-1.0.2 → task_agent-1.0.5}/.python-version +0 -0
- {task_agent-1.0.2 → task_agent-1.0.5}/.ta/agents/agy-cli/meta.toml +0 -0
- {task_agent-1.0.2 → task_agent-1.0.5}/.ta/agents/gh/meta.toml +0 -0
- {task_agent-1.0.2 → task_agent-1.0.5}/.ta/agents/minimal/meta.toml +0 -0
- {task_agent-1.0.2 → task_agent-1.0.5}/.ta/agents/uat-aws/meta.toml +0 -0
- {task_agent-1.0.2 → task_agent-1.0.5}/.ta/worker +0 -0
- {task_agent-1.0.2 → task_agent-1.0.5}/CLAUDE.md +0 -0
- {task_agent-1.0.2 → task_agent-1.0.5}/GEMINI.md +0 -0
- {task_agent-1.0.2 → task_agent-1.0.5}/Makefile +0 -0
- {task_agent-1.0.2 → task_agent-1.0.5}/README.md +0 -0
- {task_agent-1.0.2 → task_agent-1.0.5}/agent.env +0 -0
- {task_agent-1.0.2 → task_agent-1.0.5}/docs/architecture/README.md +0 -0
- {task_agent-1.0.2 → task_agent-1.0.5}/docs/chats/gemini-conversation-1773283034591.json +0 -0
- {task_agent-1.0.2 → task_agent-1.0.5}/docs/chats/project-init-to-v0.1.46.md +0 -0
- {task_agent-1.0.2 → task_agent-1.0.5}/docs/cli/README.md +0 -0
- {task_agent-1.0.2 → task_agent-1.0.5}/docs/development/README.md +0 -0
- {task_agent-1.0.2 → task_agent-1.0.5}/docs/task-agents.md +0 -0
- {task_agent-1.0.2 → task_agent-1.0.5}/docs/workflow/README.md +0 -0
- {task_agent-1.0.2 → task_agent-1.0.5}/scripts/clean_duplicates.py +0 -0
- {task_agent-1.0.2 → task_agent-1.0.5}/scripts/migrate_tasks.py +0 -0
- {task_agent-1.0.2 → task_agent-1.0.5}/scripts/recover_dates.py +0 -0
- {task_agent-1.0.2 → task_agent-1.0.5}/sidecars/adk-worker/.env.example +0 -0
- {task_agent-1.0.2 → task_agent-1.0.5}/sidecars/adk-worker/README.md +0 -0
- {task_agent-1.0.2 → task_agent-1.0.5}/sidecars/adk-worker/instructions/manager.md +0 -0
- {task_agent-1.0.2 → task_agent-1.0.5}/sidecars/adk-worker/instructions/validator.md +0 -0
- {task_agent-1.0.2 → task_agent-1.0.5}/sidecars/adk-worker/instructions/worker.md +0 -0
- {task_agent-1.0.2 → task_agent-1.0.5}/sidecars/adk-worker/profiles/default.json +0 -0
- {task_agent-1.0.2 → task_agent-1.0.5}/sidecars/adk-worker/pyproject.toml +0 -0
- {task_agent-1.0.2 → task_agent-1.0.5}/sidecars/adk-worker/uv.lock +0 -0
- {task_agent-1.0.2 → task_agent-1.0.5}/sidecars/adk-worker/worker.py +0 -0
- {task_agent-1.0.2 → task_agent-1.0.5}/src/taskagent/__init__.py +0 -0
- {task_agent-1.0.2 → task_agent-1.0.5}/src/taskagent/__main__.py +0 -0
- {task_agent-1.0.2 → task_agent-1.0.5}/src/taskagent/agent.py +0 -0
- {task_agent-1.0.2 → task_agent-1.0.5}/src/taskagent/audit.py +0 -0
- {task_agent-1.0.2 → task_agent-1.0.5}/src/taskagent/config.py +0 -0
- {task_agent-1.0.2 → task_agent-1.0.5}/src/taskagent/discovery.py +0 -0
- {task_agent-1.0.2 → task_agent-1.0.5}/src/taskagent/models/__init__.py +0 -0
- {task_agent-1.0.2 → task_agent-1.0.5}/src/taskagent/models/issue.py +0 -0
- {task_agent-1.0.2 → task_agent-1.0.5}/src/taskagent/models/metric.py +0 -0
- {task_agent-1.0.2 → task_agent-1.0.5}/src/taskagent/plugins/__init__.py +0 -0
- {task_agent-1.0.2 → task_agent-1.0.5}/src/taskagent/plugins/github.py +0 -0
- {task_agent-1.0.2 → task_agent-1.0.5}/src/taskagent/templates.py +0 -0
- {task_agent-1.0.2 → task_agent-1.0.5}/test-dir/opencode.json +0 -0
- {task_agent-1.0.2 → task_agent-1.0.5}/tests/conftest.py +0 -0
- {task_agent-1.0.2 → task_agent-1.0.5}/tests/e2e/test_agent_sandboxing.sh +0 -0
- {task_agent-1.0.2 → task_agent-1.0.5}/tests/test_agent.py +0 -0
- {task_agent-1.0.2 → task_agent-1.0.5}/tests/test_agy_cli_worker.py +0 -0
- {task_agent-1.0.2 → task_agent-1.0.5}/tests/test_cli_windows.py +0 -0
- {task_agent-1.0.2 → task_agent-1.0.5}/tests/test_discovery.py +0 -0
- {task_agent-1.0.2 → task_agent-1.0.5}/tests/test_manager_git.py +0 -0
- {task_agent-1.0.2 → task_agent-1.0.5}/tests/test_metrics.py +0 -0
- {task_agent-1.0.2 → task_agent-1.0.5}/tests/test_migration.py +0 -0
- {task_agent-1.0.2 → task_agent-1.0.5}/tests/test_templates.py +0 -0
|
@@ -12,6 +12,8 @@ Welcome to the comprehensive documentation for **Task Agent**, a prioritized, fi
|
|
|
12
12
|
- Best practices for managing tasks from draft to completion.
|
|
13
13
|
- [**Development**](./development/README.md)
|
|
14
14
|
- Information on project setup, testing, and release processes.
|
|
15
|
+
- [**Station Map**](./STATION-MAP.md)
|
|
16
|
+
- Formal declaration of task-agent as a typed file-path station system: stations, transitions, edge roles, and conformance gaps.
|
|
15
17
|
|
|
16
18
|
## 🚀 Quick Start
|
|
17
19
|
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# task-agent Station Map
|
|
2
|
+
|
|
3
|
+
> **Status: RATIFIED — installed at `docs/STATION-MAP.md` on 2026-07-15.**
|
|
4
|
+
>
|
|
5
|
+
> This document declares task-agent as an instance of the typed file-path-queue substrate
|
|
6
|
+
> ("a typed state machine where the states are portable file paths") whose vocabulary is
|
|
7
|
+
> owned by the library design spec in the cocli repo (task:
|
|
8
|
+
> `design-spec-for-reusable-typed-file-path-queue-transformer-library-extracted-from-cocli`;
|
|
9
|
+
> thesis: cocli `docs/DESCRIPTION.md`). Do not fork terminology here — if a term is missing,
|
|
10
|
+
> add it to the spec, then use it.
|
|
11
|
+
>
|
|
12
|
+
> Sections marked **observed** describe the implementation as found on 2026-07-12.
|
|
13
|
+
> Sections marked **declared (v1)** are proposals ratified on 2026-07-15.
|
|
14
|
+
|
|
15
|
+
## 1. Record type (observed)
|
|
16
|
+
|
|
17
|
+
One task = one directory, named by **slug**, containing `README.md`
|
|
18
|
+
(Markdown body + YAML frontmatter, currently `created_at` only). The runtime model is
|
|
19
|
+
`Issue` (`src/taskagent/models/issue.py`): `name`, `slug`, `blocked_by: List[str]`,
|
|
20
|
+
`subtask_of: Optional[str]`, with `priority` and `status` derived at runtime.
|
|
21
|
+
Serialization to the index is USV (`\x1f`-delimited).
|
|
22
|
+
|
|
23
|
+
Edge fields are persisted as **body prose**, regex-extracted (`manager.py` ~582):
|
|
24
|
+
`**Blocked by:**`, `**Subtask of:**`, with legacy `**Depends on:**` read as an alias
|
|
25
|
+
for `blocked_by`.
|
|
26
|
+
|
|
27
|
+
**Declared (v1):** edge fields move to structured frontmatter (`blocked_by:`,
|
|
28
|
+
`subtask_of:` YAML lists); the prose-line reader remains as an alias-tolerant fallback
|
|
29
|
+
during migration. Frontmatter is the schema; prose is presentation.
|
|
30
|
+
|
|
31
|
+
## 2. Stations (observed)
|
|
32
|
+
|
|
33
|
+
Root: `.task-agent/tasks/` (symlinked at `docs/tasks/`).
|
|
34
|
+
|
|
35
|
+
| Station | Record state | Sharding | Notes |
|
|
36
|
+
| :--- | :--- | :--- | :--- |
|
|
37
|
+
| `draft/` | intention, not yet actionable | flat | |
|
|
38
|
+
| `pending/` | actionable, unclaimed | flat | |
|
|
39
|
+
| `active/` | claimed by a worker | flat | |
|
|
40
|
+
| `completed/` | terminal fact | by year (`2026/`) | month-sharding is a pending task |
|
|
41
|
+
| `deleted/` | terminal, rejected | flat | tombstone station |
|
|
42
|
+
| `mr/` | merge request datagram | by year | **Declared (v1):** task-machine station (see §2a) |
|
|
43
|
+
| `strategy/`, `plan.md` | mission-level records | — | not task records; out of scope of the task state machine |
|
|
44
|
+
|
|
45
|
+
**Declared (v1):** every station binds `(path template, record type, serialization)`.
|
|
46
|
+
`strategy/` and `plan.md` are explicitly **out of scope of the task state machine** —
|
|
47
|
+
they are mission-level documents with their own lifecycle, not task records.
|
|
48
|
+
|
|
49
|
+
### §2a. `mr/` station (ratified)
|
|
50
|
+
|
|
51
|
+
**Record type:** Solution datagram — a text file (`<slug>.md`) containing the solution
|
|
52
|
+
explanation written by a worker agent.
|
|
53
|
+
|
|
54
|
+
**Inbound transition:** A worker that cannot directly commit writes its solution to
|
|
55
|
+
`mr/<slug>.md` instead of calling `ta done`. This is the handoff from the worker's
|
|
56
|
+
isolated environment.
|
|
57
|
+
|
|
58
|
+
**Outbound transition:** `ta merge <slug>` reads the datagram, calls `complete_issue`,
|
|
59
|
+
moves the task to `completed/`, and unlinks the MR file. This is a full task-machine
|
|
60
|
+
transition (`active/` → `completed/`) mediated by the datagram.
|
|
61
|
+
|
|
62
|
+
**Conclusion:** `mr/` is a **station of the task machine**, not a separate machine.
|
|
63
|
+
Its records are ephemeral (deleted on merge); they do not need their own durability
|
|
64
|
+
contract beyond the filesystem.
|
|
65
|
+
|
|
66
|
+
## 3. Transitions (observed)
|
|
67
|
+
|
|
68
|
+
Typed from-station→to-station moves, all implemented as directory renames:
|
|
69
|
+
|
|
70
|
+
| Transition | From → To | Notes |
|
|
71
|
+
| :--- | :--- | :--- |
|
|
72
|
+
| `create` | ∅ → `pending/` (or `draft/` with flag) | assigns slug from title |
|
|
73
|
+
| `promote` | `draft/` → `pending/` | cascades along edges |
|
|
74
|
+
| `demote` | reverse of promote | |
|
|
75
|
+
| `start` / mark-active | `pending/` → `active/` | **this is the claim** |
|
|
76
|
+
| `complete` | `active/` → `completed/YYYY/` | terminal |
|
|
77
|
+
| `restore` | `completed/` → active tree | |
|
|
78
|
+
| `delete` | → `deleted/` | tombstone, not erasure |
|
|
79
|
+
| `merge` (via `mr/`) | `mr/<slug>.md` → `complete` | worker handoff path |
|
|
80
|
+
|
|
81
|
+
**Declared (v1):** each transition is a pure typed transform on the record
|
|
82
|
+
(from-model-to-model); a transition never partially rewrites a record it isn't moving.
|
|
83
|
+
|
|
84
|
+
## 4. Edge roles: queue / WAL / index (observed + declared)
|
|
85
|
+
|
|
86
|
+
Roles attach to *consumer edges*, not to directories. Current reading:
|
|
87
|
+
|
|
88
|
+
- **Queue (future):** `pending/` → worker edge. `start` is the claim; `active/` holds the
|
|
89
|
+
lease; `completed/`, `deleted/` are terminal states. Single-worker today, so the claim
|
|
90
|
+
protocol is trivially safe; multi-agent use makes atomic-rename claiming load-bearing.
|
|
91
|
+
- **WAL (past):** `completed/YYYY/` is the fact log of the task machine — append-only,
|
|
92
|
+
year-partitioned segments, source of truth for history. The pending
|
|
93
|
+
month-sharding+WAL+compaction task extends exactly this role.
|
|
94
|
+
- **Index (present):** the USV issues index (with `datapackage` field declarations in
|
|
95
|
+
`manager.py`) is a fold over the station tree; blocked-state computation is a derived
|
|
96
|
+
view over `blocked_by` edges. Both must be rebuildable by rescanning stations —
|
|
97
|
+
**stations are the source of truth; the index is disposable.**
|
|
98
|
+
- **Event log:** `AuditLog` (`src/taskagent/audit.py`) writes JSONL lifecycle events to
|
|
99
|
+
`.task-agent/logs/YYYY-MM-DD.jsonl`, pruned after 30 days.
|
|
100
|
+
**Ratified:** `AuditLog` is **telemetry**, not a WAL. The station directories are the
|
|
101
|
+
single source of truth for task state; the JSONL log is observability data only. Pruning
|
|
102
|
+
after 30 days is correct and intentional. No compaction policy is needed.
|
|
103
|
+
|
|
104
|
+
## 5. Identity (ratified)
|
|
105
|
+
|
|
106
|
+
The **directory name (slug)** is the immutable unique identifier of a task, assigned at
|
|
107
|
+
creation from the title and never subsequently derived from mutable fields.
|
|
108
|
+
|
|
109
|
+
- Editing the `# Title` heading in `README.md` changes the task's *display name* only;
|
|
110
|
+
the slug (and therefore its station path) is unchanged.
|
|
111
|
+
- Direct directory renaming is **forbidden** as an end-user operation — it breaks the
|
|
112
|
+
index, `blocked_by`/`subtask_of` references in other tasks, and `completed/` history.
|
|
113
|
+
- A future `ta rename <slug> <new-title>` command is the only supported path for slug
|
|
114
|
+
migration. It must: rename the directory, update `mission.usv`, and rewrite all
|
|
115
|
+
`blocked_by`/`subtask_of` references that point to the old slug.
|
|
116
|
+
|
|
117
|
+
## 6. Conformance gaps (the migration backlog)
|
|
118
|
+
|
|
119
|
+
Each accepted gap becomes one follow-up task. Written as typed defects against the
|
|
120
|
+
substrate's invariants:
|
|
121
|
+
|
|
122
|
+
1. **Identity derived from mutable content.** Slug comes from the *original* title;
|
|
123
|
+
retitling a task changes its display name but not its identity, and lookups by new
|
|
124
|
+
title fail. Invariant: identity is assigned at creation and never derived from
|
|
125
|
+
mutable fields. Fix: add a `ta rename` command (see §5).
|
|
126
|
+
2. **Schema data outside the schema.** `blocked_by`/`subtask_of` live as regex-matched
|
|
127
|
+
prose lines. Move to frontmatter (§1 declared); keep the alias reader through migration.
|
|
128
|
+
3. **Non-atomic record updates.** Content updates (`update_task`) rewrite the body and
|
|
129
|
+
silently drop edges — a read-modify-write that loses fields the writer didn't know
|
|
130
|
+
about. Invariant: a record update is a whole-record typed transform; unknown fields
|
|
131
|
+
round-trip. (This is the bug that repeatedly wiped dependencies on the cocli epic.)
|
|
132
|
+
4. **Station store excluded from its own durability.** `.task-agent` is gitignored in
|
|
133
|
+
consumer repos, so `commit_tasks` fails and task state stays uncommitted. Declare the
|
|
134
|
+
durability contract: either the station tree is committed (un-ignore) or durability is
|
|
135
|
+
explicitly delegated elsewhere.
|
|
136
|
+
5. **Edge-type conflation cleanup.** `blocked_by` (ordering) vs `subtask_of` (hierarchy)
|
|
137
|
+
are now separate in the `Issue` model with a legacy alias — finish the migration:
|
|
138
|
+
stored prose lines, docstrings (`create_task` still describes deps as parent-of
|
|
139
|
+
promotion cascade), and historical records under `completed/`.
|
|
140
|
+
6. **Undeclared station resolved.** `mr/` declared in §2a.
|
|
141
|
+
7. **Log role declared.** `AuditLog` is telemetry (§4).
|
|
142
|
+
8. **Index rebuildability unproven.** No command exists to rebuild the USV index from a
|
|
143
|
+
station rescan and verify it matches. This is also the seed of the conformance tool:
|
|
144
|
+
`taskagent audit --conformance` = validate station tree against this map + rebuild
|
|
145
|
+
index and diff. That tool, once built, becomes the executable `done_check` for every
|
|
146
|
+
subsequent conformance transform (the portable-task acceptance predicate).
|
|
147
|
+
|
|
148
|
+
## 7. Relationship to the library spec
|
|
149
|
+
|
|
150
|
+
task-agent is **dogfood consumer #2** (after cocli). The library spec owns: station/
|
|
151
|
+
transition/edge-role vocabulary, the claim protocol, WAL/compaction semantics,
|
|
152
|
+
fold/watermark semantics, and schema versioning. This map owns only: which stations
|
|
153
|
+
exist here, their record types, and this repo's conformance state. When the library's
|
|
154
|
+
Protocol interfaces land, task-agent's `manager.py` should be refactorable to implement
|
|
155
|
+
them with no on-disk change — that refactor being cheap is the test that this map is true.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "task-agent"
|
|
3
|
-
version = "1.0.
|
|
3
|
+
version = "1.0.5"
|
|
4
4
|
description = "A prioritized, file-based task queue for autonomous agentic workers"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.12"
|
|
@@ -39,7 +39,7 @@ dev = [
|
|
|
39
39
|
]
|
|
40
40
|
|
|
41
41
|
[tool.bumpversion]
|
|
42
|
-
current_version = "1.0.
|
|
42
|
+
current_version = "1.0.5"
|
|
43
43
|
parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)"
|
|
44
44
|
serialize = ["{major}.{minor}.{patch}"]
|
|
45
45
|
search = "version = \"{current_version}\""
|
|
@@ -1745,8 +1745,62 @@ def cmd_new(
|
|
|
1745
1745
|
interactive: bool = False,
|
|
1746
1746
|
blocked_by: Optional[str] = None,
|
|
1747
1747
|
subtask_of: Optional[str] = None,
|
|
1748
|
+
bulk: Optional[str] = None,
|
|
1748
1749
|
):
|
|
1749
1750
|
"""Create a new issue."""
|
|
1751
|
+
if bulk:
|
|
1752
|
+
try:
|
|
1753
|
+
if bulk == "-":
|
|
1754
|
+
raw_data = sys.stdin.read()
|
|
1755
|
+
else:
|
|
1756
|
+
raw_data = Path(bulk).read_text(encoding="utf-8")
|
|
1757
|
+
|
|
1758
|
+
tasks = json.loads(raw_data)
|
|
1759
|
+
if not isinstance(tasks, list):
|
|
1760
|
+
raise ValueError("Bulk JSON must be a list/array of task objects.")
|
|
1761
|
+
|
|
1762
|
+
for idx, t in enumerate(tasks):
|
|
1763
|
+
t_title = t.get("title")
|
|
1764
|
+
t_criteria = t.get("completion_criteria")
|
|
1765
|
+
if not t_title:
|
|
1766
|
+
console.print(
|
|
1767
|
+
f"[red]Error at task {idx}: 'title' is required.[/red]"
|
|
1768
|
+
)
|
|
1769
|
+
continue
|
|
1770
|
+
if not t_criteria:
|
|
1771
|
+
console.print(
|
|
1772
|
+
f"[red]Error at task '{t_title}' (index {idx}): 'completion_criteria' is required.[/red]"
|
|
1773
|
+
)
|
|
1774
|
+
continue
|
|
1775
|
+
|
|
1776
|
+
t_body = t.get("body", "")
|
|
1777
|
+
t_draft = t.get("draft", draft)
|
|
1778
|
+
t_blocked_by = t.get("blocked_by") or t.get("depends_on")
|
|
1779
|
+
t_subtask_of = t.get("subtask_of")
|
|
1780
|
+
t_as_dir = t.get("as_dir", as_dir)
|
|
1781
|
+
|
|
1782
|
+
issue = manager.create_issue(
|
|
1783
|
+
title=t_title,
|
|
1784
|
+
body=t_body,
|
|
1785
|
+
draft=t_draft,
|
|
1786
|
+
depends_on=t_blocked_by,
|
|
1787
|
+
as_dir=t_as_dir,
|
|
1788
|
+
completion_criteria=t_criteria,
|
|
1789
|
+
blocked_by=t_blocked_by,
|
|
1790
|
+
subtask_of=t_subtask_of,
|
|
1791
|
+
)
|
|
1792
|
+
console.print(
|
|
1793
|
+
f"[bold green]Created new issue: {issue.slug}[/bold green]"
|
|
1794
|
+
)
|
|
1795
|
+
issue_file = manager.find_issue_file(issue.slug)
|
|
1796
|
+
console.print(f"File: {issue_file}")
|
|
1797
|
+
if issue.subtask_of:
|
|
1798
|
+
console.print(f"Subtask of: {issue.subtask_of}")
|
|
1799
|
+
return
|
|
1800
|
+
except Exception as e:
|
|
1801
|
+
console.print(f"[red]Error processing bulk tasks: {e}[/red]")
|
|
1802
|
+
sys.exit(1)
|
|
1803
|
+
|
|
1750
1804
|
if interactive:
|
|
1751
1805
|
editor = get_editor()
|
|
1752
1806
|
slug = manager.slugify(title or "new-task")
|
|
@@ -4045,6 +4099,10 @@ Usage:
|
|
|
4045
4099
|
"--subtask-of",
|
|
4046
4100
|
help="Slug of the parent task this task is a subtask of, e.g. 'cli-consolidation'.",
|
|
4047
4101
|
)
|
|
4102
|
+
new_parser.add_argument(
|
|
4103
|
+
"--bulk",
|
|
4104
|
+
help="Path to a JSON file containing an array of task definitions, or '-' to read JSON from stdin.",
|
|
4105
|
+
)
|
|
4048
4106
|
version_parser = subparsers.add_parser("version")
|
|
4049
4107
|
v_sub = version_parser.add_subparsers(dest="version_command")
|
|
4050
4108
|
p_v = v_sub.add_parser("promote")
|
|
@@ -4202,6 +4260,7 @@ Usage:
|
|
|
4202
4260
|
interactive=args.interactive,
|
|
4203
4261
|
blocked_by=args.blocked_by,
|
|
4204
4262
|
subtask_of=args.subtask_of,
|
|
4263
|
+
bulk=args.bulk,
|
|
4205
4264
|
)
|
|
4206
4265
|
elif args.command == "worktree":
|
|
4207
4266
|
cmd_worktree(console, manager, args)
|
|
@@ -485,7 +485,7 @@ class TaskAgent:
|
|
|
485
485
|
status = issue_file.parent.name
|
|
486
486
|
else:
|
|
487
487
|
# Fallback during migration or if file is temporarily gone
|
|
488
|
-
status = "
|
|
488
|
+
status = "unknown"
|
|
489
489
|
|
|
490
490
|
issues.append(
|
|
491
491
|
Issue(
|
|
@@ -538,8 +538,10 @@ class TaskAgent:
|
|
|
538
538
|
json.dump(datapackage, f, indent=2)
|
|
539
539
|
self._set_writable(dp_path, False)
|
|
540
540
|
|
|
541
|
-
def sync_mission(self) -> List[Issue]:
|
|
541
|
+
def sync_mission(self, ingest: bool = True) -> List[Issue]:
|
|
542
542
|
"""Load, sort by status groups, and save back."""
|
|
543
|
+
if ingest:
|
|
544
|
+
self.ingest_issues(sync=False)
|
|
543
545
|
issues = self.load_mission()
|
|
544
546
|
if not issues:
|
|
545
547
|
return []
|
|
@@ -1428,7 +1430,7 @@ class TaskAgent:
|
|
|
1428
1430
|
status="completed",
|
|
1429
1431
|
)
|
|
1430
1432
|
|
|
1431
|
-
def ingest_issues(self) -> Tuple[int, int]:
|
|
1433
|
+
def ingest_issues(self, sync: bool = True) -> Tuple[int, int]:
|
|
1432
1434
|
"""Ingest existing markdown files. Returns (num_new, num_removed)."""
|
|
1433
1435
|
self.ensure_issues_dir()
|
|
1434
1436
|
|
|
@@ -1543,7 +1545,8 @@ class TaskAgent:
|
|
|
1543
1545
|
|
|
1544
1546
|
final_issues = present_issues + new_issues
|
|
1545
1547
|
self.save_mission(final_issues)
|
|
1546
|
-
|
|
1548
|
+
if sync:
|
|
1549
|
+
self.sync_mission(ingest=False)
|
|
1547
1550
|
|
|
1548
1551
|
return len(new_issues), len(existing_issues) - len(present_issues)
|
|
1549
1552
|
|
|
@@ -455,8 +455,75 @@ def commit_tasks(message: str = "", push: bool = False) -> str:
|
|
|
455
455
|
return f"Committed: {message}"
|
|
456
456
|
except subprocess.CalledProcessError as e:
|
|
457
457
|
return f"Error: {e.stderr}"
|
|
458
|
-
|
|
459
|
-
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
@mcp.tool()
|
|
461
|
+
def create_tasks(
|
|
462
|
+
tasks: list[dict],
|
|
463
|
+
) -> str:
|
|
464
|
+
"""Create multiple new tasks in the mission queue at once.
|
|
465
|
+
|
|
466
|
+
Args:
|
|
467
|
+
tasks: A list of task definitions. Each definition is a dictionary that
|
|
468
|
+
can contain the following keys:
|
|
469
|
+
- 'title' (str, required): The title of the task.
|
|
470
|
+
- 'completion_criteria' (str, required): Clear, measurable criteria for task completion.
|
|
471
|
+
- 'body' (str, optional): Detailed description of the task.
|
|
472
|
+
- 'draft' (bool, optional): If True, creates the task in 'draft' status. Default is False (pending).
|
|
473
|
+
- 'blocked_by' (str, optional): Comma-separated list of existing task slugs this task depends on.
|
|
474
|
+
- 'depends_on' (str, optional): Deprecated alias for blocked_by.
|
|
475
|
+
- 'subtask_of' (str, optional): Slug of the parent task this task is a subtask of.
|
|
476
|
+
- 'as_dir' (bool, optional): Create as folder directory. Default is True.
|
|
477
|
+
|
|
478
|
+
Returns:
|
|
479
|
+
A summary report of created task slugs and any errors.
|
|
480
|
+
"""
|
|
481
|
+
manager = get_manager()
|
|
482
|
+
created = []
|
|
483
|
+
errors = []
|
|
484
|
+
|
|
485
|
+
for idx, t in enumerate(tasks):
|
|
486
|
+
title = t.get("title")
|
|
487
|
+
criteria = t.get("completion_criteria")
|
|
488
|
+
if not title:
|
|
489
|
+
errors.append(f"Task at index {idx} is missing required 'title'.")
|
|
490
|
+
continue
|
|
491
|
+
if not criteria:
|
|
492
|
+
errors.append(
|
|
493
|
+
f"Task '{title}' (index {idx}) is missing required 'completion_criteria'."
|
|
494
|
+
)
|
|
495
|
+
continue
|
|
496
|
+
|
|
497
|
+
body = t.get("body", "")
|
|
498
|
+
draft = t.get("draft", False)
|
|
499
|
+
blocked_by = t.get("blocked_by") or t.get("depends_on")
|
|
500
|
+
subtask_of = t.get("subtask_of")
|
|
501
|
+
as_dir = t.get("as_dir", True)
|
|
502
|
+
|
|
503
|
+
try:
|
|
504
|
+
issue = manager.create_issue(
|
|
505
|
+
title=title,
|
|
506
|
+
body=body,
|
|
507
|
+
draft=draft,
|
|
508
|
+
depends_on=blocked_by,
|
|
509
|
+
as_dir=as_dir,
|
|
510
|
+
completion_criteria=criteria,
|
|
511
|
+
blocked_by=blocked_by,
|
|
512
|
+
subtask_of=subtask_of,
|
|
513
|
+
)
|
|
514
|
+
created.append(f"{issue.slug} (Status: {issue.status})")
|
|
515
|
+
except Exception as e:
|
|
516
|
+
errors.append(f"Error creating task '{title}': {e}")
|
|
517
|
+
|
|
518
|
+
report = []
|
|
519
|
+
if created:
|
|
520
|
+
report.append(
|
|
521
|
+
"Successfully created tasks:\n" + "\n".join(f"- {c}" for c in created)
|
|
522
|
+
)
|
|
523
|
+
if errors:
|
|
524
|
+
report.append("Errors encountered:\n" + "\n".join(f"- {err}" for err in errors))
|
|
525
|
+
|
|
526
|
+
return "\n\n".join(report)
|
|
460
527
|
|
|
461
528
|
|
|
462
529
|
def run_mcp_server():
|
|
@@ -75,6 +75,35 @@ def test_cmd_new_dir(manager, temp_issues_dir):
|
|
|
75
75
|
assert issues[0].status == "draft"
|
|
76
76
|
|
|
77
77
|
|
|
78
|
+
def test_cmd_new_bulk(manager, temp_issues_dir, tmp_path):
|
|
79
|
+
console = Console()
|
|
80
|
+
import json
|
|
81
|
+
|
|
82
|
+
tasks_data = [
|
|
83
|
+
{"title": "Bulk 1", "completion_criteria": "CC 1", "body": "Body 1"},
|
|
84
|
+
{"title": "Bulk 2", "completion_criteria": "CC 2", "draft": True},
|
|
85
|
+
]
|
|
86
|
+
|
|
87
|
+
json_file = tmp_path / "bulk_tasks.json"
|
|
88
|
+
json_file.write_text(json.dumps(tasks_data), encoding="utf-8")
|
|
89
|
+
|
|
90
|
+
cmd_new(
|
|
91
|
+
console=console,
|
|
92
|
+
manager=manager,
|
|
93
|
+
title=None,
|
|
94
|
+
body="",
|
|
95
|
+
draft=False,
|
|
96
|
+
bulk=str(json_file),
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
issues = manager.load_mission()
|
|
100
|
+
assert len(issues) == 2
|
|
101
|
+
assert issues[0].slug == "bulk-1"
|
|
102
|
+
assert issues[0].status == "pending"
|
|
103
|
+
assert issues[1].slug == "bulk-2"
|
|
104
|
+
assert issues[1].status == "draft"
|
|
105
|
+
|
|
106
|
+
|
|
78
107
|
def test_cmd_done(manager, temp_issues_dir):
|
|
79
108
|
console = Console()
|
|
80
109
|
cmd_new(console, manager, "Done Task", "Body", draft=False)
|
|
@@ -187,6 +187,25 @@ def test_api_sync_mission(manager):
|
|
|
187
187
|
assert issues[1].slug == "task-a"
|
|
188
188
|
|
|
189
189
|
|
|
190
|
+
def test_api_sync_mission_auto_ingest(manager):
|
|
191
|
+
# Create an issue using the manager API (which creates files and adds to mission)
|
|
192
|
+
manager.create_issue("Task A", draft=False)
|
|
193
|
+
|
|
194
|
+
# Simulate a file manually added/created on disk
|
|
195
|
+
pending_dir = manager.issues_root / "pending" / "task-b"
|
|
196
|
+
pending_dir.mkdir(parents=True, exist_ok=True)
|
|
197
|
+
readme = pending_dir / "README.md"
|
|
198
|
+
readme.write_text("# Task B\n\n## Completion Criteria\n\nCC 2", encoding="utf-8")
|
|
199
|
+
|
|
200
|
+
# Sync mission: this should auto-ingest the manually added task-b!
|
|
201
|
+
issues = manager.sync_mission()
|
|
202
|
+
|
|
203
|
+
# Verify both tasks exist and are in the synced issues list
|
|
204
|
+
slugs = [i.slug for i in issues]
|
|
205
|
+
assert "task-a" in slugs
|
|
206
|
+
assert "task-b" in slugs
|
|
207
|
+
|
|
208
|
+
|
|
190
209
|
def test_api_demote_issue(manager):
|
|
191
210
|
manager.create_issue("Demote Me")
|
|
192
211
|
# Starts as pending
|
|
@@ -44,6 +44,22 @@ def test_mcp_create_task(mock_manager):
|
|
|
44
44
|
)
|
|
45
45
|
|
|
46
46
|
|
|
47
|
+
def test_mcp_create_tasks(mock_manager):
|
|
48
|
+
mock_manager.create_issue.return_value = Issue(
|
|
49
|
+
name="Task 1", slug="task-1", status="pending"
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
tasks = [
|
|
53
|
+
{"title": "Task 1", "completion_criteria": "Must pass tests", "body": "Desc 1"},
|
|
54
|
+
{"title": "Task 2", "completion_criteria": "Done 2", "draft": True},
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
result = mcp.create_tasks(tasks)
|
|
58
|
+
assert "Successfully created tasks:" in result
|
|
59
|
+
assert "- task-1 (Status: pending)" in result
|
|
60
|
+
assert mock_manager.create_issue.call_count == 2
|
|
61
|
+
|
|
62
|
+
|
|
47
63
|
def test_mcp_mark_task_active(mock_manager):
|
|
48
64
|
mock_manager.slugify.return_value = "task-1"
|
|
49
65
|
result = mcp.mark_task_active("Task 1")
|
|
@@ -244,6 +260,7 @@ EXPECTED_TOOLS = {
|
|
|
244
260
|
"list_tasks",
|
|
245
261
|
"list_active_tasks",
|
|
246
262
|
"create_task",
|
|
263
|
+
"create_tasks",
|
|
247
264
|
"promote_task",
|
|
248
265
|
"demote_task",
|
|
249
266
|
"mark_task_active",
|
|
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
|