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.
Files changed (60) hide show
  1. {tasks_toolkit-0.1.4/tasks_toolkit.egg-info → tasks_toolkit-0.1.6}/PKG-INFO +142 -17
  2. {tasks_toolkit-0.1.4 → tasks_toolkit-0.1.6}/README.md +142 -17
  3. {tasks_toolkit-0.1.4 → tasks_toolkit-0.1.6}/pyproject.toml +1 -1
  4. {tasks_toolkit-0.1.4 → tasks_toolkit-0.1.6}/scripts/run_stats.py +2 -2
  5. {tasks_toolkit-0.1.4 → tasks_toolkit-0.1.6}/scripts/run_summary.py +8 -12
  6. tasks_toolkit-0.1.6/scripts/runner_config.py +104 -0
  7. {tasks_toolkit-0.1.4 → tasks_toolkit-0.1.6}/scripts/sprint_auto.py +6 -4
  8. {tasks_toolkit-0.1.4 → tasks_toolkit-0.1.6}/scripts/sprint_auto_execute.py +0 -1
  9. {tasks_toolkit-0.1.4 → tasks_toolkit-0.1.6}/scripts/sprint_auto_preflight.py +41 -32
  10. {tasks_toolkit-0.1.4 → tasks_toolkit-0.1.6}/scripts/sprint_auto_prepare.py +9 -4
  11. {tasks_toolkit-0.1.4 → tasks_toolkit-0.1.6}/scripts/sprint_auto_state.py +1 -1
  12. {tasks_toolkit-0.1.4 → tasks_toolkit-0.1.6}/scripts/sprint_plan.py +29 -15
  13. {tasks_toolkit-0.1.4 → tasks_toolkit-0.1.6}/scripts/task-report.sh +1 -1
  14. {tasks_toolkit-0.1.4 → tasks_toolkit-0.1.6}/scripts/task_runner.py +30 -41
  15. {tasks_toolkit-0.1.4 → tasks_toolkit-0.1.6}/scripts/tasks.py +1 -1
  16. {tasks_toolkit-0.1.4 → tasks_toolkit-0.1.6}/scripts/tasks_mcp.py +1 -1
  17. {tasks_toolkit-0.1.4 → tasks_toolkit-0.1.6}/scripts/test_python_fast.sh +2 -0
  18. {tasks_toolkit-0.1.4 → tasks_toolkit-0.1.6}/scripts/test_python_slow.sh +1 -1
  19. {tasks_toolkit-0.1.4 → tasks_toolkit-0.1.6}/scripts/test_run_stats.py +6 -0
  20. tasks_toolkit-0.1.6/scripts/test_run_summary.py +44 -0
  21. tasks_toolkit-0.1.6/scripts/test_runner_config.py +59 -0
  22. {tasks_toolkit-0.1.4 → tasks_toolkit-0.1.6}/scripts/test_sprint_auto_execute.py +1 -1
  23. {tasks_toolkit-0.1.4 → tasks_toolkit-0.1.6}/scripts/test_sprint_auto_preflight.py +2 -1
  24. {tasks_toolkit-0.1.4 → tasks_toolkit-0.1.6}/scripts/test_sprint_lifecycle.py +31 -2
  25. tasks_toolkit-0.1.6/scripts/test_sprint_plan.py +134 -0
  26. {tasks_toolkit-0.1.4 → tasks_toolkit-0.1.6}/scripts/test_task_runner.py +77 -3
  27. tasks_toolkit-0.1.6/scripts/test_tasks_storage.py +70 -0
  28. {tasks_toolkit-0.1.4 → tasks_toolkit-0.1.6/tasks_toolkit.egg-info}/PKG-INFO +142 -17
  29. {tasks_toolkit-0.1.4 → tasks_toolkit-0.1.6}/tasks_toolkit.egg-info/SOURCES.txt +4 -0
  30. tasks_toolkit-0.1.4/scripts/test_tasks_storage.py +0 -66
  31. {tasks_toolkit-0.1.4 → tasks_toolkit-0.1.6}/LICENSE +0 -0
  32. {tasks_toolkit-0.1.4 → tasks_toolkit-0.1.6}/scripts/__init__.py +0 -0
  33. {tasks_toolkit-0.1.4 → tasks_toolkit-0.1.6}/scripts/apply-plan.sh +0 -0
  34. {tasks_toolkit-0.1.4 → tasks_toolkit-0.1.6}/scripts/run-task.sh +0 -0
  35. {tasks_toolkit-0.1.4 → tasks_toolkit-0.1.6}/scripts/spec-lint.sh +0 -0
  36. {tasks_toolkit-0.1.4 → tasks_toolkit-0.1.6}/scripts/sprint-auto.sh +0 -0
  37. {tasks_toolkit-0.1.4 → tasks_toolkit-0.1.6}/scripts/sprint_auto_resume.py +0 -0
  38. {tasks_toolkit-0.1.4 → tasks_toolkit-0.1.6}/scripts/tasks_cli.py +0 -0
  39. {tasks_toolkit-0.1.4 → tasks_toolkit-0.1.6}/scripts/tasks_commands.py +0 -0
  40. {tasks_toolkit-0.1.4 → tasks_toolkit-0.1.6}/scripts/tasks_init.py +0 -0
  41. {tasks_toolkit-0.1.4 → tasks_toolkit-0.1.6}/scripts/tasks_model.py +0 -0
  42. {tasks_toolkit-0.1.4 → tasks_toolkit-0.1.6}/scripts/tasks_plan_import.py +0 -0
  43. {tasks_toolkit-0.1.4 → tasks_toolkit-0.1.6}/scripts/tasks_render.py +0 -0
  44. {tasks_toolkit-0.1.4 → tasks_toolkit-0.1.6}/scripts/tasks_storage.py +0 -0
  45. {tasks_toolkit-0.1.4 → tasks_toolkit-0.1.6}/scripts/tasks_watch.py +0 -0
  46. {tasks_toolkit-0.1.4 → tasks_toolkit-0.1.6}/scripts/test_python_lint.sh +0 -0
  47. {tasks_toolkit-0.1.4 → tasks_toolkit-0.1.6}/scripts/test_spec_lint.sh +0 -0
  48. {tasks_toolkit-0.1.4 → tasks_toolkit-0.1.6}/scripts/test_sprint_auto.py +0 -0
  49. {tasks_toolkit-0.1.4 → tasks_toolkit-0.1.6}/scripts/test_sprint_auto_prepare.py +0 -0
  50. {tasks_toolkit-0.1.4 → tasks_toolkit-0.1.6}/scripts/test_sprint_auto_resume.py +0 -0
  51. {tasks_toolkit-0.1.4 → tasks_toolkit-0.1.6}/scripts/test_sprint_auto_state.py +0 -0
  52. {tasks_toolkit-0.1.4 → tasks_toolkit-0.1.6}/scripts/test_tasks.py +0 -0
  53. {tasks_toolkit-0.1.4 → tasks_toolkit-0.1.6}/scripts/test_tasks_mcp.py +0 -0
  54. {tasks_toolkit-0.1.4 → tasks_toolkit-0.1.6}/scripts/test_tasks_plan_import.py +0 -0
  55. {tasks_toolkit-0.1.4 → tasks_toolkit-0.1.6}/scripts/test_tasks_watch.py +0 -0
  56. {tasks_toolkit-0.1.4 → tasks_toolkit-0.1.6}/setup.cfg +0 -0
  57. {tasks_toolkit-0.1.4 → tasks_toolkit-0.1.6}/tasks_toolkit.egg-info/dependency_links.txt +0 -0
  58. {tasks_toolkit-0.1.4 → tasks_toolkit-0.1.6}/tasks_toolkit.egg-info/entry_points.txt +0 -0
  59. {tasks_toolkit-0.1.4 → tasks_toolkit-0.1.6}/tasks_toolkit.egg-info/requires.txt +0 -0
  60. {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.4
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 (OpenCode)
104
+ ## MCP clients
105
105
 
106
- ```jsonc
107
- "tasks": {
108
- "type": "local",
109
- "command": ["python3", "{env:HOME}/src/tasks-toolkit/scripts/tasks_mcp.py"],
110
- "cwd": "{env:HOME}/src/tasks-toolkit",
111
- "environment": { "TASKS_ROOT": "{env:HOME}/src/tasks-toolkit" }
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
- The default coder uses [Codex non-interactive mode](https://developers.openai.com/codex/noninteractive):
123
- `codex exec --sandbox workspace-write -`. Install and authenticate its CLI separately.
124
- To use Junie or Kilo, set `runners.junie` or `runners.kilo` to the argument array for
125
- your local adapter. Adapters receive the spec on stdin, edit files in their current
126
- working directory, leave changes uncommitted, and return nonzero on failure.
127
- The same `runners.codex` setting can select a custom Codex adapter.
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 codex
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": "codex", "serial": true, "fallback": "none", "retries": 0,
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 (OpenCode)
56
-
57
- ```jsonc
58
- "tasks": {
59
- "type": "local",
60
- "command": ["python3", "{env:HOME}/src/tasks-toolkit/scripts/tasks_mcp.py"],
61
- "cwd": "{env:HOME}/src/tasks-toolkit",
62
- "environment": { "TASKS_ROOT": "{env:HOME}/src/tasks-toolkit" }
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
- The default coder uses [Codex non-interactive mode](https://developers.openai.com/codex/noninteractive):
74
- `codex exec --sandbox workspace-write -`. Install and authenticate its CLI separately.
75
- To use Junie or Kilo, set `runners.junie` or `runners.kilo` to the argument array for
76
- your local adapter. Adapters receive the spec on stdin, edit files in their current
77
- working directory, leave changes uncommitted, and return nonzero on failure.
78
- The same `runners.codex` setting can select a custom Codex adapter.
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 codex
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": "codex", "serial": true, "fallback": "none", "retries": 0,
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.4"
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 - junie exit 0, verification PASSED").
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 - (junie|kilo|codex|haiku) exit (\d+), verification (PASSED|FAILED)")
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 (required for runner=kilo only)
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, a wrong runner or verdict, or
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"] not in RUNNERS:
67
- print(f"error: runner must be one of {', '.join(RUNNERS)}, got {data['runner']!r}", file=sys.stderr)
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
- policy = answers.get("policy", {})
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
- runner_command = runner.runner_command(config, policy.get("runner", "codex"))
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
@@ -12,7 +12,6 @@ sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
12
12
  from scripts import sprint_auto_state as sas
13
13
  from scripts import task_runner as runner
14
14
 
15
- RUNNER_DEFAULT = "codex"
16
15
  check_scope_conformity = runner.scope_conformity
17
16
 
18
17