taskchef 7.17.1 → 7.19.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 +2 -2
- package/README.md +47 -10
- package/assets/taskchef-dispatcher-instructions.md +2 -4
- package/docs/firstmate-taskchef-comparison.md +2 -2
- package/docs/spec.md +42 -2
- package/docs/workflows.md +27 -3
- package/index.js +2 -1
- package/package.json +2 -1
- package/skills/taskchef-bootstrap/SKILL.md +5 -2
- package/skills/taskchef-dashboard/SKILL.md +20 -0
- package/skills/taskchef-dashboard/agents/openai.yaml +4 -0
- package/src/codex-app.js +87 -9
- package/src/dashboard/actions.js +38 -0
- package/src/dashboard/app.js +24 -1
- package/src/dashboard/index.html +1 -0
- package/src/dashboard/state.js +7 -0
- package/src/dashboard/styles.css +3 -2
- package/src/dashboard.js +78 -0
- package/src/mcp.js +49 -0
- package/src/version.js +1 -1
- package/src/workspace.js +31 -6
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "taskchef",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.19.0",
|
|
4
4
|
"description": "Dispatch work from a data-only workspace to visible Codex project tasks.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Favo Yang",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"defaultPrompt": [
|
|
32
32
|
"$taskchef-bootstrap Set up TaskChef and index my local Codex projects.",
|
|
33
33
|
"$taskchef-delegate Do X in Y project.",
|
|
34
|
-
"
|
|
34
|
+
"$taskchef-dashboard Ensure and open the TaskChef dashboard.",
|
|
35
35
|
"$taskchef-copilot Summarize recent delegated work and tell me what needs attention."
|
|
36
36
|
],
|
|
37
37
|
"brandColor": "#D97706",
|
package/README.md
CHANGED
|
@@ -31,7 +31,7 @@ codex plugin add taskchef@favoyang-plugins
|
|
|
31
31
|
npm install --global taskchef
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
-
The plugin provides
|
|
34
|
+
The plugin provides five skills and a local MCP server. The npm installation
|
|
35
35
|
puts the `taskchef` CLI on `PATH`. TaskChef installs no hooks, schedules,
|
|
36
36
|
daemons, login items, system services, or background identity search and needs
|
|
37
37
|
no elevated permissions.
|
|
@@ -48,7 +48,7 @@ The canonical workspace is `~/.agents/taskchef`. TaskChef owns only:
|
|
|
48
48
|
|
|
49
49
|
```text
|
|
50
50
|
AGENTS.md managed dispatcher instructions plus user additions
|
|
51
|
-
taskchef.json schema-2 Codex project index and delegation metadata
|
|
51
|
+
taskchef.json schema-2 Codex project index, dashboard preference, and delegation metadata
|
|
52
52
|
tasks.jsonl one task snapshot per line (schema 9; schema 4-8 migration supported)
|
|
53
53
|
```
|
|
54
54
|
|
|
@@ -129,9 +129,10 @@ correlation marker. That skill reads the executor's own `CODEX_THREAD_ID`, self-
|
|
|
129
129
|
reports lifecycle state. Independent outcomes may become separate executors;
|
|
130
130
|
dependent work should stay together.
|
|
131
131
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
132
|
+
Activating the TaskChef MCP process best-effort ensures the dashboard by
|
|
133
|
+
default. The managed workspace instructions also retry it at the start of each
|
|
134
|
+
dispatcher turn. A startup failure never blocks MCP tools, an answer, report,
|
|
135
|
+
or delegation. Every dispatcher response ends with
|
|
135
136
|
the stable [TaskChef Dashboard](http://127.0.0.1:3210/) link; a created-task
|
|
136
137
|
directive remains on the preceding line so dispatch still returns immediately.
|
|
137
138
|
|
|
@@ -219,13 +220,14 @@ eight-character prefix.
|
|
|
219
220
|
|
|
220
221
|
## Dashboard
|
|
221
222
|
|
|
222
|
-
|
|
223
|
+
Use the packaged recovery skill to make the dashboard available and open it:
|
|
223
224
|
|
|
224
225
|
```text
|
|
225
|
-
|
|
226
|
+
$taskchef-dashboard Ensure and open the TaskChef dashboard.
|
|
226
227
|
```
|
|
227
228
|
|
|
228
|
-
|
|
229
|
+
MCP activation and `$taskchef-dashboard` call the input-free `ensure_dashboard`
|
|
230
|
+
MCP tool. It starts at
|
|
229
231
|
most one dashboard inside the existing TaskChef MCP process on
|
|
230
232
|
`127.0.0.1:3210`, or reuses a listener only when its bounded `/api/health`
|
|
231
233
|
identity proves the exact TaskChef/dashboard-server version and the same
|
|
@@ -233,8 +235,22 @@ canonical workspace. The response says `started` or `reused` and includes the
|
|
|
233
235
|
stable URL, canonical workspace, and versions.
|
|
234
236
|
|
|
235
237
|
The in-process dashboard closes with the MCP process. Closing Codex or reloading
|
|
236
|
-
the plugin may therefore stop the dashboard; the next
|
|
237
|
-
it. TaskChef adds no OS-persistent component.
|
|
238
|
+
the plugin may therefore stop the dashboard; activating the next TaskChef MCP
|
|
239
|
+
process normally restores it. TaskChef adds no OS-persistent component.
|
|
240
|
+
|
|
241
|
+
Autostart is enabled when `dashboard` is absent and in new workspaces. To opt
|
|
242
|
+
out, add this optional exact object to `taskchef.json` while retaining its other
|
|
243
|
+
fields:
|
|
244
|
+
|
|
245
|
+
```json
|
|
246
|
+
"dashboard": { "autostart": false }
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
The manual `$taskchef-dashboard` recovery skill remains available when
|
|
250
|
+
autostart is disabled or fails. It reports whether the compatible dashboard was
|
|
251
|
+
started or reused and always returns its canonical clickable URL. It may open
|
|
252
|
+
the returned localhost URL in an available in-app browser, but browser failure
|
|
253
|
+
does not make dashboard recovery fail.
|
|
238
254
|
|
|
239
255
|
For manual development, run the foreground CLI:
|
|
240
256
|
|
|
@@ -247,6 +263,12 @@ The loopback dashboard watches `tasks.jsonl`, groups current states, and opens
|
|
|
247
263
|
linked Codex tasks. List snapshots and SSE events carry only the latest
|
|
248
264
|
request/result pair; opening task details fetches the full newest-first activity
|
|
249
265
|
timeline, including clearly labeled interrupted turns.
|
|
266
|
+
Task details also offer **Archive chat** for every linked task whose current
|
|
267
|
+
TaskChef state is not `working`. After confirmation, TaskChef invokes only the
|
|
268
|
+
Codex CLI at the canonical ChatGPT or Codex desktop app location under
|
|
269
|
+
`/Applications`, to archive the exact thread UUID. The Codex chat leaves active chat lists while the TaskChef record
|
|
270
|
+
and its activity timeline remain unchanged. If the bundled CLI is unavailable,
|
|
271
|
+
the dashboard does not fall back to another `codex` executable from `PATH`.
|
|
250
272
|
The header shows the running TaskChef package version reported by the same
|
|
251
273
|
bounded health identity used for compatible-listener checks.
|
|
252
274
|
Task and result times are relative through 29 days (with minute detail for the
|
|
@@ -322,6 +344,21 @@ codex plugin marketplace upgrade favoyang-plugins
|
|
|
322
344
|
codex plugin add taskchef@favoyang-plugins
|
|
323
345
|
```
|
|
324
346
|
|
|
347
|
+
Replacing or installing plugin files cannot execute autostart by itself. After
|
|
348
|
+
installation, activate or reload the new TaskChef MCP process; installation
|
|
349
|
+
does not necessarily reload Codex. Then run `$taskchef-dashboard` (or call
|
|
350
|
+
`ensure_dashboard`) and verify the returned dashboard identity has all of:
|
|
351
|
+
|
|
352
|
+
- the expected released TaskChef version;
|
|
353
|
+
- the expected dashboard protocol `serverVersion`;
|
|
354
|
+
- the canonical TaskChef workspace path;
|
|
355
|
+
- the canonical `http://127.0.0.1:3210/` URL.
|
|
356
|
+
|
|
357
|
+
The release-install sequence is therefore: install plugin, activate or reload
|
|
358
|
+
the new MCP process, ensure the dashboard, then verify TaskChef version,
|
|
359
|
+
protocol `serverVersion`, canonical workspace, and URL. Exact-compatible
|
|
360
|
+
servers may be reused; unknown listeners remain untouched.
|
|
361
|
+
|
|
325
362
|
## Development
|
|
326
363
|
|
|
327
364
|
```sh
|
|
@@ -6,10 +6,8 @@ This folder is the canonical per-user TaskChef dispatcher workspace.
|
|
|
6
6
|
- Early in every dispatcher turn, best-effort call the TaskChef
|
|
7
7
|
`ensure_dashboard` MCP tool. Dashboard startup failure must not block direct
|
|
8
8
|
TaskChef answers, reporting, or delegation.
|
|
9
|
-
- A request to start or open the TaskChef dashboard is TaskChef
|
|
10
|
-
not delegated project work.
|
|
11
|
-
its returned URL when an in-app browser capability is available. Otherwise,
|
|
12
|
-
return the URL as a clickable link.
|
|
9
|
+
- A request to start, recover, or open the TaskChef dashboard is TaskChef
|
|
10
|
+
maintenance, not delegated project work. Use `$taskchef-dashboard`.
|
|
13
11
|
- Use `$taskchef-bootstrap` when initializing or refreshing this workspace,
|
|
14
12
|
changing or listing its configured projects, running TaskChef doctor,
|
|
15
13
|
or repairing its managed instructions.
|
|
@@ -70,7 +70,7 @@ responsibility until an outcome is landed, transferred, or safely preserved.
|
|
|
70
70
|
| Dimension | TaskChef 7 | FirstMate at `038d0f7` |
|
|
71
71
|
| --- | --- | --- |
|
|
72
72
|
| Primary goal | Put multi-project work into the right visible Codex task and make it findable later. | Let one person direct a supervised crew across projects through one liaison. |
|
|
73
|
-
| Product form | Codex plugin:
|
|
73
|
+
| Product form | Codex plugin: five skills, local MCP server, data CLI, and loopback dashboard. | Cloneable agent distribution: instructions, internal skills, scripts, policies, and private on-disk state. |
|
|
74
74
|
| Runtime | Native Codex desktop tasks are the executors and live source of truth. | Harness-driven workers in tmux by default, with documented alternative session backends; Codex can be a harness, but Codex desktop is not a runtime backend. |
|
|
75
75
|
| Coordinator lifetime | Dispatcher returns immediately and does not supervise. | First mate remains active and uses watcher/guard mechanisms to supervise meaningful events. |
|
|
76
76
|
| Project isolation | Delegates to the configured Codex project; isolation follows native Codex/project behavior. | Ship and scout workers require separate Treehouse- or backend-managed worktrees. |
|
|
@@ -219,7 +219,7 @@ harness adapter.
|
|
|
219
219
|
| --- | --- | --- |
|
|
220
220
|
| Codex | Built around native Codex projects, tasks, thread reads, and desktop deep links. | Codex is one verified harness; desktop tasks are not its worker runtime. |
|
|
221
221
|
| GitHub | Configured repository URLs aid routing; delivery remains the executor project's concern. | GitHub CLI, PR state, and configured delivery modes are part of supervised shipping workflows. |
|
|
222
|
-
| Skills |
|
|
222
|
+
| Skills | Five plugin skills with narrow bootstrap, dashboard recovery, delegate, executor, and conversational coordination responsibilities. | Internal firstmate-only skills plus standalone public skills; AGENTS.md routes conditional procedures. |
|
|
223
223
|
| MCP | Four primary local TaskChef tools plus one deprecated compatibility alias own deterministic identity and state writes. | FirstMate describes itself as an agent distribution rather than an MCP product; deterministic behavior lives largely in scripts. |
|
|
224
224
|
| Runtime extensions | Extend plugin skills/MCP/CLI/dashboard or compose native Codex capabilities. | Add or verify harness adapters, session backends, dispatch profiles, operational scripts, and optional integrations. |
|
|
225
225
|
|
package/docs/spec.md
CHANGED
|
@@ -29,7 +29,7 @@ is dated research, not contract.
|
|
|
29
29
|
| **Turn reference** | Required lifecycle identity for one executor prompt. It is the native Codex turn ID when available, otherwise a retained client-generated UUID. |
|
|
30
30
|
| **Current turn ID** | Optional Codex metadata for the reported prompt; null when native turn reading is unavailable. |
|
|
31
31
|
| **Dashboard** | The loopback, read-only UI derived from validated workspace snapshots and bounded native actions. |
|
|
32
|
-
| **Skill** | One packaged agent procedure: `taskchef-bootstrap`, `taskchef-delegate`, `taskchef-executor`, or `taskchef-copilot`. |
|
|
32
|
+
| **Skill** | One packaged agent procedure: `taskchef-bootstrap`, `taskchef-dashboard`, `taskchef-delegate`, `taskchef-executor`, or `taskchef-copilot`. |
|
|
33
33
|
|
|
34
34
|
## Components and ownership
|
|
35
35
|
|
|
@@ -43,6 +43,9 @@ is dated research, not contract.
|
|
|
43
43
|
- `taskchef-copilot` MUST own conversational outcome explanation, attention,
|
|
44
44
|
and next-action recommendations. It MUST use cached normalized briefs by
|
|
45
45
|
default and MUST NOT poll or persist inferred state.
|
|
46
|
+
- `taskchef-dashboard` MUST own manual dashboard ensure and recovery. It MUST
|
|
47
|
+
NOT dispatch work or inspect task outcomes, and browser navigation failure
|
|
48
|
+
MUST NOT suppress the returned dashboard URL.
|
|
46
49
|
- The MCP server MUST expose `ensure_dashboard`, four primary lifecycle tools,
|
|
47
50
|
and the deprecated `report_result` compatibility alias specified below.
|
|
48
51
|
- The CLI MAY administer and inspect the workspace, but MUST NOT provide a
|
|
@@ -56,7 +59,8 @@ is dated research, not contract.
|
|
|
56
59
|
TaskChef MUST manage only `AGENTS.md`, `taskchef.json`, and `tasks.jsonl`
|
|
57
60
|
inside the dispatcher workspace. It MUST preserve unrelated paths.
|
|
58
61
|
|
|
59
|
-
`taskchef.json` MUST have schema version 2 and
|
|
62
|
+
`taskchef.json` MUST have schema version 2, the following required fields, and
|
|
63
|
+
an optional exact `dashboard` object:
|
|
60
64
|
|
|
61
65
|
```json
|
|
62
66
|
{
|
|
@@ -71,6 +75,11 @@ Names and paths MUST be unique. Git projects MUST be exact Git roots.
|
|
|
71
75
|
Repository URLs MUST canonicalize to `https://github.com/<owner>/<repository>`
|
|
72
76
|
and be case-insensitively deduplicated.
|
|
73
77
|
|
|
78
|
+
When present, `dashboard` MUST contain exactly boolean `autostart`. Its absence
|
|
79
|
+
is backward-compatible and means `true`; new workspaces SHOULD write
|
|
80
|
+
`{"autostart": true}`. `false` disables MCP-lifecycle autostart but MUST NOT
|
|
81
|
+
disable the explicit `ensure_dashboard` tool.
|
|
82
|
+
|
|
74
83
|
`tasks.jsonl` MUST contain zero or more newline-terminated schema-4 through
|
|
75
84
|
schema-9 records, one per line. Schemas 4 through 8 are supported
|
|
76
85
|
migration/read formats; every new record and state mutation MUST write schema 9. Other schemas
|
|
@@ -112,6 +121,14 @@ fields MUST NOT change after recording.
|
|
|
112
121
|
|
|
113
122
|
## Required lifecycle
|
|
114
123
|
|
|
124
|
+
When the canonical TaskChef MCP server finishes connecting, it MUST invoke the
|
|
125
|
+
same serialized dashboard ensure path once by default. It MUST read the
|
|
126
|
+
canonical configuration and skip this only for explicit
|
|
127
|
+
`dashboard.autostart: false`. Initialization failures, invalid workspace state,
|
|
128
|
+
port conflicts, and dashboard errors MUST NOT prevent tool registration or MCP
|
|
129
|
+
availability. They MUST emit only a bounded non-sensitive diagnostic through
|
|
130
|
+
the MCP process logging channel. MCP initialization MUST NOT open a browser.
|
|
131
|
+
|
|
115
132
|
At the start of every dispatcher turn, the dispatcher SHOULD call
|
|
116
133
|
`ensure_dashboard` best-effort. Failure MUST NOT block direct TaskChef answers,
|
|
117
134
|
reporting, or delegation. Every dispatcher final response MUST end with the
|
|
@@ -233,6 +250,11 @@ failure MUST leave no owned listener. The MCP server MUST close its owned
|
|
|
233
250
|
dashboard when its transport or process shuts down; it MUST NOT close a reused
|
|
234
251
|
external foreground server.
|
|
235
252
|
|
|
253
|
+
The packaged `$taskchef-dashboard` skill MUST call this tool, report `started`
|
|
254
|
+
or `reused`, and return the canonical clickable URL. It MAY use an available
|
|
255
|
+
in-app browser when permitted; absent or blocked browser navigation MUST fall
|
|
256
|
+
back to the link without failing. It MUST NOT inspect task outcomes or dispatch.
|
|
257
|
+
|
|
236
258
|
**Annotations:** `readOnlyHint: false`, `destructiveHint: false`,
|
|
237
259
|
`openWorldHint: false`.
|
|
238
260
|
|
|
@@ -405,6 +427,17 @@ startup safely. Direct thread navigation
|
|
|
405
427
|
MUST require a canonical Codex UUIDv7. Otherwise it MAY open the revalidated
|
|
406
428
|
configured project. Project paths from task history MUST be matched against
|
|
407
429
|
current configuration before use.
|
|
430
|
+
The task-detail dashboard MAY offer Codex chat archival only when the stored
|
|
431
|
+
thread ID is a canonical Codex UUID and the current TaskChef state is not
|
|
432
|
+
`working`. It MUST revalidate both conditions for the POST request, require the
|
|
433
|
+
exact loopback origin, and obtain explicit user confirmation in the browser.
|
|
434
|
+
It MUST invoke the `archive` subcommand by directly executing only a Codex CLI
|
|
435
|
+
inside the canonical ChatGPT or Codex desktop application bundle under macOS
|
|
436
|
+
`/Applications`, with the thread UUID as a separate argument. It MUST NOT use a shell, a generic `PATH` fallback, a
|
|
437
|
+
private desktop endpoint, or direct session-file manipulation. Successful
|
|
438
|
+
archival MUST NOT modify dispatcher files or remove the TaskChef task record.
|
|
439
|
+
The UI MUST disclose that spawned descendant chats may also be archived and
|
|
440
|
+
that TaskChef history remains available.
|
|
408
441
|
Snapshot and SSE list payloads MUST omit full `turns` and derived `results`
|
|
409
442
|
history and include `latestTurn`. The bounded per-task detail endpoint MAY
|
|
410
443
|
return the full validated task so the dialog can render the paired timeline newest first.
|
|
@@ -473,3 +506,10 @@ system services, cron jobs, hooks, privileged components, or elevated/system
|
|
|
473
506
|
permissions for dashboard availability. Availability is best-effort while the
|
|
474
507
|
owning Codex/plugin MCP process is alive and is not guaranteed while Codex is
|
|
475
508
|
closed.
|
|
509
|
+
|
|
510
|
+
Installing or replacing plugin files MUST NOT be described as activating the
|
|
511
|
+
new MCP code. Release verification MUST install the plugin, activate or reload
|
|
512
|
+
the new MCP process, ensure the dashboard, and verify the expected TaskChef
|
|
513
|
+
version, dashboard protocol `serverVersion`, canonical workspace, and canonical
|
|
514
|
+
URL. Exact-compatible listener reuse remains valid; installation MUST NOT be
|
|
515
|
+
claimed to reload Codex automatically.
|
package/docs/workflows.md
CHANGED
|
@@ -13,6 +13,7 @@ research.
|
|
|
13
13
|
| `skills/taskchef-delegate/SKILL.md` | Split, route, record-before-create, create, return. |
|
|
14
14
|
| `skills/taskchef-executor/SKILL.md` | Own, self-link, execute, and report every executor turn. |
|
|
15
15
|
| `skills/taskchef-bootstrap/SKILL.md` | Initialize the workspace and maintain the Codex project index. |
|
|
16
|
+
| `skills/taskchef-dashboard/SKILL.md` | Ensure or recover the canonical dashboard and return its URL. |
|
|
16
17
|
| `skills/taskchef-copilot/SKILL.md` | Explain normalized cached briefs and coordinate safe next actions. |
|
|
17
18
|
| `src/mcp.js` | Dashboard ensure, four primary lifecycle tools, one deprecated alias, shutdown ownership, and MCP annotations. |
|
|
18
19
|
| `src/delegation.js` | UUID marker, concise executor-skill invocation shape, and creation-failure handling. |
|
|
@@ -27,9 +28,9 @@ per-user default.
|
|
|
27
28
|
|
|
28
29
|
## Dispatcher dashboard lifecycle
|
|
29
30
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
centralized instead of duplicating it across
|
|
31
|
+
MCP activation makes dashboard maintenance a best-effort lifecycle action. The
|
|
32
|
+
generated managed `AGENTS.md` block also retries it before dispatcher turns and
|
|
33
|
+
keeps response ordering centralized instead of duplicating it across skills.
|
|
33
34
|
|
|
34
35
|
```mermaid
|
|
35
36
|
sequenceDiagram
|
|
@@ -38,6 +39,8 @@ sequenceDiagram
|
|
|
38
39
|
participant M as TaskChef MCP
|
|
39
40
|
participant H as Loopback health
|
|
40
41
|
participant S as Dashboard server
|
|
42
|
+
M->>M: Read dashboard.autostart (absent means true)
|
|
43
|
+
M->>M: Best-effort ensure after MCP connect
|
|
41
44
|
D->>M: ensure_dashboard()
|
|
42
45
|
M->>M: Serialize concurrent ensure calls
|
|
43
46
|
M->>H: GET 127.0.0.1:3210/api/health
|
|
@@ -63,6 +66,27 @@ started. A compatible foreground `taskchef dashboard` listener may be reused
|
|
|
63
66
|
but remains owned by that CLI process. No TaskChef path terminates an unknown
|
|
64
67
|
listener or installs OS persistence.
|
|
65
68
|
|
|
69
|
+
Autostart and explicit ensures share the same manager promise, so concurrent
|
|
70
|
+
initialization and recovery calls produce at most one owned listener. An
|
|
71
|
+
explicit `dashboard.autostart: false` skips only activation-time ensure. Any
|
|
72
|
+
failure is reduced to a fixed stderr diagnostic; tool registration and MCP
|
|
73
|
+
availability continue. Activation never opens a browser.
|
|
74
|
+
|
|
75
|
+
## Release-install verification
|
|
76
|
+
|
|
77
|
+
The practical release handoff ends in this order:
|
|
78
|
+
|
|
79
|
+
1. Install the released plugin.
|
|
80
|
+
2. Activate or reload its new TaskChef MCP process.
|
|
81
|
+
3. Run `$taskchef-dashboard` or call `ensure_dashboard`.
|
|
82
|
+
4. Verify the expected TaskChef version, protocol `serverVersion`, canonical
|
|
83
|
+
workspace, and canonical URL returned by the dashboard identity.
|
|
84
|
+
|
|
85
|
+
Replacing plugin files alone cannot execute autostart because old code remains
|
|
86
|
+
in the already-running MCP process. Installation does not necessarily reload
|
|
87
|
+
Codex. An exact-compatible dashboard may be reused; an unknown listener is
|
|
88
|
+
never terminated or replaced.
|
|
89
|
+
|
|
66
90
|
## Normal delegation and self-linking
|
|
67
91
|
|
|
68
92
|
The dispatcher uses native Codex project discovery for routing and MCP for
|
package/index.js
CHANGED
|
@@ -5,6 +5,7 @@ export {
|
|
|
5
5
|
canonicalDirectory,
|
|
6
6
|
canonicalGitRoot,
|
|
7
7
|
doctorWorkspace,
|
|
8
|
+
dashboardAutostartEnabled,
|
|
8
9
|
ensureWorkspaceInstructions,
|
|
9
10
|
filterTasks,
|
|
10
11
|
importProjects,
|
|
@@ -74,4 +75,4 @@ export {
|
|
|
74
75
|
|
|
75
76
|
export { DASHBOARD_SERVER_VERSION, TASKCHEF_VERSION } from "./src/version.js";
|
|
76
77
|
|
|
77
|
-
export { createTaskChefMcpServer } from "./src/mcp.js";
|
|
78
|
+
export { createDashboardAutostart, createTaskChefMcpServer } from "./src/mcp.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "taskchef",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.19.0",
|
|
4
4
|
"description": "A non-blocking interactive dispatcher for visible Codex tasks.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Favo Yang",
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
"scripts/e2e-benchmark.js",
|
|
37
37
|
"src",
|
|
38
38
|
"skills/taskchef-bootstrap",
|
|
39
|
+
"skills/taskchef-dashboard",
|
|
39
40
|
"skills/taskchef-delegate",
|
|
40
41
|
"skills/taskchef-executor",
|
|
41
42
|
"skills/taskchef-copilot"
|
|
@@ -160,5 +160,8 @@ Example managed-workspace import entry:
|
|
|
160
160
|
}
|
|
161
161
|
```
|
|
162
162
|
|
|
163
|
-
TaskChef accepts configuration schema version 2 only.
|
|
164
|
-
|
|
163
|
+
TaskChef accepts configuration schema version 2 only. Its optional exact
|
|
164
|
+
`dashboard` object contains boolean `autostart`; absence means enabled and
|
|
165
|
+
`false` opts out of MCP-lifecycle dashboard startup without disabling manual
|
|
166
|
+
`$taskchef-dashboard` recovery. `githubRepos` is always an array; unsupported
|
|
167
|
+
configuration is rejected without being rewritten.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: taskchef-dashboard
|
|
3
|
+
description: "Ensure and recover the canonical TaskChef dashboard. Use when the user asks to start, restore, open, or get the link for the TaskChef dashboard. Do not use to dispatch work or inspect task outcomes."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# TaskChef Dashboard
|
|
7
|
+
|
|
8
|
+
Maintain only the canonical TaskChef dashboard.
|
|
9
|
+
|
|
10
|
+
1. Call the TaskChef `ensure_dashboard` MCP tool with its empty input. Do not
|
|
11
|
+
supply a workspace, host, or port.
|
|
12
|
+
2. Report whether the returned `action` is `started` or `reused` and provide
|
|
13
|
+
the returned canonical URL as a clickable link.
|
|
14
|
+
3. If the user asked to open the dashboard and an in-app browser navigation
|
|
15
|
+
capability is available and permits the returned localhost URL, open or
|
|
16
|
+
navigate to it. Browser absence, refusal, or blocked localhost navigation is
|
|
17
|
+
non-fatal: still return the clickable URL and the ensure result.
|
|
18
|
+
|
|
19
|
+
Do not dispatch project work, read task briefs, inspect executor outcomes, poll,
|
|
20
|
+
start a separate foreground CLI server, or modify TaskChef workspace files.
|
package/src/codex-app.js
CHANGED
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
import { execFile as execFileCallback } from "node:child_process";
|
|
2
|
-
import { access } from "node:fs/promises";
|
|
2
|
+
import { access, realpath } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { promisify } from "node:util";
|
|
5
5
|
|
|
6
|
+
import { normalizeCodexThreadId } from "./delegation.js";
|
|
7
|
+
|
|
6
8
|
const execFile = promisify(execFileCallback);
|
|
7
9
|
const CODEX_COMMAND_TIMEOUT_MS = 10_000;
|
|
8
|
-
const
|
|
10
|
+
const CODEX_ARCHIVE_TIMEOUT_MS = 4_000;
|
|
11
|
+
const CODEX_COMMAND_MAX_BUFFER_BYTES = 64 * 1024;
|
|
9
12
|
|
|
10
|
-
function runCodex(run, filePath, args) {
|
|
11
|
-
return run(filePath, args, {
|
|
13
|
+
function runCodex(run, filePath, args, timeout = CODEX_COMMAND_TIMEOUT_MS) {
|
|
14
|
+
return run(filePath, args, {
|
|
15
|
+
timeout,
|
|
16
|
+
killSignal: "SIGKILL",
|
|
17
|
+
maxBuffer: CODEX_COMMAND_MAX_BUFFER_BYTES,
|
|
18
|
+
});
|
|
12
19
|
}
|
|
13
20
|
|
|
14
21
|
async function executable(filePath) {
|
|
@@ -20,15 +27,29 @@ async function executable(filePath) {
|
|
|
20
27
|
}
|
|
21
28
|
}
|
|
22
29
|
|
|
23
|
-
function pathCandidates(env) {
|
|
30
|
+
function pathCandidates(env, platform = process.platform) {
|
|
24
31
|
return (env.PATH ?? "")
|
|
25
32
|
.split(path.delimiter)
|
|
26
33
|
.filter(Boolean)
|
|
27
|
-
.map((directory) => path.join(directory,
|
|
34
|
+
.map((directory) => path.join(directory, platform === "win32" ? "codex.exe" : "codex"));
|
|
28
35
|
}
|
|
29
36
|
|
|
30
37
|
function isDesktopBundleCandidate(filePath) {
|
|
31
|
-
|
|
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
|
+
];
|
|
32
53
|
}
|
|
33
54
|
|
|
34
55
|
async function supportsAppCommand(filePath, run) {
|
|
@@ -40,6 +61,37 @@ async function supportsAppCommand(filePath, run) {
|
|
|
40
61
|
}
|
|
41
62
|
}
|
|
42
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
|
+
|
|
43
95
|
export async function discoverCodexCli({
|
|
44
96
|
explicit = null,
|
|
45
97
|
env = process.env,
|
|
@@ -56,7 +108,7 @@ export async function discoverCodexCli({
|
|
|
56
108
|
}
|
|
57
109
|
|
|
58
110
|
const candidates = pathCandidates(env);
|
|
59
|
-
for (const pathCandidate of candidates.filter(isDesktopBundleCandidate)) {
|
|
111
|
+
for (const pathCandidate of candidates.filter((candidate) => isDesktopBundleCandidate(candidate))) {
|
|
60
112
|
const candidate = await executable(pathCandidate);
|
|
61
113
|
if (candidate && await supportsAppCommand(candidate, run)) {
|
|
62
114
|
return { path: candidate, source: "desktop-path" };
|
|
@@ -89,7 +141,12 @@ export async function openWorkspaceInCodex(workspace, options = {}) {
|
|
|
89
141
|
}
|
|
90
142
|
|
|
91
143
|
export function isCodexThreadDeepLinkId(threadId) {
|
|
92
|
-
|
|
144
|
+
try {
|
|
145
|
+
normalizeCodexThreadId(threadId);
|
|
146
|
+
return true;
|
|
147
|
+
} catch {
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
93
150
|
}
|
|
94
151
|
|
|
95
152
|
export async function openThreadInCodex(threadId, options = {}) {
|
|
@@ -108,3 +165,24 @@ export async function openThreadInCodex(threadId, options = {}) {
|
|
|
108
165
|
}
|
|
109
166
|
return { status: "requested", mechanism: "codex-deep-link", threadId, url };
|
|
110
167
|
}
|
|
168
|
+
|
|
169
|
+
export async function archiveThreadInCodex(threadId, options = {}) {
|
|
170
|
+
if (!isCodexThreadDeepLinkId(threadId)) {
|
|
171
|
+
throw new Error("Codex thread ID is not supported by chat archiving");
|
|
172
|
+
}
|
|
173
|
+
const run = options.run ?? execFile;
|
|
174
|
+
const cli = options.cli ?? await discoverBundledCodexCli({ run });
|
|
175
|
+
await runCodex(
|
|
176
|
+
run,
|
|
177
|
+
cli.path,
|
|
178
|
+
["archive", threadId],
|
|
179
|
+
options.timeoutMs ?? CODEX_ARCHIVE_TIMEOUT_MS,
|
|
180
|
+
);
|
|
181
|
+
return {
|
|
182
|
+
status: "archived",
|
|
183
|
+
mechanism: "codex-archive-cli",
|
|
184
|
+
codexCli: cli.path,
|
|
185
|
+
codexCliSource: cli.source,
|
|
186
|
+
threadId,
|
|
187
|
+
};
|
|
188
|
+
}
|
package/src/dashboard/actions.js
CHANGED
|
@@ -15,3 +15,41 @@ export async function openTaskFromControl(event, taskId, {
|
|
|
15
15
|
control.disabled = false;
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
|
+
|
|
19
|
+
export async function archiveTaskFromControl(event, task, {
|
|
20
|
+
confirmAction = globalThis.confirm,
|
|
21
|
+
fetchAction = globalThis.fetch,
|
|
22
|
+
onArchived = () => {},
|
|
23
|
+
showMessage,
|
|
24
|
+
} = {}) {
|
|
25
|
+
event.stopPropagation();
|
|
26
|
+
const accepted = confirmAction(
|
|
27
|
+
`Archive “${task.title}” in Codex?\n\n`
|
|
28
|
+
+ "The chat will leave active Codex lists, and spawned descendant chats may also be archived. "
|
|
29
|
+
+ "TaskChef history will remain available.",
|
|
30
|
+
);
|
|
31
|
+
if (!accepted) return false;
|
|
32
|
+
const control = event.currentTarget;
|
|
33
|
+
let archived = false;
|
|
34
|
+
control.disabled = true;
|
|
35
|
+
try {
|
|
36
|
+
const response = await fetchAction(
|
|
37
|
+
`/api/tasks/${encodeURIComponent(task.id)}/archive-codex`,
|
|
38
|
+
{ method: "POST" },
|
|
39
|
+
);
|
|
40
|
+
const result = await response.json();
|
|
41
|
+
if (!response.ok) {
|
|
42
|
+
showMessage(result.message ?? "Codex could not archive this chat.");
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
onArchived(task.threadId);
|
|
46
|
+
archived = true;
|
|
47
|
+
showMessage(result.message ?? "Archived the Codex chat. TaskChef history remains available.");
|
|
48
|
+
return true;
|
|
49
|
+
} catch {
|
|
50
|
+
showMessage("Codex chat archiving is temporarily unavailable. Try again.");
|
|
51
|
+
return false;
|
|
52
|
+
} finally {
|
|
53
|
+
if (!archived) control.disabled = false;
|
|
54
|
+
}
|
|
55
|
+
}
|
package/src/dashboard/app.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
canArchiveTask,
|
|
2
3
|
clearNotifications,
|
|
3
4
|
dismissNotification,
|
|
4
5
|
filterTasks,
|
|
@@ -15,7 +16,7 @@ import {
|
|
|
15
16
|
taskStatusLabel,
|
|
16
17
|
turnPresentation,
|
|
17
18
|
} from "./state.js";
|
|
18
|
-
import { openTaskFromControl } from "./actions.js";
|
|
19
|
+
import { archiveTaskFromControl, openTaskFromControl } from "./actions.js";
|
|
19
20
|
import {
|
|
20
21
|
githubReferenceAccessibleLabel,
|
|
21
22
|
githubReferenceDisplayLabels,
|
|
@@ -25,6 +26,7 @@ import {
|
|
|
25
26
|
import { formatRelativeTime, RelativeTimeController, parsedTimestamp } from "./time.js";
|
|
26
27
|
|
|
27
28
|
const state = {
|
|
29
|
+
archivedThreadIds: new Set(),
|
|
28
30
|
tasks: [],
|
|
29
31
|
signatures: new Map(),
|
|
30
32
|
notifications: [],
|
|
@@ -39,6 +41,7 @@ let copyTaskIdTimer = null;
|
|
|
39
41
|
const relativeTimes = new RelativeTimeController();
|
|
40
42
|
|
|
41
43
|
const elements = {
|
|
44
|
+
archiveTask: document.querySelector("#archive-codex"),
|
|
42
45
|
clearNotifications: document.querySelector("#clear-notifications"),
|
|
43
46
|
closeDialog: document.querySelector("#close-dialog"),
|
|
44
47
|
connectionDot: document.querySelector("#connection-dot"),
|
|
@@ -446,6 +449,15 @@ function renderDialog(task) {
|
|
|
446
449
|
...detailRow("Updated by", task.updatedBy),
|
|
447
450
|
);
|
|
448
451
|
configureOpenTaskControl(elements.openProject, `Open ${task.title} in Codex`);
|
|
452
|
+
const canArchive = canArchiveTask(task);
|
|
453
|
+
const archived = state.archivedThreadIds.has(task.threadId);
|
|
454
|
+
elements.archiveTask.hidden = !canArchive;
|
|
455
|
+
elements.archiveTask.disabled = archived;
|
|
456
|
+
elements.archiveTask.textContent = archived ? "Archived" : "Archive chat";
|
|
457
|
+
elements.archiveTask.setAttribute(
|
|
458
|
+
"aria-label",
|
|
459
|
+
archived ? `${task.title} is archived in Codex` : `Archive ${task.title} in Codex`,
|
|
460
|
+
);
|
|
449
461
|
}
|
|
450
462
|
|
|
451
463
|
async function openDialog(task) {
|
|
@@ -652,3 +664,14 @@ elements.openProject.addEventListener("click", async (event) => {
|
|
|
652
664
|
if (!state.selectedTask) return;
|
|
653
665
|
await openTaskFromControl(event, state.selectedTask.id, { showMessage });
|
|
654
666
|
});
|
|
667
|
+
elements.archiveTask.addEventListener("click", async (event) => {
|
|
668
|
+
const task = state.selectedTask;
|
|
669
|
+
if (!task || !canArchiveTask(task)) return;
|
|
670
|
+
await archiveTaskFromControl(event, task, {
|
|
671
|
+
onArchived: (threadId) => {
|
|
672
|
+
state.archivedThreadIds.add(threadId);
|
|
673
|
+
if (state.selectedTask?.id === task.id) renderDialog(state.selectedTask);
|
|
674
|
+
},
|
|
675
|
+
showMessage,
|
|
676
|
+
});
|
|
677
|
+
});
|
package/src/dashboard/index.html
CHANGED
|
@@ -113,6 +113,7 @@
|
|
|
113
113
|
<span>Open task</span>
|
|
114
114
|
</button>
|
|
115
115
|
<button id="copy-task-id" class="secondary-button" type="button" aria-label="Copy Task ID">Copy Task ID</button>
|
|
116
|
+
<button id="archive-codex" class="danger-button" type="button" aria-label="Archive this chat in Codex" hidden>Archive chat</button>
|
|
116
117
|
</div>
|
|
117
118
|
<nav id="dialog-related-links" class="github-links" aria-label="Related GitHub links" hidden></nav>
|
|
118
119
|
<section>
|
package/src/dashboard/state.js
CHANGED
|
@@ -13,6 +13,7 @@ export const STATUS_FILTERS = [
|
|
|
13
13
|
{ value: "completed", label: "Completed" },
|
|
14
14
|
{ value: "failed", label: "Failed" },
|
|
15
15
|
];
|
|
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;
|
|
16
17
|
|
|
17
18
|
const NOTIFICATION_TITLES = new Map([
|
|
18
19
|
["created", "Task created"],
|
|
@@ -38,6 +39,12 @@ export function taskStatusLabel(task) {
|
|
|
38
39
|
return task.status === null ? "unresolved" : task.status.replaceAll("_", " ");
|
|
39
40
|
}
|
|
40
41
|
|
|
42
|
+
export function canArchiveTask(task) {
|
|
43
|
+
return task.status !== "working"
|
|
44
|
+
&& typeof task.threadId === "string"
|
|
45
|
+
&& CODEX_THREAD_ID_PATTERN.test(task.threadId);
|
|
46
|
+
}
|
|
47
|
+
|
|
41
48
|
export function latestTurnPresentation(task) {
|
|
42
49
|
const turn = task.latestTurn ?? null;
|
|
43
50
|
const result = turn?.result ?? null;
|
package/src/dashboard/styles.css
CHANGED
|
@@ -149,10 +149,11 @@ dialog::backdrop { background: rgb(18 23 21 / 50%); backdrop-filter: blur(2px);
|
|
|
149
149
|
.dialog-header { display: flex; align-items: flex-start; justify-content: space-between; gap: 20px; }
|
|
150
150
|
.dialog-header h2 { margin-bottom: 12px; font-size: 1.6rem; }
|
|
151
151
|
.dialog-actions { display: flex; gap: 8px; margin-bottom: 26px; }
|
|
152
|
-
.primary-button, .secondary-button { padding: 8px 12px; border-radius: 7px; font-weight: 700; cursor: pointer; }
|
|
152
|
+
.primary-button, .secondary-button, .danger-button { padding: 8px 12px; border-radius: 7px; font-weight: 700; cursor: pointer; }
|
|
153
153
|
.primary-button { border: 1px solid var(--accent); background: var(--accent); color: white; }
|
|
154
154
|
.secondary-button { border: 1px solid var(--border); background: var(--surface); }
|
|
155
|
-
.
|
|
155
|
+
.danger-button { border: 1px solid var(--danger); background: var(--surface); color: var(--danger); }
|
|
156
|
+
.primary-button:disabled, .secondary-button:disabled, .danger-button:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
156
157
|
dialog section + section { margin-top: 24px; }
|
|
157
158
|
.preserve-lines { white-space: pre-wrap; }
|
|
158
159
|
.result-history { display: grid; gap: 10px; }
|
package/src/dashboard.js
CHANGED
|
@@ -6,15 +6,19 @@ import path from "node:path";
|
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
|
|
8
8
|
import {
|
|
9
|
+
archiveThreadInCodex,
|
|
10
|
+
discoverBundledCodexCli,
|
|
9
11
|
isCodexThreadDeepLinkId,
|
|
10
12
|
openThreadInCodex,
|
|
11
13
|
openWorkspaceInCodex,
|
|
12
14
|
} from "./codex-app.js";
|
|
13
15
|
import {
|
|
16
|
+
acquireWorkspaceLock,
|
|
14
17
|
canonicalDirectory,
|
|
15
18
|
canonicalGitRoot,
|
|
16
19
|
parseTaskLogContent,
|
|
17
20
|
readConfig,
|
|
21
|
+
readTask,
|
|
18
22
|
} from "./workspace.js";
|
|
19
23
|
import { DASHBOARD_SERVER_VERSION, TASKCHEF_VERSION } from "./version.js";
|
|
20
24
|
import { taskGitHubProjection } from "./dashboard/github-links.js";
|
|
@@ -439,6 +443,8 @@ function publicMonitorError() {
|
|
|
439
443
|
}
|
|
440
444
|
|
|
441
445
|
export async function createDashboardServer({
|
|
446
|
+
archiveThread = archiveThreadInCodex,
|
|
447
|
+
discoverArchiveCli = discoverBundledCodexCli,
|
|
442
448
|
workspace,
|
|
443
449
|
host = "127.0.0.1",
|
|
444
450
|
maxEventClients = DEFAULT_MAX_EVENT_CLIENTS,
|
|
@@ -472,6 +478,7 @@ export async function createDashboardServer({
|
|
|
472
478
|
throw new Error("dashboard identity exceeds the health response limit");
|
|
473
479
|
}
|
|
474
480
|
const clients = new Set();
|
|
481
|
+
const archiveRequests = new Set();
|
|
475
482
|
let allowedAuthority;
|
|
476
483
|
let allowedOrigin;
|
|
477
484
|
|
|
@@ -617,6 +624,77 @@ export async function createDashboardServer({
|
|
|
617
624
|
return;
|
|
618
625
|
}
|
|
619
626
|
|
|
627
|
+
const archiveMatch = url.pathname.match(/^\/api\/tasks\/([a-zA-Z0-9._-]+)\/archive-codex$/);
|
|
628
|
+
if (archiveMatch && method === "POST") {
|
|
629
|
+
if (request.headers.origin !== allowedOrigin) {
|
|
630
|
+
sendJson(response, 403, { message: "Dashboard origin validation failed." });
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
const taskId = archiveMatch[1];
|
|
634
|
+
const visibleTask = monitor.tasks.find((candidate) => candidate.id === taskId);
|
|
635
|
+
if (!visibleTask) {
|
|
636
|
+
sendJson(response, 404, { message: "Task not found." });
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
if (!isCodexThreadDeepLinkId(visibleTask.threadId)) {
|
|
640
|
+
sendJson(response, 409, { message: "This task does not have an archivable Codex chat." });
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
if (visibleTask.status === "working") {
|
|
644
|
+
sendJson(response, 409, { message: "Working tasks cannot be archived from the dashboard." });
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
if (archiveRequests.has(taskId)) {
|
|
648
|
+
sendJson(response, 409, { message: "This Codex chat is already being archived." });
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
archiveRequests.add(taskId);
|
|
652
|
+
let releaseWorkspaceLock = null;
|
|
653
|
+
try {
|
|
654
|
+
const cli = await discoverArchiveCli();
|
|
655
|
+
releaseWorkspaceLock = await acquireWorkspaceLock(monitor.workspace);
|
|
656
|
+
let task;
|
|
657
|
+
try {
|
|
658
|
+
task = await readTask(monitor.workspace, taskId);
|
|
659
|
+
} catch (error) {
|
|
660
|
+
if (/^task not found:/.test(error?.message ?? "")) {
|
|
661
|
+
sendJson(response, 404, { message: "Task not found." });
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
throw error;
|
|
665
|
+
}
|
|
666
|
+
if (!isCodexThreadDeepLinkId(task.threadId)) {
|
|
667
|
+
sendJson(response, 409, { message: "This task does not have an archivable Codex chat." });
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
if (task.status === "working") {
|
|
671
|
+
sendJson(response, 409, { message: "Working tasks cannot be archived from the dashboard." });
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
await archiveThread(task.threadId, { cli, timeoutMs: 4_000 });
|
|
675
|
+
sendJson(response, 200, {
|
|
676
|
+
message: "Archived the Codex chat. TaskChef history remains available.",
|
|
677
|
+
status: "archived",
|
|
678
|
+
});
|
|
679
|
+
} catch (error) {
|
|
680
|
+
if (error?.killed || error?.code === "ETIMEDOUT") {
|
|
681
|
+
sendJson(response, 504, { message: "Codex chat archiving timed out. Try again." });
|
|
682
|
+
} else if (/requires the Codex CLI bundled/i.test(error?.message ?? "")) {
|
|
683
|
+
sendJson(response, 503, {
|
|
684
|
+
message: "Chat archiving requires the Codex CLI bundled with the ChatGPT or Codex desktop app.",
|
|
685
|
+
});
|
|
686
|
+
} else {
|
|
687
|
+
sendJson(response, 409, {
|
|
688
|
+
message: "Codex could not archive this chat. It may be active, already archived, or stored on another host or profile.",
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
} finally {
|
|
692
|
+
await releaseWorkspaceLock?.();
|
|
693
|
+
archiveRequests.delete(taskId);
|
|
694
|
+
}
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
|
|
620
698
|
if (method === "POST") {
|
|
621
699
|
sendJson(response, 404, { message: "Not found." });
|
|
622
700
|
return;
|
package/src/mcp.js
CHANGED
|
@@ -6,6 +6,8 @@ import {
|
|
|
6
6
|
recordTask,
|
|
7
7
|
reportTaskState,
|
|
8
8
|
reportTaskResult,
|
|
9
|
+
dashboardAutostartEnabled,
|
|
10
|
+
readConfig,
|
|
9
11
|
} from "./workspace.js";
|
|
10
12
|
import { parseTaskChefMarker } from "./delegation.js";
|
|
11
13
|
import { createDashboardManager } from "./dashboard-manager.js";
|
|
@@ -99,6 +101,39 @@ function toolResult(key, value, message) {
|
|
|
99
101
|
};
|
|
100
102
|
}
|
|
101
103
|
|
|
104
|
+
function dashboardAutostartDiagnostic(error) {
|
|
105
|
+
const message = typeof error?.message === "string" ? error.message : "";
|
|
106
|
+
if (error?.code === "EADDRINUSE" || /port conflict|already in use/i.test(message)) {
|
|
107
|
+
return "TaskChef dashboard autostart skipped: port 127.0.0.1:3210 is unavailable; the listener was left untouched.";
|
|
108
|
+
}
|
|
109
|
+
if (/configuration|workspace|task log/i.test(message)) {
|
|
110
|
+
return "TaskChef dashboard autostart skipped: the canonical workspace is not ready.";
|
|
111
|
+
}
|
|
112
|
+
return "TaskChef dashboard autostart skipped: the dashboard could not be started.";
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function createDashboardAutostart({
|
|
116
|
+
workspace,
|
|
117
|
+
dashboardManager,
|
|
118
|
+
readConfiguration = readConfig,
|
|
119
|
+
log = (message) => process.stderr.write(`${message}\n`),
|
|
120
|
+
} = {}) {
|
|
121
|
+
let startPromise = null;
|
|
122
|
+
return async () => {
|
|
123
|
+
startPromise ??= (async () => {
|
|
124
|
+
try {
|
|
125
|
+
const config = await readConfiguration(workspace, { checkPaths: false });
|
|
126
|
+
if (!dashboardAutostartEnabled(config)) return { action: "disabled" };
|
|
127
|
+
return await dashboardManager.ensure();
|
|
128
|
+
} catch (error) {
|
|
129
|
+
try { log(dashboardAutostartDiagnostic(error)); } catch {}
|
|
130
|
+
return { action: "failed" };
|
|
131
|
+
}
|
|
132
|
+
})();
|
|
133
|
+
return startPromise;
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
102
137
|
export function createTaskChefMcpServer({
|
|
103
138
|
workspace = resolveWorkspacePath().workspace,
|
|
104
139
|
prepare = prepareDispatch,
|
|
@@ -107,6 +142,8 @@ export function createTaskChefMcpServer({
|
|
|
107
142
|
reportState = reportTaskState,
|
|
108
143
|
link = linkTask,
|
|
109
144
|
dashboardManager = createDashboardManager({ workspace }),
|
|
145
|
+
readConfiguration = readConfig,
|
|
146
|
+
logDashboardDiagnostic,
|
|
110
147
|
} = {}) {
|
|
111
148
|
const server = new McpServer(
|
|
112
149
|
{ name: "taskchef", version: TASKCHEF_VERSION },
|
|
@@ -280,5 +317,17 @@ export function createTaskChefMcpServer({
|
|
|
280
317
|
},
|
|
281
318
|
);
|
|
282
319
|
|
|
320
|
+
const originalConnect = server.connect.bind(server);
|
|
321
|
+
const autostartDashboard = createDashboardAutostart({
|
|
322
|
+
workspace,
|
|
323
|
+
dashboardManager,
|
|
324
|
+
readConfiguration,
|
|
325
|
+
...(logDashboardDiagnostic ? { log: logDashboardDiagnostic } : {}),
|
|
326
|
+
});
|
|
327
|
+
server.connect = async (...args) => {
|
|
328
|
+
await originalConnect(...args);
|
|
329
|
+
void autostartDashboard();
|
|
330
|
+
};
|
|
331
|
+
|
|
283
332
|
return server;
|
|
284
333
|
}
|
package/src/version.js
CHANGED
package/src/workspace.js
CHANGED
|
@@ -43,7 +43,8 @@ const PREVIOUS_TASK_SCHEMA_VERSION = 8;
|
|
|
43
43
|
const FIRST_TURN_HISTORY_TASK_SCHEMA_VERSION = 7;
|
|
44
44
|
const LEGACY_RESULTS_TASK_SCHEMA_VERSION = 6;
|
|
45
45
|
const FIRST_SELF_LINKING_TASK_SCHEMA_VERSION = 4;
|
|
46
|
-
const CONFIG_FIELDS = new Set(["schemaVersion", "projects"]);
|
|
46
|
+
const CONFIG_FIELDS = new Set(["schemaVersion", "projects", "dashboard"]);
|
|
47
|
+
const DASHBOARD_CONFIG_FIELDS = new Set(["autostart"]);
|
|
47
48
|
const PROJECT_FIELDS = new Set([
|
|
48
49
|
"name",
|
|
49
50
|
"path",
|
|
@@ -533,16 +534,36 @@ async function inspectProject(input, index = 0) {
|
|
|
533
534
|
}
|
|
534
535
|
|
|
535
536
|
export async function validateConfig(config, { checkPaths = true } = {}) {
|
|
536
|
-
|
|
537
|
+
if (!config || typeof config !== "object" || Array.isArray(config)) {
|
|
538
|
+
throw new Error("taskchef.json must be an object");
|
|
539
|
+
}
|
|
540
|
+
const unexpected = Object.keys(config).find((key) => !CONFIG_FIELDS.has(key));
|
|
541
|
+
if (unexpected) throw new Error(`taskchef.json has unsupported field: ${unexpected}`);
|
|
542
|
+
for (const required of ["schemaVersion", "projects"]) {
|
|
543
|
+
if (!(required in config)) throw new Error(`taskchef.json is missing field: ${required}`);
|
|
544
|
+
}
|
|
537
545
|
if (config.schemaVersion !== CURRENT_CONFIG_SCHEMA_VERSION) {
|
|
538
546
|
throw new Error("unsupported configuration schemaVersion");
|
|
539
547
|
}
|
|
548
|
+
let dashboard;
|
|
549
|
+
if ("dashboard" in config) {
|
|
550
|
+
requireExactFields(config.dashboard, DASHBOARD_CONFIG_FIELDS, "taskchef.json.dashboard");
|
|
551
|
+
if (typeof config.dashboard.autostart !== "boolean") {
|
|
552
|
+
throw new Error("taskchef.json.dashboard.autostart must be a boolean");
|
|
553
|
+
}
|
|
554
|
+
dashboard = { autostart: config.dashboard.autostart };
|
|
555
|
+
}
|
|
540
556
|
return {
|
|
541
557
|
schemaVersion: CURRENT_CONFIG_SCHEMA_VERSION,
|
|
542
558
|
projects: await normalizeProjects(config.projects, { checkPaths }),
|
|
559
|
+
...(dashboard ? { dashboard } : {}),
|
|
543
560
|
};
|
|
544
561
|
}
|
|
545
562
|
|
|
563
|
+
export function dashboardAutostartEnabled(config) {
|
|
564
|
+
return config?.dashboard?.autostart !== false;
|
|
565
|
+
}
|
|
566
|
+
|
|
546
567
|
async function ensureDispatchFile(workspaceRoot) {
|
|
547
568
|
const filePath = path.join(workspaceRoot, DISPATCH_FILE_NAME);
|
|
548
569
|
const exists = await managedRegularFileExists(filePath);
|
|
@@ -569,7 +590,11 @@ export async function initializeWorkspace(workspaceRoot) {
|
|
|
569
590
|
const configExists = await managedRegularFileExists(configPath);
|
|
570
591
|
const config = configExists
|
|
571
592
|
? await readConfig(root, { checkPaths: false })
|
|
572
|
-
: {
|
|
593
|
+
: {
|
|
594
|
+
schemaVersion: CURRENT_CONFIG_SCHEMA_VERSION,
|
|
595
|
+
projects: [],
|
|
596
|
+
dashboard: { autostart: true },
|
|
597
|
+
};
|
|
573
598
|
const dispatchPath = path.join(root, DISPATCH_FILE_NAME);
|
|
574
599
|
if (configExists && (await managedRegularFileExists(dispatchPath))) {
|
|
575
600
|
await readDispatchesUnlocked(root);
|
|
@@ -638,7 +663,7 @@ export async function addProject(workspaceRoot, input) {
|
|
|
638
663
|
const project = await inspectProject(input);
|
|
639
664
|
assertWorkspaceOutsideProject(root, project.path);
|
|
640
665
|
const updated = await validateConfig({
|
|
641
|
-
|
|
666
|
+
...config,
|
|
642
667
|
projects: [...config.projects, project],
|
|
643
668
|
});
|
|
644
669
|
await writeJsonAtomic(path.join(root, "taskchef.json"), updated);
|
|
@@ -676,7 +701,7 @@ export async function importProjects(workspaceRoot, inputs, { replace = false }
|
|
|
676
701
|
else projects[index] = project;
|
|
677
702
|
}
|
|
678
703
|
const config = await validateConfig({
|
|
679
|
-
|
|
704
|
+
...current,
|
|
680
705
|
projects,
|
|
681
706
|
});
|
|
682
707
|
await writeJsonAtomic(path.join(root, "taskchef.json"), config);
|
|
@@ -700,7 +725,7 @@ export async function removeProject(workspaceRoot, name) {
|
|
|
700
725
|
const [project] = config.projects.slice(index, index + 1);
|
|
701
726
|
const projects = config.projects.filter((_, projectIndex) => projectIndex !== index);
|
|
702
727
|
await writeJsonAtomic(path.join(root, "taskchef.json"), {
|
|
703
|
-
|
|
728
|
+
...config,
|
|
704
729
|
projects,
|
|
705
730
|
});
|
|
706
731
|
return { project };
|