taskchef 7.3.0 → 7.4.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.3.0",
3
+ "version": "7.4.0",
4
4
  "description": "Dispatch work from a data-only workspace to visible Codex project tasks.",
5
5
  "author": {
6
6
  "name": "Favo Yang",
package/README.md CHANGED
@@ -33,7 +33,8 @@ npm install --global taskchef
33
33
 
34
34
  The plugin provides four skills and a local MCP server. The npm installation
35
35
  puts the `taskchef` CLI on `PATH`. TaskChef installs no hooks, schedules,
36
- daemons, or background identity search.
36
+ daemons, login items, system services, or background identity search and needs
37
+ no elevated permissions.
37
38
 
38
39
  ## Bootstrap and configure
39
40
 
@@ -93,6 +94,12 @@ reads the executor's own `CODEX_THREAD_ID`, self-links, and reports lifecycle
93
94
  state. Independent outcomes may become separate executors; dependent work
94
95
  should stay together.
95
96
 
97
+ At the start of every dispatcher turn, the managed workspace instructions ask
98
+ the MCP server to best-effort ensure the dashboard. A startup failure never
99
+ blocks an answer, report, or delegation. Every dispatcher response ends with
100
+ the stable [TaskChef Dashboard](http://127.0.0.1:3210/) link; a created-task
101
+ directive remains on the preceding line so dispatch still returns immediately.
102
+
96
103
  For example, TaskChef generates this shape:
97
104
 
98
105
  ```text
@@ -146,12 +153,35 @@ an unambiguous eight-character prefix.
146
153
 
147
154
  ## Dashboard
148
155
 
156
+ Dispatcher turns call the input-free `ensure_dashboard` MCP tool. It starts at
157
+ most one dashboard inside the existing TaskChef MCP process on
158
+ `127.0.0.1:3210`, or reuses a listener only when its bounded `/api/health`
159
+ identity proves the exact TaskChef/dashboard-server version and the same
160
+ canonical workspace. The response says `started` or `reused` and includes the
161
+ stable URL, canonical workspace, and versions.
162
+
163
+ The in-process dashboard closes with the MCP process. Closing Codex or reloading
164
+ the plugin may therefore stop the dashboard; the next dispatcher turn restores
165
+ it. TaskChef adds no OS-persistent component.
166
+
167
+ For manual development, run the foreground CLI:
168
+
149
169
  ```sh
150
170
  taskchef dashboard
171
+ taskchef dashboard --port 3211
151
172
  ```
152
173
 
153
174
  The loopback dashboard watches `tasks.jsonl`, groups current states, and opens
154
175
  linked Codex tasks. It does not mutate TaskChef data and prints its local URL.
176
+ When a compatible foreground dashboard already owns port 3210,
177
+ `ensure_dashboard` reuses it but does not take ownership. If an unknown,
178
+ different-workspace, or stale-version process owns the port, TaskChef reports a
179
+ concise conflict and never kills or replaces that process. The foreground CLI
180
+ similarly asks you to stop the listener or choose another `--port`.
181
+
182
+ The health endpoint contains only a fixed service marker, health schema,
183
+ TaskChef version, dashboard-server version, and canonical workspace. It exposes
184
+ no task data, credentials, environment variables, process control, or secrets.
155
185
 
156
186
  ## Common recovery
157
187
 
@@ -3,6 +3,9 @@
3
3
 
4
4
  This folder is the canonical per-user TaskChef dispatcher workspace.
5
5
 
6
+ - Early in every dispatcher turn, best-effort call the TaskChef
7
+ `ensure_dashboard` MCP tool. Dashboard startup failure must not block direct
8
+ TaskChef answers, reporting, or delegation.
6
9
  - Use `$taskchef-bootstrap` when initializing or refreshing this workspace,
7
10
  changing or listing its configured projects, running TaskChef doctor,
8
11
  or repairing its managed instructions.
@@ -20,4 +23,10 @@ This folder is the canonical per-user TaskChef dispatcher workspace.
20
23
  write status or results to this workspace.
21
24
  - Explicit invocations of TaskChef skills from other Codex projects use this
22
25
  same workspace and task history through TaskChef's global resolution rules.
26
+ - Every final response in this dispatcher workspace must end with this exact
27
+ clickable link on its final non-empty line:
28
+ `[TaskChef Dashboard](http://127.0.0.1:3210/)`. Keep any
29
+ `::created-thread{...}` directive required by delegation on its own line
30
+ immediately before the dashboard link so the immediate-return contract still
31
+ holds. Include the link even when `ensure_dashboard` failed.
23
32
  <!-- taskchef:dispatcher-instructions:end -->
package/docs/spec.md CHANGED
@@ -39,8 +39,8 @@ is dated research, not contract.
39
39
  privacy, and idempotency. It MUST NOT dispatch the owned assignment again.
40
40
  - `taskchef-report` MUST own on-demand reporting. It MUST NOT poll or persist
41
41
  inferred state.
42
- - The MCP server MUST expose four primary lifecycle tools plus the deprecated
43
- `report_result` compatibility alias specified below.
42
+ - The MCP server MUST expose `ensure_dashboard`, four primary lifecycle tools,
43
+ and the deprecated `report_result` compatibility alias specified below.
44
44
  - The CLI MAY administer and inspect the workspace, but MUST NOT provide a
45
45
  second agent lifecycle protocol.
46
46
  - The dashboard MUST be read-only with respect to dispatcher files.
@@ -100,6 +100,13 @@ fields MUST NOT change after recording.
100
100
 
101
101
  ## Required lifecycle
102
102
 
103
+ At the start of every dispatcher turn, the dispatcher SHOULD call
104
+ `ensure_dashboard` best-effort. Failure MUST NOT block direct TaskChef answers,
105
+ reporting, or delegation. Every dispatcher final response MUST end with the
106
+ exact clickable `[TaskChef Dashboard](http://127.0.0.1:3210/)` link even when
107
+ ensure failed. A created-thread directive MUST remain on its own line before
108
+ the final link, preserving the delegate skill's immediate-return contract.
109
+
103
110
  1. The dispatcher MUST call `prepare_dispatch` once per outcome.
104
111
  2. It MUST choose exactly one configured project and exact native-project path.
105
112
  3. It MUST build the instruction with the user's outcome beginning on line 1
@@ -144,6 +151,43 @@ path. Success returns both one text content item and the stated structured
144
151
  object. Validation, marker, identity, uniqueness, freshness, or filesystem
145
152
  failures are surfaced as tool errors and MUST NOT partially mutate the log.
146
153
 
154
+ ### `ensure_dashboard`
155
+
156
+ **Caller:** dispatcher. **Mutation:** starts at most one in-process loopback
157
+ HTTP server; it does not mutate dispatcher workspace files.
158
+
159
+ **Input:** empty object.
160
+
161
+ **Structured output:**
162
+
163
+ ```text
164
+ { dashboard: {
165
+ action: "started" | "reused",
166
+ url: "http://127.0.0.1:3210/",
167
+ workspace: string,
168
+ taskchefVersion: string,
169
+ serverVersion: string
170
+ } }
171
+ ```
172
+
173
+ Calls MUST serialize within one MCP process. The first call starts an owned
174
+ dashboard or reuses an exact compatible listener; later and concurrent calls
175
+ are idempotent and report reuse after the single start. The stable default MUST
176
+ bind only to `127.0.0.1:3210` and MUST NOT accept a model-supplied workspace,
177
+ host, or port.
178
+
179
+ Before reuse, TaskChef MUST query a bounded loopback identity endpoint and
180
+ require the exact fixed service/schema, TaskChef version, dashboard-server
181
+ version, and canonical workspace. An unknown, malformed, different-workspace,
182
+ or stale-version listener MUST produce a concise actionable conflict. TaskChef
183
+ MUST NOT kill, replace, signal, or otherwise control that listener. A startup
184
+ failure MUST leave no owned listener. The MCP server MUST close its owned
185
+ dashboard when its transport or process shuts down; it MUST NOT close a reused
186
+ external foreground server.
187
+
188
+ **Annotations:** `readOnlyHint: false`, `destructiveHint: false`,
189
+ `openWorldHint: false`.
190
+
147
191
  ### `prepare_dispatch`
148
192
 
149
193
  **Caller:** dispatcher. **Mutation:** none.
@@ -259,7 +303,13 @@ reports MAY read a selected task once when metadata is newer or evidence is
259
303
  uncertain. Reports MUST NOT poll or classify assistant prose.
260
304
 
261
305
  The dashboard MUST bind only to loopback, validate the current workspace
262
- snapshot, and avoid sessions or shared client state. Direct thread navigation
306
+ snapshot, and avoid sessions or shared client state. `GET /api/health` MUST
307
+ return only the bounded service identity, health schema, exact TaskChef and
308
+ dashboard-server versions, and canonical workspace. It MUST NOT return task
309
+ records, secrets, credentials, environment values, or process-control data.
310
+ Identity remains available while an already-started monitor retains its last
311
+ valid snapshot after a later invalid task log; an invalid initial log MAY fail
312
+ startup safely. Direct thread navigation
263
313
  MUST require a canonical Codex UUIDv7. Otherwise it MAY open the revalidated
264
314
  configured project. Project paths from task history MUST be matched against
265
315
  current configuration before use.
@@ -279,3 +329,9 @@ loopback origin, and current configuration before acting.
279
329
  TaskChef MUST NOT use lifecycle hooks, schedules, polling, recent-thread search,
280
330
  transcript search, title matching, hidden reasoning, or token usage to discover
281
331
  identity or infer semantic results.
332
+
333
+ TaskChef MUST NOT install or require daemons, launchd agents, login items,
334
+ system services, cron jobs, hooks, privileged components, or elevated/system
335
+ permissions for dashboard availability. Availability is best-effort while the
336
+ owning Codex/plugin MCP process is alive and is not guaranteed while Codex is
337
+ closed.
package/docs/workflows.md CHANGED
@@ -14,16 +14,55 @@ research.
14
14
  | `skills/taskchef-executor/SKILL.md` | Own, self-link, execute, and report every executor turn. |
15
15
  | `skills/taskchef-bootstrap/SKILL.md` | Initialize current workspace and configure projects. |
16
16
  | `skills/taskchef-report/SKILL.md` | Select cached tasks and perform bounded live checks. |
17
- | `src/mcp.js` | Four primary lifecycle tools, one deprecated alias, and MCP annotations. |
17
+ | `src/mcp.js` | Dashboard ensure, four primary lifecycle tools, one deprecated alias, shutdown ownership, and MCP annotations. |
18
18
  | `src/delegation.js` | UUID marker, concise executor-skill invocation shape, and creation-failure handling. |
19
19
  | `src/workspace.js` | Current schemas, validation, locking, atomic JSONL writes, linking, and result freshness. |
20
20
  | `src/cli.js` | Administration, inspection, diagnostics, and dashboard startup. |
21
- | `src/dashboard.js` | Validated snapshots, SSE fan-out, and bounded open actions. |
21
+ | `src/dashboard.js` | Versioned health identity, validated snapshots, SSE fan-out, and bounded open actions. |
22
+ | `src/dashboard-manager.js` | Concurrent singleton ensure, exact listener reuse, conflicts, and owned shutdown. |
22
23
 
23
24
  The MCP process resolves `TASKCHEF_WORKSPACE` once and never accepts a model
24
25
  supplied path. The CLI resolves `--workspace`, then the environment, then the
25
26
  per-user default.
26
27
 
28
+ ## Dispatcher dashboard lifecycle
29
+
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/report skills.
33
+
34
+ ```mermaid
35
+ sequenceDiagram
36
+ autonumber
37
+ participant D as Dispatcher
38
+ participant M as TaskChef MCP
39
+ participant H as Loopback health
40
+ participant S as Dashboard server
41
+ D->>M: ensure_dashboard()
42
+ M->>M: Serialize concurrent ensure calls
43
+ M->>H: GET 127.0.0.1:3210/api/health
44
+ alt Exact service, versions, and canonical workspace
45
+ H-->>M: Bounded compatible identity
46
+ M-->>D: reused, URL, workspace, versions
47
+ else No listener
48
+ H--xM: Connection refused
49
+ M->>S: Start in this MCP process on 127.0.0.1:3210
50
+ S-->>M: Owned server
51
+ M-->>D: started, URL, workspace, versions
52
+ else Unknown, stale, or different workspace
53
+ H-->>M: Missing or incompatible identity
54
+ M-->>D: Actionable conflict, listener untouched
55
+ end
56
+ Note over D: Continue even when ensure failed
57
+ D-->>D: Answer, report, or dispatch
58
+ Note over D: Created-thread directive, when any, precedes final dashboard link
59
+ ```
60
+
61
+ When the MCP transport or plugin process closes, it closes only the server it
62
+ started. A compatible foreground `taskchef dashboard` listener may be reused
63
+ but remains owned by that CLI process. No TaskChef path terminates an unknown
64
+ listener or installs OS persistence.
65
+
27
66
  ## Normal delegation and self-linking
28
67
 
29
68
  The dispatcher uses native Codex project discovery for routing and MCP for
@@ -235,8 +274,9 @@ sequenceDiagram
235
274
  ```
236
275
 
237
276
  The dashboard binds to `127.0.0.1`, has no shared session state, limits
238
- request bodies, and checks origin/authority for stateful local actions. Historical
239
- project paths are untrusted until matched against current configuration.
277
+ request bodies, and checks origin/authority for stateful local actions. Its
278
+ bounded identity endpoint contains no task data or secrets. Historical project
279
+ paths are untrusted until matched against current configuration.
240
280
 
241
281
  ## Concurrency and trust boundaries
242
282
 
package/index.js CHANGED
@@ -57,10 +57,19 @@ export {
57
57
  } from "./src/codex-app.js";
58
58
 
59
59
  export {
60
+ DASHBOARD_HEALTH_MAX_BYTES,
61
+ DASHBOARD_HEALTH_PATH,
60
62
  DashboardMonitor,
61
63
  createDashboardServer,
62
64
  dashboardAuthority,
63
65
  sortTasksByMeaningfulUpdate,
64
66
  } from "./src/dashboard.js";
65
67
 
68
+ export {
69
+ createDashboardManager,
70
+ readDashboardIdentity,
71
+ } from "./src/dashboard-manager.js";
72
+
73
+ export { DASHBOARD_SERVER_VERSION, TASKCHEF_VERSION } from "./src/version.js";
74
+
66
75
  export { createTaskChefMcpServer } from "./src/mcp.js";
package/mcp/server.js CHANGED
@@ -5,3 +5,15 @@ import { createTaskChefMcpServer } from "../src/mcp.js";
5
5
 
6
6
  const server = createTaskChefMcpServer();
7
7
  await server.connect(new StdioServerTransport());
8
+
9
+ let shutdownPromise = null;
10
+ const shutdown = () => {
11
+ shutdownPromise ??= server.close().catch((error) => {
12
+ process.stderr.write(`TaskChef MCP shutdown failed: ${error.message}\n`);
13
+ process.exitCode = 1;
14
+ });
15
+ return shutdownPromise;
16
+ };
17
+
18
+ process.once("SIGINT", shutdown);
19
+ process.once("SIGTERM", shutdown);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "taskchef",
3
- "version": "7.3.0",
3
+ "version": "7.4.0",
4
4
  "description": "A non-blocking interactive dispatcher for visible Codex tasks.",
5
5
  "license": "MIT",
6
6
  "author": "Favo Yang",
@@ -19,8 +19,10 @@ all deterministic workspace operations.
19
19
  dispatcher workspace. Preserve unrelated user-owned paths.
20
20
  - Do not dispatch tasks or report on executor threads during bootstrap unless
21
21
  the user separately requests those actions.
22
- - Never create hooks, schedules, polling, or daemons. TaskChef executors
23
- self-link through the installed MCP server.
22
+ - Never create hooks, schedules, polling, daemons, login items, or system
23
+ services. The managed dispatcher instructions own best-effort dashboard
24
+ startup and final-link guidance; bootstrap only refreshes that managed block.
25
+ TaskChef executors self-link through the installed MCP server.
24
26
 
25
27
  ## Initialize and repair
26
28
 
package/src/cli.js CHANGED
@@ -409,10 +409,22 @@ async function dashboard(args) {
409
409
  values: ["--port", "--workspace"],
410
410
  switches: ["--json"],
411
411
  });
412
- const server = await createDashboardServer({
413
- workspace: workspaceRoot(args),
414
- port: dashboardPort(args),
415
- });
412
+ const port = dashboardPort(args);
413
+ let server;
414
+ try {
415
+ server = await createDashboardServer({
416
+ workspace: workspaceRoot(args),
417
+ port,
418
+ });
419
+ } catch (error) {
420
+ if (error?.code === "EADDRINUSE") {
421
+ throw new Error(
422
+ `dashboard port 127.0.0.1:${port} is already in use; `
423
+ + "stop the existing listener or choose another --port (TaskChef will not terminate it)",
424
+ );
425
+ }
426
+ throw error;
427
+ }
416
428
  print({
417
429
  schemaVersion: 1,
418
430
  url: server.url,
@@ -461,8 +473,10 @@ Project import reads a JSON
461
473
  array from a file, or from standard input when the source is '-' or omitted.
462
474
  Workspace resolution precedence is --workspace, TASKCHEF_WORKSPACE, then
463
475
  ~/.agents/taskchef.
464
- The dashboard binds to 127.0.0.1 and reads the canonical task log without
465
- modifying dispatcher-workspace files.
476
+ The foreground dashboard binds to 127.0.0.1 and reads the canonical task log
477
+ without modifying dispatcher-workspace files. The dispatcher MCP may reuse a
478
+ compatible foreground server on port 3210; neither mode terminates a listener
479
+ that already occupies its requested port.
466
480
  `);
467
481
  }
468
482
 
@@ -0,0 +1,183 @@
1
+ import http from "node:http";
2
+ import { realpath } from "node:fs/promises";
3
+ import path from "node:path";
4
+
5
+ import {
6
+ DASHBOARD_HEALTH_MAX_BYTES,
7
+ DASHBOARD_HEALTH_PATH,
8
+ createDashboardServer,
9
+ dashboardAuthority,
10
+ } from "./dashboard.js";
11
+ import { DASHBOARD_SERVER_VERSION, TASKCHEF_VERSION } from "./version.js";
12
+
13
+ const DEFAULT_HOST = "127.0.0.1";
14
+ const DEFAULT_PORT = 3210;
15
+ const HEALTH_TIMEOUT_MS = 750;
16
+
17
+ function expectedIdentity(workspace, taskchefVersion, serverVersion) {
18
+ return {
19
+ schemaVersion: 1,
20
+ service: "taskchef-dashboard",
21
+ taskchefVersion,
22
+ serverVersion,
23
+ workspace,
24
+ };
25
+ }
26
+
27
+ function isExactIdentity(value, expected) {
28
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
29
+ const keys = Object.keys(value).sort();
30
+ const expectedKeys = Object.keys(expected).sort();
31
+ return keys.length === expectedKeys.length
32
+ && keys.every((key, index) => key === expectedKeys[index])
33
+ && expectedKeys.every((key) => value[key] === expected[key]);
34
+ }
35
+
36
+ function listenerConflict(url, detail) {
37
+ return new Error(
38
+ `TaskChef dashboard port conflict at ${url} ${detail} `
39
+ + "Stop that listener or choose another port for the foreground dashboard CLI; TaskChef will not terminate it.",
40
+ );
41
+ }
42
+
43
+ export function readDashboardIdentity({
44
+ host = DEFAULT_HOST,
45
+ port = DEFAULT_PORT,
46
+ maximumBytes = DASHBOARD_HEALTH_MAX_BYTES,
47
+ timeoutMs = HEALTH_TIMEOUT_MS,
48
+ } = {}) {
49
+ return new Promise((resolve, reject) => {
50
+ let settled = false;
51
+ const finish = (error, value) => {
52
+ if (settled) return;
53
+ settled = true;
54
+ clearTimeout(deadline);
55
+ if (error) reject(error);
56
+ else resolve(value);
57
+ };
58
+ const request = http.get({
59
+ host,
60
+ port,
61
+ path: DASHBOARD_HEALTH_PATH,
62
+ headers: {
63
+ Accept: "application/json",
64
+ Host: dashboardAuthority(host, port),
65
+ },
66
+ }, (response) => {
67
+ const chunks = [];
68
+ let total = 0;
69
+ response.on("data", (chunk) => {
70
+ total += chunk.length;
71
+ if (total > maximumBytes) {
72
+ const error = new Error("dashboard health response exceeds the identity limit");
73
+ finish(error);
74
+ request.destroy(error);
75
+ return;
76
+ }
77
+ chunks.push(chunk);
78
+ });
79
+ response.on("end", () => {
80
+ if (response.statusCode !== 200) {
81
+ finish(new Error(`dashboard health returned HTTP ${response.statusCode}`));
82
+ return;
83
+ }
84
+ try {
85
+ finish(null, JSON.parse(Buffer.concat(chunks, total).toString("utf8")));
86
+ } catch {
87
+ finish(new Error("dashboard health returned invalid JSON"));
88
+ }
89
+ });
90
+ });
91
+ const deadline = setTimeout(() => {
92
+ const error = new Error("dashboard health request timed out");
93
+ finish(error);
94
+ request.destroy(error);
95
+ }, timeoutMs);
96
+ request.on("error", (error) => finish(error));
97
+ });
98
+ }
99
+
100
+ function listenerAbsent(error) {
101
+ return error?.code === "ECONNREFUSED" || error?.code === "EHOSTUNREACH";
102
+ }
103
+
104
+ export function createDashboardManager({
105
+ workspace,
106
+ host = DEFAULT_HOST,
107
+ port = DEFAULT_PORT,
108
+ taskchefVersion = TASKCHEF_VERSION,
109
+ serverVersion = DASHBOARD_SERVER_VERSION,
110
+ createServer = createDashboardServer,
111
+ readIdentity = readDashboardIdentity,
112
+ } = {}) {
113
+ let canonicalWorkspace;
114
+ let ownedServer = null;
115
+ let ensurePromise = null;
116
+ let closePromise = null;
117
+
118
+ const publicResult = (action) => ({
119
+ action,
120
+ url: `http://${dashboardAuthority(host, ownedServer?.port ?? port)}/`,
121
+ workspace: canonicalWorkspace,
122
+ taskchefVersion,
123
+ serverVersion,
124
+ });
125
+
126
+ const probe = async () => {
127
+ const url = `http://${dashboardAuthority(host, port)}/`;
128
+ let identity;
129
+ try {
130
+ identity = await readIdentity({ host, port });
131
+ } catch (error) {
132
+ if (listenerAbsent(error)) return false;
133
+ throw listenerConflict(url, `is occupied but did not return a compatible identity (${error.message}).`);
134
+ }
135
+ const expected = expectedIdentity(canonicalWorkspace, taskchefVersion, serverVersion);
136
+ if (!isExactIdentity(identity, expected)) {
137
+ throw listenerConflict(url, "belongs to an unknown, stale, or different-workspace service.");
138
+ }
139
+ return true;
140
+ };
141
+
142
+ const ensureOnce = async () => {
143
+ canonicalWorkspace ??= await realpath(path.resolve(workspace));
144
+ if (ownedServer) return publicResult("reused");
145
+ if (await probe()) return publicResult("reused");
146
+ try {
147
+ ownedServer = await createServer({
148
+ workspace: canonicalWorkspace,
149
+ host,
150
+ port,
151
+ taskchefVersion,
152
+ serverVersion,
153
+ });
154
+ return publicResult("started");
155
+ } catch (error) {
156
+ if (error?.code !== "EADDRINUSE") throw error;
157
+ if (await probe()) return publicResult("reused");
158
+ throw error;
159
+ }
160
+ };
161
+
162
+ return {
163
+ async ensure() {
164
+ if (closePromise) throw new Error("TaskChef dashboard manager is shutting down");
165
+ if (ensurePromise) {
166
+ await ensurePromise;
167
+ return publicResult("reused");
168
+ }
169
+ ensurePromise = ensureOnce().finally(() => { ensurePromise = null; });
170
+ return ensurePromise;
171
+ },
172
+ async close() {
173
+ closePromise ??= (async () => {
174
+ await ensurePromise?.catch(() => {});
175
+ const server = ownedServer;
176
+ ownedServer = null;
177
+ await server?.close();
178
+ })();
179
+ return closePromise;
180
+ },
181
+ get owned() { return ownedServer !== null; },
182
+ };
183
+ }
package/src/dashboard.js CHANGED
@@ -16,6 +16,7 @@ import {
16
16
  parseTaskLogContent,
17
17
  readConfig,
18
18
  } from "./workspace.js";
19
+ import { DASHBOARD_SERVER_VERSION, TASKCHEF_VERSION } from "./version.js";
19
20
 
20
21
  const TASKS_FILE_NAME = "tasks.jsonl";
21
22
  const STATIC_ROOT = fileURLToPath(new URL("./dashboard/", import.meta.url));
@@ -24,6 +25,8 @@ const LOOPBACK_HOSTS = new Set(["127.0.0.1", "::1"]);
24
25
  const DEFAULT_MAX_FILE_BYTES = 16 * 1024 * 1024;
25
26
  const DEFAULT_MAX_TASKS = 2_000;
26
27
  const DEFAULT_MAX_EVENT_CLIENTS = 16;
28
+ export const DASHBOARD_HEALTH_PATH = "/api/health";
29
+ export const DASHBOARD_HEALTH_MAX_BYTES = 8 * 1024;
27
30
  const CONTENT_SECURITY_POLICY = [
28
31
  "default-src 'self'",
29
32
  "base-uri 'none'",
@@ -402,6 +405,8 @@ export async function createDashboardServer({
402
405
  monitorOptions = {},
403
406
  openProject = null,
404
407
  openThread = null,
408
+ taskchefVersion = TASKCHEF_VERSION,
409
+ serverVersion = DASHBOARD_SERVER_VERSION,
405
410
  } = {}) {
406
411
  if (!LOOPBACK_HOSTS.has(host)) {
407
412
  throw new Error("dashboard host must be a loopback address");
@@ -414,6 +419,17 @@ export async function createDashboardServer({
414
419
  }
415
420
  const monitor = new DashboardMonitor(workspace, monitorOptions);
416
421
  await monitor.start();
422
+ const identity = Object.freeze({
423
+ schemaVersion: 1,
424
+ service: "taskchef-dashboard",
425
+ taskchefVersion,
426
+ serverVersion,
427
+ workspace: monitor.workspace,
428
+ });
429
+ if (Buffer.byteLength(`${JSON.stringify(identity)}\n`) > DASHBOARD_HEALTH_MAX_BYTES) {
430
+ monitor.close();
431
+ throw new Error("dashboard identity exceeds the health response limit");
432
+ }
417
433
  const clients = new Set();
418
434
  let allowedAuthority;
419
435
  let allowedOrigin;
@@ -446,6 +462,16 @@ export async function createDashboardServer({
446
462
  return;
447
463
  }
448
464
 
465
+ if (url.pathname === DASHBOARD_HEALTH_PATH && (method === "GET" || method === "HEAD")) {
466
+ if (method === "HEAD") {
467
+ response.writeHead(200, securityHeaders("application/json; charset=utf-8"));
468
+ response.end();
469
+ } else {
470
+ sendJson(response, 200, identity);
471
+ }
472
+ return;
473
+ }
474
+
449
475
  if (url.pathname === "/api/snapshot" && (method === "GET" || method === "HEAD")) {
450
476
  if (method === "HEAD") {
451
477
  response.writeHead(200, securityHeaders("application/json; charset=utf-8"));
@@ -574,6 +600,7 @@ export async function createDashboardServer({
574
600
  port: boundPort,
575
601
  origin: allowedOrigin,
576
602
  url: `${allowedOrigin}/`,
603
+ identity,
577
604
  monitor,
578
605
  get eventClientCount() { return clients.size; },
579
606
  async close() {
package/src/mcp.js CHANGED
@@ -8,7 +8,9 @@ import {
8
8
  reportTaskResult,
9
9
  } from "./workspace.js";
10
10
  import { parseTaskChefMarker } from "./delegation.js";
11
+ import { createDashboardManager } from "./dashboard-manager.js";
11
12
  import { resolveWorkspacePath } from "./workspace-path.js";
13
+ import { DASHBOARD_SERVER_VERSION, TASKCHEF_VERSION } from "./version.js";
12
14
 
13
15
  const projectSchema = z.object({
14
16
  name: z.string(),
@@ -49,6 +51,14 @@ const preparationSchema = z.object({
49
51
  projects: z.array(projectSchema),
50
52
  });
51
53
 
54
+ const dashboardSchema = z.object({
55
+ action: z.enum(["started", "reused"]),
56
+ url: z.string().url(),
57
+ workspace: z.string(),
58
+ taskchefVersion: z.string(),
59
+ serverVersion: z.string(),
60
+ });
61
+
52
62
  function toolResult(key, value, message) {
53
63
  return {
54
64
  structuredContent: { [key]: value },
@@ -63,15 +73,53 @@ export function createTaskChefMcpServer({
63
73
  reportResult = reportTaskResult,
64
74
  reportState = reportTaskState,
65
75
  link = linkTask,
76
+ dashboardManager = createDashboardManager({ workspace }),
66
77
  } = {}) {
67
78
  const server = new McpServer(
68
- { name: "taskchef", version: "1.0.0" },
79
+ { name: "taskchef", version: TASKCHEF_VERSION },
69
80
  {
70
81
  instructions:
71
82
  "Prepare with prepare_dispatch, call record_task before creating the Codex task, then create it natively and return immediately. Follow the active TaskChef skill for role-specific sequencing of the identity and state tools.",
72
83
  },
73
84
  );
74
85
 
86
+ const originalClose = server.close.bind(server);
87
+ let closePromise = null;
88
+ server.close = async () => {
89
+ closePromise ??= (async () => {
90
+ await dashboardManager.close();
91
+ await originalClose();
92
+ })();
93
+ return closePromise;
94
+ };
95
+ server.server.onclose = () => {
96
+ void dashboardManager.close();
97
+ };
98
+
99
+ server.registerTool(
100
+ "ensure_dashboard",
101
+ {
102
+ title: "Ensure TaskChef dashboard",
103
+ description:
104
+ "Best-effort ensure the canonical TaskChef dashboard is available on 127.0.0.1:3210. Starts one dashboard inside this MCP process or reuses only an exact compatible TaskChef dashboard for the same canonical workspace; unknown listeners are never terminated or replaced.",
105
+ inputSchema: {},
106
+ outputSchema: { dashboard: dashboardSchema },
107
+ annotations: {
108
+ readOnlyHint: false,
109
+ destructiveHint: false,
110
+ openWorldHint: false,
111
+ },
112
+ },
113
+ async () => {
114
+ const dashboard = await dashboardManager.ensure();
115
+ return toolResult(
116
+ "dashboard",
117
+ dashboard,
118
+ `${dashboard.action === "started" ? "Started" : "Reused"} TaskChef dashboard ${dashboard.url}`,
119
+ );
120
+ },
121
+ );
122
+
75
123
  server.registerTool(
76
124
  "prepare_dispatch",
77
125
  {
package/src/version.js ADDED
@@ -0,0 +1,7 @@
1
+ import { createRequire } from "node:module";
2
+
3
+ const require = createRequire(import.meta.url);
4
+ const packageMetadata = require("../package.json");
5
+
6
+ export const TASKCHEF_VERSION = packageMetadata.version;
7
+ export const DASHBOARD_SERVER_VERSION = "1";