taskchef 8.7.1 → 9.0.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.
Files changed (149) hide show
  1. package/.codex-plugin/plugin.json +7 -10
  2. package/.mcp.json +3 -2
  3. package/README.md +31 -158
  4. package/docs/mcp-app.md +3 -3
  5. package/docs/spec.md +38 -746
  6. package/docs/taskchef-next-storage.md +2 -2
  7. package/index.js +2 -99
  8. package/package.json +12 -49
  9. package/skills/delegate/SKILL.md +44 -0
  10. package/skills/delegate/agents/openai.yaml +4 -0
  11. package/src/codex-app.js +3 -152
  12. package/src/codex-session-scanner.js +2 -2
  13. package/src/file-lock.js +25 -0
  14. package/src/mcp-app/react/NextTaskDetail.test.tsx +1 -1
  15. package/src/mcp-app/react/NextTaskDetail.tsx +8 -8
  16. package/src/mcp-app/react/main.test.tsx +3 -3
  17. package/src/mcp-app/react/main.tsx +7 -7
  18. package/src/mcp-app.js +2 -2
  19. package/src/mcp.js +4 -542
  20. package/src/next-cache.js +2 -2
  21. package/src/next-github.js +3 -3
  22. package/src/next-notifications.js +2 -2
  23. package/src/{dashboard/react → ui}/components/CardStatusIcons.tsx +1 -1
  24. package/src/{dashboard/react → ui}/components/GitHubLinks.tsx +1 -1
  25. package/src/{dashboard/react → ui}/components/LinkedText.tsx +1 -1
  26. package/src/{dashboard/react → ui}/components/RelativeTime.test.tsx +1 -1
  27. package/src/{dashboard/react → ui}/components/RelativeTime.tsx +1 -1
  28. package/src/{dashboard/react → ui}/components/ReplyImagePreview.test.tsx +1 -1
  29. package/src/{dashboard/react → ui}/components/TaskBoard.tsx +1 -1
  30. package/src/{dashboard/react → ui}/components/TaskCard.tsx +1 -1
  31. package/src/{dashboard/react → ui}/components/TurnContent.tsx +1 -1
  32. package/src/{dashboard/react → ui}/presentation.ts +1 -1
  33. package/src/version.js +0 -1
  34. package/BACKLOG.md +0 -48
  35. package/assets/e2e-benchmark-example.json +0 -50
  36. package/assets/taskchef-dispatcher-instructions.md +0 -44
  37. package/bin/taskchef.js +0 -20
  38. package/docs/advanced-guide.md +0 -497
  39. package/docs/dashboard-lifecycle.md +0 -149
  40. package/docs/dashboard-ui.md +0 -40
  41. package/docs/firstmate-taskchef-comparison.md +0 -283
  42. package/docs/images/ccusage-token-consumption.png +0 -0
  43. package/docs/images/dashboard-identity.jpg +0 -0
  44. package/docs/images/dashboard-token-pending.jpg +0 -0
  45. package/docs/images/interrupted-turn-recovery.jpg +0 -0
  46. package/docs/images/notification-event-snapshots.jpg +0 -0
  47. package/docs/images/readme-board.png +0 -0
  48. package/docs/images/readme-completed.png +0 -0
  49. package/docs/images/readme-list.png +0 -0
  50. package/docs/images/readme-needs-input.png +0 -0
  51. package/docs/images/readme-roles.png +0 -0
  52. package/docs/images/reported-work-duration-desktop.jpg +0 -0
  53. package/docs/images/reported-work-duration-mobile.jpg +0 -0
  54. package/docs/images/result-history-dashboard.jpg +0 -0
  55. package/docs/images/taskchef-pr-icon-comparison.jpg +0 -0
  56. package/docs/images/taskchef-unified-status-popup.jpg +0 -0
  57. package/docs/workflows.md +0 -476
  58. package/mcp/dashboard-session.js +0 -51
  59. package/scripts/benchmark-dispatch-prepare.js +0 -90
  60. package/scripts/e2e-benchmark.js +0 -206
  61. package/scripts/roles/resolve_roles.py +0 -293
  62. package/skills/taskchef-bootstrap/SKILL.md +0 -96
  63. package/skills/taskchef-bootstrap/agents/openai.yaml +0 -4
  64. package/skills/taskchef-bootstrap/references/project-index.md +0 -154
  65. package/skills/taskchef-copilot/SKILL.md +0 -75
  66. package/skills/taskchef-copilot/agents/openai.yaml +0 -4
  67. package/skills/taskchef-dashboard/SKILL.md +0 -24
  68. package/skills/taskchef-dashboard/agents/openai.yaml +0 -4
  69. package/skills/taskchef-delegate/SKILL.md +0 -102
  70. package/skills/taskchef-delegate/agents/openai.yaml +0 -4
  71. package/skills/taskchef-delegate/references/model-roles.md +0 -29
  72. package/skills/taskchef-executor/SKILL.md +0 -154
  73. package/skills/taskchef-executor/agents/openai.yaml +0 -4
  74. package/skills/taskchef-executor/references/compatibility.md +0 -22
  75. package/skills/taskchef-executor/references/ending-actions.md +0 -26
  76. package/src/backups.js +0 -359
  77. package/src/cli.js +0 -733
  78. package/src/config-audit.js +0 -99
  79. package/src/config-mutations.js +0 -106
  80. package/src/dashboard/actions.js +0 -136
  81. package/src/dashboard/app.js +0 -1172
  82. package/src/dashboard/dist/app.js +0 -269
  83. package/src/dashboard/dist/app.js.map +0 -1
  84. package/src/dashboard/dist/index.html +0 -16
  85. package/src/dashboard/dist/styles.css +0 -1
  86. package/src/dashboard/index.html +0 -148
  87. package/src/dashboard/react/App.test.tsx +0 -346
  88. package/src/dashboard/react/App.tsx +0 -515
  89. package/src/dashboard/react/api.test.ts +0 -40
  90. package/src/dashboard/react/api.ts +0 -103
  91. package/src/dashboard/react/components/ActivityTimeline.tsx +0 -118
  92. package/src/dashboard/react/components/ModelSettings.test.tsx +0 -180
  93. package/src/dashboard/react/components/ModelSettings.tsx +0 -106
  94. package/src/dashboard/react/components/NotificationCenter.test.tsx +0 -76
  95. package/src/dashboard/react/components/NotificationCenter.tsx +0 -111
  96. package/src/dashboard/react/components/PullRequestBadges.tsx +0 -18
  97. package/src/dashboard/react/components/TaskDetail.test.tsx +0 -255
  98. package/src/dashboard/react/components/TaskDetail.tsx +0 -271
  99. package/src/dashboard/react/components/UsagePanel.tsx +0 -96
  100. package/src/dashboard/react/index.html +0 -15
  101. package/src/dashboard/react/main.tsx +0 -12
  102. package/src/dashboard/react/overlay-state.test.ts +0 -110
  103. package/src/dashboard/react/overlay-state.ts +0 -60
  104. package/src/dashboard/react/presentation.test.tsx +0 -571
  105. package/src/dashboard/styles.css +0 -261
  106. package/src/dashboard-control.js +0 -34
  107. package/src/dashboard-manager.js +0 -339
  108. package/src/dashboard-session-process.js +0 -88
  109. package/src/dashboard-session.js +0 -86
  110. package/src/dashboard.js +0 -1303
  111. package/src/delegation.js +0 -277
  112. package/src/github.js +0 -107
  113. package/src/model-roles.js +0 -70
  114. package/src/recorded-usage-tracker.js +0 -92
  115. package/src/usage-tracker.js +0 -650
  116. package/src/usage.js +0 -367
  117. package/src/workspace-path.js +0 -48
  118. package/src/workspace.js +0 -3629
  119. /package/src/{dashboard/react → ui}/components/AnimatedUsage.test.tsx +0 -0
  120. /package/src/{dashboard/react → ui}/components/CardStatusIcons.test.tsx +0 -0
  121. /package/src/{dashboard/react → ui}/components/ImageGallery.tsx +0 -0
  122. /package/src/{dashboard/react → ui}/components/OpenChatButton.test.tsx +0 -0
  123. /package/src/{dashboard/react → ui}/components/OpenChatButton.tsx +0 -0
  124. /package/src/{dashboard/react → ui}/components/PrGlyph.tsx +0 -0
  125. /package/src/{dashboard/react → ui}/components/QueuedPrompts.test.tsx +0 -0
  126. /package/src/{dashboard/react → ui}/components/QueuedPrompts.tsx +0 -0
  127. /package/src/{dashboard/react → ui}/components/ReplyCover.test.tsx +0 -0
  128. /package/src/{dashboard/react → ui}/components/ReplyCover.tsx +0 -0
  129. /package/src/{dashboard/react → ui}/components/ReplyImagePreview.tsx +0 -0
  130. /package/src/{dashboard/react → ui}/components/ReplyMarkdown.test.tsx +0 -0
  131. /package/src/{dashboard/react → ui}/components/ReplyMarkdown.tsx +0 -0
  132. /package/src/{dashboard/react → ui}/components/SavedUsageFormat.test.tsx +0 -0
  133. /package/src/{dashboard/react → ui}/components/SavedUsageFormat.tsx +0 -0
  134. /package/src/{dashboard/react → ui}/components/ShimmerText.tsx +0 -0
  135. /package/src/{dashboard/react → ui}/components/StatsGrid.tsx +0 -0
  136. /package/src/{dashboard/react → ui}/components/StatusBadge.tsx +0 -0
  137. /package/src/{dashboard/react → ui}/components/TaskBoard.test.tsx +0 -0
  138. /package/src/{dashboard/react → ui}/components/TaskCardStats.test.tsx +0 -0
  139. /package/src/{dashboard/react → ui}/components/TaskCardStats.tsx +0 -0
  140. /package/src/{dashboard/react → ui}/components/TurnContent.test.tsx +0 -0
  141. /package/src/{dashboard/react → ui}/fixtures.ts +0 -0
  142. /package/src/{dashboard → ui}/github-links.js +0 -0
  143. /package/src/{dashboard/react → ui}/image-cache.test.ts +0 -0
  144. /package/src/{dashboard/react → ui}/image-cache.ts +0 -0
  145. /package/src/{dashboard → ui}/state.js +0 -0
  146. /package/src/{dashboard/react → ui}/styles.css +0 -0
  147. /package/src/{dashboard/react → ui}/test-setup.ts +0 -0
  148. /package/src/{dashboard → ui}/time.js +0 -0
  149. /package/src/{dashboard/react → ui}/types.ts +0 -0
