taskchef 8.7.1 → 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 +3 -3
- 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 +2 -2
- 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 +0 -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/src/mcp.js
CHANGED
|
@@ -1,24 +1,6 @@
|
|
|
1
|
-
import path from "node:path";
|
|
2
1
|
import { readFileSync } from "node:fs";
|
|
3
2
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
prepareDispatch,
|
|
7
|
-
linkTask,
|
|
8
|
-
recordTask,
|
|
9
|
-
reportTaskState,
|
|
10
|
-
reportTaskResult,
|
|
11
|
-
dashboardAutostartEnabled,
|
|
12
|
-
includeProject,
|
|
13
|
-
readConfig,
|
|
14
|
-
reconcileProjects,
|
|
15
|
-
updateProjectHint,
|
|
16
|
-
} from "./workspace.js";
|
|
17
|
-
import { parseTaskChefMarker } from "./delegation.js";
|
|
18
|
-
import { createDashboardManager } from "./dashboard-manager.js";
|
|
19
|
-
import { resolveWorkspacePath } from "./workspace-path.js";
|
|
20
|
-
import { DASHBOARD_SERVER_VERSION, TASKCHEF_VERSION } from "./version.js";
|
|
21
|
-
import { createUsageTracker } from "./usage-tracker.js";
|
|
3
|
+
import { TASKCHEF_APP_NAME, TASKCHEF_VERSION } from "./version.js";
|
|
22
4
|
import { registerTaskChefApp } from "./mcp-app.js";
|
|
23
5
|
|
|
24
6
|
const sidebarSvg = readFileSync(new URL("../assets/taskchef-sidebar.svg", import.meta.url), "utf8");
|
|
@@ -27,531 +9,11 @@ const sidebarIcons = [["light", "#333333"], ["dark", "#dddddd"]].map(([theme, co
|
|
|
27
9
|
mimeType: "image/svg+xml", sizes: ["20x20"], theme,
|
|
28
10
|
}));
|
|
29
11
|
|
|
30
|
-
|
|
31
|
-
name: z.string(),
|
|
32
|
-
path: z.string(),
|
|
33
|
-
isGitRepository: z.boolean(),
|
|
34
|
-
githubRepos: z.array(z.string()),
|
|
35
|
-
description: z.string().optional(),
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
const turnProvenanceSchema = z.union([
|
|
39
|
-
z.object({ kind: z.enum(["legacy", "mcp"]) }),
|
|
40
|
-
z.object({
|
|
41
|
-
kind: z.literal("dashboard_manual"),
|
|
42
|
-
actionId: z.string(),
|
|
43
|
-
fromStatus: z.enum(["working", "needs_input", "completed", "failed"]),
|
|
44
|
-
toStatus: z.enum(["completed", "failed"]),
|
|
45
|
-
expectedTurnRef: z.string().nullable(),
|
|
46
|
-
expectedThreadId: z.string().nullable(),
|
|
47
|
-
expectedUpdatedAt: z.string(),
|
|
48
|
-
}),
|
|
49
|
-
]);
|
|
50
|
-
|
|
51
|
-
const taskSchema = z.object({
|
|
52
|
-
schemaVersion: z.union([
|
|
53
|
-
z.literal(4), z.literal(5), z.literal(6), z.literal(7), z.literal(8), z.literal(9),
|
|
54
|
-
z.literal(10),
|
|
55
|
-
]),
|
|
56
|
-
id: z.string(),
|
|
57
|
-
project: projectSchema,
|
|
58
|
-
title: z.string(),
|
|
59
|
-
instruction: z.string(),
|
|
60
|
-
threadId: z.string().nullable(),
|
|
61
|
-
createdAt: z.string(),
|
|
62
|
-
status: z.enum(["working", "needs_input", "completed", "failed"]),
|
|
63
|
-
summary: z.string().nullable(),
|
|
64
|
-
turnRef: z.string().nullable(),
|
|
65
|
-
turnId: z.string().nullable(),
|
|
66
|
-
updatedAt: z.string(),
|
|
67
|
-
updatedBy: z.enum(["dispatcher", "mcp", "dashboard"]),
|
|
68
|
-
turns: z.array(z.object({
|
|
69
|
-
turnRef: z.string().nullable(),
|
|
70
|
-
turnId: z.string().nullable(),
|
|
71
|
-
requestSummary: z.string().nullable(),
|
|
72
|
-
startedAt: z.string(),
|
|
73
|
-
result: z.object({
|
|
74
|
-
status: z.enum(["needs_input", "completed", "failed", "interrupted"]),
|
|
75
|
-
summary: z.string(),
|
|
76
|
-
updatedAt: z.string(),
|
|
77
|
-
}).nullable(),
|
|
78
|
-
provenance: turnProvenanceSchema.nullable(),
|
|
79
|
-
})),
|
|
80
|
-
latestTurn: z.object({
|
|
81
|
-
turnRef: z.string().nullable(),
|
|
82
|
-
turnId: z.string().nullable(),
|
|
83
|
-
requestSummary: z.string().nullable(),
|
|
84
|
-
startedAt: z.string(),
|
|
85
|
-
result: z.object({
|
|
86
|
-
status: z.enum(["needs_input", "completed", "failed", "interrupted"]),
|
|
87
|
-
summary: z.string(),
|
|
88
|
-
updatedAt: z.string(),
|
|
89
|
-
}).nullable(),
|
|
90
|
-
provenance: turnProvenanceSchema.nullable(),
|
|
91
|
-
}).nullable(),
|
|
92
|
-
results: z.array(z.object({
|
|
93
|
-
status: z.enum(["needs_input", "completed", "failed"]),
|
|
94
|
-
summary: z.string(),
|
|
95
|
-
turnRef: z.string().nullable(),
|
|
96
|
-
turnId: z.string().nullable(),
|
|
97
|
-
updatedAt: z.string(),
|
|
98
|
-
provenance: turnProvenanceSchema.optional(),
|
|
99
|
-
})),
|
|
100
|
-
lastResult: z.object({
|
|
101
|
-
status: z.enum(["needs_input", "completed", "failed"]),
|
|
102
|
-
summary: z.string(),
|
|
103
|
-
turnRef: z.string().nullable(),
|
|
104
|
-
turnId: z.string().nullable(),
|
|
105
|
-
updatedAt: z.string(),
|
|
106
|
-
provenance: turnProvenanceSchema.optional(),
|
|
107
|
-
}).nullable(),
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
const preparationSchema = z.object({
|
|
111
|
-
schemaVersion: z.literal(1),
|
|
112
|
-
workspace: z.string(),
|
|
113
|
-
taskId: z.string(),
|
|
114
|
-
preparedAt: z.string(),
|
|
115
|
-
marker: z.string(),
|
|
116
|
-
modelRoles: z.record(z.string(), z.unknown()).optional(),
|
|
117
|
-
projectCount: z.number(),
|
|
118
|
-
projects: z.array(projectSchema),
|
|
119
|
-
routingHints: z.array(z.object({
|
|
120
|
-
path: z.string(),
|
|
121
|
-
aliases: z.array(z.string()),
|
|
122
|
-
githubRepos: z.array(z.string()),
|
|
123
|
-
responsibilities: z.array(z.string()),
|
|
124
|
-
})).optional(),
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
const nativeProjectSnapshotSchema = z.object({
|
|
128
|
-
schemaVersion: z.literal(2),
|
|
129
|
-
projects: z.array(z.object({
|
|
130
|
-
projectId: z.string().min(1),
|
|
131
|
-
projectKind: z.string().min(1),
|
|
132
|
-
label: z.string().min(1),
|
|
133
|
-
path: z.string().nullable().optional(),
|
|
134
|
-
hostId: z.string().nullable().optional(),
|
|
135
|
-
hostDisplayName: z.string().nullable().optional(),
|
|
136
|
-
isGitRepository: z.boolean().nullable().optional(),
|
|
137
|
-
}).passthrough()),
|
|
138
|
-
}).passthrough();
|
|
139
|
-
|
|
140
|
-
const routingProvenanceSchema = z.object({
|
|
141
|
-
kind: z.enum(["explicit_user", "verified_repository", "accepted_terminal_report"]),
|
|
142
|
-
evidence: z.string().min(1).max(240),
|
|
143
|
-
taskId: z.string().min(1).nullable(),
|
|
144
|
-
threadId: z.string().min(1).nullable(),
|
|
145
|
-
turnRef: z.string().min(1).max(256).nullable(),
|
|
146
|
-
repositoryPath: z.string().min(1).nullable(),
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
const reconciliationSchema = z.object({
|
|
150
|
-
schemaVersion: z.literal(1),
|
|
151
|
-
changed: z.boolean(),
|
|
152
|
-
projectCount: z.number().int().nonnegative(),
|
|
153
|
-
addedCount: z.number().int().nonnegative(),
|
|
154
|
-
boundCount: z.number().int().nonnegative(),
|
|
155
|
-
available: z.array(z.object({
|
|
156
|
-
hostId: z.string(),
|
|
157
|
-
projectId: z.string(),
|
|
158
|
-
path: z.string(),
|
|
159
|
-
})),
|
|
160
|
-
diagnostics: z.array(z.object({
|
|
161
|
-
projectId: z.string(),
|
|
162
|
-
code: z.string(),
|
|
163
|
-
message: z.string(),
|
|
164
|
-
})),
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
const hintUpdateSchema = z.object({
|
|
168
|
-
changed: z.boolean(),
|
|
169
|
-
suppressed: z.boolean(),
|
|
170
|
-
project: z.string(),
|
|
171
|
-
kind: z.enum(["alias", "githubRepo", "responsibility"]),
|
|
172
|
-
value: z.string(),
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
const dashboardSchema = z.object({
|
|
176
|
-
action: z.enum(["started", "reused"]),
|
|
177
|
-
launcher: z.enum(["session", "standalone", "mcp"]),
|
|
178
|
-
url: z.string().url(),
|
|
179
|
-
workspace: z.string(),
|
|
180
|
-
taskchefVersion: z.string(),
|
|
181
|
-
serverVersion: z.string(),
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
function toolResult(key, value, message) {
|
|
185
|
-
return {
|
|
186
|
-
structuredContent: { [key]: value },
|
|
187
|
-
content: [{ type: "text", text: message }],
|
|
188
|
-
};
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
function dashboardAutostartDiagnostic(error) {
|
|
192
|
-
const message = typeof error?.message === "string" ? error.message : "";
|
|
193
|
-
if (typeof error?.staleTaskchefVersion === "string") {
|
|
194
|
-
return `TaskChef dashboard autostart skipped: recognized older TaskChef ${error.staleTaskchefVersion} listener refused graceful shutdown; it was left running.`;
|
|
195
|
-
}
|
|
196
|
-
if (error?.code === "EADDRINUSE" || /port conflict|already in use/i.test(message)) {
|
|
197
|
-
return "TaskChef dashboard autostart skipped: port 127.0.0.1:3210 is unavailable; the listener was left untouched.";
|
|
198
|
-
}
|
|
199
|
-
if (/configuration|workspace|task log/i.test(message)) {
|
|
200
|
-
return "TaskChef dashboard autostart skipped: the canonical workspace is not ready.";
|
|
201
|
-
}
|
|
202
|
-
return "TaskChef dashboard autostart skipped: the dashboard could not be started.";
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
export function createDashboardAutostart({
|
|
206
|
-
workspace,
|
|
207
|
-
dashboardManager,
|
|
208
|
-
readConfiguration = readConfig,
|
|
209
|
-
log = (message) => process.stderr.write(`${message}\n`),
|
|
210
|
-
} = {}) {
|
|
211
|
-
let startPromise = null;
|
|
212
|
-
return async () => {
|
|
213
|
-
startPromise ??= (async () => {
|
|
214
|
-
try {
|
|
215
|
-
const config = await readConfiguration(workspace, { checkPaths: false });
|
|
216
|
-
if (!dashboardAutostartEnabled(config)) return { action: "disabled" };
|
|
217
|
-
return await dashboardManager.ensure();
|
|
218
|
-
} catch (error) {
|
|
219
|
-
try { log(dashboardAutostartDiagnostic(error)); } catch {}
|
|
220
|
-
return { action: "failed" };
|
|
221
|
-
}
|
|
222
|
-
})();
|
|
223
|
-
return startPromise;
|
|
224
|
-
};
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
export function createTaskChefMcpServer({
|
|
228
|
-
workspace = resolveWorkspacePath().workspace,
|
|
229
|
-
prepare = prepareDispatch,
|
|
230
|
-
record = recordTask,
|
|
231
|
-
reportResult = reportTaskResult,
|
|
232
|
-
reportState = reportTaskState,
|
|
233
|
-
link = linkTask,
|
|
234
|
-
reconcile = reconcileProjects,
|
|
235
|
-
updateHint = updateProjectHint,
|
|
236
|
-
include = includeProject,
|
|
237
|
-
dashboardManager = createDashboardManager({ workspace }),
|
|
238
|
-
readConfiguration = readConfig,
|
|
239
|
-
logDashboardDiagnostic,
|
|
240
|
-
usageTracker = createUsageTracker({ workspace }),
|
|
241
|
-
} = {}) {
|
|
242
|
-
const reportingDestination = `Store lifecycle state and summaries in the configured local TaskChef dashboard log ${JSON.stringify(path.resolve(workspace, "tasks.jsonl"))}. GitHub URLs are stored as references, not published to GitHub. Also triggers local Codex usage tracking in ${JSON.stringify(path.resolve(workspace, ".taskchef-usage.json"))}. Exclude secrets. `;
|
|
12
|
+
export function createTaskChefMcpServer(options = {}) {
|
|
243
13
|
const server = new McpServer(
|
|
244
14
|
{ name: "taskchef", version: TASKCHEF_VERSION, icons: sidebarIcons },
|
|
245
|
-
{
|
|
246
|
-
instructions:
|
|
247
|
-
"Prepare with prepare_dispatch, call record_task before creating the Codex task, then create it natively and return immediately. Follow the active TaskChef skill for role-specific sequencing of the identity and state tools.",
|
|
248
|
-
},
|
|
15
|
+
{ instructions: `${TASKCHEF_APP_NAME} displays local Codex chats. Codex owns chat identity, history and execution state. Use native Codex tools to create or continue chats.` },
|
|
249
16
|
);
|
|
250
|
-
|
|
251
|
-
const originalClose = server.close.bind(server);
|
|
252
|
-
let closePromise = null;
|
|
253
|
-
let closing = false;
|
|
254
|
-
server.close = async () => {
|
|
255
|
-
closing = true;
|
|
256
|
-
closePromise ??= (async () => {
|
|
257
|
-
const results = await Promise.allSettled([
|
|
258
|
-
dashboardManager.close(),
|
|
259
|
-
originalClose(),
|
|
260
|
-
]);
|
|
261
|
-
const failure = results.find((result) => result.status === "rejected");
|
|
262
|
-
if (failure) throw failure.reason;
|
|
263
|
-
})();
|
|
264
|
-
return closePromise;
|
|
265
|
-
};
|
|
266
|
-
server.server.onclose = () => {
|
|
267
|
-
void dashboardManager.close().catch(() => {});
|
|
268
|
-
};
|
|
269
|
-
|
|
270
|
-
server.registerTool(
|
|
271
|
-
"ensure_dashboard",
|
|
272
|
-
{
|
|
273
|
-
title: "Ensure TaskChef dashboard",
|
|
274
|
-
description:
|
|
275
|
-
"Best-effort ensure the canonical TaskChef dashboard is available on 127.0.0.1:3210. Starts or reuses the exact recognized TaskChef dashboard for this Codex session and canonical workspace; recognized older same-workspace listeners are gracefully restarted to the installed version, while newer and unknown listeners remain running.",
|
|
276
|
-
inputSchema: {},
|
|
277
|
-
outputSchema: { dashboard: dashboardSchema },
|
|
278
|
-
annotations: {
|
|
279
|
-
readOnlyHint: false,
|
|
280
|
-
destructiveHint: false,
|
|
281
|
-
openWorldHint: false,
|
|
282
|
-
},
|
|
283
|
-
},
|
|
284
|
-
async () => {
|
|
285
|
-
const dashboard = await dashboardManager.ensure();
|
|
286
|
-
return toolResult(
|
|
287
|
-
"dashboard",
|
|
288
|
-
dashboard,
|
|
289
|
-
`${dashboard.action === "started" ? "Started" : "Reused"} TaskChef dashboard ${dashboard.url}`,
|
|
290
|
-
);
|
|
291
|
-
},
|
|
292
|
-
);
|
|
293
|
-
|
|
294
|
-
server.registerTool(
|
|
295
|
-
"reconcile_projects",
|
|
296
|
-
{
|
|
297
|
-
title: "Reconcile TaskChef projects",
|
|
298
|
-
description:
|
|
299
|
-
"Atomically reconcile an agent-supplied native Codex list_projects schema-2 snapshot into the local TaskChef project index. Only eligible local projects on hostId local are considered; existing curated metadata and missing entries are preserved.",
|
|
300
|
-
inputSchema: { snapshot: nativeProjectSnapshotSchema },
|
|
301
|
-
outputSchema: { reconciliation: reconciliationSchema },
|
|
302
|
-
annotations: {
|
|
303
|
-
readOnlyHint: false,
|
|
304
|
-
destructiveHint: false,
|
|
305
|
-
openWorldHint: false,
|
|
306
|
-
},
|
|
307
|
-
},
|
|
308
|
-
async ({ snapshot }) => {
|
|
309
|
-
const result = await reconcile(workspace, snapshot);
|
|
310
|
-
const reconciliation = {
|
|
311
|
-
schemaVersion: 1,
|
|
312
|
-
changed: result.changed,
|
|
313
|
-
projectCount: result.projectCount,
|
|
314
|
-
addedCount: result.added.length,
|
|
315
|
-
boundCount: result.bound.length,
|
|
316
|
-
available: result.available,
|
|
317
|
-
diagnostics: result.diagnostics,
|
|
318
|
-
};
|
|
319
|
-
return toolResult(
|
|
320
|
-
"reconciliation",
|
|
321
|
-
reconciliation,
|
|
322
|
-
`Reconciled ${reconciliation.projectCount} TaskChef project(s).`,
|
|
323
|
-
);
|
|
324
|
-
},
|
|
325
|
-
);
|
|
326
|
-
|
|
327
|
-
server.registerTool(
|
|
328
|
-
"include_project",
|
|
329
|
-
{
|
|
330
|
-
title: "Include a previously excluded TaskChef project",
|
|
331
|
-
description:
|
|
332
|
-
"Remove an explicit reconciliation exclusion by exact local native projectId or canonical path. Reconciliation must run afterward before the project becomes a routing target again.",
|
|
333
|
-
inputSchema: {
|
|
334
|
-
projectId: z.string().min(1).optional(),
|
|
335
|
-
path: z.string().min(1).optional(),
|
|
336
|
-
},
|
|
337
|
-
outputSchema: {
|
|
338
|
-
result: z.object({ changed: z.boolean(), remainingExclusionCount: z.number().int().nonnegative() }),
|
|
339
|
-
},
|
|
340
|
-
annotations: {
|
|
341
|
-
readOnlyHint: false,
|
|
342
|
-
destructiveHint: false,
|
|
343
|
-
openWorldHint: false,
|
|
344
|
-
},
|
|
345
|
-
},
|
|
346
|
-
async ({ projectId, path: projectPath }) => {
|
|
347
|
-
if (Boolean(projectId) === Boolean(projectPath)) {
|
|
348
|
-
throw new Error("include_project requires exactly one projectId or path");
|
|
349
|
-
}
|
|
350
|
-
const updated = await include(workspace, projectId
|
|
351
|
-
? { projectId, hostId: "local" }
|
|
352
|
-
: { path: projectPath });
|
|
353
|
-
const result = {
|
|
354
|
-
changed: updated.changed,
|
|
355
|
-
remainingExclusionCount: updated.exclusions.length,
|
|
356
|
-
};
|
|
357
|
-
return toolResult("result", result, result.changed
|
|
358
|
-
? "Removed the TaskChef project exclusion."
|
|
359
|
-
: "No matching TaskChef project exclusion exists.");
|
|
360
|
-
},
|
|
361
|
-
);
|
|
362
|
-
|
|
363
|
-
server.registerTool(
|
|
364
|
-
"prepare_dispatch",
|
|
365
|
-
{
|
|
366
|
-
title: "Prepare TaskChef dispatch",
|
|
367
|
-
description:
|
|
368
|
-
"Generate the task UUID, exact correlation marker, timestamp, and configured routing targets from the canonical TaskChef workspace.",
|
|
369
|
-
inputSchema: {},
|
|
370
|
-
outputSchema: { preparation: preparationSchema },
|
|
371
|
-
annotations: {
|
|
372
|
-
readOnlyHint: true,
|
|
373
|
-
destructiveHint: false,
|
|
374
|
-
openWorldHint: false,
|
|
375
|
-
},
|
|
376
|
-
},
|
|
377
|
-
async () => {
|
|
378
|
-
const preparation = await prepare(workspace);
|
|
379
|
-
return toolResult("preparation", preparation, `Prepared TaskChef task ${preparation.taskId}.`);
|
|
380
|
-
},
|
|
381
|
-
);
|
|
382
|
-
|
|
383
|
-
server.registerTool(
|
|
384
|
-
"update_project_hint",
|
|
385
|
-
{
|
|
386
|
-
title: "Remember or forget TaskChef project routing",
|
|
387
|
-
description:
|
|
388
|
-
"Remember or forget one bounded routing fact for an exact configured project. Aliases and forgetting require explicit user evidence; repository ownership requires verified origin evidence; report-derived responsibilities require the accepted current terminal task/turn identity.",
|
|
389
|
-
inputSchema: {
|
|
390
|
-
action: z.enum(["remember", "correct", "forget"]),
|
|
391
|
-
project: z.string().min(1),
|
|
392
|
-
kind: z.enum(["alias", "githubRepo", "responsibility"]),
|
|
393
|
-
value: z.string().min(1).max(256),
|
|
394
|
-
provenance: routingProvenanceSchema,
|
|
395
|
-
},
|
|
396
|
-
outputSchema: { result: hintUpdateSchema },
|
|
397
|
-
annotations: {
|
|
398
|
-
readOnlyHint: false,
|
|
399
|
-
destructiveHint: true,
|
|
400
|
-
openWorldHint: false,
|
|
401
|
-
},
|
|
402
|
-
},
|
|
403
|
-
async (input) => {
|
|
404
|
-
const updated = await updateHint(workspace, input);
|
|
405
|
-
const result = {
|
|
406
|
-
changed: updated.changed,
|
|
407
|
-
suppressed: updated.suppressed,
|
|
408
|
-
project: updated.project,
|
|
409
|
-
kind: updated.kind,
|
|
410
|
-
value: updated.value,
|
|
411
|
-
};
|
|
412
|
-
return toolResult(
|
|
413
|
-
"result",
|
|
414
|
-
result,
|
|
415
|
-
`${result.changed ? "Updated" : "Kept"} TaskChef project routing.`,
|
|
416
|
-
);
|
|
417
|
-
},
|
|
418
|
-
);
|
|
419
|
-
|
|
420
|
-
server.registerTool(
|
|
421
|
-
"record_task",
|
|
422
|
-
{
|
|
423
|
-
title: "Record TaskChef task",
|
|
424
|
-
description:
|
|
425
|
-
"Atomically append one prepared TaskChef task before creating its Codex executor. Pass the exact marked instruction and null threadId; only the executor may self-link it.",
|
|
426
|
-
inputSchema: {
|
|
427
|
-
id: z.string().min(1),
|
|
428
|
-
project: z.string().min(1).describe("Selected configured project's canonical absolute path, not its name or native project ID."),
|
|
429
|
-
title: z.string().min(1),
|
|
430
|
-
instruction: z.string().min(1),
|
|
431
|
-
threadId: z.null(),
|
|
432
|
-
},
|
|
433
|
-
outputSchema: { task: taskSchema },
|
|
434
|
-
annotations: {
|
|
435
|
-
readOnlyHint: false,
|
|
436
|
-
destructiveHint: false,
|
|
437
|
-
openWorldHint: false,
|
|
438
|
-
},
|
|
439
|
-
},
|
|
440
|
-
async (input) => {
|
|
441
|
-
if (parseTaskChefMarker(input.instruction) !== input.id) {
|
|
442
|
-
throw new Error("record_task instruction must contain its exact TaskChef marker in an accepted scaffold");
|
|
443
|
-
}
|
|
444
|
-
const task = await record(workspace, input);
|
|
445
|
-
return toolResult("task", task, `Recorded TaskChef task ${task.id}.`);
|
|
446
|
-
},
|
|
447
|
-
);
|
|
448
|
-
|
|
449
|
-
server.registerTool(
|
|
450
|
-
"link_task",
|
|
451
|
-
{
|
|
452
|
-
title: "Link TaskChef executor",
|
|
453
|
-
description:
|
|
454
|
-
"Register this executor's asserted durable Codex thread ID. The atomic one-way transition is idempotent and rejects conflicts or thread reuse.",
|
|
455
|
-
inputSchema: {
|
|
456
|
-
taskId: z.string().min(1),
|
|
457
|
-
threadId: z.string().min(1),
|
|
458
|
-
},
|
|
459
|
-
outputSchema: { task: taskSchema },
|
|
460
|
-
annotations: {
|
|
461
|
-
readOnlyHint: false,
|
|
462
|
-
destructiveHint: false,
|
|
463
|
-
openWorldHint: false,
|
|
464
|
-
},
|
|
465
|
-
},
|
|
466
|
-
async ({ taskId, threadId }) => {
|
|
467
|
-
const task = await link(workspace, taskId, threadId);
|
|
468
|
-
return toolResult("task", task, `Linked TaskChef task ${task.id}.`);
|
|
469
|
-
},
|
|
470
|
-
);
|
|
471
|
-
|
|
472
|
-
server.registerTool(
|
|
473
|
-
"report_state",
|
|
474
|
-
{
|
|
475
|
-
title: "Report TaskChef state",
|
|
476
|
-
description:
|
|
477
|
-
reportingDestination + "Report this self-linked executor turn's lifecycle state. Use one stable turnRef for working and its terminal report; pass the native Codex turn ID as both turnRef and turnId when available, otherwise pass a client-generated UUID turnRef and null turnId. Preserve known repository context and delivered links. Exact retries are idempotent; stale or mismatched turnRefs are rejected.",
|
|
478
|
-
inputSchema: {
|
|
479
|
-
taskId: z.string().min(1),
|
|
480
|
-
threadId: z.string().min(1).nullable(),
|
|
481
|
-
turnRef: z.string().min(1).max(256).nullable().optional(),
|
|
482
|
-
turnId: z.string().min(1).max(256).nullable(),
|
|
483
|
-
status: z.enum(["working", "needs_input", "completed", "failed"]),
|
|
484
|
-
summary: z.string().min(1).max(2_000).nullable().optional(),
|
|
485
|
-
requestSummary: z.string().min(1).max(1_000).nullable().optional(),
|
|
486
|
-
},
|
|
487
|
-
outputSchema: { task: taskSchema },
|
|
488
|
-
annotations: {
|
|
489
|
-
readOnlyHint: false,
|
|
490
|
-
destructiveHint: true,
|
|
491
|
-
openWorldHint: false,
|
|
492
|
-
},
|
|
493
|
-
},
|
|
494
|
-
async (input) => {
|
|
495
|
-
const task = await reportState(workspace, input);
|
|
496
|
-
void usageTracker.observe(task).catch(() => {});
|
|
497
|
-
return toolResult("task", task, `Recorded ${task.status} state for TaskChef task ${task.id}.`);
|
|
498
|
-
},
|
|
499
|
-
);
|
|
500
|
-
|
|
501
|
-
server.registerTool(
|
|
502
|
-
"report_result",
|
|
503
|
-
{
|
|
504
|
-
title: "Report TaskChef result (deprecated)",
|
|
505
|
-
description:
|
|
506
|
-
reportingDestination + "Deprecated compatibility alias for semantic results. New executors must use report_state working at turn start and report_state again with needs_input, completed, or failed before ending. This alias preserves legacy callers by implicitly starting the supplied newer turn before storing its result.",
|
|
507
|
-
inputSchema: {
|
|
508
|
-
taskId: z.string().min(1),
|
|
509
|
-
threadId: z.string().min(1).nullable(),
|
|
510
|
-
turnRef: z.string().min(1).max(256).nullable().optional(),
|
|
511
|
-
turnId: z.string().min(1).max(256).nullable(),
|
|
512
|
-
status: z.enum(["needs_input", "completed", "failed"]),
|
|
513
|
-
summary: z.string().min(1).max(2_000),
|
|
514
|
-
},
|
|
515
|
-
outputSchema: { task: taskSchema },
|
|
516
|
-
annotations: {
|
|
517
|
-
readOnlyHint: false,
|
|
518
|
-
destructiveHint: true,
|
|
519
|
-
openWorldHint: false,
|
|
520
|
-
},
|
|
521
|
-
},
|
|
522
|
-
async (input) => {
|
|
523
|
-
const task = await reportResult(workspace, input);
|
|
524
|
-
void usageTracker.observe(task).catch(() => {});
|
|
525
|
-
return toolResult("task", task, `Recorded ${task.status} result for TaskChef task ${task.id}.`);
|
|
526
|
-
},
|
|
527
|
-
);
|
|
528
|
-
|
|
529
|
-
registerTaskChefApp(server, { workspace });
|
|
530
|
-
|
|
531
|
-
const originalConnect = server.connect.bind(server);
|
|
532
|
-
const autostartDashboard = createDashboardAutostart({
|
|
533
|
-
workspace,
|
|
534
|
-
dashboardManager,
|
|
535
|
-
readConfiguration,
|
|
536
|
-
...(logDashboardDiagnostic ? { log: logDashboardDiagnostic } : {}),
|
|
537
|
-
});
|
|
538
|
-
server.connect = async (...args) => {
|
|
539
|
-
await autostartDashboard();
|
|
540
|
-
if (closing) throw new Error("TaskChef MCP server is shutting down");
|
|
541
|
-
try {
|
|
542
|
-
await originalConnect(...args);
|
|
543
|
-
if (closing) {
|
|
544
|
-
await originalClose();
|
|
545
|
-
throw new Error("TaskChef MCP server shut down during transport startup");
|
|
546
|
-
}
|
|
547
|
-
} catch (error) {
|
|
548
|
-
await Promise.allSettled([
|
|
549
|
-
Promise.resolve().then(() => dashboardManager.close()),
|
|
550
|
-
Promise.resolve().then(() => originalClose()),
|
|
551
|
-
]);
|
|
552
|
-
throw error;
|
|
553
|
-
}
|
|
554
|
-
};
|
|
555
|
-
|
|
17
|
+
registerTaskChefApp(server, options);
|
|
556
18
|
return server;
|
|
557
19
|
}
|
package/src/next-cache.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { mkdir, rename, stat } from "node:fs/promises";
|
|
2
2
|
import { dirname } from "node:path";
|
|
3
|
-
import {
|
|
3
|
+
import { acquireFileLock } from "./file-lock.js";
|
|
4
4
|
|
|
5
5
|
// Serialize migrations between plugin processes. Never replace a newer cache.
|
|
6
6
|
export async function migrateCache(previous, current) {
|
|
7
7
|
if (!previous || previous === current) return;
|
|
8
8
|
await mkdir(dirname(current), { recursive: true, mode: 0o700 });
|
|
9
|
-
const release = await
|
|
9
|
+
const release = await acquireFileLock(dirname(current));
|
|
10
10
|
try {
|
|
11
11
|
try { await stat(current); return; }
|
|
12
12
|
catch (error) { if (error.code !== "ENOENT") throw error; }
|
package/src/next-github.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { migrateCache } from "./next-cache.js";
|
|
2
|
-
import { filterTasks } from "./
|
|
2
|
+
import { filterTasks } from "./ui/state.js";
|
|
3
3
|
import { mkdir, readFile, rm } from "node:fs/promises";
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
-
import {
|
|
5
|
+
import { acquireFileLock } from "./file-lock.js";
|
|
6
6
|
import { writeDurableAtomic } from "./state-store.js";
|
|
7
7
|
|
|
8
8
|
const DEVICE_URL = "https://github.com/login/device";
|
|
@@ -33,7 +33,7 @@ export class NextGitHub {
|
|
|
33
33
|
}
|
|
34
34
|
async locked(operation) {
|
|
35
35
|
await mkdir(join(this.stateDir, "github-auth"), { recursive: true, mode: 0o700 });
|
|
36
|
-
const release = await
|
|
36
|
+
const release = await acquireFileLock(join(this.stateDir, "github-auth"));
|
|
37
37
|
try { return await operation(); } finally { await release(); }
|
|
38
38
|
}
|
|
39
39
|
get cachePath() { return join(this.stateDir, "cache", "github-prs.json"); }
|
|
@@ -2,7 +2,7 @@ import { mkdir, readFile } from "node:fs/promises";
|
|
|
2
2
|
import { dirname } from "node:path";
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
4
|
import { z } from "zod";
|
|
5
|
-
import {
|
|
5
|
+
import { acquireFileLock } from "./file-lock.js";
|
|
6
6
|
import { writeDurableAtomic } from "./state-store.js";
|
|
7
7
|
|
|
8
8
|
const itemSchema = z.object({ id: z.string(), taskId: z.string().nullable(), title: z.string(), detail: z.string(), kind: z.enum(["ready", "interrupted", "confirmation", "error"]), read: z.boolean(), timestamp: z.string() });
|
|
@@ -15,7 +15,7 @@ export class NextNotifications {
|
|
|
15
15
|
constructor(path, { now = () => new Date().toISOString(), clock = () => performance.timeOrigin + performance.now() } = {}) { this.path = path; this.now = now; this.clock = clock; }
|
|
16
16
|
async update(operation) {
|
|
17
17
|
await mkdir(dirname(this.path), { recursive: true, mode: 0o700 });
|
|
18
|
-
const release = await
|
|
18
|
+
const release = await acquireFileLock(dirname(this.path));
|
|
19
19
|
try {
|
|
20
20
|
let state;
|
|
21
21
|
try { state = schema.parse(JSON.parse(await readFile(this.path, "utf8"))); }
|
|
@@ -2,7 +2,7 @@ import { PrOpenIcon, PrStatusIcon, PrDraftIcon, PrMergedIcon, PrClosedIcon } fro
|
|
|
2
2
|
import { Anchor, Box, Popover, Stack, Text, VisuallyHidden } from "@mantine/core";
|
|
3
3
|
import { IconAlertTriangle, IconHourglass, IconCircleCheck, IconCircleX, IconLoader, IconMinus, IconClock, IconChartBar } from "@tabler/icons-react";
|
|
4
4
|
import { useEffect, useId, useRef, useState, type CSSProperties, type ReactNode } from "react";
|
|
5
|
-
import { formatWorkedDuration } from "
|
|
5
|
+
import { formatWorkedDuration } from "../time.js";
|
|
6
6
|
import { turnUsageLabel, useAnimatedUsage } from "./SavedUsageFormat";
|
|
7
7
|
import type { SavedUsage, PullRequestStatus } from "../types";
|
|
8
8
|
|
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
githubReferenceAccessibleLabel,
|
|
4
4
|
githubReferenceDisplayLabels,
|
|
5
5
|
groupRelatedGitHubLinks,
|
|
6
|
-
} from "
|
|
6
|
+
} from "../github-links.js";
|
|
7
7
|
import type { GitHubLink, Task } from "../types";
|
|
8
8
|
|
|
9
9
|
export function GitHubLinks({ task }: { task: Task }) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { MantineProvider } from "@mantine/core";
|
|
2
2
|
import { act, cleanup, fireEvent, render, screen } from "@testing-library/react";
|
|
3
3
|
import { afterEach, expect, test, vi } from "vitest";
|
|
4
|
-
import { formatCardAge, formatCardTime, formatWorkedDuration } from "
|
|
4
|
+
import { formatCardAge, formatCardTime, formatWorkedDuration } from "../time.js";
|
|
5
5
|
import { ElapsedTime, RelativeTime, RelativeTimeProvider } from "./RelativeTime";
|
|
6
6
|
|
|
7
7
|
afterEach(() => { cleanup(); vi.useRealTimers(); });
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Tooltip, UnstyledButton } from "@mantine/core";
|
|
2
2
|
import { IconClock, IconHourglass } from "@tabler/icons-react";
|
|
3
3
|
import { createContext, useContext, useEffect, useState, type ReactNode } from "react";
|
|
4
|
-
import { formatCardAge, formatCardTime, formatExactTime, formatRelativeTime, formatWorkedDuration } from "
|
|
4
|
+
import { formatCardAge, formatCardTime, formatExactTime, formatRelativeTime, formatWorkedDuration } from "../time.js";
|
|
5
5
|
|
|
6
6
|
const CardCalendarDates = createContext(false);
|
|
7
7
|
const RelativeTimeClock = createContext<number | null>(null);
|
|
@@ -2,7 +2,7 @@ import { MantineProvider } from "@mantine/core";
|
|
|
2
2
|
import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react";
|
|
3
3
|
import { afterEach, expect, test, vi } from "vitest";
|
|
4
4
|
import { fixtureTask } from "../fixtures";
|
|
5
|
-
import { NextTaskDetail } from "
|
|
5
|
+
import { NextTaskDetail } from "../../mcp-app/react/NextTaskDetail";
|
|
6
6
|
import { TurnContent } from "./TurnContent";
|
|
7
7
|
// Test TaskChef's rows and keyboard handlers without jsdom layout work.
|
|
8
8
|
// Browser QA separately exercises Mantine's real popup and positioning.
|
|
@@ -2,7 +2,7 @@ import { CardStatusLine } from "./CardStatusIcons";
|
|
|
2
2
|
import { Badge, Box, Button, Paper, Stack, Text, Title, Tooltip } from "@mantine/core";
|
|
3
3
|
import { useLayoutEffect, useRef, useState, type ReactNode, type PointerEvent as ReactPointerEvent } from "react";
|
|
4
4
|
import { IconArrowUpRight, IconFolder } from "@tabler/icons-react";
|
|
5
|
-
import { hasLinkedCodexThread, latestTurnPresentation } from "
|
|
5
|
+
import { hasLinkedCodexThread, latestTurnPresentation } from "../state.js";
|
|
6
6
|
import type { Task, TaskStatus } from "../types";
|
|
7
7
|
import { LinkedText } from "./LinkedText";
|
|
8
8
|
import { TurnContent } from "./TurnContent";
|