taskchef 8.4.2 → 8.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +18 -4
- package/assets/taskchef-dispatcher-instructions.md +2 -0
- package/docs/spec.md +50 -4
- package/docs/workflows.md +14 -6
- package/index.js +3 -0
- package/package.json +1 -1
- package/skills/taskchef-bootstrap/SKILL.md +11 -3
- package/skills/taskchef-bootstrap/references/project-index.md +22 -5
- package/skills/taskchef-delegate/SKILL.md +20 -9
- package/skills/taskchef-executor/SKILL.md +10 -0
- package/src/config-mutations.js +8 -0
- package/src/dashboard/dist/app.js +30 -30
- package/src/dashboard/dist/app.js.map +1 -1
- package/src/dashboard/dist/styles.css +1 -1
- package/src/dashboard/react/App.test.tsx +8 -0
- package/src/dashboard/react/App.tsx +3 -2
- package/src/dashboard/react/components/TaskDetail.tsx +1 -1
- package/src/dashboard/react/styles.css +1 -0
- package/src/github.js +8 -5
- package/src/mcp.js +166 -0
- package/src/workspace.js +718 -8
|
@@ -154,6 +154,8 @@ test("Settings uses the same capped content layout as List", () => {
|
|
|
154
154
|
const { container } = render(<DashboardApp connect={false} />);
|
|
155
155
|
const settingsPage = container.querySelector(".taskchef-settings-page");
|
|
156
156
|
expect(settingsPage).toHaveClass("taskchef-content");
|
|
157
|
+
expect(container.querySelector(".taskchef-header")).not.toHaveClass("taskchef-list-layout");
|
|
158
|
+
expect(container.querySelector(".taskchef-main")).not.toHaveClass("taskchef-list-layout");
|
|
157
159
|
expect(within(settingsPage as HTMLElement).getByRole("link", { name: "Back to tasks" })).toBeInTheDocument();
|
|
158
160
|
window.location.hash = "";
|
|
159
161
|
});
|
|
@@ -208,6 +210,8 @@ test("places the filtered task count below the toolbar and updates it from live
|
|
|
208
210
|
const summary = document.querySelector<HTMLElement>(".taskchef-results-summary");
|
|
209
211
|
const taskList = screen.getByRole("region", { name: "Tasks" });
|
|
210
212
|
|
|
213
|
+
expect(header.querySelector(".taskchef-header")).toHaveClass("taskchef-list-layout");
|
|
214
|
+
expect(document.querySelector(".taskchef-main")).toHaveClass("taskchef-list-layout");
|
|
211
215
|
expect(projectFilter).toHaveValue("Another project");
|
|
212
216
|
expect(screen.getByRole("combobox", { name: "Updated" })).toHaveValue("All time");
|
|
213
217
|
expect(toolbar?.firstElementChild?.firstElementChild).toContainElement(viewSwitch);
|
|
@@ -248,6 +252,8 @@ test("Board ignores the List status filter and restores it on return", () => {
|
|
|
248
252
|
render(<DashboardApp connect={false} initialFilters={{ status: "completed" }} initialTasks={tasks} />);
|
|
249
253
|
expect(screen.getByRole("radio", { name: /Completed/ })).toBeChecked();
|
|
250
254
|
fireEvent.click(screen.getByRole("radio", { name: "Board" }));
|
|
255
|
+
expect(document.querySelector(".taskchef-header")).not.toHaveClass("taskchef-list-layout");
|
|
256
|
+
expect(document.querySelector(".taskchef-main")).not.toHaveClass("taskchef-list-layout");
|
|
251
257
|
const toolbar = document.querySelector<HTMLElement>(".taskchef-toolbar");
|
|
252
258
|
expect(toolbar?.firstElementChild?.firstElementChild).toContainElement(screen.getByRole("radiogroup", { name: "View" }));
|
|
253
259
|
expect(screen.getByRole("radiogroup", { name: "View" }).compareDocumentPosition(screen.getByRole("combobox", { name: "Project" })) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
|
|
@@ -256,6 +262,8 @@ test("Board ignores the List status filter and restores it on return", () => {
|
|
|
256
262
|
expect(screen.getByRole("region", { name: "Task board" })).toHaveTextContent("Working task");
|
|
257
263
|
expect(screen.getByRole("region", { name: "Task board" })).toHaveTextContent("Completed task");
|
|
258
264
|
fireEvent.click(screen.getByRole("radio", { name: "List" }));
|
|
265
|
+
expect(document.querySelector(".taskchef-header")).toHaveClass("taskchef-list-layout");
|
|
266
|
+
expect(document.querySelector(".taskchef-main")).toHaveClass("taskchef-list-layout");
|
|
259
267
|
expect(screen.getByRole("radio", { name: /Completed/ })).toBeChecked();
|
|
260
268
|
expect(screen.getByText("Tasks: 1 of 2")).toBeInTheDocument();
|
|
261
269
|
});
|
|
@@ -239,6 +239,7 @@ export function DashboardApp({
|
|
|
239
239
|
const visible = useMemo(() => filterTasks(tasks, { project, status, date, now }), [tasks, project, status, date, now]);
|
|
240
240
|
const boardTasks = useMemo(() => filterTasks(tasks, { project, date, now }), [tasks, project, date, now]);
|
|
241
241
|
const board = preferredView === "board";
|
|
242
|
+
const listLayout = !board && !settings;
|
|
242
243
|
const counts = useMemo(() => statusFilterCounts(tasks, { project, date, now }), [tasks, project, date, now]);
|
|
243
244
|
const statusData = STATUS_FILTERS.map(({ label, value }: { label: string; value: string }) => ({
|
|
244
245
|
label: counts[value] > 0 ? `${label} ${counts[value]}` : label,
|
|
@@ -322,7 +323,7 @@ export function DashboardApp({
|
|
|
322
323
|
<RelativeTimeProvider now={now}>
|
|
323
324
|
<AppShell className="taskchef-shell" padding={0}>
|
|
324
325
|
<Box className="taskchef-header-shell" component="header">
|
|
325
|
-
<Container className={`taskchef-header${board ? " taskchef-header-board" : ""}`} size="100%">
|
|
326
|
+
<Container className={`taskchef-header${board ? " taskchef-header-board" : ""}${listLayout ? " taskchef-list-layout" : ""}`} size="100%">
|
|
326
327
|
<Group className="taskchef-header-layout" justify="space-between" wrap="nowrap">
|
|
327
328
|
<Group className="taskchef-header-copy" gap="xs" wrap="nowrap">
|
|
328
329
|
<BrandIcon />
|
|
@@ -344,7 +345,7 @@ export function DashboardApp({
|
|
|
344
345
|
</Box>
|
|
345
346
|
|
|
346
347
|
<AppShell.Main>
|
|
347
|
-
<Container className={`taskchef-main${board ? " taskchef-main-board" : ""}`} pb={80} size="100%">
|
|
348
|
+
<Container className={`taskchef-main${board ? " taskchef-main-board" : ""}${listLayout ? " taskchef-list-layout" : ""}`} pb={80} size="100%">
|
|
348
349
|
{projectIndex?.status === "unavailable" && (
|
|
349
350
|
<Alert color="yellow" icon={<IconAlertTriangle size={18} />} mb="md" title="Project index unavailable">
|
|
350
351
|
Task history is still visible. Verify the index and inspect backups before making project changes.
|
|
@@ -186,7 +186,7 @@ export function TaskDetail({
|
|
|
186
186
|
opened={opened}
|
|
187
187
|
overlayProps={{ backgroundOpacity: 0.55, blur: 2 }}
|
|
188
188
|
scrollAreaComponent={ScrollArea.Autosize}
|
|
189
|
-
size="min(
|
|
189
|
+
size="min(600px, calc(100vw - 32px))"
|
|
190
190
|
title={<span className="taskchef-visually-hidden">Task details</span>}
|
|
191
191
|
trapFocus
|
|
192
192
|
zIndex={300}
|
|
@@ -30,6 +30,7 @@ body {
|
|
|
30
30
|
.taskchef-version { margin-inline-start: 0.45rem; color: var(--taskchef-muted); font-size: 0.78em; font-weight: 600; letter-spacing: 0; text-transform: none; }
|
|
31
31
|
.taskchef-brand-icon, .taskchef-brand-icon img { display: block; flex: none; width: 28px; height: 28px; }
|
|
32
32
|
.taskchef-main { max-width: var(--taskchef-page-max-width); padding-top: 8px; padding-inline: var(--taskchef-page-gutter); }
|
|
33
|
+
.taskchef-list-layout { --taskchef-content-max-width: 600px; max-width: calc(var(--taskchef-content-max-width) + 2 * var(--taskchef-page-gutter)); }
|
|
33
34
|
.taskchef-content { width: 100%; max-width: var(--taskchef-content-max-width); }
|
|
34
35
|
.taskchef-toolbar { max-width: none; margin-bottom: 22px; padding: 8px 0 var(--mantine-spacing-md); border: 0; border-bottom: 1px solid var(--taskchef-border); background: transparent; box-shadow: none; }
|
|
35
36
|
.taskchef-toolbar-filters { width: min(100%, 488px); min-width: 0; }
|
package/src/github.js
CHANGED
|
@@ -74,8 +74,9 @@ export function normalizeGithubRepositories(
|
|
|
74
74
|
return repositories;
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
export function matchProjectForGithubUrl(value, projects) {
|
|
77
|
+
export function matchProjectForGithubUrl(value, projects, routingHints = []) {
|
|
78
78
|
if (!Array.isArray(projects)) throw new Error("projects must be an array");
|
|
79
|
+
if (!Array.isArray(routingHints)) throw new Error("routingHints must be an array");
|
|
79
80
|
let parsed;
|
|
80
81
|
try {
|
|
81
82
|
parsed = parseGithubUrl(value, "GitHub URL", { allowIssueOrPull: true });
|
|
@@ -84,15 +85,17 @@ export function matchProjectForGithubUrl(value, projects) {
|
|
|
84
85
|
}
|
|
85
86
|
const repository = `https://github.com/${parsed.owner}/${parsed.repository}`;
|
|
86
87
|
const key = repository.toLowerCase();
|
|
87
|
-
const matches = projects.filter((project) =>
|
|
88
|
-
|
|
89
|
-
|
|
88
|
+
const matches = projects.filter((project) => {
|
|
89
|
+
const learned = routingHints.find((hint) => hint?.path === project?.path)?.githubRepos ?? [];
|
|
90
|
+
return [...(Array.isArray(project?.githubRepos) ? project.githubRepos : []), ...learned]
|
|
91
|
+
.some((candidate) => {
|
|
90
92
|
try {
|
|
91
93
|
return canonicalGithubRepository(candidate).toLowerCase() === key;
|
|
92
94
|
} catch {
|
|
93
95
|
return false;
|
|
94
96
|
}
|
|
95
|
-
})
|
|
97
|
+
});
|
|
98
|
+
});
|
|
96
99
|
if (matches.length === 1) {
|
|
97
100
|
return { status: "matched", repository, project: matches[0], projects: matches };
|
|
98
101
|
}
|
package/src/mcp.js
CHANGED
|
@@ -8,7 +8,10 @@ import {
|
|
|
8
8
|
reportTaskState,
|
|
9
9
|
reportTaskResult,
|
|
10
10
|
dashboardAutostartEnabled,
|
|
11
|
+
includeProject,
|
|
11
12
|
readConfig,
|
|
13
|
+
reconcileProjects,
|
|
14
|
+
updateProjectHint,
|
|
12
15
|
} from "./workspace.js";
|
|
13
16
|
import { parseTaskChefMarker } from "./delegation.js";
|
|
14
17
|
import { createDashboardManager } from "./dashboard-manager.js";
|
|
@@ -105,6 +108,60 @@ const preparationSchema = z.object({
|
|
|
105
108
|
modelRoles: z.record(z.string(), z.unknown()).optional(),
|
|
106
109
|
projectCount: z.number(),
|
|
107
110
|
projects: z.array(projectSchema),
|
|
111
|
+
routingHints: z.array(z.object({
|
|
112
|
+
path: z.string(),
|
|
113
|
+
aliases: z.array(z.string()),
|
|
114
|
+
githubRepos: z.array(z.string()),
|
|
115
|
+
responsibilities: z.array(z.string()),
|
|
116
|
+
})).optional(),
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const nativeProjectSnapshotSchema = z.object({
|
|
120
|
+
schemaVersion: z.literal(2),
|
|
121
|
+
projects: z.array(z.object({
|
|
122
|
+
projectId: z.string().min(1),
|
|
123
|
+
projectKind: z.string().min(1),
|
|
124
|
+
label: z.string().min(1),
|
|
125
|
+
path: z.string().nullable().optional(),
|
|
126
|
+
hostId: z.string().nullable().optional(),
|
|
127
|
+
hostDisplayName: z.string().nullable().optional(),
|
|
128
|
+
isGitRepository: z.boolean().nullable().optional(),
|
|
129
|
+
}).passthrough()),
|
|
130
|
+
}).passthrough();
|
|
131
|
+
|
|
132
|
+
const routingProvenanceSchema = z.object({
|
|
133
|
+
kind: z.enum(["explicit_user", "verified_repository", "accepted_terminal_report"]),
|
|
134
|
+
evidence: z.string().min(1).max(240),
|
|
135
|
+
taskId: z.string().min(1).nullable(),
|
|
136
|
+
threadId: z.string().min(1).nullable(),
|
|
137
|
+
turnRef: z.string().min(1).max(256).nullable(),
|
|
138
|
+
repositoryPath: z.string().min(1).nullable(),
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
const reconciliationSchema = z.object({
|
|
142
|
+
schemaVersion: z.literal(1),
|
|
143
|
+
changed: z.boolean(),
|
|
144
|
+
projectCount: z.number().int().nonnegative(),
|
|
145
|
+
addedCount: z.number().int().nonnegative(),
|
|
146
|
+
boundCount: z.number().int().nonnegative(),
|
|
147
|
+
available: z.array(z.object({
|
|
148
|
+
hostId: z.string(),
|
|
149
|
+
projectId: z.string(),
|
|
150
|
+
path: z.string(),
|
|
151
|
+
})),
|
|
152
|
+
diagnostics: z.array(z.object({
|
|
153
|
+
projectId: z.string(),
|
|
154
|
+
code: z.string(),
|
|
155
|
+
message: z.string(),
|
|
156
|
+
})),
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
const hintUpdateSchema = z.object({
|
|
160
|
+
changed: z.boolean(),
|
|
161
|
+
suppressed: z.boolean(),
|
|
162
|
+
project: z.string(),
|
|
163
|
+
kind: z.enum(["alias", "githubRepo", "responsibility"]),
|
|
164
|
+
value: z.string(),
|
|
108
165
|
});
|
|
109
166
|
|
|
110
167
|
const dashboardSchema = z.object({
|
|
@@ -166,6 +223,9 @@ export function createTaskChefMcpServer({
|
|
|
166
223
|
reportResult = reportTaskResult,
|
|
167
224
|
reportState = reportTaskState,
|
|
168
225
|
link = linkTask,
|
|
226
|
+
reconcile = reconcileProjects,
|
|
227
|
+
updateHint = updateProjectHint,
|
|
228
|
+
include = includeProject,
|
|
169
229
|
dashboardManager = createDashboardManager({ workspace }),
|
|
170
230
|
readConfiguration = readConfig,
|
|
171
231
|
logDashboardDiagnostic,
|
|
@@ -223,6 +283,75 @@ export function createTaskChefMcpServer({
|
|
|
223
283
|
},
|
|
224
284
|
);
|
|
225
285
|
|
|
286
|
+
server.registerTool(
|
|
287
|
+
"reconcile_projects",
|
|
288
|
+
{
|
|
289
|
+
title: "Reconcile TaskChef projects",
|
|
290
|
+
description:
|
|
291
|
+
"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.",
|
|
292
|
+
inputSchema: { snapshot: nativeProjectSnapshotSchema },
|
|
293
|
+
outputSchema: { reconciliation: reconciliationSchema },
|
|
294
|
+
annotations: {
|
|
295
|
+
readOnlyHint: false,
|
|
296
|
+
destructiveHint: false,
|
|
297
|
+
openWorldHint: false,
|
|
298
|
+
},
|
|
299
|
+
},
|
|
300
|
+
async ({ snapshot }) => {
|
|
301
|
+
const result = await reconcile(workspace, snapshot);
|
|
302
|
+
const reconciliation = {
|
|
303
|
+
schemaVersion: 1,
|
|
304
|
+
changed: result.changed,
|
|
305
|
+
projectCount: result.projectCount,
|
|
306
|
+
addedCount: result.added.length,
|
|
307
|
+
boundCount: result.bound.length,
|
|
308
|
+
available: result.available,
|
|
309
|
+
diagnostics: result.diagnostics,
|
|
310
|
+
};
|
|
311
|
+
return toolResult(
|
|
312
|
+
"reconciliation",
|
|
313
|
+
reconciliation,
|
|
314
|
+
`Reconciled ${reconciliation.projectCount} TaskChef project(s).`,
|
|
315
|
+
);
|
|
316
|
+
},
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
server.registerTool(
|
|
320
|
+
"include_project",
|
|
321
|
+
{
|
|
322
|
+
title: "Include a previously excluded TaskChef project",
|
|
323
|
+
description:
|
|
324
|
+
"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.",
|
|
325
|
+
inputSchema: {
|
|
326
|
+
projectId: z.string().min(1).optional(),
|
|
327
|
+
path: z.string().min(1).optional(),
|
|
328
|
+
},
|
|
329
|
+
outputSchema: {
|
|
330
|
+
result: z.object({ changed: z.boolean(), remainingExclusionCount: z.number().int().nonnegative() }),
|
|
331
|
+
},
|
|
332
|
+
annotations: {
|
|
333
|
+
readOnlyHint: false,
|
|
334
|
+
destructiveHint: false,
|
|
335
|
+
openWorldHint: false,
|
|
336
|
+
},
|
|
337
|
+
},
|
|
338
|
+
async ({ projectId, path: projectPath }) => {
|
|
339
|
+
if (Boolean(projectId) === Boolean(projectPath)) {
|
|
340
|
+
throw new Error("include_project requires exactly one projectId or path");
|
|
341
|
+
}
|
|
342
|
+
const updated = await include(workspace, projectId
|
|
343
|
+
? { projectId, hostId: "local" }
|
|
344
|
+
: { path: projectPath });
|
|
345
|
+
const result = {
|
|
346
|
+
changed: updated.changed,
|
|
347
|
+
remainingExclusionCount: updated.exclusions.length,
|
|
348
|
+
};
|
|
349
|
+
return toolResult("result", result, result.changed
|
|
350
|
+
? "Removed the TaskChef project exclusion."
|
|
351
|
+
: "No matching TaskChef project exclusion exists.");
|
|
352
|
+
},
|
|
353
|
+
);
|
|
354
|
+
|
|
226
355
|
server.registerTool(
|
|
227
356
|
"prepare_dispatch",
|
|
228
357
|
{
|
|
@@ -243,6 +372,43 @@ export function createTaskChefMcpServer({
|
|
|
243
372
|
},
|
|
244
373
|
);
|
|
245
374
|
|
|
375
|
+
server.registerTool(
|
|
376
|
+
"update_project_hint",
|
|
377
|
+
{
|
|
378
|
+
title: "Remember or forget TaskChef project routing",
|
|
379
|
+
description:
|
|
380
|
+
"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.",
|
|
381
|
+
inputSchema: {
|
|
382
|
+
action: z.enum(["remember", "correct", "forget"]),
|
|
383
|
+
project: z.string().min(1),
|
|
384
|
+
kind: z.enum(["alias", "githubRepo", "responsibility"]),
|
|
385
|
+
value: z.string().min(1).max(256),
|
|
386
|
+
provenance: routingProvenanceSchema,
|
|
387
|
+
},
|
|
388
|
+
outputSchema: { result: hintUpdateSchema },
|
|
389
|
+
annotations: {
|
|
390
|
+
readOnlyHint: false,
|
|
391
|
+
destructiveHint: true,
|
|
392
|
+
openWorldHint: false,
|
|
393
|
+
},
|
|
394
|
+
},
|
|
395
|
+
async (input) => {
|
|
396
|
+
const updated = await updateHint(workspace, input);
|
|
397
|
+
const result = {
|
|
398
|
+
changed: updated.changed,
|
|
399
|
+
suppressed: updated.suppressed,
|
|
400
|
+
project: updated.project,
|
|
401
|
+
kind: updated.kind,
|
|
402
|
+
value: updated.value,
|
|
403
|
+
};
|
|
404
|
+
return toolResult(
|
|
405
|
+
"result",
|
|
406
|
+
result,
|
|
407
|
+
`${result.changed ? "Updated" : "Kept"} TaskChef project routing.`,
|
|
408
|
+
);
|
|
409
|
+
},
|
|
410
|
+
);
|
|
411
|
+
|
|
246
412
|
server.registerTool(
|
|
247
413
|
"record_task",
|
|
248
414
|
{
|