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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "taskchef",
3
- "version": "7.28.0",
3
+ "version": "7.29.0",
4
4
  "description": "Dispatch work from a data-only workspace to visible Codex project tasks.",
5
5
  "author": {
6
6
  "name": "Favo Yang",
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. Historical
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
  ![Pending token usage in the task detail view](images/dashboard-token-pending.jpg)
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "taskchef",
3
- "version": "7.28.0",
3
+ "version": "7.29.0",
4
4
  "description": "A non-blocking interactive dispatcher for visible Codex tasks.",
5
5
  "license": "MIT",
6
6
  "author": "Favo Yang",
@@ -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(project=None, codex_home=None, explicit_model=None, explicit_effort=None):
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 = local.get(role, personal.get(role))
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
- 'precedence': 'explicit user model (and its explicit effort) > project role > personal role > native defaults; explicit effort alone overrides role effort',
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.project, args.codex_home, args.model, args.effort)
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 matched project's `projectModelRoles` from preparation;
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 and per-project model previews. Use the exact
4
- matched project's preview. Role preferences use native Codex agent TOML under
5
- `$CODEX_HOME/agents/` (default `~/.codex/agents/`) and that project's
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 project role, personal
15
- role, and native defaults. An explicit model without explicit effort omits effort and uses the native
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. Project roles replace personal roles
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 --project <project-path> --role implementer --model <user-model> --effort <user-effort>
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.