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.
- package/.codex-plugin/plugin.json +7 -10
- package/.mcp.json +3 -2
- package/README.md +31 -158
- package/docs/mcp-app.md +21 -4
- package/docs/spec.md +38 -746
- package/docs/taskchef-next-storage.md +2 -2
- package/index.js +2 -99
- package/package.json +12 -49
- package/skills/delegate/SKILL.md +44 -0
- package/skills/delegate/agents/openai.yaml +4 -0
- package/src/codex-app.js +3 -152
- package/src/codex-session-scanner.js +2 -2
- package/src/file-lock.js +25 -0
- package/src/mcp-app/react/NextTaskDetail.test.tsx +1 -1
- package/src/mcp-app/react/NextTaskDetail.tsx +8 -8
- package/src/mcp-app/react/main.test.tsx +3 -3
- package/src/mcp-app/react/main.tsx +7 -7
- package/src/mcp-app.js +4 -4
- package/src/mcp.js +4 -542
- package/src/next-cache.js +2 -2
- package/src/next-github.js +3 -3
- package/src/next-notifications.js +2 -2
- package/src/{dashboard/react → ui}/components/CardStatusIcons.tsx +1 -1
- package/src/{dashboard/react → ui}/components/GitHubLinks.tsx +1 -1
- package/src/{dashboard/react → ui}/components/LinkedText.tsx +1 -1
- package/src/{dashboard/react → ui}/components/RelativeTime.test.tsx +1 -1
- package/src/{dashboard/react → ui}/components/RelativeTime.tsx +1 -1
- package/src/{dashboard/react → ui}/components/ReplyImagePreview.test.tsx +1 -1
- package/src/{dashboard/react → ui}/components/TaskBoard.tsx +1 -1
- package/src/{dashboard/react → ui}/components/TaskCard.tsx +1 -1
- package/src/{dashboard/react → ui}/components/TurnContent.tsx +1 -1
- package/src/{dashboard/react → ui}/presentation.ts +1 -1
- package/src/version.js +2 -1
- package/BACKLOG.md +0 -48
- package/assets/e2e-benchmark-example.json +0 -50
- package/assets/taskchef-dispatcher-instructions.md +0 -44
- package/bin/taskchef.js +0 -20
- package/docs/advanced-guide.md +0 -497
- package/docs/dashboard-lifecycle.md +0 -149
- package/docs/dashboard-ui.md +0 -40
- package/docs/firstmate-taskchef-comparison.md +0 -283
- package/docs/images/ccusage-token-consumption.png +0 -0
- package/docs/images/dashboard-identity.jpg +0 -0
- package/docs/images/dashboard-token-pending.jpg +0 -0
- package/docs/images/interrupted-turn-recovery.jpg +0 -0
- package/docs/images/notification-event-snapshots.jpg +0 -0
- package/docs/images/readme-board.png +0 -0
- package/docs/images/readme-completed.png +0 -0
- package/docs/images/readme-list.png +0 -0
- package/docs/images/readme-needs-input.png +0 -0
- package/docs/images/readme-roles.png +0 -0
- package/docs/images/reported-work-duration-desktop.jpg +0 -0
- package/docs/images/reported-work-duration-mobile.jpg +0 -0
- package/docs/images/result-history-dashboard.jpg +0 -0
- package/docs/images/taskchef-pr-icon-comparison.jpg +0 -0
- package/docs/images/taskchef-unified-status-popup.jpg +0 -0
- package/docs/workflows.md +0 -476
- package/mcp/dashboard-session.js +0 -51
- package/scripts/benchmark-dispatch-prepare.js +0 -90
- package/scripts/e2e-benchmark.js +0 -206
- package/scripts/roles/resolve_roles.py +0 -293
- package/skills/taskchef-bootstrap/SKILL.md +0 -96
- package/skills/taskchef-bootstrap/agents/openai.yaml +0 -4
- package/skills/taskchef-bootstrap/references/project-index.md +0 -154
- package/skills/taskchef-copilot/SKILL.md +0 -75
- package/skills/taskchef-copilot/agents/openai.yaml +0 -4
- package/skills/taskchef-dashboard/SKILL.md +0 -24
- package/skills/taskchef-dashboard/agents/openai.yaml +0 -4
- package/skills/taskchef-delegate/SKILL.md +0 -102
- package/skills/taskchef-delegate/agents/openai.yaml +0 -4
- package/skills/taskchef-delegate/references/model-roles.md +0 -29
- package/skills/taskchef-executor/SKILL.md +0 -154
- package/skills/taskchef-executor/agents/openai.yaml +0 -4
- package/skills/taskchef-executor/references/compatibility.md +0 -22
- package/skills/taskchef-executor/references/ending-actions.md +0 -26
- package/src/backups.js +0 -359
- package/src/cli.js +0 -733
- package/src/config-audit.js +0 -99
- package/src/config-mutations.js +0 -106
- package/src/dashboard/actions.js +0 -136
- package/src/dashboard/app.js +0 -1172
- package/src/dashboard/dist/app.js +0 -269
- package/src/dashboard/dist/app.js.map +0 -1
- package/src/dashboard/dist/index.html +0 -16
- package/src/dashboard/dist/styles.css +0 -1
- package/src/dashboard/index.html +0 -148
- package/src/dashboard/react/App.test.tsx +0 -346
- package/src/dashboard/react/App.tsx +0 -515
- package/src/dashboard/react/api.test.ts +0 -40
- package/src/dashboard/react/api.ts +0 -103
- package/src/dashboard/react/components/ActivityTimeline.tsx +0 -118
- package/src/dashboard/react/components/ModelSettings.test.tsx +0 -180
- package/src/dashboard/react/components/ModelSettings.tsx +0 -106
- package/src/dashboard/react/components/NotificationCenter.test.tsx +0 -76
- package/src/dashboard/react/components/NotificationCenter.tsx +0 -111
- package/src/dashboard/react/components/PullRequestBadges.tsx +0 -18
- package/src/dashboard/react/components/TaskDetail.test.tsx +0 -255
- package/src/dashboard/react/components/TaskDetail.tsx +0 -271
- package/src/dashboard/react/components/UsagePanel.tsx +0 -96
- package/src/dashboard/react/index.html +0 -15
- package/src/dashboard/react/main.tsx +0 -12
- package/src/dashboard/react/overlay-state.test.ts +0 -110
- package/src/dashboard/react/overlay-state.ts +0 -60
- package/src/dashboard/react/presentation.test.tsx +0 -571
- package/src/dashboard/styles.css +0 -261
- package/src/dashboard-control.js +0 -34
- package/src/dashboard-manager.js +0 -339
- package/src/dashboard-session-process.js +0 -88
- package/src/dashboard-session.js +0 -86
- package/src/dashboard.js +0 -1303
- package/src/delegation.js +0 -277
- package/src/github.js +0 -107
- package/src/model-roles.js +0 -70
- package/src/recorded-usage-tracker.js +0 -92
- package/src/usage-tracker.js +0 -650
- package/src/usage.js +0 -367
- package/src/workspace-path.js +0 -48
- package/src/workspace.js +0 -3629
- /package/src/{dashboard/react → ui}/components/AnimatedUsage.test.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/CardStatusIcons.test.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/ImageGallery.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/OpenChatButton.test.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/OpenChatButton.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/PrGlyph.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/QueuedPrompts.test.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/QueuedPrompts.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/ReplyCover.test.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/ReplyCover.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/ReplyImagePreview.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/ReplyMarkdown.test.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/ReplyMarkdown.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/SavedUsageFormat.test.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/SavedUsageFormat.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/ShimmerText.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/StatsGrid.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/StatusBadge.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/TaskBoard.test.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/TaskCardStats.test.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/TaskCardStats.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/TurnContent.test.tsx +0 -0
- /package/src/{dashboard/react → ui}/fixtures.ts +0 -0
- /package/src/{dashboard → ui}/github-links.js +0 -0
- /package/src/{dashboard/react → ui}/image-cache.test.ts +0 -0
- /package/src/{dashboard/react → ui}/image-cache.ts +0 -0
- /package/src/{dashboard → ui}/state.js +0 -0
- /package/src/{dashboard/react → ui}/styles.css +0 -0
- /package/src/{dashboard/react → ui}/test-setup.ts +0 -0
- /package/src/{dashboard → ui}/time.js +0 -0
- /package/src/{dashboard/react → ui}/types.ts +0 -0
package/scripts/e2e-benchmark.js
DELETED
|
@@ -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.
|