taskchef 7.28.0 → 7.28.1
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.
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +10 -0
- package/package.json +1 -1
- package/scripts/roles/resolve_roles.py +128 -7
- package/skills/taskchef-delegate/SKILL.md +1 -1
- package/skills/taskchef-delegate/references/model-roles.md +12 -9
- package/src/dashboard/dist/app.js +50 -35
- package/src/dashboard/dist/app.js.map +1 -1
- package/src/dashboard/dist/styles.css +1 -1
- package/src/dashboard/react/App.test.tsx +10 -1
- package/src/dashboard/react/App.tsx +8 -4
- package/src/dashboard/react/api.test.ts +17 -1
- package/src/dashboard/react/api.ts +8 -0
- package/src/dashboard/react/components/ModelSettings.test.tsx +57 -10
- package/src/dashboard/react/components/ModelSettings.tsx +54 -24
- package/src/dashboard/react/styles.css +15 -0
- package/src/dashboard.js +35 -6
- package/src/mcp.js +0 -1
- package/src/model-roles.js +34 -8
- package/src/workspace.js +0 -3
package/README.md
CHANGED
|
@@ -162,6 +162,16 @@ Use $taskchef-executor to execute and report this delegated TaskChef assignment.
|
|
|
162
162
|
<!-- taskchef_id=c0f010ff-84f2-4838-a69d-0ff1f5d721d7 -->
|
|
163
163
|
```
|
|
164
164
|
|
|
165
|
+
### Model roles
|
|
166
|
+
|
|
167
|
+
TaskChef can apply personal Planner, Implementer, and Reviewer model preferences
|
|
168
|
+
from `~/.codex/agents/`. Planning-only tasks use Planner, while coding tasks and
|
|
169
|
+
single tasks that plan and then execute use Implementer throughout. To apply
|
|
170
|
+
both models, finish a Planner task that saves its plan, then create a separate
|
|
171
|
+
Implementer task using that plan. Reviewer preferences apply when a workflow
|
|
172
|
+
explicitly starts a review subagent. Explicit per-task model choices take
|
|
173
|
+
precedence, and changes affect only tasks created afterward.
|
|
174
|
+
|
|
165
175
|
## Work with and report executors
|
|
166
176
|
|
|
167
177
|
Open an executor as an ordinary Codex task. Each executor reports `working`
|
package/package.json
CHANGED
|
@@ -8,6 +8,7 @@ import argparse
|
|
|
8
8
|
import json
|
|
9
9
|
import os
|
|
10
10
|
from pathlib import Path
|
|
11
|
+
import re
|
|
11
12
|
import sys
|
|
12
13
|
import tomllib
|
|
13
14
|
|
|
@@ -57,11 +58,9 @@ def read_layer(directory):
|
|
|
57
58
|
return found, problems
|
|
58
59
|
|
|
59
60
|
|
|
60
|
-
def resolve(
|
|
61
|
+
def resolve(codex_home=None, explicit_model=None, explicit_effort=None):
|
|
61
62
|
home = Path(codex_home or os.environ.get('CODEX_HOME') or Path.home() / '.codex').expanduser().resolve()
|
|
62
63
|
personal, problems = read_layer(home / 'agents')
|
|
63
|
-
local, local_problems = read_layer(Path(project).resolve() / '.codex' / 'agents') if project else ({}, [])
|
|
64
|
-
problems += local_problems
|
|
65
64
|
catalog = None
|
|
66
65
|
catalog_source = str(home / 'models_cache.json')
|
|
67
66
|
try:
|
|
@@ -81,7 +80,7 @@ def resolve(project=None, codex_home=None, explicit_model=None, explicit_effort=
|
|
|
81
80
|
problems.append('Local Codex model catalog is unreadable or malformed; validate availability with the current native tool.')
|
|
82
81
|
results = []
|
|
83
82
|
for role in ROLES:
|
|
84
|
-
entry =
|
|
83
|
+
entry = personal.get(role)
|
|
85
84
|
source = entry['source'] if entry else None
|
|
86
85
|
model = entry['model'] if entry else None
|
|
87
86
|
effort = entry['effort'] if entry else None
|
|
@@ -122,8 +121,14 @@ def resolve(project=None, codex_home=None, explicit_model=None, explicit_effort=
|
|
|
122
121
|
'taskOverrides': overrides,
|
|
123
122
|
'subagentOverrides': {('reasoning_effort' if k == 'thinking' else k): v for k, v in overrides.items()},
|
|
124
123
|
'fallback': 'inherit parent settings' if role == 'reviewer' else 'native new-task default (not guaranteed dispatcher inheritance)'})
|
|
124
|
+
model_options = [] if catalog is None else [
|
|
125
|
+
{'value': slug, 'label': entry.get('display_name') or slug,
|
|
126
|
+
'efforts': [level['effort'] for level in entry['supported_reasoning_levels']]}
|
|
127
|
+
for slug, entry in catalog.items() if entry.get('visibility') != 'hide'
|
|
128
|
+
]
|
|
125
129
|
return {'roles': results, 'problems': problems, 'catalogSource': catalog_source if catalog is not None else None,
|
|
126
|
-
'
|
|
130
|
+
'modelOptions': model_options,
|
|
131
|
+
'precedence': 'explicit user model (and its explicit effort) > personal role > native defaults; explicit effort alone overrides role effort',
|
|
127
132
|
'scope': 'Model and effort only; agent instructions, tools, permissions, and other TOML keys are not applied by this adapter.'}
|
|
128
133
|
|
|
129
134
|
|
|
@@ -148,21 +153,137 @@ def setup(role, model, effort, codex_home=None):
|
|
|
148
153
|
return {'created': True, 'role': role, 'source': str(target), 'model': model, 'effort': effort}
|
|
149
154
|
|
|
150
155
|
|
|
156
|
+
def multiline_state(line, state=None):
|
|
157
|
+
"""Track TOML multiline strings without interpreting their contents as keys."""
|
|
158
|
+
index = 0
|
|
159
|
+
while index < len(line):
|
|
160
|
+
if state:
|
|
161
|
+
closing = line.find(state, index)
|
|
162
|
+
while closing >= 0 and state == '"""':
|
|
163
|
+
escapes = 0
|
|
164
|
+
cursor = closing - 1
|
|
165
|
+
while cursor >= 0 and line[cursor] == '\\':
|
|
166
|
+
escapes += 1
|
|
167
|
+
cursor -= 1
|
|
168
|
+
if escapes % 2 == 0:
|
|
169
|
+
break
|
|
170
|
+
closing = line.find(state, closing + 3)
|
|
171
|
+
if closing < 0:
|
|
172
|
+
return state
|
|
173
|
+
state = None
|
|
174
|
+
index = closing + 3
|
|
175
|
+
continue
|
|
176
|
+
if line[index] == '#':
|
|
177
|
+
return None
|
|
178
|
+
if line.startswith('"""', index) or line.startswith("'''", index):
|
|
179
|
+
state = line[index:index + 3]
|
|
180
|
+
index += 3
|
|
181
|
+
continue
|
|
182
|
+
if line[index] in ('"', "'"):
|
|
183
|
+
quote = line[index]
|
|
184
|
+
index += 1
|
|
185
|
+
while index < len(line):
|
|
186
|
+
if line[index] == quote and (quote == "'" or index == 0 or line[index - 1] != '\\'):
|
|
187
|
+
index += 1
|
|
188
|
+
break
|
|
189
|
+
if quote == '"' and line[index] == '\\':
|
|
190
|
+
index += 2
|
|
191
|
+
else:
|
|
192
|
+
index += 1
|
|
193
|
+
continue
|
|
194
|
+
index += 1
|
|
195
|
+
return state
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def replace_top_level_preferences(content, model, effort):
|
|
199
|
+
replacements = {
|
|
200
|
+
'model': json.dumps(model),
|
|
201
|
+
'model_reasoning_effort': json.dumps(effort),
|
|
202
|
+
}
|
|
203
|
+
assignment = re.compile(
|
|
204
|
+
r'^(?P<prefix>\s*(?P<key>model|model_reasoning_effort|["\']model["\']|["\']model_reasoning_effort["\'])\s*=\s*).*$'
|
|
205
|
+
)
|
|
206
|
+
lines = content.splitlines(keepends=True)
|
|
207
|
+
state = None
|
|
208
|
+
boundary = len(lines)
|
|
209
|
+
found = set()
|
|
210
|
+
for index, line in enumerate(lines):
|
|
211
|
+
if state is None and re.match(r'^\s*\[', line):
|
|
212
|
+
boundary = index
|
|
213
|
+
break
|
|
214
|
+
if state is None:
|
|
215
|
+
body = line.rstrip('\r\n')
|
|
216
|
+
ending = line[len(body):]
|
|
217
|
+
match = assignment.match(body)
|
|
218
|
+
if match:
|
|
219
|
+
key = match.group('key').strip('"\'')
|
|
220
|
+
lines[index] = f"{match.group('prefix')}{replacements[key]}{ending}"
|
|
221
|
+
found.add(key)
|
|
222
|
+
state = multiline_state(line, state)
|
|
223
|
+
missing = [f'{key} = {value}\n' for key, value in replacements.items() if key not in found]
|
|
224
|
+
if missing:
|
|
225
|
+
prefix = '' if boundary == 0 or lines[boundary - 1].endswith(('\n', '\r')) else '\n'
|
|
226
|
+
lines[boundary:boundary] = [prefix + ''.join(missing)]
|
|
227
|
+
candidate = ''.join(lines)
|
|
228
|
+
expected = tomllib.loads(content)
|
|
229
|
+
expected['model'] = model
|
|
230
|
+
expected['model_reasoning_effort'] = effort
|
|
231
|
+
if tomllib.loads(candidate) != expected:
|
|
232
|
+
raise ValueError('agent TOML could not be updated without changing unrelated settings')
|
|
233
|
+
return candidate
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def update(role, model, effort, codex_home=None):
|
|
237
|
+
home = Path(codex_home or os.environ.get('CODEX_HOME') or Path.home() / '.codex').expanduser().resolve()
|
|
238
|
+
resolved = resolve(codex_home=str(home))
|
|
239
|
+
options = {entry['value']: entry for entry in resolved['modelOptions']}
|
|
240
|
+
if model not in options or effort not in options[model]['efforts']:
|
|
241
|
+
raise ValueError('model or reasoning effort is unavailable')
|
|
242
|
+
existing, layer_problems = read_layer(home / 'agents')
|
|
243
|
+
role_entry = existing.get(role)
|
|
244
|
+
if layer_problems or (role_entry and role_entry['problems']):
|
|
245
|
+
raise ValueError('existing role configuration must be fixed before editing')
|
|
246
|
+
if role_entry is None:
|
|
247
|
+
return setup(role, model, effort, str(home))
|
|
248
|
+
target = Path(role_entry['source'])
|
|
249
|
+
agents = (home / 'agents').resolve()
|
|
250
|
+
if target.is_symlink() or target.resolve().parent != agents:
|
|
251
|
+
raise ValueError('role source is outside the personal agent directory')
|
|
252
|
+
content = replace_top_level_preferences(target.read_text(), model, effort)
|
|
253
|
+
temporary = target.with_name(f'.{target.name}.{os.getpid()}.tmp')
|
|
254
|
+
try:
|
|
255
|
+
temporary.write_text(content)
|
|
256
|
+
os.chmod(temporary, target.stat().st_mode & 0o777)
|
|
257
|
+
os.replace(temporary, target)
|
|
258
|
+
finally:
|
|
259
|
+
try:
|
|
260
|
+
temporary.unlink()
|
|
261
|
+
except FileNotFoundError:
|
|
262
|
+
pass
|
|
263
|
+
return {'updated': True, 'role': role, 'source': str(target), 'model': model, 'effort': effort}
|
|
264
|
+
|
|
265
|
+
|
|
151
266
|
def main():
|
|
152
267
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
153
|
-
parser.add_argument('--project')
|
|
154
268
|
parser.add_argument('--codex-home')
|
|
155
269
|
parser.add_argument('--role', choices=ROLES)
|
|
156
270
|
parser.add_argument('--model')
|
|
157
271
|
parser.add_argument('--effort')
|
|
158
272
|
parser.add_argument('--setup', action='store_true')
|
|
273
|
+
parser.add_argument('--update', action='store_true')
|
|
159
274
|
args = parser.parse_args()
|
|
275
|
+
if args.setup and args.update:
|
|
276
|
+
parser.error('--setup and --update are mutually exclusive')
|
|
160
277
|
if args.setup:
|
|
161
278
|
if not (args.role and args.model and args.effort):
|
|
162
279
|
parser.error('--setup requires --role, --model, and --effort')
|
|
163
280
|
result = setup(args.role, args.model, args.effort, args.codex_home)
|
|
281
|
+
elif args.update:
|
|
282
|
+
if not (args.role and args.model and args.effort):
|
|
283
|
+
parser.error('--update requires --role, --model, and --effort')
|
|
284
|
+
result = update(args.role, args.model, args.effort, args.codex_home)
|
|
164
285
|
else:
|
|
165
|
-
result = resolve(args.
|
|
286
|
+
result = resolve(args.codex_home, args.model, args.effort)
|
|
166
287
|
if args.role:
|
|
167
288
|
result['roles'] = [r for r in result['roles'] if r['role'] == args.role]
|
|
168
289
|
print(json.dumps(result, indent=2))
|
|
@@ -50,7 +50,7 @@ stop and report that the TaskChef plugin must be reloaded or installed.
|
|
|
50
50
|
`githubRepos`; use `path` only as checkout identity. Require exactly one
|
|
51
51
|
match and an exact native-project path. Ask instead of guessing.
|
|
52
52
|
Select `planner` for a requested planning assignment and `implementer` for
|
|
53
|
-
coding work. Use the
|
|
53
|
+
coding work. Use the personal `modelRoles` from preparation;
|
|
54
54
|
resolve explicit user choices and follow [model roles](references/model-roles.md).
|
|
55
55
|
Small coding changes need no separate planning task. For a planning/coding
|
|
56
56
|
split, the saved plan is the handoff to a separately requested implementation
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
# Dispatch model roles
|
|
2
2
|
|
|
3
|
-
Preparation includes personal
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
`.codex/agents/`; they are optional. The resolver requires Python 3.11+.
|
|
3
|
+
Preparation includes personal model previews. Role preferences use native Codex
|
|
4
|
+
agent TOML under `$CODEX_HOME/agents/` (default `~/.codex/agents/`); they are
|
|
5
|
+
optional. The resolver requires Python 3.11+.
|
|
7
6
|
|
|
8
7
|
Select `planner` for a planning assignment or `implementer` for coding. Pass
|
|
9
8
|
its `taskOverrides` as native `model` and `thinking` arguments only after checking
|
|
@@ -11,16 +10,15 @@ those values against the current create-task tool's supported model/effort
|
|
|
11
10
|
choices. A missing role means omit both arguments and use the native new-task
|
|
12
11
|
default. This is not guaranteed to inherit the dispatcher's model.
|
|
13
12
|
|
|
14
|
-
Explicit user model choices take precedence over the
|
|
15
|
-
|
|
13
|
+
Explicit user model choices take precedence over the personal role and native
|
|
14
|
+
defaults. An explicit model without explicit effort omits effort and uses the native
|
|
16
15
|
interface's omission behavior; do not mix in a different role's effort or claim
|
|
17
16
|
a specific effective effort without evidence. Explicit effort
|
|
18
|
-
alone replaces the resolved role effort.
|
|
19
|
-
as a unit. To resolve explicit choices, or recheck a preview after configuration
|
|
17
|
+
alone replaces the resolved role effort. To resolve explicit choices, or recheck a preview after configuration
|
|
20
18
|
changes, run:
|
|
21
19
|
|
|
22
20
|
```sh
|
|
23
|
-
python3 <plugin-root>/scripts/roles/resolve_roles.py --
|
|
21
|
+
python3 <plugin-root>/scripts/roles/resolve_roles.py --role implementer --model <user-model> --effort <user-effort>
|
|
24
22
|
```
|
|
25
23
|
|
|
26
24
|
Omit flags the user did not specify. Resolve the plugin root from this skill's
|
|
@@ -47,3 +45,8 @@ implements the function, runs the example, and uses fresh reviewer subagents.
|
|
|
47
45
|
The dispatcher returns after each creation; it does not wait for or launch
|
|
48
46
|
subsequent phases automatically. A small addition change can instead go
|
|
49
47
|
straight to the implementer.
|
|
48
|
+
|
|
49
|
+
A single assignment that asks to plan and then execute is an implementation
|
|
50
|
+
assignment. Use the implementer model for that whole task; do not switch models
|
|
51
|
+
mid-turn. To apply both configured roles, dispatch a planner task first, wait
|
|
52
|
+
for its saved plan, then dispatch a separate implementer task using that plan.
|