taskchef 8.7.0 → 9.0.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.
Files changed (149) hide show
  1. package/.codex-plugin/plugin.json +7 -10
  2. package/.mcp.json +3 -2
  3. package/README.md +31 -158
  4. package/docs/mcp-app.md +21 -4
  5. package/docs/spec.md +38 -746
  6. package/docs/taskchef-next-storage.md +2 -2
  7. package/index.js +2 -99
  8. package/package.json +12 -49
  9. package/skills/delegate/SKILL.md +44 -0
  10. package/skills/delegate/agents/openai.yaml +4 -0
  11. package/src/codex-app.js +3 -152
  12. package/src/codex-session-scanner.js +2 -2
  13. package/src/file-lock.js +25 -0
  14. package/src/mcp-app/react/NextTaskDetail.test.tsx +1 -1
  15. package/src/mcp-app/react/NextTaskDetail.tsx +8 -8
  16. package/src/mcp-app/react/main.test.tsx +3 -3
  17. package/src/mcp-app/react/main.tsx +7 -7
  18. package/src/mcp-app.js +4 -4
  19. package/src/mcp.js +4 -542
  20. package/src/next-cache.js +2 -2
  21. package/src/next-github.js +3 -3
  22. package/src/next-notifications.js +2 -2
  23. package/src/{dashboard/react → ui}/components/CardStatusIcons.tsx +1 -1
  24. package/src/{dashboard/react → ui}/components/GitHubLinks.tsx +1 -1
  25. package/src/{dashboard/react → ui}/components/LinkedText.tsx +1 -1
  26. package/src/{dashboard/react → ui}/components/RelativeTime.test.tsx +1 -1
  27. package/src/{dashboard/react → ui}/components/RelativeTime.tsx +1 -1
  28. package/src/{dashboard/react → ui}/components/ReplyImagePreview.test.tsx +1 -1
  29. package/src/{dashboard/react → ui}/components/TaskBoard.tsx +1 -1
  30. package/src/{dashboard/react → ui}/components/TaskCard.tsx +1 -1
  31. package/src/{dashboard/react → ui}/components/TurnContent.tsx +1 -1
  32. package/src/{dashboard/react → ui}/presentation.ts +1 -1
  33. package/src/version.js +2 -1
  34. package/BACKLOG.md +0 -48
  35. package/assets/e2e-benchmark-example.json +0 -50
  36. package/assets/taskchef-dispatcher-instructions.md +0 -44
  37. package/bin/taskchef.js +0 -20
  38. package/docs/advanced-guide.md +0 -497
  39. package/docs/dashboard-lifecycle.md +0 -149
  40. package/docs/dashboard-ui.md +0 -40
  41. package/docs/firstmate-taskchef-comparison.md +0 -283
  42. package/docs/images/ccusage-token-consumption.png +0 -0
  43. package/docs/images/dashboard-identity.jpg +0 -0
  44. package/docs/images/dashboard-token-pending.jpg +0 -0
  45. package/docs/images/interrupted-turn-recovery.jpg +0 -0
  46. package/docs/images/notification-event-snapshots.jpg +0 -0
  47. package/docs/images/readme-board.png +0 -0
  48. package/docs/images/readme-completed.png +0 -0
  49. package/docs/images/readme-list.png +0 -0
  50. package/docs/images/readme-needs-input.png +0 -0
  51. package/docs/images/readme-roles.png +0 -0
  52. package/docs/images/reported-work-duration-desktop.jpg +0 -0
  53. package/docs/images/reported-work-duration-mobile.jpg +0 -0
  54. package/docs/images/result-history-dashboard.jpg +0 -0
  55. package/docs/images/taskchef-pr-icon-comparison.jpg +0 -0
  56. package/docs/images/taskchef-unified-status-popup.jpg +0 -0
  57. package/docs/workflows.md +0 -476
  58. package/mcp/dashboard-session.js +0 -51
  59. package/scripts/benchmark-dispatch-prepare.js +0 -90
  60. package/scripts/e2e-benchmark.js +0 -206
  61. package/scripts/roles/resolve_roles.py +0 -293
  62. package/skills/taskchef-bootstrap/SKILL.md +0 -96
  63. package/skills/taskchef-bootstrap/agents/openai.yaml +0 -4
  64. package/skills/taskchef-bootstrap/references/project-index.md +0 -154
  65. package/skills/taskchef-copilot/SKILL.md +0 -75
  66. package/skills/taskchef-copilot/agents/openai.yaml +0 -4
  67. package/skills/taskchef-dashboard/SKILL.md +0 -24
  68. package/skills/taskchef-dashboard/agents/openai.yaml +0 -4
  69. package/skills/taskchef-delegate/SKILL.md +0 -102
  70. package/skills/taskchef-delegate/agents/openai.yaml +0 -4
  71. package/skills/taskchef-delegate/references/model-roles.md +0 -29
  72. package/skills/taskchef-executor/SKILL.md +0 -154
  73. package/skills/taskchef-executor/agents/openai.yaml +0 -4
  74. package/skills/taskchef-executor/references/compatibility.md +0 -22
  75. package/skills/taskchef-executor/references/ending-actions.md +0 -26
  76. package/src/backups.js +0 -359
  77. package/src/cli.js +0 -733
  78. package/src/config-audit.js +0 -99
  79. package/src/config-mutations.js +0 -106
  80. package/src/dashboard/actions.js +0 -136
  81. package/src/dashboard/app.js +0 -1172
  82. package/src/dashboard/dist/app.js +0 -269
  83. package/src/dashboard/dist/app.js.map +0 -1
  84. package/src/dashboard/dist/index.html +0 -16
  85. package/src/dashboard/dist/styles.css +0 -1
  86. package/src/dashboard/index.html +0 -148
  87. package/src/dashboard/react/App.test.tsx +0 -346
  88. package/src/dashboard/react/App.tsx +0 -515
  89. package/src/dashboard/react/api.test.ts +0 -40
  90. package/src/dashboard/react/api.ts +0 -103
  91. package/src/dashboard/react/components/ActivityTimeline.tsx +0 -118
  92. package/src/dashboard/react/components/ModelSettings.test.tsx +0 -180
  93. package/src/dashboard/react/components/ModelSettings.tsx +0 -106
  94. package/src/dashboard/react/components/NotificationCenter.test.tsx +0 -76
  95. package/src/dashboard/react/components/NotificationCenter.tsx +0 -111
  96. package/src/dashboard/react/components/PullRequestBadges.tsx +0 -18
  97. package/src/dashboard/react/components/TaskDetail.test.tsx +0 -255
  98. package/src/dashboard/react/components/TaskDetail.tsx +0 -271
  99. package/src/dashboard/react/components/UsagePanel.tsx +0 -96
  100. package/src/dashboard/react/index.html +0 -15
  101. package/src/dashboard/react/main.tsx +0 -12
  102. package/src/dashboard/react/overlay-state.test.ts +0 -110
  103. package/src/dashboard/react/overlay-state.ts +0 -60
  104. package/src/dashboard/react/presentation.test.tsx +0 -571
  105. package/src/dashboard/styles.css +0 -261
  106. package/src/dashboard-control.js +0 -34
  107. package/src/dashboard-manager.js +0 -339
  108. package/src/dashboard-session-process.js +0 -88
  109. package/src/dashboard-session.js +0 -86
  110. package/src/dashboard.js +0 -1303
  111. package/src/delegation.js +0 -277
  112. package/src/github.js +0 -107
  113. package/src/model-roles.js +0 -70
  114. package/src/recorded-usage-tracker.js +0 -92
  115. package/src/usage-tracker.js +0 -650
  116. package/src/usage.js +0 -367
  117. package/src/workspace-path.js +0 -48
  118. package/src/workspace.js +0 -3629
  119. /package/src/{dashboard/react → ui}/components/AnimatedUsage.test.tsx +0 -0
  120. /package/src/{dashboard/react → ui}/components/CardStatusIcons.test.tsx +0 -0
  121. /package/src/{dashboard/react → ui}/components/ImageGallery.tsx +0 -0
  122. /package/src/{dashboard/react → ui}/components/OpenChatButton.test.tsx +0 -0
  123. /package/src/{dashboard/react → ui}/components/OpenChatButton.tsx +0 -0
  124. /package/src/{dashboard/react → ui}/components/PrGlyph.tsx +0 -0
  125. /package/src/{dashboard/react → ui}/components/QueuedPrompts.test.tsx +0 -0
  126. /package/src/{dashboard/react → ui}/components/QueuedPrompts.tsx +0 -0
  127. /package/src/{dashboard/react → ui}/components/ReplyCover.test.tsx +0 -0
  128. /package/src/{dashboard/react → ui}/components/ReplyCover.tsx +0 -0
  129. /package/src/{dashboard/react → ui}/components/ReplyImagePreview.tsx +0 -0
  130. /package/src/{dashboard/react → ui}/components/ReplyMarkdown.test.tsx +0 -0
  131. /package/src/{dashboard/react → ui}/components/ReplyMarkdown.tsx +0 -0
  132. /package/src/{dashboard/react → ui}/components/SavedUsageFormat.test.tsx +0 -0
  133. /package/src/{dashboard/react → ui}/components/SavedUsageFormat.tsx +0 -0
  134. /package/src/{dashboard/react → ui}/components/ShimmerText.tsx +0 -0
  135. /package/src/{dashboard/react → ui}/components/StatsGrid.tsx +0 -0
  136. /package/src/{dashboard/react → ui}/components/StatusBadge.tsx +0 -0
  137. /package/src/{dashboard/react → ui}/components/TaskBoard.test.tsx +0 -0
  138. /package/src/{dashboard/react → ui}/components/TaskCardStats.test.tsx +0 -0
  139. /package/src/{dashboard/react → ui}/components/TaskCardStats.tsx +0 -0
  140. /package/src/{dashboard/react → ui}/components/TurnContent.test.tsx +0 -0
  141. /package/src/{dashboard/react → ui}/fixtures.ts +0 -0
  142. /package/src/{dashboard → ui}/github-links.js +0 -0
  143. /package/src/{dashboard/react → ui}/image-cache.test.ts +0 -0
  144. /package/src/{dashboard/react → ui}/image-cache.ts +0 -0
  145. /package/src/{dashboard → ui}/state.js +0 -0
  146. /package/src/{dashboard/react → ui}/styles.css +0 -0
  147. /package/src/{dashboard/react → ui}/test-setup.ts +0 -0
  148. /package/src/{dashboard → ui}/time.js +0 -0
  149. /package/src/{dashboard/react → ui}/types.ts +0 -0
