tasks-toolkit 0.1.4__tar.gz → 0.1.6__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.
- {tasks_toolkit-0.1.4/tasks_toolkit.egg-info → tasks_toolkit-0.1.6}/PKG-INFO +142 -17
- {tasks_toolkit-0.1.4 → tasks_toolkit-0.1.6}/README.md +142 -17
- {tasks_toolkit-0.1.4 → tasks_toolkit-0.1.6}/pyproject.toml +1 -1
- {tasks_toolkit-0.1.4 → tasks_toolkit-0.1.6}/scripts/run_stats.py +2 -2
- {tasks_toolkit-0.1.4 → tasks_toolkit-0.1.6}/scripts/run_summary.py +8 -12
- tasks_toolkit-0.1.6/scripts/runner_config.py +104 -0
- {tasks_toolkit-0.1.4 → tasks_toolkit-0.1.6}/scripts/sprint_auto.py +6 -4
- {tasks_toolkit-0.1.4 → tasks_toolkit-0.1.6}/scripts/sprint_auto_execute.py +0 -1
- {tasks_toolkit-0.1.4 → tasks_toolkit-0.1.6}/scripts/sprint_auto_preflight.py +41 -32
- {tasks_toolkit-0.1.4 → tasks_toolkit-0.1.6}/scripts/sprint_auto_prepare.py +9 -4
- {tasks_toolkit-0.1.4 → tasks_toolkit-0.1.6}/scripts/sprint_auto_state.py +1 -1
- {tasks_toolkit-0.1.4 → tasks_toolkit-0.1.6}/scripts/sprint_plan.py +29 -15
- {tasks_toolkit-0.1.4 → tasks_toolkit-0.1.6}/scripts/task-report.sh +1 -1
- {tasks_toolkit-0.1.4 → tasks_toolkit-0.1.6}/scripts/task_runner.py +30 -41
- {tasks_toolkit-0.1.4 → tasks_toolkit-0.1.6}/scripts/tasks.py +1 -1
- {tasks_toolkit-0.1.4 → tasks_toolkit-0.1.6}/scripts/tasks_mcp.py +1 -1
- {tasks_toolkit-0.1.4 → tasks_toolkit-0.1.6}/scripts/test_python_fast.sh +2 -0
- {tasks_toolkit-0.1.4 → tasks_toolkit-0.1.6}/scripts/test_python_slow.sh +1 -1
- {tasks_toolkit-0.1.4 → tasks_toolkit-0.1.6}/scripts/test_run_stats.py +6 -0
- tasks_toolkit-0.1.6/scripts/test_run_summary.py +44 -0
- tasks_toolkit-0.1.6/scripts/test_runner_config.py +59 -0
- {tasks_toolkit-0.1.4 → tasks_toolkit-0.1.6}/scripts/test_sprint_auto_execute.py +1 -1
- {tasks_toolkit-0.1.4 → tasks_toolkit-0.1.6}/scripts/test_sprint_auto_preflight.py +2 -1
- {tasks_toolkit-0.1.4 → tasks_toolkit-0.1.6}/scripts/test_sprint_lifecycle.py +31 -2
- tasks_toolkit-0.1.6/scripts/test_sprint_plan.py +134 -0
- {tasks_toolkit-0.1.4 → tasks_toolkit-0.1.6}/scripts/test_task_runner.py +77 -3
- tasks_toolkit-0.1.6/scripts/test_tasks_storage.py +70 -0
- {tasks_toolkit-0.1.4 → tasks_toolkit-0.1.6/tasks_toolkit.egg-info}/PKG-INFO +142 -17
- {tasks_toolkit-0.1.4 → tasks_toolkit-0.1.6}/tasks_toolkit.egg-info/SOURCES.txt +4 -0
- tasks_toolkit-0.1.4/scripts/test_tasks_storage.py +0 -66
- {tasks_toolkit-0.1.4 → tasks_toolkit-0.1.6}/LICENSE +0 -0
- {tasks_toolkit-0.1.4 → tasks_toolkit-0.1.6}/scripts/__init__.py +0 -0
- {tasks_toolkit-0.1.4 → tasks_toolkit-0.1.6}/scripts/apply-plan.sh +0 -0
- {tasks_toolkit-0.1.4 → tasks_toolkit-0.1.6}/scripts/run-task.sh +0 -0
- {tasks_toolkit-0.1.4 → tasks_toolkit-0.1.6}/scripts/spec-lint.sh +0 -0
- {tasks_toolkit-0.1.4 → tasks_toolkit-0.1.6}/scripts/sprint-auto.sh +0 -0
- {tasks_toolkit-0.1.4 → tasks_toolkit-0.1.6}/scripts/sprint_auto_resume.py +0 -0
- {tasks_toolkit-0.1.4 → tasks_toolkit-0.1.6}/scripts/tasks_cli.py +0 -0
- {tasks_toolkit-0.1.4 → tasks_toolkit-0.1.6}/scripts/tasks_commands.py +0 -0
- {tasks_toolkit-0.1.4 → tasks_toolkit-0.1.6}/scripts/tasks_init.py +0 -0
- {tasks_toolkit-0.1.4 → tasks_toolkit-0.1.6}/scripts/tasks_model.py +0 -0
- {tasks_toolkit-0.1.4 → tasks_toolkit-0.1.6}/scripts/tasks_plan_import.py +0 -0
- {tasks_toolkit-0.1.4 → tasks_toolkit-0.1.6}/scripts/tasks_render.py +0 -0
- {tasks_toolkit-0.1.4 → tasks_toolkit-0.1.6}/scripts/tasks_storage.py +0 -0
- {tasks_toolkit-0.1.4 → tasks_toolkit-0.1.6}/scripts/tasks_watch.py +0 -0
- {tasks_toolkit-0.1.4 → tasks_toolkit-0.1.6}/scripts/test_python_lint.sh +0 -0
- {tasks_toolkit-0.1.4 → tasks_toolkit-0.1.6}/scripts/test_spec_lint.sh +0 -0
- {tasks_toolkit-0.1.4 → tasks_toolkit-0.1.6}/scripts/test_sprint_auto.py +0 -0
- {tasks_toolkit-0.1.4 → tasks_toolkit-0.1.6}/scripts/test_sprint_auto_prepare.py +0 -0
- {tasks_toolkit-0.1.4 → tasks_toolkit-0.1.6}/scripts/test_sprint_auto_resume.py +0 -0
- {tasks_toolkit-0.1.4 → tasks_toolkit-0.1.6}/scripts/test_sprint_auto_state.py +0 -0
- {tasks_toolkit-0.1.4 → tasks_toolkit-0.1.6}/scripts/test_tasks.py +0 -0
- {tasks_toolkit-0.1.4 → tasks_toolkit-0.1.6}/scripts/test_tasks_mcp.py +0 -0
- {tasks_toolkit-0.1.4 → tasks_toolkit-0.1.6}/scripts/test_tasks_plan_import.py +0 -0
- {tasks_toolkit-0.1.4 → tasks_toolkit-0.1.6}/scripts/test_tasks_watch.py +0 -0
- {tasks_toolkit-0.1.4 → tasks_toolkit-0.1.6}/setup.cfg +0 -0
- {tasks_toolkit-0.1.4 → tasks_toolkit-0.1.6}/tasks_toolkit.egg-info/dependency_links.txt +0 -0
- {tasks_toolkit-0.1.4 → tasks_toolkit-0.1.6}/tasks_toolkit.egg-info/entry_points.txt +0 -0
- {tasks_toolkit-0.1.4 → tasks_toolkit-0.1.6}/tasks_toolkit.egg-info/requires.txt +0 -0
- {tasks_toolkit-0.1.4 → tasks_toolkit-0.1.6}/tasks_toolkit.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tasks-toolkit
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.6
|
|
4
4
|
Summary: Task tracker toolkit: CLI + MCP server for sprint/task/subtask management, delegation helpers.
|
|
5
5
|
Author: prachwal
|
|
6
6
|
License: MIT License
|
|
@@ -101,17 +101,112 @@ export TASKS_ROOT=/path/to/data
|
|
|
101
101
|
python3 scripts/tasks.py overview
|
|
102
102
|
```
|
|
103
103
|
|
|
104
|
-
## MCP
|
|
104
|
+
## MCP clients
|
|
105
105
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
106
|
+
The `tasks-mcp` command runs the same stdio server for every client. Set `TASKS_ROOT`
|
|
107
|
+
to the absolute path of the project whose tasks you want to manage. It points to
|
|
108
|
+
the project, not to the toolkit installation. An installed server needs no copy of
|
|
109
|
+
this source repository. Use an absolute executable path if the client cannot find
|
|
110
|
+
`tasks-mcp` on its PATH; `command -v tasks-mcp` shows it on Unix.
|
|
111
|
+
|
|
112
|
+
### Claude Desktop
|
|
113
|
+
|
|
114
|
+
Open **Settings → Developer → Edit Config**, merge this entry into
|
|
115
|
+
`claude_desktop_config.json`, and restart the app:
|
|
116
|
+
|
|
117
|
+
```json
|
|
118
|
+
{
|
|
119
|
+
"mcpServers": {
|
|
120
|
+
"tasks": {
|
|
121
|
+
"command": "/absolute/path/to/tasks-mcp",
|
|
122
|
+
"args": [],
|
|
123
|
+
"env": {"TASKS_ROOT": "/absolute/path/to/your-project"}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
See the [official local-server setup guide](https://modelcontextprotocol.io/docs/develop/connect-local-servers)
|
|
130
|
+
for configuration locations and troubleshooting. The toolkit currently uses POSIX
|
|
131
|
+
file locking (`fcntl`); run the server on macOS or Linux. The Windows Desktop app
|
|
132
|
+
needs a launcher into a Linux environment such as WSL, rather than a native Windows
|
|
133
|
+
Python process.
|
|
134
|
+
|
|
135
|
+
### Claude Code
|
|
136
|
+
|
|
137
|
+
From your project directory, register a project-scoped stdio server:
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
claude mcp add --transport stdio --scope project \
|
|
141
|
+
--env TASKS_ROOT="$(pwd)" tasks -- /absolute/path/to/tasks-mcp
|
|
142
|
+
claude mcp list
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Claude Code saves project-scoped servers to `.mcp.json`; see its
|
|
146
|
+
[official MCP documentation](https://code.claude.com/docs/en/mcp).
|
|
147
|
+
|
|
148
|
+
### VS Code / GitHub Copilot
|
|
149
|
+
|
|
150
|
+
Add a server to `.vscode/mcp.json`:
|
|
151
|
+
|
|
152
|
+
```json
|
|
153
|
+
{
|
|
154
|
+
"servers": {
|
|
155
|
+
"tasks": {
|
|
156
|
+
"type": "stdio",
|
|
157
|
+
"command": "/absolute/path/to/tasks-mcp",
|
|
158
|
+
"args": [],
|
|
159
|
+
"env": {"TASKS_ROOT": "${workspaceFolder}"}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
112
162
|
}
|
|
113
163
|
```
|
|
114
164
|
|
|
165
|
+
See [VS Code's MCP configuration reference](https://code.visualstudio.com/docs/agents/reference/mcp-configuration).
|
|
166
|
+
|
|
167
|
+
### Cursor
|
|
168
|
+
|
|
169
|
+
Add this to `.cursor/mcp.json` in your project (or `~/.cursor/mcp.json` for all projects):
|
|
170
|
+
|
|
171
|
+
```json
|
|
172
|
+
{
|
|
173
|
+
"mcpServers": {
|
|
174
|
+
"tasks": {
|
|
175
|
+
"command": "/absolute/path/to/tasks-mcp",
|
|
176
|
+
"env": {"TASKS_ROOT": "/absolute/path/to/your-project"}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### OpenCode
|
|
183
|
+
|
|
184
|
+
Add this entry to `opencode.json`:
|
|
185
|
+
|
|
186
|
+
```json
|
|
187
|
+
{
|
|
188
|
+
"mcp": {
|
|
189
|
+
"tasks": {
|
|
190
|
+
"type": "local",
|
|
191
|
+
"command": ["/absolute/path/to/tasks-mcp"],
|
|
192
|
+
"enabled": true,
|
|
193
|
+
"environment": {"TASKS_ROOT": "/absolute/path/to/your-project"}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
See the [OpenCode MCP documentation](https://opencode.ai/docs/mcp-servers/).
|
|
200
|
+
|
|
201
|
+
### Other stdio clients and source installs
|
|
202
|
+
|
|
203
|
+
Configure any MCP client to launch `tasks-mcp`, use stdio transport and pass
|
|
204
|
+
`TASKS_ROOT` in the server environment. Clients use different JSON keys; adapt one
|
|
205
|
+
of the examples above to the client's schema. To run from a source checkout,
|
|
206
|
+
replace the executable with `python3` and pass
|
|
207
|
+
`/absolute/path/to/tasks-toolkit/scripts/tasks_mcp.py` as its first argument
|
|
208
|
+
(or use that two-element array for OpenCode's `command`).
|
|
209
|
+
|
|
115
210
|
## Verified task and sprint execution
|
|
116
211
|
|
|
117
212
|
Copy `.tasks-toolkit.example.json` to `.tasks-toolkit.json`, choose the checks for
|
|
@@ -119,12 +214,40 @@ Copy `.tasks-toolkit.example.json` to `.tasks-toolkit.json`, choose the checks f
|
|
|
119
214
|
no shell interpolation is performed. Every configured check must pass. Keep
|
|
120
215
|
`tmp/` and generated `tasks/` files in `.gitignore`, as in this repository.
|
|
121
216
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
217
|
+
Runner names and commands are defined entirely in `.tasks-toolkit.json`:
|
|
218
|
+
|
|
219
|
+
```json
|
|
220
|
+
{
|
|
221
|
+
"default_runner": "coder",
|
|
222
|
+
"runners": {
|
|
223
|
+
"coder": ["python3", "tools/coder_adapter.py"],
|
|
224
|
+
"backup-coder": ["/absolute/path/to/another-adapter"]
|
|
225
|
+
},
|
|
226
|
+
"checks": [["python3", "-m", "pytest", "-q"]],
|
|
227
|
+
"timeout": 120
|
|
228
|
+
}
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
Replace these sample adapter commands with your own. Adapters receive the spec on
|
|
232
|
+
stdin, edit files in their current working directory, leave changes uncommitted,
|
|
233
|
+
and return nonzero on failure. There are no built-in providers or model choices.
|
|
234
|
+
`--runner NAME` selects any registered adapter; otherwise `default_runner` is used.
|
|
235
|
+
If no default is configured, select a runner explicitly. `--fallback NAME` uses
|
|
236
|
+
the same registry; `none` is reserved to disable fallback. Reports and statistics
|
|
237
|
+
accept any runner name containing letters, digits, underscores and hyphens.
|
|
238
|
+
|
|
239
|
+
Optional `protected_paths` adds project-specific files or directories to the built-in
|
|
240
|
+
protection, for example `"protected_paths": ["config/private/", "./credentials.json"]`.
|
|
241
|
+
Entries are relative to the project root; `./` and trailing separators are normalized.
|
|
242
|
+
Absolute paths and entries containing `..` are rejected. Directory entries protect
|
|
243
|
+
the directory and its descendants. Planner commands resolve the project using the
|
|
244
|
+
same current-directory, Git-worktree and `TASKS_ROOT` rules as the tracker; their
|
|
245
|
+
helper scripts are loaded from the toolkit installation.
|
|
246
|
+
|
|
247
|
+
When upgrading from 0.1.4, register the command you previously relied on explicitly:
|
|
248
|
+
implicit provider commands and the implicit default have been removed. Existing
|
|
249
|
+
checkpoint and report runner names remain valid when registered in the configuration.
|
|
250
|
+
Changing configuration still invalidates a resume checkpoint's approval fingerprint.
|
|
128
251
|
|
|
129
252
|
Installed commands are `tasks-run` and `tasks-sprint`; from source, the corresponding
|
|
130
253
|
wrappers are `scripts/run-task.sh` and `scripts/sprint-auto.sh`.
|
|
@@ -133,7 +256,7 @@ Run one task in a clean working tree, with an explicit allowed scope:
|
|
|
133
256
|
|
|
134
257
|
```bash
|
|
135
258
|
tasks-run result \
|
|
136
|
-
--file tmp/tasks/result.md --scope src/ --runner
|
|
259
|
+
--file tmp/tasks/result.md --scope src/ --runner coder
|
|
137
260
|
```
|
|
138
261
|
|
|
139
262
|
Alternatively, `--task-id 1.1` reads scope links from the tracker. The runner checks
|
|
@@ -147,14 +270,15 @@ that left the working tree clean.
|
|
|
147
270
|
For a complete sprint, supply a plan containing `sprint` (`slug`, `title`, `goal`)
|
|
148
271
|
and `tasks` (`key`, `title`, `scope`, `spec_name`, `spec`, `depends_on`). Specs must
|
|
149
272
|
pass `scripts/spec-lint.sh`; dependencies reference earlier task keys. An answers
|
|
150
|
-
file binds the reviewed goal, allowed paths and execution policy
|
|
273
|
+
file binds the reviewed goal, allowed paths and execution policy. `policy.runner`
|
|
274
|
+
overrides the configured default; delegated decisions use the same registry:
|
|
151
275
|
|
|
152
276
|
```json
|
|
153
277
|
{
|
|
154
278
|
"goal_reviewed": true,
|
|
155
279
|
"allowed_scopes": ["src/"],
|
|
156
280
|
"policy": {
|
|
157
|
-
"runner": "
|
|
281
|
+
"runner": "coder", "serial": true, "fallback": "none", "retries": 0,
|
|
158
282
|
"push": false, "accept_flags": false, "reset": false, "clean": false
|
|
159
283
|
}
|
|
160
284
|
}
|
|
@@ -220,6 +344,7 @@ builds to pass. VS Code tracker commands are provided in `.vscode/tasks.json`.
|
|
|
220
344
|
| `sprint_auto_resume.py` | Resume and finish owned sprints. |
|
|
221
345
|
| `apply-plan.sh` | Import a validated sprint plan (`tmp/plans/<name>.json`) into the tracker. |
|
|
222
346
|
| `run-task.sh` / `task_runner.py` | Run configured coders and verify actual changes before committing. |
|
|
347
|
+
| `runner_config.py` | Provider-independent runner registry, default selection and configuration validation. |
|
|
223
348
|
| `task-report.sh` | Report delegation progress to the tracker. |
|
|
224
349
|
| `run_summary.py` | Write a run summary JSON for finished runs. |
|
|
225
350
|
| `run_stats.py` | Delivery-speed statistics for delegation runners. |
|
|
@@ -52,17 +52,112 @@ export TASKS_ROOT=/path/to/data
|
|
|
52
52
|
python3 scripts/tasks.py overview
|
|
53
53
|
```
|
|
54
54
|
|
|
55
|
-
## MCP
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
55
|
+
## MCP clients
|
|
56
|
+
|
|
57
|
+
The `tasks-mcp` command runs the same stdio server for every client. Set `TASKS_ROOT`
|
|
58
|
+
to the absolute path of the project whose tasks you want to manage. It points to
|
|
59
|
+
the project, not to the toolkit installation. An installed server needs no copy of
|
|
60
|
+
this source repository. Use an absolute executable path if the client cannot find
|
|
61
|
+
`tasks-mcp` on its PATH; `command -v tasks-mcp` shows it on Unix.
|
|
62
|
+
|
|
63
|
+
### Claude Desktop
|
|
64
|
+
|
|
65
|
+
Open **Settings → Developer → Edit Config**, merge this entry into
|
|
66
|
+
`claude_desktop_config.json`, and restart the app:
|
|
67
|
+
|
|
68
|
+
```json
|
|
69
|
+
{
|
|
70
|
+
"mcpServers": {
|
|
71
|
+
"tasks": {
|
|
72
|
+
"command": "/absolute/path/to/tasks-mcp",
|
|
73
|
+
"args": [],
|
|
74
|
+
"env": {"TASKS_ROOT": "/absolute/path/to/your-project"}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
See the [official local-server setup guide](https://modelcontextprotocol.io/docs/develop/connect-local-servers)
|
|
81
|
+
for configuration locations and troubleshooting. The toolkit currently uses POSIX
|
|
82
|
+
file locking (`fcntl`); run the server on macOS or Linux. The Windows Desktop app
|
|
83
|
+
needs a launcher into a Linux environment such as WSL, rather than a native Windows
|
|
84
|
+
Python process.
|
|
85
|
+
|
|
86
|
+
### Claude Code
|
|
87
|
+
|
|
88
|
+
From your project directory, register a project-scoped stdio server:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
claude mcp add --transport stdio --scope project \
|
|
92
|
+
--env TASKS_ROOT="$(pwd)" tasks -- /absolute/path/to/tasks-mcp
|
|
93
|
+
claude mcp list
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Claude Code saves project-scoped servers to `.mcp.json`; see its
|
|
97
|
+
[official MCP documentation](https://code.claude.com/docs/en/mcp).
|
|
98
|
+
|
|
99
|
+
### VS Code / GitHub Copilot
|
|
100
|
+
|
|
101
|
+
Add a server to `.vscode/mcp.json`:
|
|
102
|
+
|
|
103
|
+
```json
|
|
104
|
+
{
|
|
105
|
+
"servers": {
|
|
106
|
+
"tasks": {
|
|
107
|
+
"type": "stdio",
|
|
108
|
+
"command": "/absolute/path/to/tasks-mcp",
|
|
109
|
+
"args": [],
|
|
110
|
+
"env": {"TASKS_ROOT": "${workspaceFolder}"}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
See [VS Code's MCP configuration reference](https://code.visualstudio.com/docs/agents/reference/mcp-configuration).
|
|
117
|
+
|
|
118
|
+
### Cursor
|
|
119
|
+
|
|
120
|
+
Add this to `.cursor/mcp.json` in your project (or `~/.cursor/mcp.json` for all projects):
|
|
121
|
+
|
|
122
|
+
```json
|
|
123
|
+
{
|
|
124
|
+
"mcpServers": {
|
|
125
|
+
"tasks": {
|
|
126
|
+
"command": "/absolute/path/to/tasks-mcp",
|
|
127
|
+
"env": {"TASKS_ROOT": "/absolute/path/to/your-project"}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### OpenCode
|
|
134
|
+
|
|
135
|
+
Add this entry to `opencode.json`:
|
|
136
|
+
|
|
137
|
+
```json
|
|
138
|
+
{
|
|
139
|
+
"mcp": {
|
|
140
|
+
"tasks": {
|
|
141
|
+
"type": "local",
|
|
142
|
+
"command": ["/absolute/path/to/tasks-mcp"],
|
|
143
|
+
"enabled": true,
|
|
144
|
+
"environment": {"TASKS_ROOT": "/absolute/path/to/your-project"}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
63
147
|
}
|
|
64
148
|
```
|
|
65
149
|
|
|
150
|
+
See the [OpenCode MCP documentation](https://opencode.ai/docs/mcp-servers/).
|
|
151
|
+
|
|
152
|
+
### Other stdio clients and source installs
|
|
153
|
+
|
|
154
|
+
Configure any MCP client to launch `tasks-mcp`, use stdio transport and pass
|
|
155
|
+
`TASKS_ROOT` in the server environment. Clients use different JSON keys; adapt one
|
|
156
|
+
of the examples above to the client's schema. To run from a source checkout,
|
|
157
|
+
replace the executable with `python3` and pass
|
|
158
|
+
`/absolute/path/to/tasks-toolkit/scripts/tasks_mcp.py` as its first argument
|
|
159
|
+
(or use that two-element array for OpenCode's `command`).
|
|
160
|
+
|
|
66
161
|
## Verified task and sprint execution
|
|
67
162
|
|
|
68
163
|
Copy `.tasks-toolkit.example.json` to `.tasks-toolkit.json`, choose the checks for
|
|
@@ -70,12 +165,40 @@ Copy `.tasks-toolkit.example.json` to `.tasks-toolkit.json`, choose the checks f
|
|
|
70
165
|
no shell interpolation is performed. Every configured check must pass. Keep
|
|
71
166
|
`tmp/` and generated `tasks/` files in `.gitignore`, as in this repository.
|
|
72
167
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
168
|
+
Runner names and commands are defined entirely in `.tasks-toolkit.json`:
|
|
169
|
+
|
|
170
|
+
```json
|
|
171
|
+
{
|
|
172
|
+
"default_runner": "coder",
|
|
173
|
+
"runners": {
|
|
174
|
+
"coder": ["python3", "tools/coder_adapter.py"],
|
|
175
|
+
"backup-coder": ["/absolute/path/to/another-adapter"]
|
|
176
|
+
},
|
|
177
|
+
"checks": [["python3", "-m", "pytest", "-q"]],
|
|
178
|
+
"timeout": 120
|
|
179
|
+
}
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Replace these sample adapter commands with your own. Adapters receive the spec on
|
|
183
|
+
stdin, edit files in their current working directory, leave changes uncommitted,
|
|
184
|
+
and return nonzero on failure. There are no built-in providers or model choices.
|
|
185
|
+
`--runner NAME` selects any registered adapter; otherwise `default_runner` is used.
|
|
186
|
+
If no default is configured, select a runner explicitly. `--fallback NAME` uses
|
|
187
|
+
the same registry; `none` is reserved to disable fallback. Reports and statistics
|
|
188
|
+
accept any runner name containing letters, digits, underscores and hyphens.
|
|
189
|
+
|
|
190
|
+
Optional `protected_paths` adds project-specific files or directories to the built-in
|
|
191
|
+
protection, for example `"protected_paths": ["config/private/", "./credentials.json"]`.
|
|
192
|
+
Entries are relative to the project root; `./` and trailing separators are normalized.
|
|
193
|
+
Absolute paths and entries containing `..` are rejected. Directory entries protect
|
|
194
|
+
the directory and its descendants. Planner commands resolve the project using the
|
|
195
|
+
same current-directory, Git-worktree and `TASKS_ROOT` rules as the tracker; their
|
|
196
|
+
helper scripts are loaded from the toolkit installation.
|
|
197
|
+
|
|
198
|
+
When upgrading from 0.1.4, register the command you previously relied on explicitly:
|
|
199
|
+
implicit provider commands and the implicit default have been removed. Existing
|
|
200
|
+
checkpoint and report runner names remain valid when registered in the configuration.
|
|
201
|
+
Changing configuration still invalidates a resume checkpoint's approval fingerprint.
|
|
79
202
|
|
|
80
203
|
Installed commands are `tasks-run` and `tasks-sprint`; from source, the corresponding
|
|
81
204
|
wrappers are `scripts/run-task.sh` and `scripts/sprint-auto.sh`.
|
|
@@ -84,7 +207,7 @@ Run one task in a clean working tree, with an explicit allowed scope:
|
|
|
84
207
|
|
|
85
208
|
```bash
|
|
86
209
|
tasks-run result \
|
|
87
|
-
--file tmp/tasks/result.md --scope src/ --runner
|
|
210
|
+
--file tmp/tasks/result.md --scope src/ --runner coder
|
|
88
211
|
```
|
|
89
212
|
|
|
90
213
|
Alternatively, `--task-id 1.1` reads scope links from the tracker. The runner checks
|
|
@@ -98,14 +221,15 @@ that left the working tree clean.
|
|
|
98
221
|
For a complete sprint, supply a plan containing `sprint` (`slug`, `title`, `goal`)
|
|
99
222
|
and `tasks` (`key`, `title`, `scope`, `spec_name`, `spec`, `depends_on`). Specs must
|
|
100
223
|
pass `scripts/spec-lint.sh`; dependencies reference earlier task keys. An answers
|
|
101
|
-
file binds the reviewed goal, allowed paths and execution policy
|
|
224
|
+
file binds the reviewed goal, allowed paths and execution policy. `policy.runner`
|
|
225
|
+
overrides the configured default; delegated decisions use the same registry:
|
|
102
226
|
|
|
103
227
|
```json
|
|
104
228
|
{
|
|
105
229
|
"goal_reviewed": true,
|
|
106
230
|
"allowed_scopes": ["src/"],
|
|
107
231
|
"policy": {
|
|
108
|
-
"runner": "
|
|
232
|
+
"runner": "coder", "serial": true, "fallback": "none", "retries": 0,
|
|
109
233
|
"push": false, "accept_flags": false, "reset": false, "clean": false
|
|
110
234
|
}
|
|
111
235
|
}
|
|
@@ -171,6 +295,7 @@ builds to pass. VS Code tracker commands are provided in `.vscode/tasks.json`.
|
|
|
171
295
|
| `sprint_auto_resume.py` | Resume and finish owned sprints. |
|
|
172
296
|
| `apply-plan.sh` | Import a validated sprint plan (`tmp/plans/<name>.json`) into the tracker. |
|
|
173
297
|
| `run-task.sh` / `task_runner.py` | Run configured coders and verify actual changes before committing. |
|
|
298
|
+
| `runner_config.py` | Provider-independent runner registry, default selection and configuration validation. |
|
|
174
299
|
| `task-report.sh` | Report delegation progress to the tracker. |
|
|
175
300
|
| `run_summary.py` | Write a run summary JSON for finished runs. |
|
|
176
301
|
| `run_stats.py` | Delivery-speed statistics for delegation runners. |
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "tasks-toolkit"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.6"
|
|
8
8
|
description = "Task tracker toolkit: CLI + MCP server for sprint/task/subtask management, delegation helpers."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { file = "LICENSE" }
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"""Delivery-speed statistics for the delegation runners.
|
|
3
3
|
|
|
4
4
|
Reads tmp/runs/<name>.summary.json when present (exact) and falls back to the last line of
|
|
5
|
-
tmp/runs/<name>.status for older runs ("[mm:ss] done -
|
|
5
|
+
tmp/runs/<name>.status for older runs ("[mm:ss] done - worker exit 0, verification PASSED").
|
|
6
6
|
A run whose status never reached "done" counts as unfinished (killed, stalled or crashed).
|
|
7
7
|
Prints, per runner: runs, verified, failed, unfinished, median and p90 duration, plus the
|
|
8
8
|
share of runs that did not end verified ("rework rate"). Standard library only, read-only.
|
|
@@ -21,7 +21,7 @@ from pathlib import Path
|
|
|
21
21
|
|
|
22
22
|
LAST_STAMP = re.compile(r"^\[(\d+):(\d{2})\]\s+(.*)$")
|
|
23
23
|
RUNNER_TAIL = re.compile(r"\brunner=(\w+)")
|
|
24
|
-
DONE_RUNNER = re.compile(r"^done - (
|
|
24
|
+
DONE_RUNNER = re.compile(r"^done - ([A-Za-z0-9][A-Za-z0-9_-]*) exit (\d+), verification (PASSED|FAILED)")
|
|
25
25
|
|
|
26
26
|
|
|
27
27
|
def parse_status(path: Path) -> dict | None:
|
|
@@ -11,13 +11,13 @@ Usage: scripts/run_summary.py <outfile> <key=value> [<key=value> ...]
|
|
|
11
11
|
|
|
12
12
|
Keys:
|
|
13
13
|
name, runner, model, task_id, patch str (required)
|
|
14
|
-
agent str (
|
|
14
|
+
agent str (optional, for any runner)
|
|
15
15
|
started, finished int, epoch seconds (required)
|
|
16
16
|
agent_exit, verify_exit int (required)
|
|
17
17
|
files_changed int (required)
|
|
18
18
|
verdict verified|verify_failed|agent_failed
|
|
19
19
|
|
|
20
|
-
duration_s is derived as finished - started. Unknown keys,
|
|
20
|
+
duration_s is derived as finished - started. Unknown keys, an invalid runner name or verdict, or
|
|
21
21
|
a missing field are errors and nothing is written. Output is deterministic (sorted keys,
|
|
22
22
|
2-space indent, trailing newline).
|
|
23
23
|
"""
|
|
@@ -25,11 +25,14 @@ from __future__ import annotations
|
|
|
25
25
|
|
|
26
26
|
import json
|
|
27
27
|
import sys
|
|
28
|
+
from pathlib import Path
|
|
28
29
|
from typing import Any
|
|
29
30
|
|
|
31
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
|
32
|
+
from scripts.runner_config import valid_runner_name
|
|
33
|
+
|
|
30
34
|
STR_FIELDS = ("name", "runner", "model", "agent", "task_id", "patch", "verdict")
|
|
31
35
|
INT_FIELDS = ("started", "finished", "agent_exit", "verify_exit", "files_changed")
|
|
32
|
-
RUNNERS = ("junie", "kilo", "codex")
|
|
33
36
|
VERDICTS = ("verified", "verify_failed", "agent_failed")
|
|
34
37
|
|
|
35
38
|
|
|
@@ -63,19 +66,12 @@ def main(argv: list[str]) -> int:
|
|
|
63
66
|
print(f"error: missing fields: {', '.join(missing)}", file=sys.stderr)
|
|
64
67
|
return 2
|
|
65
68
|
|
|
66
|
-
if data["runner"]
|
|
67
|
-
print(
|
|
69
|
+
if not valid_runner_name(data["runner"]):
|
|
70
|
+
print("error: runner must be a name containing letters, digits, underscores or hyphens; 'none' is reserved", file=sys.stderr)
|
|
68
71
|
return 2
|
|
69
72
|
if data["verdict"] not in VERDICTS:
|
|
70
73
|
print(f"error: verdict must be one of {', '.join(VERDICTS)}, got {data['verdict']!r}", file=sys.stderr)
|
|
71
74
|
return 2
|
|
72
|
-
if data["runner"] == "kilo" and "agent" not in data:
|
|
73
|
-
print("error: field 'agent' is required for runner=kilo", file=sys.stderr)
|
|
74
|
-
return 2
|
|
75
|
-
if data["runner"] == "junie" and "agent" in data:
|
|
76
|
-
print("error: field 'agent' is only allowed for runner=kilo", file=sys.stderr)
|
|
77
|
-
return 2
|
|
78
|
-
|
|
79
75
|
duration = int(data["finished"]) - int(data["started"])
|
|
80
76
|
if duration < 0:
|
|
81
77
|
print("error: finished is before started", file=sys.stderr)
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""Provider-independent runner registry and execution configuration.
|
|
2
|
+
|
|
3
|
+
Each registered command receives a task spec on stdin and runs in the task
|
|
4
|
+
worktree. Providers and their CLI arguments belong in project configuration.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import re
|
|
10
|
+
from pathlib import Path, PurePosixPath, PureWindowsPath
|
|
11
|
+
|
|
12
|
+
CONFIG_FILE = ".tasks-toolkit.json"
|
|
13
|
+
RUNNER_NAME = re.compile(r"[A-Za-z0-9][A-Za-z0-9_-]*")
|
|
14
|
+
|
|
15
|
+
# Always protected: version control, the tracker's own data and the config itself.
|
|
16
|
+
# Project-specific paths (agent or tool config dirs) go in `protected_paths` in CONFIG_FILE.
|
|
17
|
+
BUILTIN_PROTECTED = (".git", ".githooks", "tmp", "tasks/tasks.db", "tasks/.lock", CONFIG_FILE)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class RunnerError(Exception):
|
|
21
|
+
"""Runner configuration or an execution prerequisite is invalid."""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _normalize_protected_path(path: object) -> str:
|
|
25
|
+
"""Normalize harmless dot/separator spelling and reject ambiguous or escaping paths."""
|
|
26
|
+
if not isinstance(path, str) or not path or path != path.strip():
|
|
27
|
+
raise RunnerError("protected_paths entries must be non-empty repo-relative paths")
|
|
28
|
+
if (PurePosixPath(path).is_absolute() or PureWindowsPath(path).drive or "\\" in path
|
|
29
|
+
or ".." in path.split("/") or any(ord(char) < 32 for char in path)):
|
|
30
|
+
raise RunnerError(f"protected_paths entry must be repo-relative without '..': {path!r}")
|
|
31
|
+
normalized = str(PurePosixPath(path))
|
|
32
|
+
if normalized == ".":
|
|
33
|
+
raise RunnerError("protected_paths entries must name a file or directory inside the project")
|
|
34
|
+
return normalized
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def protected_paths(root: Path) -> tuple[str, ...]:
|
|
38
|
+
"""Built-in protected paths plus `protected_paths` from the project config, if present."""
|
|
39
|
+
path = root / CONFIG_FILE
|
|
40
|
+
extra: object = []
|
|
41
|
+
if path.is_file():
|
|
42
|
+
config = json.loads(path.read_text(encoding="utf-8"))
|
|
43
|
+
if not isinstance(config, dict):
|
|
44
|
+
raise RunnerError("runner config must be an object")
|
|
45
|
+
extra = config.get("protected_paths", [])
|
|
46
|
+
if not isinstance(extra, list):
|
|
47
|
+
raise RunnerError("protected_paths must be a list of repo-relative paths")
|
|
48
|
+
return BUILTIN_PROTECTED + tuple(_normalize_protected_path(p) for p in extra)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def is_protected(path: str, protected: tuple[str, ...]) -> bool:
|
|
52
|
+
path = str(PurePosixPath(path))
|
|
53
|
+
return any(path == p or path.startswith(p + "/") for p in protected)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def valid_runner_name(name: object) -> bool:
|
|
57
|
+
return isinstance(name, str) and name != "none" and RUNNER_NAME.fullmatch(name) is not None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def valid_command(command: object) -> bool:
|
|
61
|
+
return isinstance(command, list) and bool(command) and all(isinstance(v, str) and bool(v) for v in command)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def runner_command(config: dict, runner: str) -> list[str]:
|
|
65
|
+
if not valid_runner_name(runner):
|
|
66
|
+
raise RunnerError("runner must be a name containing letters, digits, underscores or hyphens; 'none' is reserved")
|
|
67
|
+
command = config.get("runners", {}).get(runner)
|
|
68
|
+
if not valid_command(command):
|
|
69
|
+
raise RunnerError(f"configure runners.{runner} in {CONFIG_FILE}; the command receives the spec on stdin")
|
|
70
|
+
return list(command)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def select_runner(config: dict, requested: str | None = None) -> str:
|
|
74
|
+
name = requested if requested is not None else config.get("default_runner")
|
|
75
|
+
if name is None:
|
|
76
|
+
raise RunnerError(f"choose --runner or policy.runner, or set default_runner in {CONFIG_FILE}")
|
|
77
|
+
runner_command(config, name)
|
|
78
|
+
return str(name)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def load_config(root: Path) -> dict:
|
|
82
|
+
path = root / CONFIG_FILE
|
|
83
|
+
if not path.is_file():
|
|
84
|
+
raise RunnerError(f"create {CONFIG_FILE} with runners and a non-empty checks list before running tasks")
|
|
85
|
+
config = json.loads(path.read_text(encoding="utf-8"))
|
|
86
|
+
if not isinstance(config, dict):
|
|
87
|
+
raise RunnerError("runner config must be an object")
|
|
88
|
+
checks = config.get("checks")
|
|
89
|
+
if not isinstance(checks, list) or not checks or not all(valid_command(c) for c in checks):
|
|
90
|
+
raise RunnerError("checks must be a non-empty list of command argument arrays")
|
|
91
|
+
runners = config.get("runners")
|
|
92
|
+
if not isinstance(runners, dict) or not runners:
|
|
93
|
+
raise RunnerError("runners must be a non-empty mapping of names to command argument arrays")
|
|
94
|
+
for name in runners:
|
|
95
|
+
runner_command(config, name)
|
|
96
|
+
if "default_runner" in config:
|
|
97
|
+
if not valid_runner_name(config["default_runner"]):
|
|
98
|
+
raise RunnerError("default_runner must name a configured runner")
|
|
99
|
+
select_runner(config)
|
|
100
|
+
timeout = config.get("timeout", 120)
|
|
101
|
+
if type(timeout) is not int or timeout < 1:
|
|
102
|
+
raise RunnerError("timeout must be a positive integer")
|
|
103
|
+
protected_paths(root) # validates the optional protected_paths list
|
|
104
|
+
return config
|
|
@@ -10,6 +10,7 @@ from pathlib import Path
|
|
|
10
10
|
|
|
11
11
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
|
12
12
|
from scripts import sprint_auto_execute as execute
|
|
13
|
+
from scripts import sprint_auto_preflight as preflight
|
|
13
14
|
from scripts import sprint_auto_prepare as prepare
|
|
14
15
|
from scripts import sprint_auto_state as sas
|
|
15
16
|
from scripts import task_runner as runner
|
|
@@ -61,7 +62,10 @@ def run(root: Path, goal: str, name: str, answers: dict, plan: Path) -> dict:
|
|
|
61
62
|
if not runner.NAME.fullmatch(name):
|
|
62
63
|
raise runner.RunnerError("invalid run name")
|
|
63
64
|
config = runner.load_config(root)
|
|
64
|
-
|
|
65
|
+
report = preflight.collect_preflight(root, {"goal": goal}, answers)
|
|
66
|
+
if report["blockers"] or report["questions"]:
|
|
67
|
+
return {"status": "blocked" if report["blockers"] else "needs_answers", "preflight": report}
|
|
68
|
+
policy = report["approval_scope"]["policy"]
|
|
65
69
|
if policy.get("fallback", "none") != "none" or policy.get("retries", 0) != 0:
|
|
66
70
|
raise runner.RunnerError("automated sprints require fallback=none and retries=0")
|
|
67
71
|
for flag in ("push", "accept_flags", "reset", "clean"):
|
|
@@ -69,9 +73,7 @@ def run(root: Path, goal: str, name: str, answers: dict, plan: Path) -> dict:
|
|
|
69
73
|
raise runner.RunnerError(f"automated sprints do not support {flag}=true")
|
|
70
74
|
if policy.get("serial", True) is not True:
|
|
71
75
|
raise runner.RunnerError("automated sprints execute serially")
|
|
72
|
-
|
|
73
|
-
if not runner_command:
|
|
74
|
-
raise runner.RunnerError("runner command missing")
|
|
76
|
+
runner.select_runner(config, policy.get("runner"))
|
|
75
77
|
prepared = prepare.prepare_run(root, {"goal": goal}, answers, name, plan)
|
|
76
78
|
if prepared["status"] != "prepared":
|
|
77
79
|
return prepared
|