taskchef 7.21.2 → 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.2",
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,14 +273,15 @@ 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. The menu disclosure is the deliberate first
278
- step; choosing a terminal outcome submits it immediately without a second
279
- 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
 
package/docs/spec.md CHANGED
@@ -483,14 +483,15 @@ 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. **Archive chat** MUST remain hidden while the archive capability
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
494
495
  gate is disabled because the bundled CLI does not reliably archive desktop-app
495
496
  threads. The dormant server endpoint MUST reject requests before discovering or
496
497
  invoking the CLI. Re-enabling requires a reliable supported app-callable archive
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "taskchef",
3
- "version": "7.21.2",
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
@@ -57,8 +57,15 @@ export function canArchiveTask(task) {
57
57
  return CODEX_CHAT_ARCHIVE_ENABLED && isArchiveTaskEligible(task);
58
58
  }
59
59
 
60
- export function canManuallyTransitionTask(task) {
61
- return ["working", "needs_input"].includes(task.status);
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);
62
69
  }
63
70
 
64
71
  export function manualTransitionExpectedState(task) {
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}`,