taskgate 0.1.0__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 (35) hide show
  1. taskgate-0.1.0/LICENSE +21 -0
  2. taskgate-0.1.0/PKG-INFO +156 -0
  3. taskgate-0.1.0/README.md +128 -0
  4. taskgate-0.1.0/pyproject.toml +57 -0
  5. taskgate-0.1.0/setup.cfg +4 -0
  6. taskgate-0.1.0/src/taskgate/__init__.py +3 -0
  7. taskgate-0.1.0/src/taskgate/__main__.py +5 -0
  8. taskgate-0.1.0/src/taskgate/cli.py +133 -0
  9. taskgate-0.1.0/src/taskgate/clients.py +52 -0
  10. taskgate-0.1.0/src/taskgate/commands/__init__.py +1 -0
  11. taskgate-0.1.0/src/taskgate/commands/common.py +25 -0
  12. taskgate-0.1.0/src/taskgate/commands/flow.py +307 -0
  13. taskgate-0.1.0/src/taskgate/commands/plan.py +101 -0
  14. taskgate-0.1.0/src/taskgate/commands/registry.py +70 -0
  15. taskgate-0.1.0/src/taskgate/config.py +135 -0
  16. taskgate-0.1.0/src/taskgate/mcp/__init__.py +1 -0
  17. taskgate-0.1.0/src/taskgate/mcp/bridge.py +42 -0
  18. taskgate-0.1.0/src/taskgate/mcp/protocol.py +76 -0
  19. taskgate-0.1.0/src/taskgate/mcp/registry.py +44 -0
  20. taskgate-0.1.0/src/taskgate/mcp/server.py +25 -0
  21. taskgate-0.1.0/src/taskgate/mcp/tools.py +207 -0
  22. taskgate-0.1.0/src/taskgate/model.py +158 -0
  23. taskgate-0.1.0/src/taskgate/ordering.py +119 -0
  24. taskgate-0.1.0/src/taskgate/render.py +39 -0
  25. taskgate-0.1.0/src/taskgate/store.py +143 -0
  26. taskgate-0.1.0/src/taskgate.egg-info/PKG-INFO +156 -0
  27. taskgate-0.1.0/src/taskgate.egg-info/SOURCES.txt +33 -0
  28. taskgate-0.1.0/src/taskgate.egg-info/dependency_links.txt +1 -0
  29. taskgate-0.1.0/src/taskgate.egg-info/entry_points.txt +3 -0
  30. taskgate-0.1.0/src/taskgate.egg-info/requires.txt +5 -0
  31. taskgate-0.1.0/src/taskgate.egg-info/top_level.txt +1 -0
  32. taskgate-0.1.0/tests/test_clients.py +50 -0
  33. taskgate-0.1.0/tests/test_config.py +89 -0
  34. taskgate-0.1.0/tests/test_flow.py +240 -0
  35. taskgate-0.1.0/tests/test_mcp.py +109 -0
