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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "taskchef",
3
- "version": "7.17.1",
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
- "Start and open the TaskChef dashboard.",
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 four skills and a local MCP server. The npm installation
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
- At the start of every dispatcher turn, the managed workspace instructions ask
133
- the MCP server to best-effort ensure the dashboard. A startup failure never
134
- blocks an answer, report, or delegation. Every dispatcher response ends with
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
- Ask the dispatcher to make the dashboard available and open it:
223
+ Use the packaged recovery skill to make the dashboard available and open it:
223
224
 
224
225
  ```text
225
- Start and open the TaskChef dashboard.
226
+ $taskchef-dashboard Ensure and open the TaskChef dashboard.
226
227
  ```
227
228
 
228
- Dispatcher turns call the input-free `ensure_dashboard` MCP tool. It starts at
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 dispatcher turn restores
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 maintenance,
10
- not delegated project work. Call `ensure_dashboard`, then open or navigate to
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: four skills, local MCP server, data CLI, and loopback dashboard. | Cloneable agent distribution: instructions, internal skills, scripts, policies, and private on-disk state. |
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 | Four plugin skills with narrow bootstrap, delegate, executor, and conversational coordination responsibilities. | Internal firstmate-only skills plus standalone public skills; AGENTS.md routes conditional procedures. |
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 exactly:
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
- The generated managed `AGENTS.md` block makes dashboard maintenance a
31
- best-effort prelude to every dispatcher turn and keeps response ordering
32
- centralized instead of duplicating it across delegate/copilot skills.
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.17.1",
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. `githubRepos` is always
164
- an array; unsupported configuration is rejected without being rewritten.
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.
@@ -0,0 +1,4 @@
1
+ interface:
2
+ display_name: "TaskChef Dashboard"
3
+ short_description: "Start or recover the TaskChef dashboard"
4
+ default_prompt: "Use $taskchef-dashboard to ensure the TaskChef dashboard is available and give me its link."
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 CODEX_THREAD_ID_PATTERN = /^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$/i;
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, { timeout: CODEX_COMMAND_TIMEOUT_MS, killSignal: "SIGKILL" });
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, process.platform === "win32" ? "codex.exe" : "codex"));
34
+ .map((directory) => path.join(directory, platform === "win32" ? "codex.exe" : "codex"));
28
35
  }
29
36
 
30
37
  function isDesktopBundleCandidate(filePath) {
31
- return filePath.includes(`${path.sep}Contents${path.sep}Resources${path.sep}`);
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
- return typeof threadId === "string" && CODEX_THREAD_ID_PATTERN.test(threadId);
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
+ }
@@ -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
+ }
@@ -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
+ });
@@ -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>
@@ -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;
@@ -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
- .primary-button:disabled, .secondary-button:disabled { opacity: 0.5; cursor: not-allowed; }
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
@@ -4,4 +4,4 @@ const require = createRequire(import.meta.url);
4
4
  const packageMetadata = require("../package.json");
5
5
 
6
6
  export const TASKCHEF_VERSION = packageMetadata.version;
7
- export const DASHBOARD_SERVER_VERSION = "1";
7
+ export const DASHBOARD_SERVER_VERSION = "2";
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
- requireExactFields(config, CONFIG_FIELDS, "taskchef.json");
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
- : { schemaVersion: CURRENT_CONFIG_SCHEMA_VERSION, projects: [] };
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
- schemaVersion: CURRENT_CONFIG_SCHEMA_VERSION,
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
- schemaVersion: CURRENT_CONFIG_SCHEMA_VERSION,
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
- schemaVersion: CURRENT_CONFIG_SCHEMA_VERSION,
728
+ ...config,
704
729
  projects,
705
730
  });
706
731
  return { project };