taskchef 7.28.0 → 7.29.0
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 +16 -1
- package/docs/dashboard-lifecycle.md +7 -0
- package/docs/spec.md +9 -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 +71 -10
- package/src/dashboard/react/components/ModelSettings.tsx +68 -25
- package/src/dashboard/react/styles.css +15 -0
- package/src/dashboard.js +62 -8
- package/src/mcp.js +0 -1
- package/src/model-roles.js +34 -8
- package/src/usage-tracker.js +192 -23
- package/src/usage.js +40 -12
- 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`
|
|
@@ -332,7 +342,10 @@ server path also rejects requests before discovering or invoking the CLI.
|
|
|
332
342
|
Task details also show whole-task and per-turn token usage when `ccusage` can
|
|
333
343
|
map the linked Codex thread. A completed turn briefly shows “Calculating token
|
|
334
344
|
usage…” while TaskChef performs bounded deferred reconciliation, because
|
|
335
|
-
the terminal lifecycle callback precedes Codex's final output write.
|
|
345
|
+
the terminal lifecycle callback precedes Codex's final output write. The
|
|
346
|
+
dashboard also preloads missing, interrupted, and retry-eligible usage in a
|
|
347
|
+
bounded background queue at startup, after task changes, and on a periodic
|
|
348
|
+
recovery pass; opening task details is not required to start collection. Historical
|
|
336
349
|
tasks may show a trustworthy task total while older turns remain “Token usage
|
|
337
350
|
unavailable” when no cumulative turn boundaries were recorded. Input, cached
|
|
338
351
|
input, output, reasoning, and total counts retain ccusage's categories. Dollar
|
|
@@ -351,6 +364,8 @@ wall-clock elapsed time, not model compute time, and malformed or reversed
|
|
|
351
364
|
timestamp ranges are shown as unavailable.
|
|
352
365
|
The header shows the running TaskChef package version reported by the same
|
|
353
366
|
bounded health identity used for compatible-listener checks.
|
|
367
|
+
The Settings page shows the ccusage version reported by the executable that
|
|
368
|
+
TaskChef resolves at runtime.
|
|
354
369
|
The canonical port is owned by a dashboard session process initialized by the
|
|
355
370
|
TaskChef MCP before its tool transport connects. Health identity records a
|
|
356
371
|
`session` launcher, and MCP recovery reuses only another exact-compatible,
|
|
@@ -33,6 +33,13 @@ the token total and API-equivalent USD estimate. Task cards keep the latest
|
|
|
33
33
|
cumulative value visible during a newer active turn and label it as updating;
|
|
34
34
|
pending is reserved for tasks that do not have a usage snapshot yet.
|
|
35
35
|
|
|
36
|
+
The dashboard owns a small background recovery queue for derived usage. It
|
|
37
|
+
deduplicates tasks, limits concurrent ccusage reads, preserves successful cache
|
|
38
|
+
records, and revisits missing, interrupted, or cooldown-expired records without
|
|
39
|
+
requiring a detail request. The queue starts with the dashboard, reacts to task
|
|
40
|
+
log revisions, performs periodic recovery passes, and stops with the dashboard.
|
|
41
|
+
Settings reports the version returned by the resolved ccusage executable.
|
|
42
|
+
|
|
36
43
|

|
|
37
44
|
|
|
38
45
|
## Reported work presentation
|
package/docs/spec.md
CHANGED
|
@@ -162,6 +162,13 @@ into totals a second time.
|
|
|
162
162
|
|
|
163
163
|
After a terminal report, TaskChef MUST mark the turn `calculating` and perform
|
|
164
164
|
bounded deferred reconciliation without delaying the lifecycle response.
|
|
165
|
+
The dashboard MUST also preload eligible derived usage in its existing service
|
|
166
|
+
lifecycle without requiring a task-detail request. Startup, task-log changes,
|
|
167
|
+
and periodic recovery passes MUST consider missing, interrupted, and
|
|
168
|
+
cooldown-expired records; successful current-generation records MUST remain
|
|
169
|
+
cached. Background work MUST be deduplicated, MUST enforce a small global
|
|
170
|
+
concurrency limit, MUST use bounded delayed retries, and MUST stop when the
|
|
171
|
+
dashboard closes.
|
|
165
172
|
Per-turn usage MUST be a non-negative delta between adjacent reliable cumulative
|
|
166
173
|
boundaries. A first recorded turn MAY use zero as its baseline. Historical
|
|
167
174
|
turns without boundaries and decreasing or ambiguous snapshots MUST be labeled
|
|
@@ -183,6 +190,8 @@ cumulative boundaries use different analyzer versions, requested pricing modes,
|
|
|
183
190
|
or declared cost coverage. For GPT-5.6 with ccusage 20.0.20, provenance MUST
|
|
184
191
|
disclose unverified cache-write coverage and TaskChef MUST NOT claim that the
|
|
185
192
|
estimate includes those charges.
|
|
193
|
+
The dashboard Settings response MUST report the provider version returned by
|
|
194
|
+
the executable resolved for runtime use, not merely copy the dependency pin.
|
|
186
195
|
|
|
187
196
|
Task IDs and non-null thread identities MUST be unique. The immutable intent
|
|
188
197
|
fields MUST NOT change after recording.
|
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.
|