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.
- package/.codex-plugin/plugin.json +7 -10
- package/.mcp.json +3 -2
- package/README.md +31 -158
- package/docs/mcp-app.md +3 -3
- package/docs/spec.md +38 -746
- package/docs/taskchef-next-storage.md +2 -2
- package/index.js +2 -99
- package/package.json +12 -49
- package/skills/delegate/SKILL.md +44 -0
- package/skills/delegate/agents/openai.yaml +4 -0
- package/src/codex-app.js +3 -152
- package/src/codex-session-scanner.js +2 -2
- package/src/file-lock.js +25 -0
- package/src/mcp-app/react/NextTaskDetail.test.tsx +1 -1
- package/src/mcp-app/react/NextTaskDetail.tsx +8 -8
- package/src/mcp-app/react/main.test.tsx +3 -3
- package/src/mcp-app/react/main.tsx +7 -7
- package/src/mcp-app.js +2 -2
- package/src/mcp.js +4 -542
- package/src/next-cache.js +2 -2
- package/src/next-github.js +3 -3
- package/src/next-notifications.js +2 -2
- package/src/{dashboard/react → ui}/components/CardStatusIcons.tsx +1 -1
- package/src/{dashboard/react → ui}/components/GitHubLinks.tsx +1 -1
- package/src/{dashboard/react → ui}/components/LinkedText.tsx +1 -1
- package/src/{dashboard/react → ui}/components/RelativeTime.test.tsx +1 -1
- package/src/{dashboard/react → ui}/components/RelativeTime.tsx +1 -1
- package/src/{dashboard/react → ui}/components/ReplyImagePreview.test.tsx +1 -1
- package/src/{dashboard/react → ui}/components/TaskBoard.tsx +1 -1
- package/src/{dashboard/react → ui}/components/TaskCard.tsx +1 -1
- package/src/{dashboard/react → ui}/components/TurnContent.tsx +1 -1
- package/src/{dashboard/react → ui}/presentation.ts +1 -1
- package/src/version.js +0 -1
- package/BACKLOG.md +0 -48
- package/assets/e2e-benchmark-example.json +0 -50
- package/assets/taskchef-dispatcher-instructions.md +0 -44
- package/bin/taskchef.js +0 -20
- package/docs/advanced-guide.md +0 -497
- package/docs/dashboard-lifecycle.md +0 -149
- package/docs/dashboard-ui.md +0 -40
- package/docs/firstmate-taskchef-comparison.md +0 -283
- package/docs/images/ccusage-token-consumption.png +0 -0
- package/docs/images/dashboard-identity.jpg +0 -0
- package/docs/images/dashboard-token-pending.jpg +0 -0
- package/docs/images/interrupted-turn-recovery.jpg +0 -0
- package/docs/images/notification-event-snapshots.jpg +0 -0
- package/docs/images/readme-board.png +0 -0
- package/docs/images/readme-completed.png +0 -0
- package/docs/images/readme-list.png +0 -0
- package/docs/images/readme-needs-input.png +0 -0
- package/docs/images/readme-roles.png +0 -0
- package/docs/images/reported-work-duration-desktop.jpg +0 -0
- package/docs/images/reported-work-duration-mobile.jpg +0 -0
- package/docs/images/result-history-dashboard.jpg +0 -0
- package/docs/images/taskchef-pr-icon-comparison.jpg +0 -0
- package/docs/images/taskchef-unified-status-popup.jpg +0 -0
- package/docs/workflows.md +0 -476
- package/mcp/dashboard-session.js +0 -51
- package/scripts/benchmark-dispatch-prepare.js +0 -90
- package/scripts/e2e-benchmark.js +0 -206
- package/scripts/roles/resolve_roles.py +0 -293
- package/skills/taskchef-bootstrap/SKILL.md +0 -96
- package/skills/taskchef-bootstrap/agents/openai.yaml +0 -4
- package/skills/taskchef-bootstrap/references/project-index.md +0 -154
- package/skills/taskchef-copilot/SKILL.md +0 -75
- package/skills/taskchef-copilot/agents/openai.yaml +0 -4
- package/skills/taskchef-dashboard/SKILL.md +0 -24
- package/skills/taskchef-dashboard/agents/openai.yaml +0 -4
- package/skills/taskchef-delegate/SKILL.md +0 -102
- package/skills/taskchef-delegate/agents/openai.yaml +0 -4
- package/skills/taskchef-delegate/references/model-roles.md +0 -29
- package/skills/taskchef-executor/SKILL.md +0 -154
- package/skills/taskchef-executor/agents/openai.yaml +0 -4
- package/skills/taskchef-executor/references/compatibility.md +0 -22
- package/skills/taskchef-executor/references/ending-actions.md +0 -26
- package/src/backups.js +0 -359
- package/src/cli.js +0 -733
- package/src/config-audit.js +0 -99
- package/src/config-mutations.js +0 -106
- package/src/dashboard/actions.js +0 -136
- package/src/dashboard/app.js +0 -1172
- package/src/dashboard/dist/app.js +0 -269
- package/src/dashboard/dist/app.js.map +0 -1
- package/src/dashboard/dist/index.html +0 -16
- package/src/dashboard/dist/styles.css +0 -1
- package/src/dashboard/index.html +0 -148
- package/src/dashboard/react/App.test.tsx +0 -346
- package/src/dashboard/react/App.tsx +0 -515
- package/src/dashboard/react/api.test.ts +0 -40
- package/src/dashboard/react/api.ts +0 -103
- package/src/dashboard/react/components/ActivityTimeline.tsx +0 -118
- package/src/dashboard/react/components/ModelSettings.test.tsx +0 -180
- package/src/dashboard/react/components/ModelSettings.tsx +0 -106
- package/src/dashboard/react/components/NotificationCenter.test.tsx +0 -76
- package/src/dashboard/react/components/NotificationCenter.tsx +0 -111
- package/src/dashboard/react/components/PullRequestBadges.tsx +0 -18
- package/src/dashboard/react/components/TaskDetail.test.tsx +0 -255
- package/src/dashboard/react/components/TaskDetail.tsx +0 -271
- package/src/dashboard/react/components/UsagePanel.tsx +0 -96
- package/src/dashboard/react/index.html +0 -15
- package/src/dashboard/react/main.tsx +0 -12
- package/src/dashboard/react/overlay-state.test.ts +0 -110
- package/src/dashboard/react/overlay-state.ts +0 -60
- package/src/dashboard/react/presentation.test.tsx +0 -571
- package/src/dashboard/styles.css +0 -261
- package/src/dashboard-control.js +0 -34
- package/src/dashboard-manager.js +0 -339
- package/src/dashboard-session-process.js +0 -88
- package/src/dashboard-session.js +0 -86
- package/src/dashboard.js +0 -1303
- package/src/delegation.js +0 -277
- package/src/github.js +0 -107
- package/src/model-roles.js +0 -70
- package/src/recorded-usage-tracker.js +0 -92
- package/src/usage-tracker.js +0 -650
- package/src/usage.js +0 -367
- package/src/workspace-path.js +0 -48
- package/src/workspace.js +0 -3629
- /package/src/{dashboard/react → ui}/components/AnimatedUsage.test.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/CardStatusIcons.test.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/ImageGallery.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/OpenChatButton.test.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/OpenChatButton.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/PrGlyph.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/QueuedPrompts.test.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/QueuedPrompts.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/ReplyCover.test.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/ReplyCover.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/ReplyImagePreview.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/ReplyMarkdown.test.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/ReplyMarkdown.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/SavedUsageFormat.test.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/SavedUsageFormat.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/ShimmerText.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/StatsGrid.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/StatusBadge.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/TaskBoard.test.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/TaskCardStats.test.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/TaskCardStats.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/TurnContent.test.tsx +0 -0
- /package/src/{dashboard/react → ui}/fixtures.ts +0 -0
- /package/src/{dashboard → ui}/github-links.js +0 -0
- /package/src/{dashboard/react → ui}/image-cache.test.ts +0 -0
- /package/src/{dashboard/react → ui}/image-cache.ts +0 -0
- /package/src/{dashboard → ui}/state.js +0 -0
- /package/src/{dashboard/react → ui}/styles.css +0 -0
- /package/src/{dashboard/react → ui}/test-setup.ts +0 -0
- /package/src/{dashboard → ui}/time.js +0 -0
- /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
|
-
|
|
165
|
-
|
|
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
|
-
|
|
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": "
|
|
4
|
-
"description": "A
|
|
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/
|
|
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/
|
|
57
|
-
"scripts/benchmark-dispatch-prepare.js",
|
|
58
|
-
"scripts/e2e-benchmark.js",
|
|
27
|
+
"scripts/prepare-github-keyring.js",
|
|
59
28
|
"src",
|
|
60
|
-
"skills/
|
|
61
|
-
"
|
|
62
|
-
"
|
|
63
|
-
"
|
|
64
|
-
"
|
|
65
|
-
"docs/taskchef-next-
|
|
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": ">=
|
|
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.
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
|
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;
|
package/src/file-lock.js
ADDED
|
@@ -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 "../../
|
|
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 "../../
|
|
6
|
-
import { PullRequestInfo, nextRunLabel } from "../../
|
|
7
|
-
import { OpenChatButton } from "../../
|
|
8
|
-
import { QueuedPrompts } from "../../
|
|
9
|
-
import { TurnContent } from "../../
|
|
10
|
-
import { compactTokens, TokenBreakdown, useAnimatedUsage } from "../../
|
|
11
|
-
import { GitHubLinks } from "../../
|
|
12
|
-
import { formatWorkedDuration } from "../../
|
|
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 "../../
|
|
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("../../
|
|
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("../../
|
|
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 "../../
|
|
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 "../../
|
|
8
|
-
import { filterTasks } from "../../
|
|
9
|
-
import { TaskBoard } from "../../
|
|
10
|
-
import { TaskCard } from "../../
|
|
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 "../../
|
|
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 "../../
|
|
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 {
|
|
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
|
|
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));
|