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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "taskchef",
3
- "version": "7.21.1",
3
+ "version": "7.22.0",
4
4
  "description": "Dispatch work from a data-only workspace to visible Codex project tasks.",
5
5
  "author": {
6
6
  "name": "Favo Yang",
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**, direct **Mark completed** and **Mark failed** actions for a
277
- `working` or `needs_input` task, and **Archive chat** whenever archival is
278
- eligible. The menu disclosure is the deliberate first step; choosing a terminal
279
- outcome submits it immediately without a second confirmation. There is no
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. Terminal tasks cannot be rewritten. Stale or concurrent
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 More actions list offers **Archive chat** for every linked task whose current
288
- TaskChef state is not `working`. After confirmation, TaskChef invokes only the
289
- Codex CLI at the canonical ChatGPT or Codex desktop app location under
290
- `/Applications`, to archive the exact thread UUID. The Codex chat leaves active chat lists while the TaskChef record
291
- and its activity timeline remain unchanged. If the bundled CLI is unavailable,
292
- the dashboard does not fall back to another `codex` executable from `PATH`.
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 terminal outcomes only for current
487
- `working` or `needs_input` tasks. It MUST permit exactly `completed` and
488
- `failed`, MUST NOT rewrite a terminal task, and MUST keep this infrequent
489
- administrative action out of list cards. A keyboard-accessible **More task
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**, **Mark completed**, and **Mark failed** for
493
- eligible tasks, plus **Archive chat** when archival is eligible. Opening the
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "taskchef",
3
- "version": "7.21.1",
3
+ "version": "7.22.0",
4
4
  "description": "A non-blocking interactive dispatcher for visible Codex tasks.",
5
5
  "license": "MIT",
6
6
  "author": "Favo Yang",
@@ -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 = !eligible;
526
- elements.markTaskFailed.hidden = !eligible;
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 (!task || !canManuallyTransitionTask(task) || manualTransitionPending()) return;
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
@@ -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 canArchiveTask(task) {
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 canManuallyTransitionTask(task) {
51
- return ["working", "needs_input"].includes(task.status);
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(["working", "needs_input"]);
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
- : predecessor === null || predecessor.result?.status === "interrupted";
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 === "needs_input"
1052
- ? predecessor.result.updatedAt
1053
- : predecessor?.startedAt ?? null;
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 (!MANUAL_TRANSITION_STATUSES.has(dispatch.status)) {
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}`,