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.
Files changed (71) hide show
  1. {task_agent-1.0.2 → task_agent-1.0.5}/PKG-INFO +1 -1
  2. {task_agent-1.0.2 → task_agent-1.0.5}/docs/README.md +2 -0
  3. task_agent-1.0.5/docs/STATION-MAP.md +155 -0
  4. {task_agent-1.0.2 → task_agent-1.0.5}/pyproject.toml +2 -2
  5. {task_agent-1.0.2 → task_agent-1.0.5}/src/taskagent/cli.py +59 -0
  6. {task_agent-1.0.2 → task_agent-1.0.5}/src/taskagent/manager.py +7 -4
  7. {task_agent-1.0.2 → task_agent-1.0.5}/src/taskagent/mcp.py +69 -2
  8. {task_agent-1.0.2 → task_agent-1.0.5}/tests/test_cli.py +29 -0
  9. {task_agent-1.0.2 → task_agent-1.0.5}/tests/test_manager.py +19 -0
  10. {task_agent-1.0.2 → task_agent-1.0.5}/tests/test_mcp.py +17 -0
  11. {task_agent-1.0.2 → task_agent-1.0.5}/uv.lock +1 -1
  12. {task_agent-1.0.2 → task_agent-1.0.5}/.env +0 -0
  13. {task_agent-1.0.2 → task_agent-1.0.5}/.gemini/settings.json +0 -0
  14. {task_agent-1.0.2 → task_agent-1.0.5}/.github/workflows/ci.yml +0 -0
  15. {task_agent-1.0.2 → task_agent-1.0.5}/.github/workflows/publish.yml +0 -0
  16. {task_agent-1.0.2 → task_agent-1.0.5}/.gitignore +0 -0
  17. {task_agent-1.0.2 → task_agent-1.0.5}/.mise.toml +0 -0
  18. {task_agent-1.0.2 → task_agent-1.0.5}/.pre-commit-config.yaml +0 -0
  19. {task_agent-1.0.2 → task_agent-1.0.5}/.python-version +0 -0
  20. {task_agent-1.0.2 → task_agent-1.0.5}/.ta/agents/agy-cli/meta.toml +0 -0
  21. {task_agent-1.0.2 → task_agent-1.0.5}/.ta/agents/gh/meta.toml +0 -0
  22. {task_agent-1.0.2 → task_agent-1.0.5}/.ta/agents/minimal/meta.toml +0 -0
  23. {task_agent-1.0.2 → task_agent-1.0.5}/.ta/agents/uat-aws/meta.toml +0 -0
  24. {task_agent-1.0.2 → task_agent-1.0.5}/.ta/worker +0 -0
  25. {task_agent-1.0.2 → task_agent-1.0.5}/CLAUDE.md +0 -0
  26. {task_agent-1.0.2 → task_agent-1.0.5}/GEMINI.md +0 -0
  27. {task_agent-1.0.2 → task_agent-1.0.5}/Makefile +0 -0
  28. {task_agent-1.0.2 → task_agent-1.0.5}/README.md +0 -0
  29. {task_agent-1.0.2 → task_agent-1.0.5}/agent.env +0 -0
  30. {task_agent-1.0.2 → task_agent-1.0.5}/docs/architecture/README.md +0 -0
  31. {task_agent-1.0.2 → task_agent-1.0.5}/docs/chats/gemini-conversation-1773283034591.json +0 -0
  32. {task_agent-1.0.2 → task_agent-1.0.5}/docs/chats/project-init-to-v0.1.46.md +0 -0
  33. {task_agent-1.0.2 → task_agent-1.0.5}/docs/cli/README.md +0 -0
  34. {task_agent-1.0.2 → task_agent-1.0.5}/docs/development/README.md +0 -0
  35. {task_agent-1.0.2 → task_agent-1.0.5}/docs/task-agents.md +0 -0
  36. {task_agent-1.0.2 → task_agent-1.0.5}/docs/workflow/README.md +0 -0
  37. {task_agent-1.0.2 → task_agent-1.0.5}/scripts/clean_duplicates.py +0 -0
  38. {task_agent-1.0.2 → task_agent-1.0.5}/scripts/migrate_tasks.py +0 -0
  39. {task_agent-1.0.2 → task_agent-1.0.5}/scripts/recover_dates.py +0 -0
  40. {task_agent-1.0.2 → task_agent-1.0.5}/sidecars/adk-worker/.env.example +0 -0
  41. {task_agent-1.0.2 → task_agent-1.0.5}/sidecars/adk-worker/README.md +0 -0
  42. {task_agent-1.0.2 → task_agent-1.0.5}/sidecars/adk-worker/instructions/manager.md +0 -0
  43. {task_agent-1.0.2 → task_agent-1.0.5}/sidecars/adk-worker/instructions/validator.md +0 -0
  44. {task_agent-1.0.2 → task_agent-1.0.5}/sidecars/adk-worker/instructions/worker.md +0 -0
  45. {task_agent-1.0.2 → task_agent-1.0.5}/sidecars/adk-worker/profiles/default.json +0 -0
  46. {task_agent-1.0.2 → task_agent-1.0.5}/sidecars/adk-worker/pyproject.toml +0 -0
  47. {task_agent-1.0.2 → task_agent-1.0.5}/sidecars/adk-worker/uv.lock +0 -0
  48. {task_agent-1.0.2 → task_agent-1.0.5}/sidecars/adk-worker/worker.py +0 -0
  49. {task_agent-1.0.2 → task_agent-1.0.5}/src/taskagent/__init__.py +0 -0
  50. {task_agent-1.0.2 → task_agent-1.0.5}/src/taskagent/__main__.py +0 -0
  51. {task_agent-1.0.2 → task_agent-1.0.5}/src/taskagent/agent.py +0 -0
  52. {task_agent-1.0.2 → task_agent-1.0.5}/src/taskagent/audit.py +0 -0
  53. {task_agent-1.0.2 → task_agent-1.0.5}/src/taskagent/config.py +0 -0
  54. {task_agent-1.0.2 → task_agent-1.0.5}/src/taskagent/discovery.py +0 -0
  55. {task_agent-1.0.2 → task_agent-1.0.5}/src/taskagent/models/__init__.py +0 -0
  56. {task_agent-1.0.2 → task_agent-1.0.5}/src/taskagent/models/issue.py +0 -0
  57. {task_agent-1.0.2 → task_agent-1.0.5}/src/taskagent/models/metric.py +0 -0
  58. {task_agent-1.0.2 → task_agent-1.0.5}/src/taskagent/plugins/__init__.py +0 -0
  59. {task_agent-1.0.2 → task_agent-1.0.5}/src/taskagent/plugins/github.py +0 -0
  60. {task_agent-1.0.2 → task_agent-1.0.5}/src/taskagent/templates.py +0 -0
  61. {task_agent-1.0.2 → task_agent-1.0.5}/test-dir/opencode.json +0 -0
  62. {task_agent-1.0.2 → task_agent-1.0.5}/tests/conftest.py +0 -0
  63. {task_agent-1.0.2 → task_agent-1.0.5}/tests/e2e/test_agent_sandboxing.sh +0 -0
  64. {task_agent-1.0.2 → task_agent-1.0.5}/tests/test_agent.py +0 -0
  65. {task_agent-1.0.2 → task_agent-1.0.5}/tests/test_agy_cli_worker.py +0 -0
  66. {task_agent-1.0.2 → task_agent-1.0.5}/tests/test_cli_windows.py +0 -0
  67. {task_agent-1.0.2 → task_agent-1.0.5}/tests/test_discovery.py +0 -0
  68. {task_agent-1.0.2 → task_agent-1.0.5}/tests/test_manager_git.py +0 -0
  69. {task_agent-1.0.2 → task_agent-1.0.5}/tests/test_metrics.py +0 -0
  70. {task_agent-1.0.2 → task_agent-1.0.5}/tests/test_migration.py +0 -0
  71. {task_agent-1.0.2 → task_agent-1.0.5}/tests/test_templates.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: task-agent
3
- Version: 1.0.2
3
+ Version: 1.0.5
4
4
  Summary: A prioritized, file-based task queue for autonomous agentic workers
5
5
  Author-email: Mark Stouffer <1802850+InTEGr8or@users.noreply.github.com>
6
6
  License: MIT
@@ -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.2"
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.2"
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 = "pending"
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
- self.sync_mission()
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
- except Exception as e:
459
- return f"Error: {e}"
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",
@@ -1135,7 +1135,7 @@ wheels = [
1135
1135
 
1136
1136
  [[package]]
1137
1137
  name = "task-agent"
1138
- version = "1.0.2"
1138
+ version = "1.0.5"
1139
1139
  source = { editable = "." }
1140
1140
  dependencies = [
1141
1141
  { name = "githubkit" },
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