@@ -161,7 +161,7 @@ Board disk caches share the state directory's `cache/` folder:
161
161
  The reader moves older caches into these locations on first use. It preserves
162
162
  an existing destination. Settings, Done marks, and authentication data remain
163
163
  outside `cache/` because they are persistent state, not rebuildable caches.
164
- The web dispatcher keeps its workspace-specific `.taskchef-usage.json` projection
165
- file for compatibility with existing task records.
164
+ Legacy dispatcher workspaces and `.taskchef-usage.json` files remain on disk
165
+ for historical reference; the native product does not read or update them.
166
166
 
167
167
  Usage caches retain normalized token counters for known rollout sources, without message content. If an older source disappears, those counters are reconciled with live counters so new usage continues to increase the known subtotal.
package/index.js CHANGED
@@ -1,99 +1,2 @@
1
- export {
2
- buildCopilotBrief,
3
- addProject,
4
- createWorkspaceBackup,
5
- buildTaskSummary,
6
- canonicalDirectory,
7
- canonicalGitRoot,
8
- doctorWorkspace,
9
- dashboardAutostartEnabled,
10
- ensureWorkspaceInstructions,
11
- filterTasks,
12
- importProjects,
13
- includeProject,
14
- initializeWorkspace,
15
- linkTask,
16
- listProjects,
17
- listWorkspaceBackups,
18
- pruneWorkspaceBackups,
19
- readProjectIndex,
20
- readWorkspaceMaintenance,
21
- manuallyTransitionTask,
22
- migrateTaskLog,
23
- prepareDispatch,
24
- reconcileProjects,
25
- readConfig,
26
- listTasks,
27
- readTask,
28
- recordTask,
29
- reportTaskState,
30
- reportTaskResult,
31
- requireSafeId,
32
- removeProject,
33
- restoreWorkspaceBackup,
34
- validateConfig,
35
- updateProject,
36
- updateProjectHint,
37
- verifyWorkspaceBackup,
38
- } from "./src/workspace.js";
39
-
40
- export {
41
- configHash,
42
- projectDiff,
43
- projectSetHash,
44
- } from "./src/config-mutations.js";
45
-
46
- export {
47
- canonicalGithubRepository,
48
- matchProjectForGithubUrl,
49
- normalizeGithubRepositories,
50
- } from "./src/github.js";
51
-
52
- export {
53
- EXECUTOR_OWNERSHIP_PARAGRAPH,
54
- EXECUTOR_LINK_PARAGRAPH,
55
- EXECUTOR_RESULT_PARAGRAPH,
56
- EXECUTOR_WORKING_PARAGRAPH,
57
- EXECUTOR_SKILL_INVOCATION,
58
- EXECUTOR_REPORTING_AUTHORIZATION,
59
- createAndRecordDelegation,
60
- isProvisionalThreadId,
61
- normalizeCodexThreadId,
62
- normalizeDurableThreadId,
63
- parseTaskChefMarker,
64
- prepareDelegation,
65
- taskChefMarker,
66
- } from "./src/delegation.js";
67
-
68
- export {
69
- TASKCHEF_WORKSPACE_ENV,
70
- defaultWorkspacePath,
71
- resolveWorkspacePath,
72
- } from "./src/workspace-path.js";
73
-
74
- export {
75
- discoverCodexCli,
76
- isCodexThreadDeepLinkId,
77
- openThreadInCodex,
78
- openWorkspaceInCodex,
79
- } from "./src/codex-app.js";
80
-
81
- export {
82
- DASHBOARD_HEALTH_MAX_BYTES,
83
- DASHBOARD_HEALTH_PATH,
84
- DashboardMonitor,
85
- UsageSummaryMonitor,
86
- createDashboardServer,
87
- dashboardAuthority,
88
- sortTasksByMeaningfulUpdate,
89
- taskListUsageProjection,
90
- } from "./src/dashboard.js";
91
-
92
- export {
93
- createDashboardManager,
94
- readDashboardIdentity,
95
- } from "./src/dashboard-manager.js";
96
-
97
- export { DASHBOARD_SERVER_VERSION, TASKCHEF_VERSION } from "./src/version.js";
98
-
99
- export { createDashboardAutostart, createTaskChefMcpServer } from "./src/mcp.js";
1
+ export { createTaskChefMcpServer } from "./src/mcp.js";
2
+ export { TASKCHEF_APP_NAME, TASKCHEF_VERSION } from "./src/version.js";
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "taskchef",
3
- "version": "8.7.1",
4
- "description": "A non-blocking interactive dispatcher for visible Codex tasks.",
3
+ "version": "9.0.0",
4
+ "description": "A native Codex dashboard for chats, usage, pull requests, and schedules.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Favo Yang",
7
7
  "repository": {
@@ -14,59 +14,27 @@
14
14
  "homepage": "https://github.com/favoyang/taskchef#readme",
15
15
  "type": "module",
16
16
  "exports": "./index.js",
17
- "bin": {
18
- "taskchef": "bin/taskchef.js"
19
- },
20
17
  "files": [
21
18
  ".codex-plugin",
22
19
  ".mcp.json",
23
20
  "assets",
24
- "BACKLOG.md",
25
- "bin",
26
- "docs/advanced-guide.md",
27
- "docs/spec.md",
28
- "docs/workflows.md",
29
- "docs/dashboard-lifecycle.md",
30
- "docs/dashboard-ui.md",
31
21
  "docs/mcp-app.md",
22
+ "docs/spec.md",
32
23
  "docs/taskchef-next-storage.md",
33
- "docs/diagrams/taskchef-next-storage.drawio",
34
- "docs/diagrams/taskchef-next-storage.drawio.png",
35
- "docs/diagrams/taskchef-next-reply-lookup.drawio",
36
- "docs/diagrams/taskchef-next-reply-lookup.drawio.png",
37
- "docs/taskchef-next-status-research.md",
38
- "docs/firstmate-taskchef-comparison.md",
39
- "docs/images/dashboard-identity.jpg",
40
- "docs/images/taskchef-unified-status-popup.jpg",
41
- "docs/images/taskchef-pr-icon-comparison.jpg",
42
- "docs/images/ccusage-token-consumption.png",
43
- "docs/images/notification-event-snapshots.jpg",
44
- "docs/images/result-history-dashboard.jpg",
45
- "docs/images/interrupted-turn-recovery.jpg",
46
- "docs/images/dashboard-token-pending.jpg",
47
- "docs/images/reported-work-duration-desktop.jpg",
48
- "docs/images/reported-work-duration-mobile.jpg",
49
- "docs/images/readme-board.png",
50
- "docs/images/readme-list.png",
51
- "docs/images/readme-needs-input.png",
52
- "docs/images/readme-completed.png",
53
- "docs/images/readme-roles.png",
24
+ "docs/taskchef-next-github.md",
54
25
  "index.js",
55
26
  "mcp",
56
- "scripts/roles",
57
- "scripts/benchmark-dispatch-prepare.js",
58
- "scripts/e2e-benchmark.js",
27
+ "scripts/prepare-github-keyring.js",
59
28
  "src",
60
- "skills/taskchef-bootstrap",
61
- "skills/taskchef-dashboard",
62
- "skills/taskchef-delegate",
63
- "skills/taskchef-executor",
64
- "skills/taskchef-copilot",
65
- "docs/taskchef-next-github.md",
66
- "scripts/prepare-github-keyring.js"
29
+ "skills/delegate",
30
+ "docs/taskchef-next-status-research.md",
31
+ "docs/diagrams/taskchef-next-storage.drawio",
32
+ "docs/diagrams/taskchef-next-storage.drawio.png",
33
+ "docs/diagrams/taskchef-next-reply-lookup.drawio",
34
+ "docs/diagrams/taskchef-next-reply-lookup.drawio.png"
67
35
  ],
68
36
  "engines": {
69
- "node": ">=18"
37
+ "node": "^22.18.0 || >=23.2.0"
70
38
  },
71
39
  "devDependencies": {
72
40
  "@modelcontextprotocol/ext-apps": "^1.1.2",
@@ -87,14 +55,9 @@
87
55
  "yaml": "^2.9.0"
88
56
  },
89
57
  "scripts": {
90
- "benchmark:dispatch": "node scripts/benchmark-dispatch-prepare.js",
91
- "benchmark:e2e": "node scripts/e2e-benchmark.js",
92
- "build:dashboard": "vite build --config vite.config.ts",
93
58
  "build:mcp-app": "vite build --config vite.mcp-app.config.ts",
94
59
  "update:mcp-app-preview": "npm run build:mcp-app && node scripts/update-mcp-app-preview.js",
95
- "check:dashboard": "tsc --noEmit -p tsconfig.dashboard.json",
96
60
  "check:mcp-app": "tsc --noEmit -p tsconfig.mcp-app.json",
97
- "preview:dashboard": "node scripts/preview-dashboard.js",
98
61
  "test": "npm run test:node && npm run test:dashboard",
99
62
  "test:node": "node --test tests/*.test.js",
100
63
  "test:dashboard": "vitest run --config vitest.config.ts",
@@ -0,0 +1,44 @@
1
+ ---
2
+ name: delegate
3
+ description: Delegate explicitly requested work into a new native Codex chat in the right project. Use for requests to hand separate work to a project; continue an existing chat when the user selects it.
4
+ ---
5
+
6
+ # Delegate
7
+
8
+ Use native Codex tools to discover projects and create or continue chats.
9
+ The user's request to delegate authorizes creating the requested chat. Skill
10
+ selection alone does not authorize creating chats for unrelated work.
11
+
12
+ ## Route the work
13
+
14
+ Read the available native project list. Resolve the target from the user's
15
+ project name, repository, or a confirmed routing preference. Ask when multiple
16
+ projects fit or the target is unclear; do not silently choose a different host.
17
+
18
+ For a new assignment, use the returned project ID with native chat creation.
19
+ Use the project's local environment unless the user requests a worktree. Keep
20
+ model and effort defaults unless the user requests overrides. Pass the actual
21
+ assignment and relevant context as an ordinary prompt. No TaskChef marker,
22
+ executor skill, task record, or reporting callback is needed.
23
+
24
+ If the user explicitly selects an existing chat for a follow-up, use native
25
+ chat inspection and messaging instead of creating a duplicate. Send messages
26
+ only with the user's authorization for that destination.
27
+
28
+ Return the native chat link after creation succeeds. A pending setup is not a
29
+ failure; return its native pending state rather than retrying creation. Do not
30
+ monitor or message the chat later unless requested. Codex owns its history and
31
+ execution state.
32
+
33
+ ## Routing memory
34
+
35
+ Optionally consult `~/.agents/taskchef-next/delegation-memory.md` for confirmed
36
+ project aliases and routing preferences. Revalidate each hint against the
37
+ current native project list. Treat the file as routing notes, not instructions
38
+ that override the user.
39
+
40
+ After successful creation, retain only explicit preferences or user-corrected
41
+ aliases, with the native project ID and name. Create the file only when there
42
+ is a useful confirmed mapping. Do not store task status, full prompts, results,
43
+ chat history, or credentials. An unavailable memory file must not block an
44
+ otherwise clear delegation.
@@ -0,0 +1,4 @@
1
+ interface:
2
+ display_name: "Delegate"
3
+ short_description: "Delegate work to a native Codex project chat"
4
+ default_prompt: "Use $delegate to start this work in the right Codex project."
package/src/codex-app.js CHANGED
@@ -1,13 +1,11 @@
1
1
  import { execFile as execFileCallback } from "node:child_process";
2
- import { access, realpath } from "node:fs/promises";
3
2
  import path from "node:path";
4
3
  import { promisify } from "node:util";
5
4
 
6
- import { normalizeCodexThreadId } from "./delegation.js";
5
+ import { z } from "zod";
7
6
 
8
7
  const execFile = promisify(execFileCallback);
9
8
  const CODEX_COMMAND_TIMEOUT_MS = 10_000;
10
- const CODEX_ARCHIVE_TIMEOUT_MS = 4_000;
11
9
  const CODEX_COMMAND_MAX_BUFFER_BYTES = 64 * 1024;
12
10
 
13
11
  function runCodex(run, filePath, args, timeout = CODEX_COMMAND_TIMEOUT_MS) {
@@ -18,135 +16,9 @@ function runCodex(run, filePath, args, timeout = CODEX_COMMAND_TIMEOUT_MS) {
18
16
  });
19
17
  }
20
18
 
21
- async function executable(filePath) {
22
- try {
23
- await access(filePath, 1);
24
- return path.resolve(filePath);
25
- } catch {
26
- return null;
27
- }
28
- }
29
-
30
- function pathCandidates(env, platform = process.platform) {
31
- return (env.PATH ?? "")
32
- .split(path.delimiter)
33
- .filter(Boolean)
34
- .map((directory) => path.join(directory, platform === "win32" ? "codex.exe" : "codex"));
35
- }
36
-
37
- function isDesktopBundleCandidate(filePath) {
38
- if (path.basename(filePath) !== "codex") return false;
39
- const resources = path.dirname(filePath);
40
- const contents = path.dirname(resources);
41
- const application = path.dirname(contents);
42
- return path.basename(resources) === "Resources"
43
- && path.basename(contents) === "Contents"
44
- && new Set(["ChatGPT.app", "Codex.app"]).has(path.basename(application));
45
- }
46
-
47
- function defaultDesktopBundleCandidates(_env, platform) {
48
- if (platform !== "darwin") return [];
49
- return [
50
- "/Applications/ChatGPT.app/Contents/Resources/codex",
51
- "/Applications/Codex.app/Contents/Resources/codex",
52
- ];
53
- }
54
-
55
- async function supportsAppCommand(filePath, run) {
56
- try {
57
- const { stdout, stderr } = await runCodex(run, filePath, ["app", "--help"]);
58
- return /(?:^|\n)Usage:\s+codex\s+app(?:\s|$)/.test(`${stdout}\n${stderr}`);
59
- } catch {
60
- return false;
61
- }
62
- }
63
-
64
- async function supportsArchiveCommand(filePath, run) {
65
- try {
66
- const { stdout, stderr } = await runCodex(run, filePath, ["archive", "--help"]);
67
- return /(?:^|\n)Usage:\s+codex\s+archive(?:\s|$)/.test(`${stdout}\n${stderr}`);
68
- } catch {
69
- return false;
70
- }
71
- }
72
-
73
- export async function discoverBundledCodexCli({
74
- candidates = null,
75
- env = process.env,
76
- platform = process.platform,
77
- run = execFile,
78
- trustedBundlePaths = null,
79
- } = {}) {
80
- const trustedPaths = trustedBundlePaths ?? defaultDesktopBundleCandidates(env, platform);
81
- const trustedCandidates = new Set(trustedPaths.map((candidate) => path.resolve(candidate)));
82
- const bundledCandidates = candidates ?? trustedPaths;
83
- for (const filePath of new Set(bundledCandidates.map((candidate) => path.resolve(candidate)))) {
84
- const executableCandidate = await executable(filePath);
85
- const candidate = executableCandidate === null ? null : await realpath(executableCandidate);
86
- if (platform === "darwin" && candidate && trustedCandidates.has(candidate)
87
- && isDesktopBundleCandidate(candidate)
88
- && await supportsArchiveCommand(candidate, run)) {
89
- return { path: candidate, source: "desktop-bundle" };
90
- }
91
- }
92
- throw new Error("Chat archiving requires the Codex CLI bundled with the ChatGPT or Codex desktop app");
93
- }
94
-
95
- export async function discoverCodexCli({
96
- explicit = null,
97
- env = process.env,
98
- run = execFile,
99
- } = {}) {
100
- const override = explicit ?? env.TASKCHEF_CODEX_CLI ?? null;
101
- if (override !== null) {
102
- const candidate = await executable(path.resolve(override));
103
- if (!candidate) throw new Error(`Codex CLI is not executable: ${override}`);
104
- if (!(await supportsAppCommand(candidate, run))) {
105
- throw new Error(`Codex CLI does not support the app command: ${candidate}`);
106
- }
107
- return { path: candidate, source: explicit !== null ? "explicit" : "environment" };
108
- }
109
-
110
- const candidates = pathCandidates(env);
111
- for (const pathCandidate of candidates.filter((candidate) => isDesktopBundleCandidate(candidate))) {
112
- const candidate = await executable(pathCandidate);
113
- if (candidate && await supportsAppCommand(candidate, run)) {
114
- return { path: candidate, source: "desktop-path" };
115
- }
116
- }
117
-
118
- let candidate = null;
119
- for (const pathCandidate of candidates) {
120
- candidate = await executable(pathCandidate);
121
- if (candidate) break;
122
- }
123
- if (!candidate) throw new Error("Codex CLI was not found in PATH; pass --codex-cli");
124
- if (!(await supportsAppCommand(candidate, run))) {
125
- throw new Error(`Codex CLI does not support the app command: ${candidate}`);
126
- }
127
- return { path: candidate, source: "path" };
128
- }
129
-
130
- export async function openWorkspaceInCodex(workspace, options = {}) {
131
- const run = options.run ?? execFile;
132
- const cli = await discoverCodexCli({ ...options, run });
133
- await runCodex(run, cli.path, ["app", workspace]);
134
- return {
135
- status: "requested",
136
- mechanism: "codex-app",
137
- codexCli: cli.path,
138
- codexCliSource: cli.source,
139
- workspace,
140
- };
141
- }
142
-
19
+ // MCP callers can supply IDs; validate only the UUID boundary, not its version.
143
20
  export function isCodexThreadDeepLinkId(threadId) {
144
- try {
145
- normalizeCodexThreadId(threadId);
146
- return true;
147
- } catch {
148
- return false;
149
- }
21
+ return z.string().uuid().safeParse(threadId).success;
150
22
  }
151
23
 
152
24
  export async function openThreadInCodex(threadId, options = {}) {
@@ -183,24 +55,3 @@ async function openDesktopLink(url, options) {
183
55
  await runCodex(run, "xdg-open", [url]);
184
56
  }
185
57
  }
186
-
187
- export async function archiveThreadInCodex(threadId, options = {}) {
188
- if (!isCodexThreadDeepLinkId(threadId)) {
189
- throw new Error("Codex thread ID is not supported by chat archiving");
190
- }
191
- const run = options.run ?? execFile;
192
- const cli = options.cli ?? await discoverBundledCodexCli({ run });
193
- await runCodex(
194
- run,
195
- cli.path,
196
- ["archive", threadId],
197
- options.timeoutMs ?? CODEX_ARCHIVE_TIMEOUT_MS,
198
- );
199
- return {
200
- status: "archived",
201
- mechanism: "codex-archive-cli",
202
- codexCli: cli.path,
203
- codexCliSource: cli.source,
204
- threadId,
205
- };
206
- }
@@ -8,7 +8,7 @@ import { replyImage, replyImages, localReplyImage } from "./reply-image.js";
8
8
  import { homedir } from "node:os";
9
9
  import { basename, isAbsolute, join } from "node:path";
10
10
  import { open, stat, readFile, readdir, mkdir } from "node:fs/promises";
11
- import { acquireWorkspaceLock } from "./workspace.js";
11
+ import { acquireFileLock } from "./file-lock.js";
12
12
  import { writeDurableAtomic } from "./state-store.js";
13
13
  import { dirname } from "node:path";
14
14
  import { parse as parseToml } from "smol-toml";
@@ -718,7 +718,7 @@ export class CodexSessionScanner {
718
718
  if (!scheduled && done && task.pullRequests?.some(pr => !mergedPrUrls.includes(pr.url))) throw new Error("Chats with attached PRs require confirmed merge status before marking Done.");
719
719
  if (task.observed.archive || task.observed.lastTurnEvent === "inProgress") throw new Error("Archived or in-progress chats cannot be marked from TaskChef.");
720
720
  await mkdir(dirname(this.statePath), { recursive: true, mode: 0o700 });
721
- const release = await acquireWorkspaceLock(dirname(this.statePath));
721
+ const release = await acquireFileLock(dirname(this.statePath));
722
722
  try {
723
723
  const marks = await readDoneMarks(this.statePath);
724
724
  const key = scheduled ? `scheduled:${id}` : id;
@@ -0,0 +1,25 @@
1
+ import path from "node:path";
2
+ import lockfile from "proper-lockfile";
3
+
4
+ // Keep the existing lock filename and timing for shared native state.
5
+ export async function acquireFileLock(workspaceRoot, {
6
+ lock = lockfile.lock,
7
+ waitImpl = (delayMs) => new Promise((resolve) => setTimeout(resolve, delayMs)),
8
+ } = {}) {
9
+ const lockPath = path.join(workspaceRoot, ".taskchef-workspace.lock");
10
+ for (let attempt = 0; attempt <= 70; attempt += 1) {
11
+ try {
12
+ return await lock(workspaceRoot, {
13
+ realpath: false,
14
+ lockfilePath: lockPath,
15
+ stale: 600_000,
16
+ update: 10_000,
17
+ retries: 0,
18
+ });
19
+ } catch (error) {
20
+ if (error.code !== "ELOCKED" || attempt === 70) throw error;
21
+ await waitImpl(100);
22
+ }
23
+ }
24
+ throw new Error("workspace lock retry loop ended unexpectedly");
25
+ }
@@ -1,7 +1,7 @@
1
1
  import { MantineProvider } from "@mantine/core";
2
2
  import { cleanup, fireEvent, render, screen, within } from "@testing-library/react";
3
3
  import { afterEach, expect, test, vi } from "vitest";
4
- import { fixtureTask } from "../../dashboard/react/fixtures";
4
+ import { fixtureTask } from "../../ui/fixtures";
5
5
  import { NextTaskDetail } from "./NextTaskDetail";
6
6
 
7
7
  afterEach(cleanup);
@@ -2,14 +2,14 @@ import { Alert, Anchor, Badge, Box, Drawer, Group, Modal, ScrollArea, Stack, Tex
2
2
  import { useMediaQuery } from "@mantine/hooks";
3
3
  import { IconArrowUpRight, IconClock, IconFolder, IconHelpCircle, IconHourglass, IconRobot } from "@tabler/icons-react";
4
4
  import { useCallback, useEffect, useState, type ReactNode } from "react";
5
- import type { Task, SavedUsage } from "../../dashboard/react/types";
6
- import { PullRequestInfo, nextRunLabel } from "../../dashboard/react/components/CardStatusIcons";
7
- import { OpenChatButton } from "../../dashboard/react/components/OpenChatButton";
8
- import { QueuedPrompts } from "../../dashboard/react/components/QueuedPrompts";
9
- import { TurnContent } from "../../dashboard/react/components/TurnContent";
10
- import { compactTokens, TokenBreakdown, useAnimatedUsage } from "../../dashboard/react/components/SavedUsageFormat";
11
- import { GitHubLinks } from "../../dashboard/react/components/GitHubLinks";
12
- import { formatWorkedDuration } from "../../dashboard/time.js";
5
+ import type { Task, SavedUsage } from "../../ui/types";
6
+ import { PullRequestInfo, nextRunLabel } from "../../ui/components/CardStatusIcons";
7
+ import { OpenChatButton } from "../../ui/components/OpenChatButton";
8
+ import { QueuedPrompts } from "../../ui/components/QueuedPrompts";
9
+ import { TurnContent } from "../../ui/components/TurnContent";
10
+ import { compactTokens, TokenBreakdown, useAnimatedUsage } from "../../ui/components/SavedUsageFormat";
11
+ import { GitHubLinks } from "../../ui/components/GitHubLinks";
12
+ import { formatWorkedDuration } from "../../ui/time.js";
13
13
 
14
14
  export function NextTaskDetail({ task, opened, busy, usageLoading = false, error, onClose, onOpenCodex, extraActions, loadImage }: {
15
15
  task: Task | null; opened: boolean; busy: boolean; usageLoading?: boolean; error: string | null;
@@ -2,7 +2,7 @@ import { afterEach, beforeEach, expect, test, vi } from "vitest";
2
2
  import "@testing-library/jest-dom/vitest";
3
3
  import { act, fireEvent, render, screen, waitFor, within, cleanup } from "@testing-library/react";
4
4
  import { useState } from "react";
5
- import type { Task } from "../../dashboard/react/types";
5
+ import type { Task } from "../../ui/types";
6
6
 
7
7
  const server = vi.hoisted(() => ({ call: vi.fn(), openLink: vi.fn(), displayMode: "unknown", hostContextChanged: undefined as undefined | ((context: { displayMode: string }) => void), teardown: undefined as undefined | (() => object) }));
8
8
  vi.mock("./ProjectPicker", () => ({ ProjectPicker: ({ data, value, onChange }: { data: Array<{label: string; value: string}>; value: string; onChange: (value: string) => void }) => <select aria-label="Project" value={value} onChange={(event) => onChange(event.target.value)}>{data.map((item) => <option key={item.value} value={item.value}>{item.label}</option>)}</select> }));
@@ -32,10 +32,10 @@ vi.mock("@modelcontextprotocol/ext-apps", () => ({
32
32
  set onteardown(handler: () => object) { server.teardown = handler; }
33
33
  },
34
34
  }));
35
- vi.mock("../../dashboard/react/components/TaskCard", () => ({
35
+ vi.mock("../../ui/components/TaskCard", () => ({
36
36
  TaskCard: ({ task, onOpenCodex, onOpenDetail }: { task: Task; onOpenCodex: (task: Task) => void; onOpenDetail: (task: Task) => void }) => <article><button onClick={() => onOpenDetail(task)}>{task.title}</button><button onClick={() => onOpenCodex(task)}>Open chat for {task.title}</button></article>,
37
37
  }));
38
- vi.mock("../../dashboard/react/components/TaskBoard", () => ({
38
+ vi.mock("../../ui/components/TaskBoard", () => ({
39
39
  TaskBoard: ({ showCardUsage, tasks, onOpenDetail, onOpenCodex, doneNotice, onVisibleTasksChange }: { showCardUsage?: boolean; onVisibleTasksChange?: (ids: string[]) => void; doneNotice?: React.ReactNode; tasks: Task[]; onOpenDetail: (task: Task) => void; onOpenCodex: (task: Task) => void }) => <section aria-label="Task board" data-card-usage={String(showCardUsage)}><button onClick={() => onVisibleTasksChange?.([tasks[0].id])}>Report card in view</button>{doneNotice}{tasks.map((task) => <article key={task.id}><button onClick={() => onOpenDetail(task)}>{task.title}</button><button onClick={() => onOpenCodex(task)}>Open chat for {task.title}</button></article>)}</section>,
40
40
  }));
41
41
  vi.mock("./NextTaskDetail", () => ({
@@ -1,15 +1,15 @@
1
- import { cachedImageLoader } from "../../dashboard/react/image-cache";
1
+ import { cachedImageLoader } from "../../ui/image-cache";
2
2
  import { App } from "@modelcontextprotocol/ext-apps";
3
3
  import { ActionIcon, Alert, Box, Button, Group, MantineProvider, Modal, createTheme, Paper, SegmentedControl, Select, Stack, Text, TextInput, Title } from "@mantine/core";
4
4
  import { IconRefresh, IconSearch, IconSettings } from "@tabler/icons-react";
5
5
  import { createRoot } from "react-dom/client";
6
6
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
7
- import type { DashboardSnapshot, Task, Project } from "../../dashboard/react/types";
8
- import { filterTasks } from "../../dashboard/state.js";
9
- import { TaskBoard } from "../../dashboard/react/components/TaskBoard";
10
- import { TaskCard } from "../../dashboard/react/components/TaskCard";
7
+ import type { DashboardSnapshot, Task, Project } from "../../ui/types";
8
+ import { filterTasks } from "../../ui/state.js";
9
+ import { TaskBoard } from "../../ui/components/TaskBoard";
10
+ import { TaskCard } from "../../ui/components/TaskCard";
11
11
  import { NextTaskDetail } from "./NextTaskDetail";
12
- import { RelativeTimeProvider } from "../../dashboard/react/components/RelativeTime";
12
+ import { RelativeTimeProvider } from "../../ui/components/RelativeTime";
13
13
  import { NextNotificationCenter, type NextNotification, type NextNotificationState } from "./NextNotificationCenter";
14
14
  import { GitHubConnection, type GitHubAuth } from "./GitHubConnection";
15
15
  import { AppearancePicker } from "./AppearancePicker";
@@ -17,7 +17,7 @@ import { savedVisualTheme, themeVariables, visualTheme, VISUAL_THEME_KEY } from
17
17
  import { ProjectPicker } from "./ProjectPicker";
18
18
  import brandIcon from "../../../assets/taskchef-dark.svg";
19
19
  import "@mantine/core/styles.css";
20
- import "../../dashboard/react/styles.css";
20
+ import "../../ui/styles.css";
21
21
  import "./styles.css";
22
22
 
23
23
  const theme = createTheme({
package/src/mcp-app.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { mkdir, readFile } from "node:fs/promises";
2
2
  import { homedir } from "node:os";
3
3
  import { basename, dirname, isAbsolute, join } from "node:path";
4
- import { acquireWorkspaceLock } from "./workspace.js";
4
+ import { acquireFileLock } from "./file-lock.js";
5
5
  import { writeDurableAtomic } from "./state-store.js";
6
6
  import { fileURLToPath } from "node:url";
7
7
  import { parse as parseToml } from "smol-toml";
@@ -150,7 +150,7 @@ export function registerTaskChefApp(server, {
150
150
  annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false, idempotentHint: true },
151
151
  }, async ({ set }) => {
152
152
  await mkdir(dirname(settingsPath), { recursive: true, mode: 0o700 });
153
- const release = await acquireWorkspaceLock(dirname(settingsPath));
153
+ const release = await acquireFileLock(dirname(settingsPath));
154
154
  try {
155
155
  const values = { ...await readSettings(), ...set };
156
156
  await writeDurableAtomic(settingsPath, JSON.stringify(values));