@@ -1,206 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import { mkdir, readFile, readdir, unlink, writeFile } from "node:fs/promises";
4
- import path from "node:path";
5
- import { fileURLToPath } from "node:url";
6
- import { normalizeCodexThreadId } from "../src/delegation.js";
7
-
8
- const BENCHMARK_NAME = "taskchef-executor-self-link-e2e";
9
- const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
10
- const RESULT_FILENAME = new RegExp(
11
- `^\\d{4}-\\d{2}-\\d{2}T\\d{2}-\\d{2}-\\d{2}\\.\\d{3}Z-${BENCHMARK_NAME}\\.json$`,
12
- );
13
-
14
- function managedResultFilename(name) {
15
- if (!RESULT_FILENAME.test(name)) return false;
16
- const timestampPart = name.slice(0, -`-${BENCHMARK_NAME}.json`.length);
17
- const iso = `${timestampPart.slice(0, 13)}:${timestampPart.slice(14, 16)}:${timestampPart.slice(17)}`;
18
- try {
19
- timestamp(iso, "result filename timestamp");
20
- return true;
21
- } catch {
22
- return false;
23
- }
24
- }
25
-
26
- function object(value, name) {
27
- if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${name} must be an object`);
28
- return value;
29
- }
30
-
31
- function exact(value, keys, name) {
32
- object(value, name);
33
- const expected = new Set(keys);
34
- const unexpected = Object.keys(value).find((key) => !expected.has(key));
35
- if (unexpected) throw new Error(`${name} has unsupported field: ${unexpected}`);
36
- const missing = keys.find((key) => !(key in value));
37
- if (missing) throw new Error(`${name} is missing field: ${missing}`);
38
- return value;
39
- }
40
-
41
- function string(value, name) {
42
- if (typeof value !== "string" || value.trim() === "") throw new Error(`${name} must be a non-empty string`);
43
- return value;
44
- }
45
-
46
- function timestamp(value, name) {
47
- string(value, name);
48
- const milliseconds = Date.parse(value);
49
- if (
50
- !/^\d{4}-\d{2}-\d{2}T.*Z$/.test(value)
51
- || Number.isNaN(milliseconds)
52
- || new Date(milliseconds).toISOString() !== value
53
- ) {
54
- throw new Error(`${name} must be a canonical four-digit-year UTC timestamp`);
55
- }
56
- return milliseconds;
57
- }
58
-
59
- function uuid(value, name) {
60
- string(value, name);
61
- if (!UUID_PATTERN.test(value)) throw new Error(`${name} must be a lowercase full UUID`);
62
- return value;
63
- }
64
-
65
- function codexUuid(value, name) {
66
- return normalizeCodexThreadId(value, name);
67
- }
68
-
69
- export function normalizeBenchmarkResult(input, { requireDerived = false } = {}) {
70
- const value = structuredClone(object(input, "benchmark result"));
71
- if (!("summary" in value) && !requireDerived) value.summary = undefined;
72
- exact(value, [
73
- "schemaVersion", "benchmark", "taskchefVersion", "runId", "startedAt", "completedAt",
74
- "workload", "task", "turns", "events", "validation", "summary",
75
- ], "benchmark result");
76
- if (value.schemaVersion !== 2) throw new Error("schemaVersion must be 2");
77
- if (value.benchmark !== BENCHMARK_NAME) throw new Error(`benchmark must be ${BENCHMARK_NAME}`);
78
- string(value.taskchefVersion, "taskchefVersion");
79
- string(value.runId, "runId");
80
- const started = timestamp(value.startedAt, "startedAt");
81
- const completed = timestamp(value.completedAt, "completedAt");
82
- if (completed < started) throw new Error("completedAt must not precede startedAt");
83
-
84
- exact(value.workload, ["project", "title", "prompt"], "workload");
85
- for (const key of ["project", "title", "prompt"]) string(value.workload[key], `workload.${key}`);
86
- if (/<!--\s*taskchef_id=/i.test(value.workload.prompt)) throw new Error("workload.prompt must not contain a TaskChef marker");
87
-
88
- exact(value.task, ["taskId", "threadId", "clientThreadId", "recorded", "linked"], "task");
89
- uuid(value.task.taskId, "task.taskId");
90
- value.task.threadId = codexUuid(value.task.threadId, "task.threadId");
91
- if (value.task.clientThreadId !== null) {
92
- string(value.task.clientThreadId, "task.clientThreadId");
93
- if (!value.task.clientThreadId.startsWith("local:")) throw new Error("task.clientThreadId must be provisional");
94
- }
95
- for (const key of ["recorded", "linked"]) {
96
- if (typeof value.task[key] !== "boolean") throw new Error(`task.${key} must be boolean`);
97
- }
98
- if (!value.task.recorded || !value.task.linked) throw new Error("benchmark task must be recorded and self-linked");
99
-
100
- exact(value.turns, ["needsInput", "completion"], "turns");
101
- value.turns.needsInput = codexUuid(value.turns.needsInput, "turns.needsInput");
102
- value.turns.completion = codexUuid(value.turns.completion, "turns.completion");
103
- if (value.turns.completion <= value.turns.needsInput) {
104
- throw new Error("follow-up completion must use a newer turn ID");
105
- }
106
-
107
- const eventNames = ["preparedAt", "recordedAt", "createdAt", "linkedAt", "needsInputAt", "followedUpAt", "completedAt"];
108
- exact(value.events, eventNames, "events");
109
- let previous = started;
110
- for (const name of eventNames) {
111
- const current = timestamp(value.events[name], `events.${name}`);
112
- if (current < previous || current > completed) throw new Error("events must be ordered within the benchmark interval");
113
- previous = current;
114
- }
115
- if (Date.parse(value.events.recordedAt) > Date.parse(value.events.createdAt)) throw new Error("record must precede native creation");
116
-
117
- const validationNames = [
118
- "exactMarkerCorrelated", "noDispatcherPostCreateReads", "childIdentityVerified",
119
- "parentIdentityRejected", "linkRetryVerified", "needsInputVerified",
120
- "followUpTurnFresh", "dashboardDeepLinkVerified", "outputVerified",
121
- ];
122
- exact(value.validation, validationNames, "validation");
123
- for (const name of validationNames) {
124
- if (typeof value.validation[name] !== "boolean") throw new Error(`validation.${name} must be boolean`);
125
- if (!value.validation[name]) throw new Error(`validation.${name} must be true for a successful benchmark`);
126
- }
127
-
128
- const summary = {
129
- totalWallMs: completed - started,
130
- provisionalPath: value.task.clientThreadId !== null,
131
- linked: value.task.linked,
132
- freshFollowUp: value.turns.needsInput !== value.turns.completion,
133
- };
134
- if (requireDerived && value.summary === undefined) throw new Error("summary is required in a saved result");
135
- if (value.summary !== undefined) {
136
- exact(value.summary, Object.keys(summary), "summary");
137
- if (Object.entries(summary).some(([key, expected]) => value.summary[key] !== expected)) {
138
- throw new Error("summary does not match the derived benchmark summary");
139
- }
140
- }
141
- value.summary = summary;
142
- return value;
143
- }
144
-
145
- function timestampFilename(iso) {
146
- return iso.replaceAll(":", "-");
147
- }
148
-
149
- export async function writeBenchmarkResult(input, outputDirectory) {
150
- const packageJson = JSON.parse(await readFile(new URL("../package.json", import.meta.url), "utf8"));
151
- const result = normalizeBenchmarkResult({ ...input, taskchefVersion: packageJson.version });
152
- await mkdir(outputDirectory, { recursive: true });
153
- const outputPath = path.join(outputDirectory, `${timestampFilename(result.startedAt)}-${BENCHMARK_NAME}.json`);
154
- await writeFile(outputPath, `${JSON.stringify(result, null, 2)}\n`, { flag: "wx" });
155
- return { outputPath, result };
156
- }
157
-
158
- export async function cleanBenchmarkResults(outputDirectory) {
159
- const entries = await readdir(outputDirectory, { withFileTypes: true }).catch((error) => {
160
- if (error.code === "ENOENT") return [];
161
- throw error;
162
- });
163
- const removed = [];
164
- for (const entry of entries) {
165
- if (!entry.isFile() || !managedResultFilename(entry.name)) continue;
166
- await unlink(path.join(outputDirectory, entry.name));
167
- removed.push(entry.name);
168
- }
169
- return removed.sort();
170
- }
171
-
172
- async function readJsonStdin() {
173
- if (process.stdin.isTTY) throw new Error("write requires non-interactive JSON on stdin");
174
- let input = "";
175
- process.stdin.setEncoding("utf8");
176
- for await (const chunk of process.stdin) input += chunk;
177
- if (input.trim() === "") throw new Error("expected benchmark JSON on stdin");
178
- return JSON.parse(input);
179
- }
180
-
181
- async function main(args) {
182
- const command = args[0];
183
- const target = path.resolve(args[1] ?? "reports/e2e-benchmarks");
184
- if (command === "write") {
185
- const { outputPath, result } = await writeBenchmarkResult(await readJsonStdin(), target);
186
- process.stdout.write(`${JSON.stringify({ outputPath, summary: result.summary })}\n`);
187
- return;
188
- }
189
- if (command === "validate") {
190
- const result = normalizeBenchmarkResult(JSON.parse(await readFile(target, "utf8")), { requireDerived: true });
191
- process.stdout.write(`${JSON.stringify({ inputPath: target, summary: result.summary })}\n`);
192
- return;
193
- }
194
- if (command === "clean") {
195
- process.stdout.write(`${JSON.stringify({ outputDirectory: target, removed: await cleanBenchmarkResults(target) })}\n`);
196
- return;
197
- }
198
- throw new Error("usage: e2e-benchmark.js <write|validate|clean> [path]");
199
- }
200
-
201
- if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
202
- main(process.argv.slice(2)).catch((error) => {
203
- process.stderr.write(`${error.message}\n`);
204
- process.exitCode = 1;
205
- });
206
- }
@@ -1,293 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Read native Codex agent preferences without applying their instruction layers.
3
-
4
- Requires Python 3.11+. Standalone, standard library only. TaskChef packages an
5
- identical copy; neither installation imports the other at runtime.
6
- """
7
- import argparse
8
- import json
9
- import os
10
- from pathlib import Path
11
- import re
12
- import sys
13
- import tomllib
14
-
15
- ROLES = ('orchestrator', 'planner', 'implementer', 'reviewer')
16
- EFFORTS = ('none', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max', 'ultra')
17
-
18
-
19
- def read_layer(directory):
20
- found = {}
21
- problems = []
22
- try:
23
- with os.scandir(directory) as entries:
24
- files = sorted(Path(entry.path) for entry in entries if entry.name.endswith('.toml'))
25
- for file in files:
26
- try:
27
- if file.stat().st_size > 65536:
28
- raise ValueError('profile exceeds 64 KiB')
29
- with file.open('rb') as stream:
30
- data = tomllib.load(stream)
31
- name = data.get('name')
32
- if name not in ROLES:
33
- if file.stem in ROLES:
34
- raise ValueError('name must match the role filename')
35
- continue
36
- errors = []
37
- for field in ('description', 'developer_instructions'):
38
- if not isinstance(data.get(field), str) or not data[field].strip():
39
- errors.append(f'{field} must be a nonempty string')
40
- model = data.get('model')
41
- effort = data.get('model_reasoning_effort')
42
- entry = {'source': str(file), 'model': model, 'effort': effort, 'problems': errors}
43
- if name in found:
44
- entry['problems'].append('duplicate role name in the same directory')
45
- found[name] = entry
46
- except (OSError, ValueError) as error:
47
- # Do not expose TOML lines, which can contain unrelated private data.
48
- message = f'{file}: unreadable or malformed agent TOML ({type(error).__name__})'
49
- problems.append(message)
50
- if file.stem in ROLES:
51
- found[file.stem] = {'source': str(file), 'model': None, 'effort': None, 'problems': [message]}
52
- except FileNotFoundError:
53
- return found, problems
54
- except OSError:
55
- message = f'{directory}: cannot read agent directory'
56
- problems.append(message)
57
- found = {role: {'source': str(directory), 'model': None, 'effort': None, 'problems': [message]} for role in ROLES}
58
- return found, problems
59
-
60
-
61
- def resolve(codex_home=None, explicit_model=None, explicit_effort=None):
62
- home = Path(codex_home or os.environ.get('CODEX_HOME') or Path.home() / '.codex').expanduser().resolve()
63
- personal, problems = read_layer(home / 'agents')
64
- catalog = None
65
- catalog_source = str(home / 'models_cache.json')
66
- try:
67
- cache = json.loads(Path(catalog_source).read_text())
68
- if not isinstance(cache, dict) or not isinstance(cache.get('models'), list):
69
- raise ValueError('invalid catalog')
70
- for model in cache['models']:
71
- if not isinstance(model, dict) or not isinstance(model.get('slug'), str):
72
- raise ValueError('invalid model entry')
73
- levels = model.get('supported_reasoning_levels')
74
- if not isinstance(levels, list) or any(not isinstance(level, dict) or not isinstance(level.get('effort'), str) for level in levels):
75
- raise ValueError('invalid effort catalog')
76
- catalog = {model['slug']: model for model in cache['models']}
77
- except FileNotFoundError:
78
- pass
79
- except (OSError, ValueError, KeyError, TypeError):
80
- problems.append('Local Codex model catalog is unreadable or malformed; validate availability with the current native tool.')
81
- results = []
82
- for role in ROLES:
83
- entry = personal.get(role)
84
- source = entry['source'] if entry else None
85
- model = entry['model'] if entry else None
86
- effort = entry['effort'] if entry else None
87
- errors = list(entry['problems']) if entry else []
88
- if explicit_model is not None:
89
- model, effort = explicit_model, explicit_effort
90
- effective_source = 'explicit user model choice'
91
- errors = []
92
- else:
93
- effective_source = source
94
- if explicit_effort is not None:
95
- effort = explicit_effort
96
- if model is not None and (not isinstance(model, str) or not model.strip() or len(model) > 256):
97
- errors.append('model must be a nonempty string of at most 256 characters')
98
- if effort is not None and effort not in EFFORTS:
99
- errors.append('reasoning effort is unsupported')
100
- status = 'invalid' if errors else 'configured' if (model is not None or effort is not None) else 'missing'
101
- availability = 'not checked; native tool must validate before dispatch'
102
- if model and not errors and catalog is not None:
103
- if model not in catalog:
104
- errors.append('model absent from local Codex catalog; confirm against the current native tool before dispatch')
105
- status = 'unavailable'
106
- else:
107
- supported = [v.get('effort') for v in catalog[model].get('supported_reasoning_levels', []) if isinstance(v, dict)]
108
- if effort and effort not in supported:
109
- errors.append('effort absent from local model catalog; confirm against the current native tool')
110
- status = 'unavailable'
111
- availability = 'present in cached catalog; current native tool is authoritative'
112
- overrides = {}
113
- if status == 'configured':
114
- if model is not None:
115
- overrides['model'] = model
116
- if effort is not None:
117
- overrides['thinking'] = effort
118
- results.append({'role': role, 'source': source, 'effectiveSource': effective_source,
119
- 'model': model, 'effort': effort, 'status': status,
120
- 'availability': availability, 'problems': errors,
121
- 'taskOverrides': overrides,
122
- 'subagentOverrides': {('reasoning_effort' if k == 'thinking' else k): v for k, v in overrides.items()},
123
- 'fallback': 'native new-task default' if role == 'orchestrator' else 'inherit parent settings'})
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
- ]
129
- return {'roles': results, 'problems': problems, 'catalogSource': catalog_source if catalog is not None else None,
130
- 'modelOptions': model_options,
131
- 'precedence': 'explicit user model (and its explicit effort) > personal role > native defaults; explicit effort alone overrides role effort',
132
- 'scope': 'Model and effort only; agent instructions, tools, permissions, and other TOML keys are not applied by this adapter.'}
133
-
134
-
135
- def setup(role, model, effort, codex_home=None):
136
- home = Path(codex_home or os.environ.get('CODEX_HOME') or Path.home() / '.codex').expanduser().resolve()
137
- existing, problems = read_layer(home / 'agents')
138
- if role in existing or problems:
139
- return {'created': False, 'role': role, 'problems': problems or ['existing role preserved'], 'source': existing.get(role, {}).get('source')}
140
- if not model.strip() or len(model) > 256 or effort not in EFFORTS:
141
- raise ValueError('invalid model or effort')
142
- directory = home / 'agents'
143
- directory.mkdir(parents=True, exist_ok=True)
144
- target = directory / f'{role}.toml'
145
- data = {'name': role, 'description': f'{role.capitalize()} model preferences',
146
- 'developer_instructions': f'Perform the assigned {role} work within the user\'s requested scope.',
147
- 'model': model, 'model_reasoning_effort': effort}
148
- try:
149
- with target.open('x') as stream:
150
- stream.write(''.join(f'{key} = {json.dumps(value)}\n' for key, value in data.items()))
151
- except FileExistsError:
152
- return {'created': False, 'role': role, 'source': str(target), 'problems': ['existing file preserved']}
153
- return {'created': True, 'role': role, 'source': str(target), 'model': model, 'effort': effort}
154
-
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
-
266
- def main():
267
- parser = argparse.ArgumentParser(description=__doc__)
268
- parser.add_argument('--codex-home')
269
- parser.add_argument('--role', choices=ROLES)
270
- parser.add_argument('--model')
271
- parser.add_argument('--effort')
272
- parser.add_argument('--setup', action='store_true')
273
- parser.add_argument('--update', action='store_true')
274
- args = parser.parse_args()
275
- if args.setup and args.update:
276
- parser.error('--setup and --update are mutually exclusive')
277
- if args.setup:
278
- if not (args.role and args.model and args.effort):
279
- parser.error('--setup requires --role, --model, and --effort')
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)
285
- else:
286
- result = resolve(args.codex_home, args.model, args.effort)
287
- if args.role:
288
- result['roles'] = [r for r in result['roles'] if r['role'] == args.role]
289
- print(json.dumps(result, indent=2))
290
-
291
-
292
- if __name__ == '__main__':
293
- main()
@@ -1,96 +0,0 @@
1
- ---
2
- name: taskchef-bootstrap
3
- description: "Initialize or repair the TaskChef dispatcher workspace and manage its local Codex project index. Use for setup, doctor, import, or reindex requests; not dispatch or executor reporting."
4
- ---
5
-
6
- # TaskChef Bootstrap
7
-
8
- Initialize or refresh the per-user TaskChef dispatcher workspace at
9
- `~/.agents/taskchef`.
10
-
11
- Resolve this skill directory with `realpath`. The TaskChef plugin root is two
12
- parents above the skill directory. Invoke `<plugin-root>/bin/taskchef.js` for
13
- all deterministic workspace operations.
14
-
15
- ## Boundaries
16
-
17
- - Keep implementation, tests, and reports in the TaskChef source repository.
18
- - Create and manage only AGENTS.md, taskchef.json, tasks.jsonl,
19
- config-audit.jsonl, and TaskChef's private backups and maintenance artifacts
20
- in a dispatcher workspace. Preserve unrelated user-owned paths.
21
- - Do not dispatch tasks or report on executor threads during bootstrap unless
22
- the user separately requests those actions.
23
- - Never create hooks, schedules, polling, daemons, login items, or system
24
- services. The managed dispatcher instructions own best-effort dashboard
25
- startup and final-link guidance; bootstrap only refreshes that managed block.
26
- TaskChef executors self-link through the installed MCP server.
27
-
28
- ## Initialize and repair
29
-
30
- 1. Run `workspace path --json` and use its returned canonical path for native
31
- project comparisons. The CLI resolves `--workspace`, then
32
- an absolute (or `~/`-prefixed) `TASKCHEF_WORKSPACE`, then
33
- `~/.agents/taskchef`; do not infer a workspace
34
- from the current project.
35
- 2. List native Codex projects once. If an exact canonical-path local project
36
- already exists, run `workspace init --json`. Otherwise run
37
- `workspace init --register-codex --json`, then list native projects once more
38
- and require one exact canonical-path local project. `--register-codex`
39
- invokes the supported `codex app <path>` command through a validated Codex
40
- CLI discovered from the current desktop environment; never invoke
41
- `codex add` or hard-code an application bundle path.
42
- 3. After registration is verified, pass the final native schema-2 snapshot to
43
- `reconcile_projects` once. Preserve its diagnostics; one unavailable or
44
- ineligible project does not invalidate eligible additions.
45
- 4. `workspace init` takes no stdin, creates an empty
46
- configuration when missing, creates the one-entry-per-task JSONL log, and
47
- refreshes managed instructions. The installed plugin provides all four
48
- TaskChef skills outside the dispatcher workspace.
49
- 5. Run `doctor --json` after setup or when the user asks to diagnose the
50
- workspace. Doctor is read-only. Rerun `workspace init --json` to repair the
51
- managed scaffold.
52
- 6. Report the actions or failed checks. A successful initialization with failed
53
- Codex opening remains initialized but not verified as a saved local project.
54
- End without dispatching unless the user
55
- separately requested work.
56
-
57
- ## Project index operations
58
-
59
- For adding, importing, removing, listing, or reindexing projects, read
60
- [project index](references/project-index.md). Preserve exact canonical local
61
- Codex project identity and existing curated entries; opening a folder is not
62
- proof of registration. Completion requires verifying the intended index entry
63
- and the complete project set. Any operation that removes a project entry
64
- requires the user's explicit permission for the exact removed names; removing a
65
- repository URL from an entry does not authorize removing the entry.
66
- Removal records a reconciliation exclusion. Use `include_project` only after an
67
- explicit request to include that exact native project again, then reconcile a
68
- fresh native snapshot. An identity/path move remains diagnostic until the old
69
- exact entry is removed through its preview-bound workflow, its exclusion is
70
- explicitly cleared, and reconciliation validates the new target.
71
-
72
- ## Optional model roles
73
-
74
- Offer planner, implementer, and reviewer profiles during setup. Model and effort
75
- preferences belong in user configuration, never defaults hard-coded in skills.
76
- If the user provides values, configure them without asking again. Otherwise
77
- explain the optional profiles and retain native defaults until values are chosen.
78
-
79
- Run `python3 <plugin-root>/scripts/roles/resolve_roles.py` to preview personal
80
- roles. Python 3.11+ is required for this optional helper. Create each requested
81
- missing profile with:
82
-
83
- ```sh
84
- python3 <plugin-root>/scripts/roles/resolve_roles.py --setup --role <role> --model <requested-model> --effort <requested-effort>
85
- ```
86
-
87
- This authorized role setup may create `$CODEX_HOME/agents/<role>.toml` (default
88
- `~/.codex/agents/`) outside the dispatcher workspace. Existing definitions,
89
- including custom filenames, are preserved. Report conflicts and diagnostics;
90
- never overwrite a custom role. Re-run the preview and check model/effort against
91
- the available native interface. Missing roles are optional: TaskChef omits
92
- new-task overrides; reviewers inherit parent settings. Native new-task defaults
93
- are not guaranteed dispatcher inheritance. Read the delegate's
94
- [role resolution](../taskchef-delegate/references/model-roles.md) for precedence
95
- and adapter limits. Open the dashboard's `/#settings` preview to show the
96
- configured sources, requested effective settings, fallback, and problems.
@@ -1,4 +0,0 @@
1
- interface:
2
- display_name: "TaskChef Bootstrap"
3
- short_description: "Bootstrap the per-user TaskChef workspace"
4
- default_prompt: "Use $taskchef-bootstrap to initialize or refresh my per-user TaskChef dispatcher workspace."