taskchef 7.26.1 → 7.27.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 +4 -1
- package/docs/spec.md +14 -1
- package/docs/workflows.md +2 -1
- package/index.js +1 -0
- package/package.json +1 -1
- package/skills/taskchef-delegate/SKILL.md +3 -1
- package/skills/taskchef-executor/SKILL.md +41 -83
- package/skills/taskchef-executor/references/compatibility.md +22 -0
- package/skills/taskchef-executor/references/ending-actions.md +26 -0
- package/src/dashboard/dist/app.js +39 -39
- package/src/dashboard/dist/app.js.map +1 -1
- package/src/dashboard/dist/styles.css +1 -1
- package/src/dashboard/react/components/TaskCard.tsx +14 -1
- package/src/dashboard/react/components/TaskDetail.test.tsx +1 -1
- package/src/dashboard/react/components/TaskUsageSummary.test.tsx +23 -2
- package/src/dashboard/react/components/UsagePanel.tsx +23 -9
- package/src/dashboard/react/presentation.test.tsx +37 -12
- package/src/dashboard/react/presentation.ts +44 -76
- package/src/dashboard/react/styles.css +6 -4
- package/src/dashboard/react/types.ts +7 -0
- package/src/dashboard.js +2 -0
- package/src/delegation.js +7 -3
- package/src/mcp.js +4 -2
- package/src/reported-work.js +61 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Box, Group, Paper, Stack, Text, Title } from "@mantine/core";
|
|
2
2
|
import { latestTurnPresentation } from "../../state.js";
|
|
3
3
|
import type { Task } from "../types";
|
|
4
|
+
import { taskReportedWorkView } from "../presentation";
|
|
4
5
|
import { GitHubLinks } from "./GitHubLinks";
|
|
5
6
|
import { LinkedText } from "./LinkedText";
|
|
6
7
|
import { OpenChatButton } from "./OpenChatButton";
|
|
@@ -19,6 +20,7 @@ export function TaskCard({
|
|
|
19
20
|
task: Task;
|
|
20
21
|
}) {
|
|
21
22
|
const latest = latestTurnPresentation(task);
|
|
23
|
+
const reportedWork = taskReportedWorkView(task);
|
|
22
24
|
return (
|
|
23
25
|
<Paper className="taskchef-task-row" component="article" p="md" radius="md" withBorder>
|
|
24
26
|
<Stack className="taskchef-task-main" gap="sm">
|
|
@@ -52,7 +54,18 @@ export function TaskCard({
|
|
|
52
54
|
|
|
53
55
|
<GitHubLinks task={task} />
|
|
54
56
|
<Group className="taskchef-card-footer" gap="md" justify="space-between">
|
|
55
|
-
<
|
|
57
|
+
<Group className="taskchef-card-timing" gap="sm">
|
|
58
|
+
<RelativeTime label={`Updated time for ${task.title}`} value={task.meaningfulUpdatedAt ?? task.updatedAt} />
|
|
59
|
+
<Text
|
|
60
|
+
aria-label={reportedWork.accessibleLabel}
|
|
61
|
+
className="taskchef-card-duration"
|
|
62
|
+
size="xs"
|
|
63
|
+
title={reportedWork.title}
|
|
64
|
+
>
|
|
65
|
+
<span>Reported work</span>
|
|
66
|
+
<span>{reportedWork.value}</span>
|
|
67
|
+
</Text>
|
|
68
|
+
</Group>
|
|
56
69
|
<OpenChatButton onClick={() => onOpenCodex(task)} taskTitle={task.title} />
|
|
57
70
|
</Group>
|
|
58
71
|
</Stack>
|
|
@@ -133,7 +133,7 @@ test("covers terminal, active, and unavailable timeline metrics", () => {
|
|
|
133
133
|
});
|
|
134
134
|
render(<MantineProvider><ActivityTimeline highlightTurnRef={null} task={task} /></MantineProvider>);
|
|
135
135
|
expect(screen.getByLabelText(/elapsed unavailable.*reported wall-clock/i))
|
|
136
|
-
.toHaveTextContent("
|
|
136
|
+
.toHaveTextContent("Elapsedn/a");
|
|
137
137
|
expect(screen.getByText("Manual dashboard turns do not have usage boundaries.")).toBeVisible();
|
|
138
138
|
expect(screen.getByLabelText(/tokens unavailable: manual dashboard turns/i)).toBeVisible();
|
|
139
139
|
}
|
|
@@ -36,7 +36,7 @@ describe("task list usage summary", () => {
|
|
|
36
36
|
status: "completed",
|
|
37
37
|
usage: { status: "available", task: { totalTokens: 1_324_567, estimatedCostUsd: null } },
|
|
38
38
|
}));
|
|
39
|
-
expect(screen.getByText("1.32M tokens · cost
|
|
39
|
+
expect(screen.getByText("1.32M tokens · cost n/a")).toHaveAccessibleName(
|
|
40
40
|
/1,324,567 tokens; estimated cost unavailable/i,
|
|
41
41
|
);
|
|
42
42
|
});
|
|
@@ -76,7 +76,7 @@ describe("task list usage summary", () => {
|
|
|
76
76
|
status: "completed",
|
|
77
77
|
usage: { status: "unavailable", reason: "No matching cached boundary." },
|
|
78
78
|
}));
|
|
79
|
-
const summary = screen.getByText("
|
|
79
|
+
const summary = screen.getByText("n/a tokens");
|
|
80
80
|
expect(summary).toHaveAttribute("data-usage-state", "unavailable");
|
|
81
81
|
expect(summary).toHaveAttribute("title", "No matching cached boundary.");
|
|
82
82
|
});
|
|
@@ -98,4 +98,25 @@ describe("task list usage summary", () => {
|
|
|
98
98
|
expect(screen.getByRole("button", { name: /^review checkout reconciliation$/i })).toBeVisible();
|
|
99
99
|
expect(screen.getByRole("button", { name: /open chat/i })).toBeVisible();
|
|
100
100
|
});
|
|
101
|
+
|
|
102
|
+
test("places reported work beside the updated time with matching typography", () => {
|
|
103
|
+
const { container } = render(
|
|
104
|
+
<MantineProvider>
|
|
105
|
+
<TaskCard
|
|
106
|
+
onOpenCodex={vi.fn()}
|
|
107
|
+
onOpenDetail={vi.fn()}
|
|
108
|
+
task={fixtureTask({
|
|
109
|
+
turns: undefined,
|
|
110
|
+
reportedWork: { terminalTurns: 1, validTurns: 1, totalMilliseconds: 1_112_000 },
|
|
111
|
+
})}
|
|
112
|
+
/>
|
|
113
|
+
</MantineProvider>,
|
|
114
|
+
);
|
|
115
|
+
const timing = container.querySelector(".taskchef-card-timing");
|
|
116
|
+
expect(timing).not.toBeNull();
|
|
117
|
+
expect(timing?.children[0]).toHaveClass("taskchef-time");
|
|
118
|
+
expect(timing?.children[1]).toHaveClass("taskchef-card-duration");
|
|
119
|
+
expect(screen.getByText("Reported work")).not.toHaveClass("taskchef-field-label");
|
|
120
|
+
expect(timing?.children[1]?.lastChild).toHaveTextContent("18m 32s");
|
|
121
|
+
});
|
|
101
122
|
});
|
|
@@ -17,12 +17,14 @@ export function UsagePanel({ task }: { task: Task }) {
|
|
|
17
17
|
const input = ready?.usage.inputTokens ?? 0;
|
|
18
18
|
const cacheRatio = ready && input + cached > 0
|
|
19
19
|
? `${Math.round((cached / (input + cached)) * 100)}%`
|
|
20
|
-
: "
|
|
20
|
+
: "n/a";
|
|
21
|
+
const models = Object.keys(ready?.usage.models ?? {}).join(", ");
|
|
21
22
|
const stateValue = usage.kind === "pending"
|
|
22
23
|
? "Pending"
|
|
23
24
|
: usage.kind === "calculating"
|
|
24
25
|
? "Calculating"
|
|
25
|
-
: "
|
|
26
|
+
: "n/a";
|
|
27
|
+
const accessibleState = usage.kind === "unavailable" ? "unavailable" : stateValue.toLowerCase();
|
|
26
28
|
const values: Array<{
|
|
27
29
|
accessibleValue?: string;
|
|
28
30
|
animated?: boolean;
|
|
@@ -44,25 +46,37 @@ export function UsagePanel({ task }: { task: Task }) {
|
|
|
44
46
|
? ready.usage.estimatedCostUsd == null
|
|
45
47
|
? "Estimated cost unavailable"
|
|
46
48
|
: `Estimated cost ${formatEstimatedCost(ready.usage.estimatedCostUsd)}`
|
|
47
|
-
: `Estimated cost ${
|
|
49
|
+
: `Estimated cost ${accessibleState}${
|
|
48
50
|
usage.kind === "unavailable" ? `: ${usage.label}` : ""
|
|
49
51
|
}`,
|
|
50
52
|
animated: usage.kind === "pending" || usage.kind === "calculating",
|
|
51
53
|
label: "Estimated cost",
|
|
52
54
|
title: ready?.usage.estimatedCostUsd == null
|
|
53
|
-
?
|
|
55
|
+
? "Estimated cost unavailable"
|
|
54
56
|
: `Unrounded estimate: $${ready.usage.estimatedCostUsd}`,
|
|
55
|
-
value: ready
|
|
57
|
+
value: ready
|
|
58
|
+
? ready.usage.estimatedCostUsd == null ? "n/a" : formatEstimatedCost(ready.usage.estimatedCostUsd)
|
|
59
|
+
: stateValue,
|
|
56
60
|
},
|
|
57
61
|
{
|
|
58
62
|
accessibleValue: reportedWork.accessibleLabel,
|
|
59
|
-
label: "
|
|
63
|
+
label: "Reported work",
|
|
60
64
|
title: reportedWork.title,
|
|
61
65
|
value: reportedWork.value,
|
|
62
66
|
},
|
|
63
67
|
...(ready ? [
|
|
64
|
-
{
|
|
65
|
-
|
|
68
|
+
{
|
|
69
|
+
accessibleValue: models || "Model unavailable",
|
|
70
|
+
label: "Model",
|
|
71
|
+
title: models || "Model unavailable",
|
|
72
|
+
value: models || "n/a",
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
accessibleValue: cacheRatio === "n/a" ? "Cache ratio unavailable" : `Cache ratio ${cacheRatio}`,
|
|
76
|
+
label: "Cache ratio",
|
|
77
|
+
title: cacheRatio === "n/a" ? "Cache ratio unavailable because no input-token total is available." : undefined,
|
|
78
|
+
value: cacheRatio,
|
|
79
|
+
},
|
|
66
80
|
] : []),
|
|
67
81
|
];
|
|
68
82
|
return (
|
|
@@ -83,7 +97,7 @@ export function UsagePanel({ task }: { task: Task }) {
|
|
|
83
97
|
</Paper>
|
|
84
98
|
))}
|
|
85
99
|
</Box>
|
|
86
|
-
{usage.kind === "unavailable" && (
|
|
100
|
+
{usage.kind === "unavailable" && usage.label !== "Token usage unavailable" && (
|
|
87
101
|
<Text c="dimmed" mt="xs" size="xs">{usage.label}</Text>
|
|
88
102
|
)}
|
|
89
103
|
</Box>
|
|
@@ -139,13 +139,34 @@ describe("token and working presentation", () => {
|
|
|
139
139
|
|
|
140
140
|
expect(screen.getByText("Tokens").nextSibling).toHaveTextContent("330");
|
|
141
141
|
expect(screen.getByText("Estimated cost").nextSibling).toHaveTextContent("$0.12");
|
|
142
|
-
expect(screen.getByText("
|
|
143
|
-
expect(screen.getByLabelText(/
|
|
142
|
+
expect(screen.getByText("Reported work").nextSibling).toHaveTextContent("18m 32s");
|
|
143
|
+
expect(screen.getByLabelText(/reported work 18m 32s.*wall-clock/i)).toBeVisible();
|
|
144
144
|
expect(screen.getByText("Model").nextSibling).toHaveTextContent("gpt-5.6-sol, gpt-5.6-luna");
|
|
145
145
|
expect(screen.getByText("Cache ratio").nextSibling).toHaveTextContent("67%");
|
|
146
146
|
expect(screen.queryByText(/API-equivalent/i)).not.toBeInTheDocument();
|
|
147
147
|
});
|
|
148
148
|
|
|
149
|
+
test("uses n/a for missing ready usage metrics with explanatory accessibility", () => {
|
|
150
|
+
render(
|
|
151
|
+
<MantineProvider>
|
|
152
|
+
<UsagePanel task={fixtureTask({
|
|
153
|
+
status: "completed",
|
|
154
|
+
usage: {
|
|
155
|
+
status: "available",
|
|
156
|
+
task: { totalTokens: 330, estimatedCostUsd: null },
|
|
157
|
+
},
|
|
158
|
+
})} />
|
|
159
|
+
</MantineProvider>,
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
expect(screen.getByText("Estimated cost").nextSibling).toHaveTextContent("n/a");
|
|
163
|
+
expect(screen.getByLabelText("Estimated cost unavailable")).toBeVisible();
|
|
164
|
+
expect(screen.getByText("Model").nextSibling).toHaveTextContent("n/a");
|
|
165
|
+
expect(screen.getByLabelText("Model unavailable")).toBeVisible();
|
|
166
|
+
expect(screen.getByText("Cache ratio").nextSibling).toHaveTextContent("n/a");
|
|
167
|
+
expect(screen.getByLabelText("Cache ratio unavailable")).toBeVisible();
|
|
168
|
+
});
|
|
169
|
+
|
|
149
170
|
test("keeps reported work visible across pending, calculating, and unavailable usage", () => {
|
|
150
171
|
for (const [task, expected, accessibleCost] of [
|
|
151
172
|
[fixtureTask(), "Pending", "Estimated cost pending"],
|
|
@@ -156,7 +177,7 @@ describe("token and working presentation", () => {
|
|
|
156
177
|
],
|
|
157
178
|
[
|
|
158
179
|
fixtureTask({ status: "completed", usage: { status: "unavailable" } }),
|
|
159
|
-
"
|
|
180
|
+
"n/a",
|
|
160
181
|
"Estimated cost unavailable: Token usage unavailable",
|
|
161
182
|
],
|
|
162
183
|
] as const) {
|
|
@@ -164,7 +185,7 @@ describe("token and working presentation", () => {
|
|
|
164
185
|
expect(screen.getByText("Tokens").nextSibling).toHaveTextContent(expected);
|
|
165
186
|
expect(screen.getByText("Estimated cost").nextSibling).toHaveTextContent(expected);
|
|
166
187
|
expect(screen.getByLabelText(accessibleCost)).toBeVisible();
|
|
167
|
-
expect(screen.getByText("
|
|
188
|
+
expect(screen.getByText("Reported work").nextSibling).toHaveTextContent("n/a");
|
|
168
189
|
cleanup();
|
|
169
190
|
}
|
|
170
191
|
});
|
|
@@ -343,14 +364,14 @@ describe("reported wall-clock work presentation", () => {
|
|
|
343
364
|
result: { status: "completed", summary: "Done", updatedAt: "2026-08-30T08:00:00.000Z" },
|
|
344
365
|
}],
|
|
345
366
|
});
|
|
346
|
-
expect(taskReportedWorkView(invalid)).toMatchObject({ kind: "unavailable", value: "
|
|
367
|
+
expect(taskReportedWorkView(invalid)).toMatchObject({ kind: "unavailable", value: "n/a" });
|
|
347
368
|
expect(durationBetween("2026-08-30T08:01:00.000Z", "2026-08-30T08:00:00.000Z")).toBeNull();
|
|
348
369
|
expect(durationBetween("2026-02-30T08:00:00.000Z", "2026-03-02T08:01:00.000Z")).toBeNull();
|
|
349
370
|
expect(durationBetween("March 2, 2026 08:00:00 UTC", "2026-03-02T08:01:00.000Z")).toBeNull();
|
|
350
371
|
expect(turnReportedWorkView(invalid.turns![0])).toMatchObject({
|
|
351
372
|
kind: "unavailable",
|
|
352
373
|
label: "Elapsed",
|
|
353
|
-
value: "
|
|
374
|
+
value: "n/a",
|
|
354
375
|
});
|
|
355
376
|
}
|
|
356
377
|
|
|
@@ -374,9 +395,9 @@ describe("reported wall-clock work presentation", () => {
|
|
|
374
395
|
provenance: { kind: "mcp" },
|
|
375
396
|
};
|
|
376
397
|
for (const turn of [legacyTurn, terminalOnlyTurn]) {
|
|
377
|
-
expect(turnReportedWorkView(turn)).toMatchObject({ kind: "unavailable", value: "
|
|
398
|
+
expect(turnReportedWorkView(turn)).toMatchObject({ kind: "unavailable", value: "n/a" });
|
|
378
399
|
expect(taskReportedWorkView(fixtureTask({ status: "completed", turns: [turn] })))
|
|
379
|
-
.toMatchObject({ kind: "unavailable", value: "
|
|
400
|
+
.toMatchObject({ kind: "unavailable", value: "n/a" });
|
|
380
401
|
}
|
|
381
402
|
}
|
|
382
403
|
|
|
@@ -393,7 +414,7 @@ describe("reported wall-clock work presentation", () => {
|
|
|
393
414
|
updatedAt: "2026-08-30T08:00:00.000Z",
|
|
394
415
|
},
|
|
395
416
|
};
|
|
396
|
-
expect(turnReportedWorkView(observed)).toMatchObject({ kind: "unavailable", value: "
|
|
417
|
+
expect(turnReportedWorkView(observed)).toMatchObject({ kind: "unavailable", value: "n/a" });
|
|
397
418
|
expect(turnReportedWorkView({
|
|
398
419
|
...observed,
|
|
399
420
|
result: { ...observed.result, updatedAt: "2026-08-30T08:00:00.500Z" },
|
|
@@ -415,10 +436,14 @@ describe("reported wall-clock work presentation", () => {
|
|
|
415
436
|
});
|
|
416
437
|
expect(taskReportedWorkView(compactTerminal)).toMatchObject({
|
|
417
438
|
kind: "unavailable",
|
|
418
|
-
value: "
|
|
439
|
+
value: "n/a",
|
|
419
440
|
});
|
|
441
|
+
expect(taskReportedWorkView({
|
|
442
|
+
...compactTerminal,
|
|
443
|
+
reportedWork: { terminalTurns: 1, validTurns: 1, totalMilliseconds: 1_112_000 },
|
|
444
|
+
})).toMatchObject({ kind: "available", value: "18m 32s" });
|
|
420
445
|
render(<MantineProvider><UsagePanel task={compactTerminal} /></MantineProvider>);
|
|
421
|
-
expect(screen.getByText("
|
|
446
|
+
expect(screen.getByText("Reported work").nextSibling).toHaveTextContent("n/a");
|
|
422
447
|
cleanup();
|
|
423
448
|
}
|
|
424
449
|
|
|
@@ -436,7 +461,7 @@ describe("reported wall-clock work presentation", () => {
|
|
|
436
461
|
turns: undefined,
|
|
437
462
|
lastResult: priorResult,
|
|
438
463
|
results: undefined,
|
|
439
|
-
}))).toMatchObject({ kind: "unavailable", value: "
|
|
464
|
+
}))).toMatchObject({ kind: "unavailable", value: "n/a" });
|
|
440
465
|
}
|
|
441
466
|
|
|
442
467
|
{
|
|
@@ -1,49 +1,17 @@
|
|
|
1
1
|
import type { Task, TaskTurn, UsageNumbers, UsageProjection } from "./types";
|
|
2
|
+
import {
|
|
3
|
+
durationBetween,
|
|
4
|
+
reportedWorkSummary,
|
|
5
|
+
terminalTurnDuration,
|
|
6
|
+
} from "../../reported-work.js";
|
|
7
|
+
|
|
8
|
+
export { durationBetween };
|
|
2
9
|
|
|
3
10
|
const SECOND_MS = 1_000;
|
|
4
11
|
const MINUTE_MS = 60 * SECOND_MS;
|
|
5
12
|
const HOUR_MS = 60 * MINUTE_MS;
|
|
6
13
|
const DAY_MS = 24 * HOUR_MS;
|
|
7
|
-
|
|
8
|
-
export function durationBetween(startedAt: unknown, endedAt: unknown) {
|
|
9
|
-
const start = parseLifecycleTimestamp(startedAt);
|
|
10
|
-
const end = parseLifecycleTimestamp(endedAt);
|
|
11
|
-
if (start === null || end === null) return null;
|
|
12
|
-
const duration = end - start;
|
|
13
|
-
return duration >= 0 ? duration : null;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function parseLifecycleTimestamp(value: unknown) {
|
|
17
|
-
if (typeof value !== "string") return null;
|
|
18
|
-
const match = value.match(
|
|
19
|
-
/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,3}))?(Z|[+-]\d{2}:\d{2})$/,
|
|
20
|
-
);
|
|
21
|
-
if (!match) return null;
|
|
22
|
-
const [, yearText, monthText, dayText, hourText, minuteText, secondText, , zone] = match;
|
|
23
|
-
const year = Number(yearText);
|
|
24
|
-
const month = Number(monthText);
|
|
25
|
-
const day = Number(dayText);
|
|
26
|
-
const hour = Number(hourText);
|
|
27
|
-
const minute = Number(minuteText);
|
|
28
|
-
const second = Number(secondText);
|
|
29
|
-
const zoneHour = zone === "Z" ? 0 : Number(zone.slice(1, 3));
|
|
30
|
-
const zoneMinute = zone === "Z" ? 0 : Number(zone.slice(4, 6));
|
|
31
|
-
const leapYear = year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
|
|
32
|
-
const daysInMonth = [31, leapYear ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
|
33
|
-
if (
|
|
34
|
-
month < 1
|
|
35
|
-
|| month > 12
|
|
36
|
-
|| day < 1
|
|
37
|
-
|| day > daysInMonth[month - 1]
|
|
38
|
-
|| hour > 23
|
|
39
|
-
|| minute > 59
|
|
40
|
-
|| second > 59
|
|
41
|
-
|| zoneHour > 23
|
|
42
|
-
|| zoneMinute > 59
|
|
43
|
-
) return null;
|
|
44
|
-
const parsed = Date.parse(value);
|
|
45
|
-
return Number.isFinite(parsed) ? parsed : null;
|
|
46
|
-
}
|
|
14
|
+
const NOT_AVAILABLE = "n/a";
|
|
47
15
|
|
|
48
16
|
export function formatReportedDuration(milliseconds: number) {
|
|
49
17
|
if (!Number.isFinite(milliseconds) || milliseconds < 0) return null;
|
|
@@ -69,49 +37,50 @@ export type ReportedWorkView = {
|
|
|
69
37
|
};
|
|
70
38
|
|
|
71
39
|
export function taskReportedWorkView(task: Task): ReportedWorkView {
|
|
72
|
-
|
|
40
|
+
const summary = task.turns === undefined
|
|
41
|
+
? task.reportedWork
|
|
42
|
+
: reportedWorkSummary(task.turns ?? []);
|
|
43
|
+
if (summary === undefined) {
|
|
73
44
|
return {
|
|
74
|
-
accessibleLabel: "
|
|
45
|
+
accessibleLabel: "Reported work unavailable because terminal turn history is not available.",
|
|
75
46
|
kind: "unavailable",
|
|
76
47
|
title: "Reported wall-clock elapsed time requires the terminal turn history from the task detail projection.",
|
|
77
|
-
value:
|
|
48
|
+
value: NOT_AVAILABLE,
|
|
78
49
|
};
|
|
79
50
|
}
|
|
80
|
-
|
|
81
|
-
if (terminalTurns.length === 0) {
|
|
51
|
+
if (summary.terminalTurns === 0) {
|
|
82
52
|
return {
|
|
83
|
-
accessibleLabel: "
|
|
53
|
+
accessibleLabel: "Reported work not yet reported. Unfinished turns are excluded.",
|
|
84
54
|
kind: "not-reported",
|
|
85
55
|
title: "Reported wall-clock elapsed time is added only after a turn reaches a terminal state.",
|
|
86
|
-
value:
|
|
56
|
+
value: NOT_AVAILABLE,
|
|
87
57
|
};
|
|
88
58
|
}
|
|
89
|
-
|
|
90
|
-
const validDurations = durations.filter((duration): duration is number => duration !== null);
|
|
91
|
-
if (validDurations.length === 0) {
|
|
59
|
+
if (summary.validTurns === 0) {
|
|
92
60
|
return {
|
|
93
|
-
accessibleLabel: "
|
|
61
|
+
accessibleLabel: "Reported work unavailable because completed turn timestamps are missing or invalid.",
|
|
94
62
|
kind: "unavailable",
|
|
95
63
|
title: "Reported wall-clock elapsed time is unavailable because no terminal turn has a valid start and end timestamp.",
|
|
96
|
-
value:
|
|
64
|
+
value: NOT_AVAILABLE,
|
|
97
65
|
};
|
|
98
66
|
}
|
|
99
|
-
const
|
|
100
|
-
|
|
67
|
+
const value = summary.totalMilliseconds === null
|
|
68
|
+
? null
|
|
69
|
+
: formatReportedDuration(summary.totalMilliseconds);
|
|
101
70
|
if (value === null) {
|
|
102
71
|
return {
|
|
103
|
-
accessibleLabel: "
|
|
72
|
+
accessibleLabel: "Reported work unavailable because the duration total is outside the supported range.",
|
|
104
73
|
kind: "unavailable",
|
|
105
74
|
title: "Reported wall-clock elapsed time is unavailable because the duration total is outside the supported range.",
|
|
106
|
-
value:
|
|
75
|
+
value: NOT_AVAILABLE,
|
|
107
76
|
};
|
|
108
77
|
}
|
|
109
|
-
const excluded = terminalTurns
|
|
78
|
+
const excluded = summary.terminalTurns - summary.validTurns;
|
|
110
79
|
const exclusion = excluded > 0
|
|
111
80
|
? ` ${excluded} terminal ${excluded === 1 ? "turn was" : "turns were"} unavailable and excluded.`
|
|
112
81
|
: "";
|
|
113
82
|
return {
|
|
114
|
-
accessibleLabel: `
|
|
83
|
+
accessibleLabel: `Reported work ${value}. This is reported wall-clock elapsed time.${exclusion}`,
|
|
115
84
|
kind: "available",
|
|
116
85
|
title: `Sum of valid terminal-turn wall-clock elapsed times; unfinished turns and idle gaps are excluded.${exclusion}`,
|
|
117
86
|
value,
|
|
@@ -124,9 +93,9 @@ export function turnReportedWorkView(turn: TaskTurn, now = Date.now()) {
|
|
|
124
93
|
? durationBetween(turn.startedAt, new Date(now).toISOString())
|
|
125
94
|
: terminalTurnDuration(turn);
|
|
126
95
|
const label = active ? "Elapsed so far" : "Elapsed";
|
|
127
|
-
const value = duration === null ?
|
|
96
|
+
const value = duration === null ? NOT_AVAILABLE : formatReportedDuration(duration) ?? NOT_AVAILABLE;
|
|
128
97
|
return {
|
|
129
|
-
accessibleLabel: `${label} ${value}. ${active ? "Current" : "Completed"} turn reported wall-clock elapsed time.`,
|
|
98
|
+
accessibleLabel: `${label} ${duration === null ? "unavailable" : value}. ${active ? "Current" : "Completed"} turn reported wall-clock elapsed time.`,
|
|
130
99
|
kind: duration === null ? "unavailable" as const : "available" as const,
|
|
131
100
|
label,
|
|
132
101
|
title: duration === null
|
|
@@ -136,11 +105,6 @@ export function turnReportedWorkView(turn: TaskTurn, now = Date.now()) {
|
|
|
136
105
|
};
|
|
137
106
|
}
|
|
138
107
|
|
|
139
|
-
function terminalTurnDuration(turn: TaskTurn) {
|
|
140
|
-
const duration = durationBetween(turn.startedAt, turn.result?.updatedAt);
|
|
141
|
-
return duration === 0 ? null : duration;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
108
|
export function formatCompactTokens(value: number, locales?: Intl.LocalesArgument) {
|
|
145
109
|
return new Intl.NumberFormat(locales, {
|
|
146
110
|
maximumFractionDigits: 2,
|
|
@@ -194,7 +158,7 @@ export function usageView(task: Task): UsageView {
|
|
|
194
158
|
function readyUsageView(usage: UsageNumbers, knownSoFar: boolean): UsageView {
|
|
195
159
|
const tokens = formatCompactTokens(usage.totalTokens);
|
|
196
160
|
const cost = usage.estimatedCostUsd == null
|
|
197
|
-
? "Estimated cost
|
|
161
|
+
? "Estimated cost n/a"
|
|
198
162
|
: `Estimated cost ${formatEstimatedCost(usage.estimatedCostUsd)}`;
|
|
199
163
|
return {
|
|
200
164
|
kind: "ready",
|
|
@@ -214,7 +178,7 @@ export function turnUsageView(task: Task, turn: NonNullable<Task["turns"]>[numbe
|
|
|
214
178
|
if (usage?.status === "available" && typeof usage.totalTokens === "number") {
|
|
215
179
|
const tokens = formatFullTokens(usage.totalTokens);
|
|
216
180
|
const cost = usage.estimatedCostUsd == null
|
|
217
|
-
? "Estimated cost
|
|
181
|
+
? "Estimated cost n/a"
|
|
218
182
|
: `Estimated cost ${formatEstimatedCost(usage.estimatedCostUsd)}`;
|
|
219
183
|
return { kind: "ready" as const, label: `${tokens} tokens · ${cost}` };
|
|
220
184
|
}
|
|
@@ -229,13 +193,16 @@ export function turnUsageMetricsView(task: Task, turn: NonNullable<Task["turns"]
|
|
|
229
193
|
if (view.kind === "ready") {
|
|
230
194
|
const identity = turn.turnRef ?? turn.turnId;
|
|
231
195
|
const usage = identity ? task.usage?.turns?.[identity] : null;
|
|
232
|
-
const tokens = typeof usage?.totalTokens === "number" ? formatFullTokens(usage.totalTokens) :
|
|
196
|
+
const tokens = typeof usage?.totalTokens === "number" ? formatFullTokens(usage.totalTokens) : NOT_AVAILABLE;
|
|
233
197
|
const cost = usage?.estimatedCostUsd == null
|
|
234
|
-
?
|
|
198
|
+
? NOT_AVAILABLE
|
|
235
199
|
: formatEstimatedCost(usage.estimatedCostUsd);
|
|
236
200
|
return {
|
|
237
201
|
animated: false,
|
|
238
|
-
cost: {
|
|
202
|
+
cost: {
|
|
203
|
+
accessibleLabel: `Estimated cost ${usage?.estimatedCostUsd == null ? "unavailable" : cost.toLowerCase()}`,
|
|
204
|
+
value: cost,
|
|
205
|
+
},
|
|
239
206
|
kind: view.kind,
|
|
240
207
|
note: null,
|
|
241
208
|
title: view.label,
|
|
@@ -246,18 +213,19 @@ export function turnUsageMetricsView(task: Task, turn: NonNullable<Task["turns"]
|
|
|
246
213
|
? "Pending"
|
|
247
214
|
: view.kind === "calculating"
|
|
248
215
|
? "Calculating"
|
|
249
|
-
:
|
|
216
|
+
: NOT_AVAILABLE;
|
|
217
|
+
const accessibleValue = view.kind === "unavailable" ? "unavailable" : value.toLowerCase();
|
|
250
218
|
return {
|
|
251
219
|
animated: view.kind === "pending" || view.kind === "calculating",
|
|
252
220
|
cost: {
|
|
253
|
-
accessibleLabel: `Estimated cost ${
|
|
221
|
+
accessibleLabel: `Estimated cost ${accessibleValue}${view.kind === "unavailable" ? `: ${view.label}` : ""}`,
|
|
254
222
|
value,
|
|
255
223
|
},
|
|
256
224
|
kind: view.kind,
|
|
257
|
-
note: view.kind === "unavailable" ? view.label : null,
|
|
225
|
+
note: view.kind === "unavailable" && view.label !== "Turn usage unavailable" ? view.label : null,
|
|
258
226
|
title: view.label,
|
|
259
227
|
tokens: {
|
|
260
|
-
accessibleLabel: `Tokens ${
|
|
228
|
+
accessibleLabel: `Tokens ${accessibleValue}${view.kind === "unavailable" ? `: ${view.label}` : ""}`,
|
|
261
229
|
value,
|
|
262
230
|
},
|
|
263
231
|
};
|
|
@@ -290,14 +258,14 @@ export function listUsageView(task: Task): ListUsageView {
|
|
|
290
258
|
return {
|
|
291
259
|
accessibleLabel: `Token usage unavailable${view.label === "Token usage unavailable" ? "" : `: ${view.label}`}`,
|
|
292
260
|
kind: "unavailable",
|
|
293
|
-
label:
|
|
261
|
+
label: `${NOT_AVAILABLE} tokens`,
|
|
294
262
|
title: view.label,
|
|
295
263
|
};
|
|
296
264
|
}
|
|
297
265
|
const compactTokens = formatCompactTokens(view.usage.totalTokens);
|
|
298
266
|
const fullTokens = formatFullTokens(view.usage.totalTokens);
|
|
299
267
|
const cost = view.usage.estimatedCostUsd == null
|
|
300
|
-
? "cost
|
|
268
|
+
? "cost n/a"
|
|
301
269
|
: `est. ${formatEstimatedCost(view.usage.estimatedCostUsd)}`;
|
|
302
270
|
const qualifier = view.knownSoFar ? " · Updating…" : "";
|
|
303
271
|
const accessibleQualifier = view.knownSoFar ? "; updating" : "";
|
|
@@ -34,8 +34,8 @@ body {
|
|
|
34
34
|
.taskchef-task-main { min-width: 0; flex: 1; }
|
|
35
35
|
.taskchef-card-heading { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: start; gap: var(--mantine-spacing-sm); }
|
|
36
36
|
.taskchef-card-metadata { width: max-content; min-width: 0; max-width: min(25rem, 45vw); }
|
|
37
|
-
.taskchef-status-badge { flex: none; }
|
|
38
|
-
.taskchef-task-usage { max-width: 100%; text-align: right;
|
|
37
|
+
.taskchef-status-badge { flex: none; font-size: var(--mantine-font-size-xs); }
|
|
38
|
+
.taskchef-task-usage { max-width: 100%; text-align: right; overflow-wrap: anywhere; }
|
|
39
39
|
.taskchef-title-button {
|
|
40
40
|
all: unset;
|
|
41
41
|
border-radius: 3px;
|
|
@@ -55,10 +55,12 @@ body {
|
|
|
55
55
|
.taskchef-github-group { min-width: 0; }
|
|
56
56
|
.taskchef-github-reference-ambiguous { border-bottom: 2px dotted var(--mantine-color-yellow-7); cursor: help; }
|
|
57
57
|
.taskchef-github-reference-ambiguous:focus-visible { outline: 2px solid var(--mantine-color-yellow-7); outline-offset: 2px; }
|
|
58
|
-
.taskchef-time { display: inline-flex; align-items: center; gap: 4px; width: fit-content; padding: 2px 3px; border-radius: 4px; color: var(--taskchef-muted); font-size:
|
|
58
|
+
.taskchef-time, .taskchef-card-duration { display: inline-flex; align-items: center; gap: 4px; width: fit-content; padding: 2px 3px; border-radius: 4px; color: var(--taskchef-muted); font-size: var(--mantine-font-size-xs); font-weight: 400; letter-spacing: normal; line-height: var(--mantine-line-height-xs); text-transform: none; }
|
|
59
59
|
.taskchef-time:hover { color: var(--taskchef-text); background: var(--taskchef-surface-muted); }
|
|
60
60
|
.taskchef-time:focus-visible { outline: 2px solid var(--mantine-color-teal-6); outline-offset: 2px; }
|
|
61
61
|
.taskchef-time-label { white-space: nowrap; }
|
|
62
|
+
.taskchef-card-timing { min-width: 0; flex-wrap: wrap; }
|
|
63
|
+
.taskchef-card-duration { min-width: 0; white-space: nowrap; }
|
|
62
64
|
.taskchef-empty { border-style: dashed; background: transparent; }
|
|
63
65
|
|
|
64
66
|
.taskchef-notifications {
|
|
@@ -123,7 +125,7 @@ body {
|
|
|
123
125
|
.taskchef-toolbar .mantine-SegmentedControl-root { overflow-x: auto; }
|
|
124
126
|
.taskchef-notifications { right: 16px; bottom: 16px; width: min(330px, calc(100vw - 32px)); }
|
|
125
127
|
.taskchef-metadata { grid-template-columns: 1fr; }
|
|
126
|
-
.taskchef-metrics { grid-template-columns: repeat(
|
|
128
|
+
.taskchef-metrics { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
|
127
129
|
.taskchef-turn-metrics { grid-template-columns: 1fr; }
|
|
128
130
|
.taskchef-turn-heading { align-items: flex-start; flex-direction: column; gap: 6px; }
|
|
129
131
|
.taskchef-turn-timing { flex-wrap: wrap; justify-content: flex-start; }
|
|
@@ -59,6 +59,12 @@ export interface UsageProjection {
|
|
|
59
59
|
updatedAt?: string | null;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
export interface ReportedWorkSummary {
|
|
63
|
+
terminalTurns: number;
|
|
64
|
+
validTurns: number;
|
|
65
|
+
totalMilliseconds: number | null;
|
|
66
|
+
}
|
|
67
|
+
|
|
62
68
|
export interface Task {
|
|
63
69
|
schemaVersion?: number;
|
|
64
70
|
id: string;
|
|
@@ -78,6 +84,7 @@ export interface Task {
|
|
|
78
84
|
latestTurn: TaskTurn | null;
|
|
79
85
|
turns?: TaskTurn[];
|
|
80
86
|
results?: TaskResult[];
|
|
87
|
+
reportedWork?: ReportedWorkSummary;
|
|
81
88
|
usage?: UsageProjection | null;
|
|
82
89
|
relatedGitHubLinks?: GitHubLink[];
|
|
83
90
|
relatedGitHubLinksTruncated?: boolean;
|
package/src/dashboard.js
CHANGED
|
@@ -27,6 +27,7 @@ import { taskGitHubProjection } from "./dashboard/github-links.js";
|
|
|
27
27
|
import { CODEX_CHAT_ARCHIVE_ENABLED } from "./dashboard/state.js";
|
|
28
28
|
import { createUsageTracker } from "./usage-tracker.js";
|
|
29
29
|
import { readUsageStore, usageStorePath } from "./usage.js";
|
|
30
|
+
import { reportedWorkSummary } from "./reported-work.js";
|
|
30
31
|
import {
|
|
31
32
|
MAX_DASHBOARD_SESSION_PIDS,
|
|
32
33
|
MAX_TRANSFERRED_DASHBOARD_SESSION_PIDS,
|
|
@@ -229,6 +230,7 @@ function taskListProjection(task) {
|
|
|
229
230
|
const { turns: _turns, results: _results, ...projection } = {
|
|
230
231
|
...task,
|
|
231
232
|
...taskGitHubProjection(task),
|
|
233
|
+
reportedWork: reportedWorkSummary(task.turns ?? []),
|
|
232
234
|
};
|
|
233
235
|
return projection;
|
|
234
236
|
}
|
package/src/delegation.js
CHANGED
|
@@ -8,6 +8,7 @@ export const EXECUTOR_LINK_PARAGRAPH = "Before any other work, read this executo
|
|
|
8
8
|
export const EXECUTOR_WORKING_PARAGRAPH = "After a successful initial link, and at the start of every follow-up turn before substantive work, read this exact Codex thread natively to obtain the current turn ID and call TaskChef report_state with the marked task ID, the self-linked thread ID, that current turn ID, status working, summary omitted or null, and a concise requestSummary for this turn. link_task remains the first TaskChef action on the initial turn; do not report working before identity is linked. Never reuse a prior turn ID after a follow-up.";
|
|
9
9
|
/** @deprecated Historical v7 inline-prompt snapshot. New delegations use taskchef-executor. */
|
|
10
10
|
export const EXECUTOR_RESULT_PARAGRAPH = "Before ending, read this exact Codex thread again and call TaskChef report_state for the same current working turn with status completed, needs_input, or failed and a concise summary. Use needs_input only for a semantic decision or information the user must provide; a native approval prompt is live Codex state, not a TaskChef result. Do not include secrets, transcripts, or raw command output.";
|
|
11
|
+
export const EXECUTOR_REPORTING_AUTHORIZATION = "Report this task and its follow-ups to my local TaskChef dashboard. Relevant private-repository links and concise work, test, and deployment results are authorized; exclude secrets.";
|
|
11
12
|
export const EXECUTOR_SKILL_INVOCATION = "Use $taskchef-executor to execute and report this delegated TaskChef assignment.";
|
|
12
13
|
const HISTORICAL_RESULT_WITH_TURN_PARAGRAPH = "Before ending, call the TaskChef report_result MCP tool with the marked task ID, this executor's self-linked thread ID, the current turn ID from an exact native read of that same thread, completed, needs_input, or failed, and a concise summary. Never reuse a prior turn ID after a follow-up. Use needs_input only for a semantic decision or information the user must provide; a native approval prompt is live Codex state, not a TaskChef result. Do not include secrets, transcripts, or raw command output.";
|
|
13
14
|
const HISTORICAL_RESULT_PARAGRAPH = "Before ending, call the TaskChef report_result MCP tool with completed, needs_input, or failed and a concise summary. Use needs_input only for a semantic decision or information the user must provide; a native approval prompt is live Codex state, not a TaskChef result. Do not include secrets, transcripts, or raw command output.";
|
|
@@ -28,7 +29,7 @@ const HISTORICAL_INLINE_PROTOCOLS = [
|
|
|
28
29
|
|
|
29
30
|
function hasTaskSpecificContent(lines) {
|
|
30
31
|
return lines.some((line) => (
|
|
31
|
-
line.trim().length > 0 && !HISTORICAL_EXECUTOR_SCAFFOLD_LINES.has(line)
|
|
32
|
+
line.trim().length > 0 && line !== EXECUTOR_REPORTING_AUTHORIZATION && !HISTORICAL_EXECUTOR_SCAFFOLD_LINES.has(line)
|
|
32
33
|
));
|
|
33
34
|
}
|
|
34
35
|
|
|
@@ -134,7 +135,7 @@ export function parseTaskChefMarker(instruction) {
|
|
|
134
135
|
}
|
|
135
136
|
|
|
136
137
|
const containsInlineScaffold = rest.some((line) => HISTORICAL_EXECUTOR_SCAFFOLD_LINES.has(line));
|
|
137
|
-
if (!containsInlineScaffold) return rest
|
|
138
|
+
if (!containsInlineScaffold) return hasTaskSpecificContent(rest);
|
|
138
139
|
return HISTORICAL_INLINE_PROTOCOLS.some((protocol) => {
|
|
139
140
|
const prefix = ["", ...protocol.flatMap((line) => [line, ""])];
|
|
140
141
|
if (!prefix.every((line, index) => rest[index] === line)) return false;
|
|
@@ -216,10 +217,13 @@ export function prepareDelegation(instruction, { taskId = randomUUID() } = {}) {
|
|
|
216
217
|
if (bodyLines.some((line) => HISTORICAL_EXECUTOR_SCAFFOLD_LINES.has(line))) {
|
|
217
218
|
throw new Error("instruction contains reserved historical TaskChef lifecycle scaffolding");
|
|
218
219
|
}
|
|
220
|
+
if (bodyLines.includes(EXECUTOR_REPORTING_AUTHORIZATION)) {
|
|
221
|
+
throw new Error("instruction contains reserved TaskChef reporting authorization");
|
|
222
|
+
}
|
|
219
223
|
const id = requireUuid(taskId);
|
|
220
224
|
return {
|
|
221
225
|
id,
|
|
222
|
-
instruction: `${body}\n\n${EXECUTOR_SKILL_INVOCATION}\n${taskChefMarker(id)}`,
|
|
226
|
+
instruction: `${body}\n\n${EXECUTOR_REPORTING_AUTHORIZATION}\n\n${EXECUTOR_SKILL_INVOCATION}\n${taskChefMarker(id)}`,
|
|
223
227
|
};
|
|
224
228
|
}
|
|
225
229
|
|
package/src/mcp.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import path from "node:path";
|
|
1
2
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
3
|
import { z } from "zod";
|
|
3
4
|
import {
|
|
@@ -169,6 +170,7 @@ export function createTaskChefMcpServer({
|
|
|
169
170
|
logDashboardDiagnostic,
|
|
170
171
|
usageTracker = createUsageTracker({ workspace }),
|
|
171
172
|
} = {}) {
|
|
173
|
+
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. `;
|
|
172
174
|
const server = new McpServer(
|
|
173
175
|
{ name: "taskchef", version: TASKCHEF_VERSION },
|
|
174
176
|
{
|
|
@@ -297,7 +299,7 @@ export function createTaskChefMcpServer({
|
|
|
297
299
|
{
|
|
298
300
|
title: "Report TaskChef state",
|
|
299
301
|
description:
|
|
300
|
-
"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.",
|
|
302
|
+
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.",
|
|
301
303
|
inputSchema: {
|
|
302
304
|
taskId: z.string().min(1),
|
|
303
305
|
threadId: z.string().min(1).nullable(),
|
|
@@ -326,7 +328,7 @@ export function createTaskChefMcpServer({
|
|
|
326
328
|
{
|
|
327
329
|
title: "Report TaskChef result (deprecated)",
|
|
328
330
|
description:
|
|
329
|
-
"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.",
|
|
331
|
+
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.",
|
|
330
332
|
inputSchema: {
|
|
331
333
|
taskId: z.string().min(1),
|
|
332
334
|
threadId: z.string().min(1).nullable(),
|