taskgate-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 prachwal
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,156 @@
1
+ Metadata-Version: 2.4
2
+ Name: taskgate
3
+ Version: 0.1.0
4
+ Summary: Gated task registry with an MCP server: strict status flow, verification and review evidence, for Claude Code, Codex, VS Code Copilot and other MCP clients.
5
+ Author: prachwal
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/prachwal/taskgate
8
+ Project-URL: Repository, https://github.com/prachwal/taskgate
9
+ Project-URL: Issues, https://github.com/prachwal/taskgate/issues
10
+ Keywords: mcp,model-context-protocol,task-tracker,agents,workflow,claude-code,codex,copilot
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Software Development :: Quality Assurance
20
+ Requires-Python: >=3.11
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ Provides-Extra: dev
24
+ Requires-Dist: pytest>=8; extra == "dev"
25
+ Requires-Dist: ruff>=0.6; extra == "dev"
26
+ Requires-Dist: mypy>=1.11; extra == "dev"
27
+ Dynamic: license-file
28
+
29
+ # taskgate
30
+
31
+ A task registry that makes "done" mean something. Tasks move through a strict status flow, and the
32
+ transitions that matter are gated by evidence: a task enters review only with a green verification, and
33
+ closes only with a review from a *different* identity that lists the commands it checked. It ships as a CLI
34
+ and as an MCP server, so coding agents (Claude Code, Codex, VS Code Copilot, Cursor, Gemini CLI) and humans
35
+ work from the same registry.
36
+
37
+ - stdlib-only, no runtime dependencies; Python 3.11+; Linux, macOS and Windows
38
+ - state is plain files you commit: `tasks.json` (source of truth), an append-only `tasks.log.jsonl` and a
39
+ generated `tasks.md`
40
+ - one code path: every MCP tool calls the same CLI command a human would run
41
+
42
+ ## Quick start
43
+
44
+ ```sh
45
+ cd your-project
46
+ uvx taskgate init
47
+ uvx taskgate add --id T-1 --title "Parse the header" --verification-cmd "pytest tests/test_header.py"
48
+ uvx taskgate next # which task may start now
49
+ uvx taskgate claim T-1 --by dev
50
+ ```
51
+
52
+ Add it to your agent (prints a ready-to-paste block; see [docs/clients.md](docs/clients.md)):
53
+
54
+ ```sh
55
+ uvx taskgate config-snippet claude # also: vscode, codex, cursor, gemini, generic
56
+ ```
57
+
58
+ From a checkout instead of PyPI: `uvx --from /path/to/taskgate taskgate ...`, or
59
+ `taskgate config-snippet claude --from /path/to/taskgate`.
60
+
61
+ ## The flow
62
+
63
+ ```
64
+ draft -> planned -> in_progress -> in_review -> done
65
+ | ^ |
66
+ v | +--> in_progress (review rejected)
67
+ blocked
68
+ any non-terminal status -> cancelled
69
+ ```
70
+
71
+ | Rule | What it enforces |
72
+ | --- | --- |
73
+ | order | `claim` works only on tasks whose predecessors in the registry are `done` or `cancelled` |
74
+ | phase gate | a task in a later phase waits until every earlier phase is closed and the registry validates |
75
+ | dependencies | `depends_on` tasks must be `done` before the task can start |
76
+ | verification | `in_review` and `done` need a green `verification_result`; the tool stamps who recorded it and when |
77
+ | review | `done` needs a green `review_result` from the reviewer identity, different from the verifier, listing `checks`, recorded after a delay, in the same call as the closing update |
78
+ | fresh evidence | a new verification voids an earlier review; results are never accepted together in one call |
79
+ | planning | adding, moving and re-linking tasks is limited to the `planners` identities; started work is never reordered |
80
+
81
+ Identities (`--by`, or the `TASKGATE_BY` environment variable, default `human`) are *declared by the caller*.
82
+ This is a process barrier that keeps roles apart and leaves a trail in the log; it is not authentication.
83
+
84
+ ## Configuration
85
+
86
+ Optional `taskgate.toml` in the project root; every key has a default
87
+ ([examples/taskgate.toml](examples/taskgate.toml)).
88
+
89
+ | Key | Default | Meaning |
90
+ | --- | --- | --- |
91
+ | `data_dir` | `.taskgate` | where `tasks.json`, `tasks.log.jsonl`, `tasks.md` live |
92
+ | `title` | `Tasks` | heading of the Markdown view |
93
+ | `id_pattern` | `^[A-Za-z][A-Za-z0-9]*-\d+$` | task id regex; a named group `(?P<phase>\d+)` ties the id to the phase |
94
+ | `phase_names` | none | `{0 = "Foundations"}` labels for the view |
95
+ | `planners` | `["human"]` | identities allowed to add, move and re-link tasks |
96
+ | `reviewer` | `"reviewer"` | identity whose `review_result` closes a task |
97
+ | `owners` | any | restrict `owner` values |
98
+ | `require_review` | `true` | `false`: `done` needs only a green verification |
99
+ | `review_delay_seconds` | `60` | minimum time between verification and review |
100
+ | `[quality]` | none | named gates for the `quality_check` tool, each a list of argv arrays |
101
+
102
+ The project root is `TASKGATE_ROOT` if set, otherwise the nearest ancestor of the working directory that has
103
+ `taskgate.toml` or `.taskgate/`.
104
+ [examples/embersail.toml](examples/embersail.toml) reproduces a stricter real-world setup (ids `P<phase>-NN`,
105
+ architect/human planners) to show the engine is not tied to one convention.
106
+
107
+ ## Commands
108
+
109
+ `init`, `validate`, `render [--check]`, `next`, `list [--status]`, `claim`, `update`, `add`, `sort`, `move`,
110
+ `depends`, `mcp`, `config-snippet`. `taskgate <command> --help` lists the flags.
111
+
112
+ Recording evidence, in separate calls:
113
+
114
+ ```sh
115
+ taskgate update T-1 --verification-result '{"ok": true}' --by dev
116
+ taskgate update T-1 --status in_review --by dev
117
+ taskgate update T-1 --status done --by reviewer \
118
+ --review-result '{"ok": true, "by": "reviewer", "checks": ["pytest -q"]}'
119
+ ```
120
+
121
+ ## MCP tools
122
+
123
+ `task_list`, `task_next`, `task_validate` (read-only, so clients can skip the approval prompt), `task_claim`,
124
+ `task_update`, `task_add`, `task_move`, `task_depends`, `task_sort`, `task_render` and `quality_check`.
125
+ Tool failures come back as `isError` results rather than protocol errors, so an agent sees the reason.
126
+
127
+ The server speaks newline-delimited JSON-RPC 2.0 over stdio. Run it with `taskgate mcp` or `taskgate-mcp`.
128
+
129
+ ## Development
130
+
131
+ ```sh
132
+ python -m pip install -e ".[dev]"
133
+ ruff check . && mypy && pytest
134
+ ```
135
+
136
+ ## Releasing
137
+
138
+ Pushing to `main` runs the tests on Linux, macOS and Windows and builds the package. If `version` in
139
+ `pyproject.toml` has no `v<version>` tag yet, the workflow publishes to PyPI through trusted publishing and
140
+ creates the tag and GitHub release. To release: bump `version` in `pyproject.toml` and
141
+ `src/taskgate/__init__.py` (a test keeps them equal), add a changelog entry, merge to `main`.
142
+
143
+ One-time setup, either way: a GitHub environment named `pypi`. Then either store a PyPI API token as the
144
+ repository secret `PYPI_API_TOKEN` (the first release of a new project needs an account-wide token; narrow it
145
+ to the project afterwards), or add a (pending) trusted publisher on PyPI for this repository, workflow
146
+ `ci.yml`, environment `pypi`. With the secret set it is used; without it the workflow falls back to trusted
147
+ publishing.
148
+
149
+ ## Status
150
+
151
+ Alpha. Not included: synchronization with a forge (GitLab/GitHub issues). It originated in the Embersail
152
+ project's task tooling.
153
+
154
+ ## License
155
+
156
+ MIT
@@ -0,0 +1,128 @@
1
+ # taskgate
2
+
3
+ A task registry that makes "done" mean something. Tasks move through a strict status flow, and the
4
+ transitions that matter are gated by evidence: a task enters review only with a green verification, and
5
+ closes only with a review from a *different* identity that lists the commands it checked. It ships as a CLI
6
+ and as an MCP server, so coding agents (Claude Code, Codex, VS Code Copilot, Cursor, Gemini CLI) and humans
7
+ work from the same registry.
8
+
9
+ - stdlib-only, no runtime dependencies; Python 3.11+; Linux, macOS and Windows
10
+ - state is plain files you commit: `tasks.json` (source of truth), an append-only `tasks.log.jsonl` and a
11
+ generated `tasks.md`
12
+ - one code path: every MCP tool calls the same CLI command a human would run
13
+
14
+ ## Quick start
15
+
16
+ ```sh
17
+ cd your-project
18
+ uvx taskgate init
19
+ uvx taskgate add --id T-1 --title "Parse the header" --verification-cmd "pytest tests/test_header.py"
20
+ uvx taskgate next # which task may start now
21
+ uvx taskgate claim T-1 --by dev
22
+ ```
23
+
24
+ Add it to your agent (prints a ready-to-paste block; see [docs/clients.md](docs/clients.md)):
25
+
26
+ ```sh
27
+ uvx taskgate config-snippet claude # also: vscode, codex, cursor, gemini, generic
28
+ ```
29
+
30
+ From a checkout instead of PyPI: `uvx --from /path/to/taskgate taskgate ...`, or
31
+ `taskgate config-snippet claude --from /path/to/taskgate`.
32
+
33
+ ## The flow
34
+
35
+ ```
36
+ draft -> planned -> in_progress -> in_review -> done
37
+ | ^ |
38
+ v | +--> in_progress (review rejected)
39
+ blocked
40
+ any non-terminal status -> cancelled
41
+ ```
42
+
43
+ | Rule | What it enforces |
44
+ | --- | --- |
45
+ | order | `claim` works only on tasks whose predecessors in the registry are `done` or `cancelled` |
46
+ | phase gate | a task in a later phase waits until every earlier phase is closed and the registry validates |
47
+ | dependencies | `depends_on` tasks must be `done` before the task can start |
48
+ | verification | `in_review` and `done` need a green `verification_result`; the tool stamps who recorded it and when |
49
+ | review | `done` needs a green `review_result` from the reviewer identity, different from the verifier, listing `checks`, recorded after a delay, in the same call as the closing update |
50
+ | fresh evidence | a new verification voids an earlier review; results are never accepted together in one call |
51
+ | planning | adding, moving and re-linking tasks is limited to the `planners` identities; started work is never reordered |
52
+
53
+ Identities (`--by`, or the `TASKGATE_BY` environment variable, default `human`) are *declared by the caller*.
54
+ This is a process barrier that keeps roles apart and leaves a trail in the log; it is not authentication.
55
+
56
+ ## Configuration
57
+
58
+ Optional `taskgate.toml` in the project root; every key has a default
59
+ ([examples/taskgate.toml](examples/taskgate.toml)).
60
+
61
+ | Key | Default | Meaning |
62
+ | --- | --- | --- |
63
+ | `data_dir` | `.taskgate` | where `tasks.json`, `tasks.log.jsonl`, `tasks.md` live |
64
+ | `title` | `Tasks` | heading of the Markdown view |
65
+ | `id_pattern` | `^[A-Za-z][A-Za-z0-9]*-\d+$` | task id regex; a named group `(?P<phase>\d+)` ties the id to the phase |
66
+ | `phase_names` | none | `{0 = "Foundations"}` labels for the view |
67
+ | `planners` | `["human"]` | identities allowed to add, move and re-link tasks |
68
+ | `reviewer` | `"reviewer"` | identity whose `review_result` closes a task |
69
+ | `owners` | any | restrict `owner` values |
70
+ | `require_review` | `true` | `false`: `done` needs only a green verification |
71
+ | `review_delay_seconds` | `60` | minimum time between verification and review |
72
+ | `[quality]` | none | named gates for the `quality_check` tool, each a list of argv arrays |
73
+
74
+ The project root is `TASKGATE_ROOT` if set, otherwise the nearest ancestor of the working directory that has
75
+ `taskgate.toml` or `.taskgate/`.
76
+ [examples/embersail.toml](examples/embersail.toml) reproduces a stricter real-world setup (ids `P<phase>-NN`,
77
+ architect/human planners) to show the engine is not tied to one convention.
78
+
79
+ ## Commands
80
+
81
+ `init`, `validate`, `render [--check]`, `next`, `list [--status]`, `claim`, `update`, `add`, `sort`, `move`,
82
+ `depends`, `mcp`, `config-snippet`. `taskgate <command> --help` lists the flags.
83
+
84
+ Recording evidence, in separate calls:
85
+
86
+ ```sh
87
+ taskgate update T-1 --verification-result '{"ok": true}' --by dev
88
+ taskgate update T-1 --status in_review --by dev
89
+ taskgate update T-1 --status done --by reviewer \
90
+ --review-result '{"ok": true, "by": "reviewer", "checks": ["pytest -q"]}'
91
+ ```
92
+
93
+ ## MCP tools
94
+
95
+ `task_list`, `task_next`, `task_validate` (read-only, so clients can skip the approval prompt), `task_claim`,
96
+ `task_update`, `task_add`, `task_move`, `task_depends`, `task_sort`, `task_render` and `quality_check`.
97
+ Tool failures come back as `isError` results rather than protocol errors, so an agent sees the reason.
98
+
99
+ The server speaks newline-delimited JSON-RPC 2.0 over stdio. Run it with `taskgate mcp` or `taskgate-mcp`.
100
+
101
+ ## Development
102
+
103
+ ```sh
104
+ python -m pip install -e ".[dev]"
105
+ ruff check . && mypy && pytest
106
+ ```
107
+
108
+ ## Releasing
109
+
110
+ Pushing to `main` runs the tests on Linux, macOS and Windows and builds the package. If `version` in
111
+ `pyproject.toml` has no `v<version>` tag yet, the workflow publishes to PyPI through trusted publishing and
112
+ creates the tag and GitHub release. To release: bump `version` in `pyproject.toml` and
113
+ `src/taskgate/__init__.py` (a test keeps them equal), add a changelog entry, merge to `main`.
114
+
115
+ One-time setup, either way: a GitHub environment named `pypi`. Then either store a PyPI API token as the
116
+ repository secret `PYPI_API_TOKEN` (the first release of a new project needs an account-wide token; narrow it
117
+ to the project afterwards), or add a (pending) trusted publisher on PyPI for this repository, workflow
118
+ `ci.yml`, environment `pypi`. With the secret set it is used; without it the workflow falls back to trusted
119
+ publishing.
120
+
121
+ ## Status
122
+
123
+ Alpha. Not included: synchronization with a forge (GitLab/GitHub issues). It originated in the Embersail
124
+ project's task tooling.
125
+
126
+ ## License
127
+
128
+ MIT
@@ -0,0 +1,57 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "taskgate"
7
+ version = "0.1.0"
8
+ description = "Gated task registry with an MCP server: strict status flow, verification and review evidence, for Claude Code, Codex, VS Code Copilot and other MCP clients."
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ license-files = ["LICENSE"]
12
+ requires-python = ">=3.11"
13
+ authors = [{ name = "prachwal" }]
14
+ keywords = ["mcp", "model-context-protocol", "task-tracker", "agents", "workflow", "claude-code", "codex", "copilot"]
15
+ classifiers = [
16
+ "Development Status :: 3 - Alpha",
17
+ "Environment :: Console",
18
+ "Intended Audience :: Developers",
19
+ "Operating System :: OS Independent",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3.11",
22
+ "Programming Language :: Python :: 3.12",
23
+ "Programming Language :: Python :: 3.13",
24
+ "Topic :: Software Development :: Quality Assurance",
25
+ ]
26
+ dependencies = []
27
+
28
+ [project.optional-dependencies]
29
+ dev = ["pytest>=8", "ruff>=0.6", "mypy>=1.11"]
30
+
31
+ [project.urls]
32
+ Homepage = "https://github.com/prachwal/taskgate"
33
+ Repository = "https://github.com/prachwal/taskgate"
34
+ Issues = "https://github.com/prachwal/taskgate/issues"
35
+
36
+ [project.scripts]
37
+ taskgate = "taskgate.cli:run"
38
+ taskgate-mcp = "taskgate.mcp.server:run"
39
+
40
+ [tool.setuptools.packages.find]
41
+ where = ["src"]
42
+
43
+ [tool.pytest.ini_options]
44
+ testpaths = ["tests"]
45
+ addopts = "-q"
46
+
47
+ [tool.ruff]
48
+ line-length = 130
49
+ target-version = "py311"
50
+
51
+ [tool.ruff.lint]
52
+ select = ["E", "F", "I", "UP", "B", "SIM"]
53
+
54
+ [tool.mypy]
55
+ python_version = "3.11"
56
+ strict = true
57
+ files = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,3 @@
1
+ """taskgate: a gated task registry with an MCP server."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,5 @@
1
+ """``python -m taskgate``."""
2
+
3
+ from .cli import run
4
+
5
+ run()
@@ -0,0 +1,133 @@
1
+ """CLI entrypoint: ``taskgate`` / ``python -m taskgate``."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import sys
7
+
8
+ from . import __version__, clients, model
9
+ from .commands.flow import cmd_add, cmd_claim, cmd_list, cmd_next, cmd_update
10
+ from .commands.plan import cmd_depends, cmd_move
11
+ from .commands.registry import cmd_init, cmd_render, cmd_sort, cmd_validate
12
+
13
+ BY_HELP = "identity (default TASKGATE_BY env / 'human')"
14
+
15
+
16
+ def _cmd_snippet(args: argparse.Namespace) -> int:
17
+ try:
18
+ print(clients.snippet(args.client, source=args.source, root=args.root), end="")
19
+ except ValueError as exc:
20
+ print(f"error: {exc}", file=sys.stderr)
21
+ return 1
22
+ return 0
23
+
24
+
25
+ def _cmd_mcp(_args: argparse.Namespace) -> int:
26
+ from .mcp import server # lazy: the server imports this module's main()
27
+
28
+ server.serve()
29
+ return 0
30
+
31
+
32
+ def build_parser() -> argparse.ArgumentParser:
33
+ """Assemble the argparse tree."""
34
+ parser = argparse.ArgumentParser(
35
+ prog="taskgate",
36
+ description="Gated task registry: strict status flow with verification and review evidence.",
37
+ )
38
+ parser.add_argument("--version", action="version", version=f"taskgate {__version__}")
39
+ sub = parser.add_subparsers(dest="command", required=True)
40
+
41
+ p = sub.add_parser("init", help="create the registry in the current directory (or TASKGATE_ROOT)")
42
+ p.add_argument("--seed", help="path to seed JSON (tasks array)")
43
+ p.add_argument("--force", action="store_true", help="overwrite existing registry")
44
+ p.add_argument("--by", help=BY_HELP)
45
+ p.set_defaults(func=cmd_init)
46
+
47
+ p = sub.add_parser("validate", help="schema + flow-rule validation")
48
+ p.set_defaults(func=cmd_validate)
49
+
50
+ p = sub.add_parser("render", help="generate the Markdown view")
51
+ p.add_argument("--check", action="store_true", help="fail when the view drifted from tasks.json")
52
+ p.add_argument("--out", help="alternative output path (ignored with --check)")
53
+ p.set_defaults(func=cmd_render)
54
+
55
+ p = sub.add_parser("next", help="show the one task that may start now (queue head)")
56
+ p.add_argument("--owner", help="only tasks unowned or owned by this identity")
57
+ p.set_defaults(func=cmd_next)
58
+
59
+ p = sub.add_parser("list", help="list all tasks")
60
+ p.add_argument("--status", choices=model.STATUSES, help="filter by status")
61
+ p.set_defaults(func=cmd_list)
62
+
63
+ p = sub.add_parser("claim", help="claim a planned task (-> in_progress)")
64
+ p.add_argument("id")
65
+ p.add_argument("--by", help=BY_HELP)
66
+ p.add_argument("--force", action="store_true", help="steal from another owner")
67
+ p.set_defaults(func=cmd_claim)
68
+
69
+ p = sub.add_parser("update", help="change status / record verification or review")
70
+ p.add_argument("id")
71
+ p.add_argument("--status", choices=model.STATUSES)
72
+ p.add_argument("--reason", help="required by blocked; recorded for cancelled")
73
+ p.add_argument("--verification-result", help="JSON object, e.g. '{\"ok\": true}'")
74
+ p.add_argument(
75
+ "--review-result",
76
+ help="JSON object from the reviewer, e.g. '{\"ok\": true, \"by\": \"reviewer\", \"checks\": [\"cmd\"]}'",
77
+ )
78
+ p.add_argument("--by", help=BY_HELP)
79
+ p.set_defaults(func=cmd_update)
80
+
81
+ p = sub.add_parser("add", help="append a new task to the registry")
82
+ p.add_argument("--id", required=True, help="id matching the configured id_pattern")
83
+ p.add_argument("--title", required=True)
84
+ p.add_argument("--phase", type=int, help="phase (default: from the id's 'phase' group, else 0)")
85
+ p.add_argument("--verification-cmd", required=True, dest="verification_cmd")
86
+ p.add_argument("--spec", help="spec_source anchor (path + section)")
87
+ p.add_argument("--status", default="planned", choices=model.ADD_STATUSES)
88
+ p.add_argument("--owner")
89
+ p.add_argument("--depends", help="comma-separated task ids")
90
+ p.add_argument("--by", help=BY_HELP)
91
+ p.set_defaults(func=cmd_add)
92
+
93
+ p = sub.add_parser("sort", help="reset the execution order to phase, then id")
94
+ p.add_argument("--by", help=BY_HELP)
95
+ p.set_defaults(func=cmd_sort)
96
+
97
+ p = sub.add_parser("move", help="place a draft/planned task before or after another")
98
+ p.add_argument("id")
99
+ anchor = p.add_mutually_exclusive_group(required=True)
100
+ anchor.add_argument("--before", metavar="ID", help="place directly before this task")
101
+ anchor.add_argument("--after", metavar="ID", help="place directly after this task")
102
+ p.add_argument("--by", help=BY_HELP)
103
+ p.set_defaults(func=cmd_move)
104
+
105
+ p = sub.add_parser("depends", help="change the dependencies of a draft/planned task")
106
+ p.add_argument("id")
107
+ p.add_argument("--set", help="comma-separated ids replacing the dependencies ('' clears)")
108
+ p.add_argument("--add", help="comma-separated ids to add")
109
+ p.add_argument("--remove", help="comma-separated ids to remove")
110
+ p.add_argument("--by", help=BY_HELP)
111
+ p.set_defaults(func=cmd_depends)
112
+
113
+ p = sub.add_parser("mcp", help="start the MCP stdio server (JSON-RPC 2.0, newline-delimited)")
114
+ p.set_defaults(func=_cmd_mcp)
115
+
116
+ p = sub.add_parser("config-snippet", help="print MCP configuration for a client")
117
+ p.add_argument("client", choices=list(clients.CLIENTS), help="; ".join(f"{k}: {v}" for k, v in clients.CLIENTS.items()))
118
+ p.add_argument("--from", dest="source", help="uvx source (path, git URL or version spec); default: PyPI")
119
+ p.add_argument("--root", help="absolute project root, written as TASKGATE_ROOT")
120
+ p.set_defaults(func=_cmd_snippet)
121
+ return parser
122
+
123
+
124
+ def main(argv: list[str] | None = None) -> int:
125
+ """Program entry: dispatch commands, return the exit code."""
126
+ args = build_parser().parse_args(argv)
127
+ code: int = args.func(args)
128
+ return code
129
+
130
+
131
+ def run() -> None:
132
+ """Console-script entry point."""
133
+ sys.exit(main())
@@ -0,0 +1,52 @@
1
+ """Ready-to-paste MCP server configuration for common clients (``taskgate config-snippet``)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+
7
+ CLIENTS = {
8
+ "claude": "Claude Code: .mcp.json in the project (or `claude mcp add`)",
9
+ "vscode": "VS Code (GitHub Copilot agent mode): .vscode/mcp.json",
10
+ "codex": "OpenAI Codex CLI: ~/.codex/config.toml",
11
+ "cursor": "Cursor: .cursor/mcp.json",
12
+ "gemini": "Gemini CLI: .gemini/settings.json",
13
+ "generic": "any client that takes a command and arguments",
14
+ }
15
+
16
+
17
+ def launch(source: str | None) -> tuple[str, list[str]]:
18
+ """Command and arguments that start the stdio server through ``uvx``."""
19
+ args = ["--from", source, "taskgate", "mcp"] if source else ["taskgate", "mcp"]
20
+ return "uvx", args
21
+
22
+
23
+ def snippet(client: str, *, source: str | None = None, root: str | None = None) -> str:
24
+ """The configuration text for ``client``; raises ``ValueError`` for an unknown client."""
25
+ if client not in CLIENTS:
26
+ raise ValueError(f"unknown client {client!r}; choose one of {', '.join(CLIENTS)}")
27
+ command, args = launch(source)
28
+ env = {"TASKGATE_ROOT": root} if root else {}
29
+ if client == "codex":
30
+ lines = ["[mcp_servers.taskgate]", f"command = {json.dumps(command)}", f"args = {json.dumps(args)}"]
31
+ if env:
32
+ lines += ["", "[mcp_servers.taskgate.env]", f"TASKGATE_ROOT = {json.dumps(root)}"]
33
+ return "\n".join(lines) + "\n"
34
+ if client == "claude":
35
+ add = ["claude", "mcp", "add", "taskgate"]
36
+ if root:
37
+ add += ["-e", f"TASKGATE_ROOT={root}"]
38
+ add += ["--", command, *args]
39
+ entry: dict[str, object] = {"type": "stdio", "command": command, "args": args}
40
+ if env:
41
+ entry["env"] = env
42
+ text = json.dumps({"mcpServers": {"taskgate": entry}}, indent=2)
43
+ return f"# one command:\n{' '.join(add)}\n\n# or .mcp.json:\n{text}\n"
44
+ if client == "vscode":
45
+ entry = {"type": "stdio", "command": command, "args": args}
46
+ if env:
47
+ entry["env"] = env
48
+ return json.dumps({"servers": {"taskgate": entry}}, indent=2) + "\n"
49
+ entry = {"command": command, "args": args}
50
+ if env:
51
+ entry["env"] = env
52
+ return json.dumps({"mcpServers": {"taskgate": entry}}, indent=2) + "\n"
@@ -0,0 +1 @@
1
+ """CLI commands."""
@@ -0,0 +1,25 @@
1
+ """Helpers shared by the CLI commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import os
7
+ from typing import Any
8
+
9
+ EMPTY_REGISTRY: dict[str, Any] = {"version": 1, "tasks": []}
10
+
11
+ VERIFICATION_STATUSES = ("in_review", "done")
12
+
13
+
14
+ def identity(args: argparse.Namespace) -> str:
15
+ """Declared caller identity: ``--by``, else ``TASKGATE_BY``, else ``human``."""
16
+ by: str | None = getattr(args, "by", None)
17
+ return by or os.environ.get("TASKGATE_BY", "human")
18
+
19
+
20
+ def find(data: dict[str, Any], task_id: str) -> tuple[dict[str, Any], bool]:
21
+ """The task with this id and whether it exists."""
22
+ for task in data.get("tasks", []):
23
+ if task.get("id") == task_id:
24
+ return task, True
25
+ return {}, False