taskchef 7.21.1 → 7.22.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 +1 -1
- package/README.md +14 -12
- package/docs/spec.md +11 -6
- package/package.json +1 -1
- package/src/dashboard/app.js +7 -4
- package/src/dashboard/state.js +20 -3
- package/src/dashboard.js +6 -0
- package/src/mcp.js +1 -1
- package/src/workspace.js +20 -6
package/README.md
CHANGED
|
@@ -273,23 +273,25 @@ timeline, including clearly labeled interrupted turns.
|
|
|
273
273
|
Task details offer infrequent administrative actions without cluttering task
|
|
274
274
|
cards. Selecting **More task actions** (`…`) reveals the action list immediately
|
|
275
275
|
beside the disclosure and changes it to a back/hide control. The list contains
|
|
276
|
-
**Copy Task ID
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
276
|
+
**Copy Task ID** and direct **Mark completed** and **Mark failed** actions.
|
|
277
|
+
Working and needs-input tasks offer either terminal outcome; completed tasks
|
|
278
|
+
offer **Mark failed**, and failed tasks offer **Mark completed**. The menu
|
|
279
|
+
disclosure is the deliberate first step; choosing an outcome submits it
|
|
280
|
+
immediately without a second confirmation. There is no
|
|
280
281
|
free-form reason: the audit turn records a fixed summary, timestamp, dashboard
|
|
281
282
|
provenance, optimistic preconditions, and a unique action ID while preserving
|
|
282
|
-
every executor turn.
|
|
283
|
-
changes are rejected and the dialog refreshes to the current task. A stalled
|
|
283
|
+
every executor turn. Same-state terminal rewrites remain invalid. Stale or
|
|
284
|
+
concurrent changes are rejected and the dialog refreshes to the current task. A stalled
|
|
284
285
|
local request is aborted after a bounded wait so the dialog cannot remain
|
|
285
286
|
permanently locked; retry keeps the same idempotency identity.
|
|
286
287
|
|
|
287
|
-
The
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
288
|
+
The archive implementation is retained behind a disabled capability gate, and
|
|
289
|
+
the **Archive chat** control is hidden. The bundled `codex archive` command can
|
|
290
|
+
reject a valid idle desktop-app thread even when the dashboard runs in the MCP
|
|
291
|
+
host environment, while Codex's native app archive operation succeeds. TaskChef
|
|
292
|
+
will keep the feature disabled until Codex exposes a reliable supported
|
|
293
|
+
app-callable archive interface or guarantees CLI compatibility. The dormant
|
|
294
|
+
server path also rejects requests before discovering or invoking the CLI.
|
|
293
295
|
|
|
294
296
|
Task details also show whole-task and per-turn token usage when `ccusage` can
|
|
295
297
|
map the linked Codex thread. A completed turn briefly shows “Tokens:
|
package/docs/spec.md
CHANGED
|
@@ -483,14 +483,19 @@ startup safely. Direct thread navigation
|
|
|
483
483
|
MUST require a canonical Codex UUIDv7. Otherwise it MAY open the revalidated
|
|
484
484
|
configured project. Project paths from task history MUST be matched against
|
|
485
485
|
current configuration before use.
|
|
486
|
-
The task-detail dashboard MAY offer manual
|
|
487
|
-
`
|
|
488
|
-
`failed
|
|
489
|
-
administrative action out of list
|
|
486
|
+
The task-detail dashboard MAY offer manual outcomes from `working` or
|
|
487
|
+
`needs_input` to either `completed` or `failed`, from `completed` to `failed`,
|
|
488
|
+
and from `failed` to `completed`. It MUST reject same-state terminal
|
|
489
|
+
transitions and MUST keep this infrequent administrative action out of list
|
|
490
|
+
cards. A keyboard-accessible **More task
|
|
490
491
|
actions** disclosure MUST reveal its action list immediately beside it and
|
|
491
492
|
change from an ellipsis to an accessible back/hide control while expanded. The
|
|
492
|
-
list MUST group **Copy Task ID
|
|
493
|
-
|
|
493
|
+
list MUST group **Copy Task ID** and each currently valid **Mark completed** or
|
|
494
|
+
**Mark failed** action. **Archive chat** MUST remain hidden while the archive capability
|
|
495
|
+
gate is disabled because the bundled CLI does not reliably archive desktop-app
|
|
496
|
+
threads. The dormant server endpoint MUST reject requests before discovering or
|
|
497
|
+
invoking the CLI. Re-enabling requires a reliable supported app-callable archive
|
|
498
|
+
interface or guaranteed CLI compatibility. Opening the
|
|
494
499
|
list is the deliberate disclosure step; choosing a terminal outcome submits it
|
|
495
500
|
immediately without a second confirmation. Escape MUST close the idle list,
|
|
496
501
|
pending controls MUST be
|
package/package.json
CHANGED
package/src/dashboard/app.js
CHANGED
|
@@ -506,7 +506,6 @@ function replaceCurrentTask(task) {
|
|
|
506
506
|
function renderManualTransition(task) {
|
|
507
507
|
const activeElement = document.activeElement;
|
|
508
508
|
const focusWasInPanel = elements.manualTransitionPanel.contains?.(activeElement) ?? false;
|
|
509
|
-
const eligible = canManuallyTransitionTask(task);
|
|
510
509
|
state.manualTransition = reconcileManualTransition(state.manualTransition, task);
|
|
511
510
|
const pending = manualTransitionPending();
|
|
512
511
|
const expanded = Boolean(state.manualTransition);
|
|
@@ -522,8 +521,8 @@ function renderManualTransition(task) {
|
|
|
522
521
|
elements.manualTransitionPanel.hidden = state.manualTransition === null;
|
|
523
522
|
elements.manualTransitionPanel.setAttribute("aria-busy", String(pending));
|
|
524
523
|
elements.copyTaskId.disabled = pending || !task.id;
|
|
525
|
-
elements.markTaskCompleted.hidden = !
|
|
526
|
-
elements.markTaskFailed.hidden = !
|
|
524
|
+
elements.markTaskCompleted.hidden = !canManuallyTransitionTask(task, "completed");
|
|
525
|
+
elements.markTaskFailed.hidden = !canManuallyTransitionTask(task, "failed");
|
|
527
526
|
elements.markTaskCompleted.disabled = pending;
|
|
528
527
|
elements.markTaskFailed.disabled = pending;
|
|
529
528
|
const archivePending = state.archivePendingThreadIds.has(task.threadId);
|
|
@@ -561,7 +560,11 @@ function renderManualTransition(task) {
|
|
|
561
560
|
|
|
562
561
|
async function submitManualTransition(targetStatus, event) {
|
|
563
562
|
const task = state.selectedTask;
|
|
564
|
-
if (
|
|
563
|
+
if (
|
|
564
|
+
!task
|
|
565
|
+
|| !canManuallyTransitionTask(task, targetStatus)
|
|
566
|
+
|| manualTransitionPending()
|
|
567
|
+
) return;
|
|
565
568
|
const previous = state.manualTransition;
|
|
566
569
|
const actionId = previous?.targetStatus === targetStatus && previous.actionId
|
|
567
570
|
? previous.actionId
|
package/src/dashboard/state.js
CHANGED
|
@@ -15,6 +15,12 @@ export const STATUS_FILTERS = [
|
|
|
15
15
|
];
|
|
16
16
|
const CODEX_THREAD_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
17
17
|
|
|
18
|
+
// Disabled because the bundled `codex archive` command can reject valid idle
|
|
19
|
+
// desktop-app threads while Codex's native archive operation succeeds. Keep
|
|
20
|
+
// the implementation behind this gate until Codex exposes a reliable,
|
|
21
|
+
// supported app-callable archive interface or guarantees CLI compatibility.
|
|
22
|
+
export const CODEX_CHAT_ARCHIVE_ENABLED = false;
|
|
23
|
+
|
|
18
24
|
const NOTIFICATION_TITLES = new Map([
|
|
19
25
|
["created", "Task created"],
|
|
20
26
|
["task_started", "Task started"],
|
|
@@ -41,14 +47,25 @@ export function taskStatusLabel(task) {
|
|
|
41
47
|
return task.status === null ? "unresolved" : task.status.replaceAll("_", " ");
|
|
42
48
|
}
|
|
43
49
|
|
|
44
|
-
export function
|
|
50
|
+
export function isArchiveTaskEligible(task) {
|
|
45
51
|
return task.status !== "working"
|
|
46
52
|
&& typeof task.threadId === "string"
|
|
47
53
|
&& CODEX_THREAD_ID_PATTERN.test(task.threadId);
|
|
48
54
|
}
|
|
49
55
|
|
|
50
|
-
export function
|
|
51
|
-
return
|
|
56
|
+
export function canArchiveTask(task) {
|
|
57
|
+
return CODEX_CHAT_ARCHIVE_ENABLED && isArchiveTaskEligible(task);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function canManuallyTransitionTask(task, targetStatus = null) {
|
|
61
|
+
const targets = task.status === "completed"
|
|
62
|
+
? ["failed"]
|
|
63
|
+
: task.status === "failed"
|
|
64
|
+
? ["completed"]
|
|
65
|
+
: ["working", "needs_input"].includes(task.status)
|
|
66
|
+
? ["completed", "failed"]
|
|
67
|
+
: [];
|
|
68
|
+
return targetStatus === null ? targets.length > 0 : targets.includes(targetStatus);
|
|
52
69
|
}
|
|
53
70
|
|
|
54
71
|
export function manualTransitionExpectedState(task) {
|
package/src/dashboard.js
CHANGED
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
} from "./workspace.js";
|
|
24
24
|
import { DASHBOARD_SERVER_VERSION, TASKCHEF_VERSION } from "./version.js";
|
|
25
25
|
import { taskGitHubProjection } from "./dashboard/github-links.js";
|
|
26
|
+
import { CODEX_CHAT_ARCHIVE_ENABLED } from "./dashboard/state.js";
|
|
26
27
|
import { createUsageTracker } from "./usage-tracker.js";
|
|
27
28
|
|
|
28
29
|
const TASKS_FILE_NAME = "tasks.jsonl";
|
|
@@ -529,6 +530,7 @@ function publicMonitorError() {
|
|
|
529
530
|
}
|
|
530
531
|
|
|
531
532
|
export async function createDashboardServer({
|
|
533
|
+
archiveEnabled = CODEX_CHAT_ARCHIVE_ENABLED,
|
|
532
534
|
archiveThread = archiveThreadInCodex,
|
|
533
535
|
discoverArchiveCli = discoverBundledCodexCli,
|
|
534
536
|
workspace,
|
|
@@ -759,6 +761,10 @@ export async function createDashboardServer({
|
|
|
759
761
|
|
|
760
762
|
const archiveMatch = url.pathname.match(/^\/api\/tasks\/([a-zA-Z0-9._-]+)\/archive-codex$/);
|
|
761
763
|
if (archiveMatch && method === "POST") {
|
|
764
|
+
if (!archiveEnabled) {
|
|
765
|
+
sendJson(response, 404, { message: "Chat archiving is not available." });
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
762
768
|
if (request.headers.origin !== allowedOrigin) {
|
|
763
769
|
sendJson(response, 403, { message: "Dashboard origin validation failed." });
|
|
764
770
|
return;
|
package/src/mcp.js
CHANGED
|
@@ -28,7 +28,7 @@ const turnProvenanceSchema = z.union([
|
|
|
28
28
|
z.object({
|
|
29
29
|
kind: z.literal("dashboard_manual"),
|
|
30
30
|
actionId: z.string(),
|
|
31
|
-
fromStatus: z.enum(["working", "needs_input"]),
|
|
31
|
+
fromStatus: z.enum(["working", "needs_input", "completed", "failed"]),
|
|
32
32
|
toStatus: z.enum(["completed", "failed"]),
|
|
33
33
|
expectedTurnRef: z.string().nullable(),
|
|
34
34
|
expectedThreadId: z.string().nullable(),
|
package/src/workspace.js
CHANGED
|
@@ -105,7 +105,9 @@ const MANUAL_TURN_PROVENANCE_FIELDS = new Set([
|
|
|
105
105
|
"expectedUpdatedAt",
|
|
106
106
|
]);
|
|
107
107
|
const INTERRUPTED_TURN_SUMMARY = "Turn interrupted before a terminal report.";
|
|
108
|
-
const MANUAL_TRANSITION_STATUSES = new Set([
|
|
108
|
+
const MANUAL_TRANSITION_STATUSES = new Set([
|
|
109
|
+
"working", "needs_input", "completed", "failed",
|
|
110
|
+
]);
|
|
109
111
|
const MANUAL_TARGET_STATUSES = new Set(["completed", "failed"]);
|
|
110
112
|
const MANUAL_TRANSITION_FIELDS = new Set([
|
|
111
113
|
"actionId", "expected", "targetStatus",
|
|
@@ -1035,9 +1037,14 @@ async function validateDispatchShape(dispatch, name = "task") {
|
|
|
1035
1037
|
if (provenance.expectedTurnRef !== (predecessor?.turnRef ?? null)) {
|
|
1036
1038
|
throw new Error(`${turnName}.provenance.expectedTurnRef must match the prior turn`);
|
|
1037
1039
|
}
|
|
1040
|
+
if (!canManuallyTransition(provenance.fromStatus, provenance.toStatus)) {
|
|
1041
|
+
throw new Error(`${turnName}.provenance describes an invalid manual transition`);
|
|
1042
|
+
}
|
|
1038
1043
|
const validPriorState = provenance.fromStatus === "needs_input"
|
|
1039
1044
|
? predecessor?.result?.status === "needs_input"
|
|
1040
|
-
:
|
|
1045
|
+
: provenance.fromStatus === "working"
|
|
1046
|
+
? predecessor === null || predecessor.result?.status === "interrupted"
|
|
1047
|
+
: predecessor?.result?.status === provenance.fromStatus;
|
|
1041
1048
|
if (!validPriorState) {
|
|
1042
1049
|
throw new Error(`${turnName}.provenance.fromStatus does not match the prior turn`);
|
|
1043
1050
|
}
|
|
@@ -1048,9 +1055,9 @@ async function validateDispatchShape(dispatch, name = "task") {
|
|
|
1048
1055
|
) {
|
|
1049
1056
|
throw new Error(`${turnName} must share its timestamp with the interrupted prior turn`);
|
|
1050
1057
|
}
|
|
1051
|
-
const expectedPriorTimestamp = provenance.fromStatus === "
|
|
1052
|
-
? predecessor
|
|
1053
|
-
: predecessor
|
|
1058
|
+
const expectedPriorTimestamp = provenance.fromStatus === "working"
|
|
1059
|
+
? predecessor?.startedAt ?? null
|
|
1060
|
+
: predecessor.result.updatedAt;
|
|
1054
1061
|
if (
|
|
1055
1062
|
Date.parse(provenance.expectedUpdatedAt) < Date.parse(normalized.createdAt)
|
|
1056
1063
|
|| (
|
|
@@ -1666,6 +1673,13 @@ function manualTransitionRequestSummary(fromStatus, toStatus) {
|
|
|
1666
1673
|
return `Manual dashboard transition from ${fromStatus} to ${toStatus}.`;
|
|
1667
1674
|
}
|
|
1668
1675
|
|
|
1676
|
+
function canManuallyTransition(fromStatus, toStatus) {
|
|
1677
|
+
if (!MANUAL_TRANSITION_STATUSES.has(fromStatus) || !MANUAL_TARGET_STATUSES.has(toStatus)) {
|
|
1678
|
+
return false;
|
|
1679
|
+
}
|
|
1680
|
+
return ["working", "needs_input"].includes(fromStatus) || fromStatus !== toStatus;
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1669
1683
|
function taskOperationError(code, message, task = null) {
|
|
1670
1684
|
const error = new Error(message);
|
|
1671
1685
|
error.code = code;
|
|
@@ -2009,7 +2023,7 @@ export async function manuallyTransitionTask(
|
|
|
2009
2023
|
}
|
|
2010
2024
|
|
|
2011
2025
|
const dispatch = dispatches[index];
|
|
2012
|
-
if (!
|
|
2026
|
+
if (!canManuallyTransition(dispatch.status, normalizedInput.targetStatus)) {
|
|
2013
2027
|
throw taskOperationError(
|
|
2014
2028
|
"invalid_transition",
|
|
2015
2029
|
`task status cannot be changed manually from ${dispatch.status}: ${id}`,
|